[CORL-1180] Moderation + Editing Improvements (#3020)

* fix: handle moderating edited comments better

* fix: add back deleted audit trail entry

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Wyatt Johnson
2020-07-16 19:59:51 +00:00
committed by GitHub
parent 4f5c0e285c
commit 5b589bfe2b
9 changed files with 98 additions and 21 deletions
@@ -47,7 +47,7 @@ const ApproveCommentMutation = createMutation(
}
}
}
...ModeratedByContainer_comment
...ModerateCardContainer_comment
}
moderationQueues(
storyID: $storyID
@@ -83,7 +83,22 @@ const ApproveCommentMutation = createMutation(
proxy.setValue("APPROVED", "status");
proxy.setValue(true, "viewerDidModerate");
},
updater: (store) => {
updater: (store, data) => {
// If no comment came back or the returned comment status was the same,
// don't remove it from the connections!
if (
!data.approveComment ||
!data.approveComment.comment ||
data.approveComment.comment.status !== "APPROVED"
) {
return;
}
// Ensure that the comment retains the viewerDidModerate state after it
// comes back from the update.
const proxy = store.get(input.commentID)!;
proxy.setValue(true, "viewerDidModerate");
const connections = [
getQueueConnection(
store,
@@ -47,7 +47,7 @@ const RejectCommentMutation = createMutation(
}
}
}
...ModeratedByContainer_comment
...ModerateCardContainer_comment
}
moderationQueues(
storyID: $storyID
@@ -83,7 +83,22 @@ const RejectCommentMutation = createMutation(
proxy.setValue("REJECTED", "status");
proxy.setValue(true, "viewerDidModerate");
},
updater: (store) => {
updater: (store, data) => {
// If no comment came back or the returned comment status was the same,
// don't remove it from the connections!
if (
!data.rejectComment ||
!data.rejectComment.comment ||
data.rejectComment.comment.status !== "REJECTED"
) {
return;
}
// Ensure that the comment retains the viewerDidModerate state after it
// comes back from the update.
const proxy = store.get(input.commentID)!;
proxy.setValue(true, "viewerDidModerate");
const connections = [
getQueueConnection(store, "REPORTED", input.storyID),
getQueueConnection(store, "PENDING", input.storyID),
@@ -375,7 +375,7 @@ it("approves comment in reported queue", async () => {
});
return {
comment: {
id: reportedComments[0].id,
...reportedComments[0],
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
@@ -473,7 +473,7 @@ it("rejects comment in reported queue", async () => {
});
return {
comment: {
id: reportedComments[0].id,
...reportedComments[0],
status: GQLCOMMENT_STATUS.REJECTED,
statusHistory: {
edges: [
@@ -245,7 +245,7 @@ it("approves comment in rejected queue", async () => {
});
return {
comment: {
id: rejectedComments[0].id,
...rejectedComments[0],
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
@@ -95,7 +95,7 @@ it("approves single comment", async () => {
});
return {
comment: {
id: comment.id,
...comment,
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
@@ -148,9 +148,8 @@ it("rejects single comment", async () => {
});
return {
comment: {
id: comment.id,
...comment,
status: GQLCOMMENT_STATUS.REJECTED,
author: comment.author,
statusHistory: {
edges: [
{
@@ -47,6 +47,13 @@ export function getLatestRevision(
return comment.revisions[comment.revisions.length - 1];
}
export function hasRevision(
comment: Pick<Comment, "revisions">,
revisionID: string
): boolean {
return comment.revisions.some((revision) => revision.id === revisionID);
}
export function calculateRejectionRate(counts: CommentStatusCounts): number {
const published = calculateTotalPublishedCommentCount(counts);
const rejected = counts[GQLCOMMENT_STATUS.REJECTED];
@@ -5,7 +5,12 @@ import {
createCommentModerationAction,
CreateCommentModerationActionInput,
} from "coral-server/models/action/moderation/comment";
import { updateCommentStatus } from "coral-server/models/comment";
import {
getLatestRevision,
hasRevision,
retrieveComment,
updateCommentStatus,
} from "coral-server/models/comment";
import { Tenant } from "coral-server/models/tenant";
export type Moderate = CreateCommentModerationActionInput;
@@ -18,18 +23,32 @@ export default async function moderate(
) {
// TODO: wrap these operations in a transaction?
// Create the moderation action in the audit log.
const action = await createCommentModerationAction(
mongo,
tenant.id,
input,
now
);
if (!action) {
// TODO: wrap in better error?
throw new Error("could not create moderation action");
// Get the comment that we're moderating.
const comment = await retrieveComment(mongo, tenant.id, input.commentID);
if (!comment) {
throw new CommentNotFoundError(input.commentID, input.commentRevisionID);
}
// Get the latest revision on that comment.
const revision = getLatestRevision(comment);
// Ensure that the latest revision is the same revision that we're moderating.
if (revision.id !== input.commentRevisionID) {
// The revision has been updated since then! Ensure that this revision ID
// does exist.
if (!hasRevision(comment, input.commentRevisionID)) {
throw new CommentNotFoundError(input.commentID, input.commentRevisionID);
}
// The comment has this revision, it just isn't the latest one. Return the
// same comment back because we didn't modify anything.
return { before: comment, after: null };
}
// TODO: (wyattjoh) this is a pretty race condition prone check here, replace
// with a more concrete query that can prevent the comment being edited in the
// time it takes to go from the above block to the next block.
// Update the Comment's status.
const result = await updateCommentStatus(
mongo,
@@ -42,5 +61,17 @@ export default async function moderate(
throw new CommentNotFoundError(input.commentID, input.commentRevisionID);
}
// Create the moderation action in the audit log.
const action = await createCommentModerationAction(
mongo,
tenant.id,
input,
now
);
if (!action) {
// TODO: wrap in better error?
throw new Error("could not create moderation action");
}
return result;
}
+5
View File
@@ -32,6 +32,11 @@ const approveComment = async (
now
);
// If the comment hasn't been updated, skip the rest of the steps.
if (!result.after) {
return result.before;
}
// Update all the comment counts on stories and users.
const counts = await updateAllCommentCounts(mongo, redis, {
...result,
+5
View File
@@ -37,6 +37,11 @@ const rejectComment = async (
now
);
// If the comment hasn't been updated, skip the rest of the steps.
if (!result.after) {
return result.before;
}
// Update all the comment counts on stories and users.
const counts = await updateAllCommentCounts(mongo, redis, {
...result,