FEATURE: New 'Reviewable' model to make reviewable items generic

Includes support for flags, reviewable users and queued posts, with REST API
backwards compatibility.

Co-Authored-By: romanrizzi <romanalejandro@gmail.com>
Co-Authored-By: jjaffeux <j.jaffeux@gmail.com>
This commit is contained in:
Robin Ward
2019-01-03 12:03:01 -05:00
parent 9a56b398a1
commit b58867b6e9
354 changed files with 8090 additions and 5225 deletions

View File

@@ -1,128 +0,0 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Admin - Flagging", { loggedIn: true });
QUnit.test("flagged posts", async assert => {
await visit("/admin/flags/active");
assert.equal(find(".flagged-posts .flagged-post").length, 1);
assert.equal(
find(".flagged-post .flag-user").length,
1,
"shows who flagged it"
);
assert.equal(find(".flagged-post-response").length, 2);
assert.equal(find(".flagged-post-response:eq(0) img.avatar").length, 1);
assert.equal(
find(".flagged-post-user-details .username").length,
1,
"shows the flagged username"
);
});
QUnit.test("flagged posts - agree", async assert => {
const agreeFlag = selectKit(".agree-flag");
await visit("/admin/flags/active");
await agreeFlag.expand();
await agreeFlag.selectRowByValue("confirm-agree-keep");
assert.equal(
find(".admin-flags .flagged-post").length,
0,
"post was removed"
);
});
QUnit.test("flagged posts - agree + hide", async assert => {
const agreeFlag = selectKit(".agree-flag");
await visit("/admin/flags/active");
await agreeFlag.expand();
await agreeFlag.selectRowByValue("confirm-agree-hide");
assert.equal(
find(".admin-flags .flagged-post").length,
0,
"post was removed"
);
});
QUnit.test("flagged posts - agree + deleteSpammer", async assert => {
const agreeFlag = selectKit(".agree-flag");
await visit("/admin/flags/active");
await agreeFlag.expand();
await agreeFlag.selectRowByValue("delete-spammer");
await click(".confirm-delete");
assert.equal(
find(".admin-flags .flagged-post").length,
0,
"post was removed"
);
});
QUnit.test("flagged posts - disagree", async assert => {
await visit("/admin/flags/active");
await click(".disagree-flag");
assert.equal(find(".admin-flags .flagged-post").length, 0);
});
QUnit.test("flagged posts - defer", async assert => {
await visit("/admin/flags/active");
await click(".defer-flag");
assert.equal(find(".admin-flags .flagged-post").length, 0);
});
QUnit.test("flagged posts - delete + defer", async assert => {
const deleteFlag = selectKit(".delete-flag");
await visit("/admin/flags/active");
await deleteFlag.expand();
await deleteFlag.selectRowByValue("delete-defer");
assert.equal(find(".admin-flags .flagged-post").length, 0);
});
QUnit.test("flagged posts - delete + agree", async assert => {
const deleteFlag = selectKit(".delete-flag");
await visit("/admin/flags/active");
await deleteFlag.expand();
await deleteFlag.selectRowByValue("delete-agree");
assert.equal(find(".admin-flags .flagged-post").length, 0);
});
QUnit.test("flagged posts - delete + deleteSpammer", async assert => {
const deleteFlag = selectKit(".delete-flag");
await visit("/admin/flags/active");
await deleteFlag.expand();
await deleteFlag.selectRowByValue("delete-spammer");
await click(".confirm-delete");
assert.equal(find(".admin-flags .flagged-post").length, 0);
});
QUnit.test("topics with flags", async assert => {
await visit("/admin/flags/topics");
assert.equal(find(".flagged-topics .flagged-topic").length, 1);
assert.equal(find(".flagged-topic .flagged-topic-user").length, 2);
assert.equal(find(".flagged-topic div.flag-counts").length, 3);
await click(".flagged-topic .show-details");
assert.equal(currentURL(), "/admin/flags/topics/280");
});

View File

@@ -1,183 +0,0 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Queued Posts", {
loggedIn: true,
settings: { tagging_enabled: true },
pretend(server, helper) {
server.get("/queued_posts", () => {
return helper.response({
users: [
{
id: 3,
username: "test_user",
avatar_template:
"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png",
active: true,
admin: false,
moderator: false,
last_seen_at: "2017-08-11T20:48:05.405Z",
last_emailed_at: null,
created_at: "2017-08-07T02:23:33.309Z",
last_seen_age: "1d",
last_emailed_age: null,
created_at_age: "6d",
username_lower: "test_user",
trust_level: 0,
trust_level_locked: false,
flag_level: 0,
title: null,
suspended_at: null,
suspended_till: null,
suspended: null,
silenced: false,
time_read: "19m",
staged: false,
days_visited: 4,
posts_read_count: 12,
topics_entered: 6,
post_count: 2
}
],
queued_posts: [
{
id: 22,
queue: "default",
user_id: 3,
state: 1,
topic_id: null,
approved_by_id: null,
rejected_by_id: null,
raw: "some content",
post_options: {
archetype: "regular",
category: "1",
typing_duration_msecs: "3200",
composer_open_duration_msecs: "19007",
visible: true,
is_warning: false,
title: "a new topic that needs to be reviewed",
ip_address: "172.17.0.1",
first_post_checks: true,
is_poll: true
},
created_at: "2017-08-11T20:43:41.115Z",
category_id: 1,
can_delete_user: true
}
],
__rest_serializer: "1",
refresh_queued_posts: "/queued_posts?status=new"
});
});
}
});
QUnit.test(
"For topics: body of post, title, category and tags are all editable",
async assert => {
await visit("/queued-posts");
await click(".queued-posts .queued-post button.edit");
assert.ok(exists(".d-editor-container"), "the body should be editable");
assert.ok(
exists(".edit-title .ember-text-field"),
"the title should be editable"
);
assert.ok(exists(".category-chooser"), "category should be editable");
assert.ok(exists(".tag-chooser"), "tags should be editable");
}
);
QUnit.test("For replies: only the body of post is editable", async assert => {
// prettier-ignore
server.get("/queued_posts", () => { //eslint-disable-line no-undef
return [
200,
{ "Content-Type": "application/json" },
{
users: [
{
id: 3,
username: "test_user",
avatar_template:
"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png",
active: true,
admin: false,
moderator: false,
last_seen_at: "2017-08-11T20:48:05.405Z",
last_emailed_at: null,
created_at: "2017-08-07T02:23:33.309Z",
last_seen_age: "1d",
last_emailed_age: null,
created_at_age: "6d",
username_lower: "test_user",
trust_level: 0,
trust_level_locked: false,
flag_level: 0,
title: null,
suspended_at: null,
suspended_till: null,
suspended: null,
silenced: false,
time_read: "19m",
staged: false,
days_visited: 4,
posts_read_count: 12,
topics_entered: 6,
post_count: 2
}
],
topics: [
{
id: 11,
title: "This is a topic",
fancy_title: "This is a topic",
slug: "this-is-a-topic",
posts_count: 2
}
],
queued_posts: [
{
id: 4,
queue: "default",
user_id: 3,
state: 1,
topic_id: 11,
approved_by_id: null,
rejected_by_id: null,
raw: "edited haahaasdfasdfasdfasdf",
post_options: {
archetype: "regular",
category: "3",
reply_to_post_number: "2",
typing_duration_msecs: "1900",
composer_open_duration_msecs: "12096",
visible: true,
is_warning: false,
featured_link: "",
ip_address: "172.17.0.1",
first_post_checks: true,
is_poll: true
},
created_at: "2017-08-07T19:11:52.018Z",
category_id: 3,
can_delete_user: true
}
],
__rest_serializer: "1",
refresh_queued_posts: "/queued_posts?status=new"
}
];
});
await visit("/queued-posts");
await click(".queued-posts .queued-post button.edit");
assert.ok(exists(".d-editor-container"), "the body should be editable");
assert.notOk(
exists(".edit-title .ember-text-field"),
"title should not be editbale"
);
assert.notOk(exists(".category-chooser"), "category should not be editable");
assert.notOk(exists("div.tag-chooser"), "tags should not be editable");
});

View File

@@ -0,0 +1,137 @@
import { acceptance } from "helpers/qunit-helpers";
acceptance("Review", {
loggedIn: true
});
const user = ".reviewable-item[data-reviewable-id=1234]";
QUnit.test("It returns a list of reviewable items", async assert => {
await visit("/review");
assert.ok(find(".reviewable-item").length, "has a list of items");
assert.ok(find(user).length);
assert.ok(
find(`${user}.reviewable-user`).length,
"applies a class for the type"
);
assert.ok(
find(`${user} .reviewable-action.approve`).length,
"creates a button for approve"
);
assert.ok(
find(`${user} .reviewable-action.reject`).length,
"creates a button for reject"
);
});
QUnit.test("Grouped by topic", async assert => {
await visit("/review/topics");
assert.ok(
find(".reviewable-topic").length,
"it has a list of reviewable topics"
);
});
QUnit.test("Flag related", async assert => {
await visit("/review");
assert.ok(
find(".reviewable-flagged-post .post-contents .username a[href]").length,
"it has a link to the user"
);
assert.equal(
find(".reviewable-flagged-post .post-body")
.html()
.trim(),
"<b>cooked content</b>"
);
assert.equal(find(".reviewable-flagged-post .reviewable-score").length, 2);
});
QUnit.test("Flag related", async assert => {
await visit("/review/1");
assert.ok(
find(".reviewable-flagged-post").length,
"it shows the flagged post"
);
});
QUnit.test("Clicking the buttons triggers actions", async assert => {
await visit("/review");
await click(`${user} .reviewable-action.approve`);
assert.equal(find(user).length, 0, "it removes the reviewable on success");
});
QUnit.test("Editing a reviewable", async assert => {
const topic = ".reviewable-item[data-reviewable-id=4321]";
await visit("/review");
assert.ok(find(`${topic} .reviewable-action.approve`).length);
assert.ok(!find(`${topic} .category-name`).length);
assert.equal(find(`${topic} .discourse-tag:eq(0)`).text(), "hello");
assert.equal(find(`${topic} .discourse-tag:eq(1)`).text(), "world");
assert.equal(
find(`${topic} .post-body`)
.text()
.trim(),
"existing body"
);
await click(`${topic} .reviewable-action.edit`);
await click(`${topic} .reviewable-action.save-edit`);
assert.ok(
find(`${topic} .reviewable-action.approve`).length,
"saving without changes is a cancel"
);
await click(`${topic} .reviewable-action.edit`);
assert.equal(
find(`${topic} .reviewable-action.approve`).length,
0,
"when editing actions are disabled"
);
await fillIn(".editable-field.payload-raw textarea", "new raw contents");
await click(`${topic} .reviewable-action.cancel-edit`);
assert.equal(
find(`${topic} .post-body`)
.text()
.trim(),
"existing body",
"cancelling does not update the value"
);
await click(`${topic} .reviewable-action.edit`);
let category = selectKit(`${topic} .category-id .select-kit`);
await category.expand();
await category.selectRowByValue("6");
let tags = selectKit(`${topic} .payload-tags .mini-tag-chooser`);
await tags.expand();
await tags.fillInFilter("monkey");
await tags.keyboard("enter");
await fillIn(".editable-field.payload-raw textarea", "new raw contents");
await click(`${topic} .reviewable-action.save-edit`);
assert.equal(find(`${topic} .discourse-tag:eq(0)`).text(), "hello");
assert.equal(find(`${topic} .discourse-tag:eq(1)`).text(), "world");
assert.equal(find(`${topic} .discourse-tag:eq(2)`).text(), "monkey");
assert.equal(
find(`${topic} .post-body`)
.text()
.trim(),
"new raw contents"
);
assert.equal(
find(`${topic} .category-name`)
.text()
.trim(),
"support"
);
});

View File

@@ -3441,7 +3441,6 @@ export default {
category_id: 1,
word_count: 198,
deleted_at: null,
pending_posts_count: 0,
draft: null,
draft_key: "topic_28830",
draft_sequence: null,
@@ -3890,7 +3889,6 @@ export default {
category_id: 24,
word_count: 15,
deleted_at: null,
pending_posts_count: 0,
user_id: 1,
draft: null,
draft_key: "topic_9",
@@ -4455,7 +4453,6 @@ export default {
category_id: 1,
word_count: 15,
deleted_at: null,
pending_posts_count: 0,
user_id: 1,
draft: null,
draft_key: "topic_9",
@@ -4751,7 +4748,6 @@ export default {
category_id: 1,
word_count: 15,
deleted_at: null,
pending_posts_count: 0,
user_id: 1,
draft: null,
draft_key: "topic_9",

View File

@@ -1,18 +1,15 @@
import storePretender from "helpers/store-pretender";
import fixturePretender from "helpers/fixture-pretender";
import flagPretender from "helpers/flag-pretender";
export function parsePostData(query) {
const result = {};
query.split("&").forEach(function(part) {
const item = part.split("=");
const firstSeg = decodeURIComponent(item[0]);
const m = /^([^\[]+)\[([^\]]+)\]/.exec(firstSeg);
const m = /^([^\[]+)\[(.+)\]/.exec(firstSeg);
const val = decodeURIComponent(item[1]).replace(/\+/g, " ");
if (m) {
result[m[1]] = result[m[1]] || {};
result[m[1]][m[2]] = val;
let key = m[1];
result[key] = result[key] || {};
result[key][m[2].replace("][", ".")] = val;
} else {
result[firstSeg] = val;
}
@@ -38,9 +35,16 @@ export let fixturesByUrl;
export default function() {
const server = new Pretender(function() {
storePretender.call(this, helpers);
flagPretender.call(this, helpers);
fixturesByUrl = fixturePretender.call(this, helpers);
// Autoload any `*-pretender` files
Object.keys(requirejs.entries).forEach(e => {
let m = e.match(/^helpers\/([a-z]+)\-pretender$/);
if (m && m[1] !== "create") {
let result = requirejs(e).default.call(this, helpers);
if (m[1] === "fixture") {
fixturesByUrl = result;
}
}
});
this.get("/admin/plugins", () => response({ plugins: [] }));

View File

@@ -1,73 +0,0 @@
export default function(helpers) {
const { response, success } = helpers;
const eviltrout = {
id: 1,
username: "eviltrout",
avatar_template: "/images/avatar.png"
};
const sam = {
id: 2,
username: "sam",
avatar_template: "/images/avatar.png",
can_delete_all_posts: true,
can_be_deleted: true,
post_count: 1,
topic_count: 0
};
this.get("/admin/flagged_topics", () => {
return response(200, {
flagged_topics: [
{
id: 280,
user_ids: [eviltrout.id, sam.id],
flag_counts: [
{ flag_type_id: 1, count: 3 },
{ flag_type_id: 2, count: 2 },
{ flag_type_id: 3, count: 1 }
]
}
],
users: [eviltrout, sam],
__rest_serializer: "1"
});
});
this.get("/admin/flags/active.json", () => {
return response(200, {
flagged_posts: [
{
id: 1,
user_id: sam.id,
post_action_ids: [1]
}
],
users: [eviltrout, sam],
topics: [],
post_actions: [
{
id: 1,
user_id: eviltrout.id,
post_action_type_id: 8,
name_key: "spam",
conversation: {
response: {
user_id: eviltrout.id,
excerpt: "hello"
},
reply: {
user_id: eviltrout.id,
excerpt: "goodbye"
}
}
}
],
__rest_serializer: "1"
});
});
this.post("/admin/flags/agree/1", success);
this.post("/admin/flags/defer/1", success);
this.post("/admin/flags/disagree/1", success);
}

View File

@@ -0,0 +1,114 @@
export default function(helpers) {
const { response } = helpers;
let flag = {
id: 6667,
type: "ReviewableFlaggedPost",
score: 3.0,
target_created_by_id: 1,
cooked: "<b>cooked content</b>",
reviewable_score_ids: [1, 2]
};
this.get("/review", () => {
return response(200, {
reviewables: [
{
id: 1234,
type: "ReviewableUser",
status: 0,
version: 0,
score: 4.0,
target_created_by_id: 1,
created_at: "2019-01-14T19:49:53.571Z",
username: "newbie",
email: "newbie@example.com",
bundled_action_ids: ["approve", "reject"]
},
{
id: 4321,
type: "ReviewableQueuedPost",
status: 0,
score: 4.0,
created_at: "2019-01-14T19:49:53.571Z",
target_created_by_id: 1,
payload: {
raw: "existing body",
tags: ["hello", "world"]
},
version: 1,
can_edit: true,
editable_fields: [
{ id: "category_id", type: "category" },
{ id: "payload.title", type: "text" },
{ id: "payload.raw", type: "textarea" },
{ id: "payload.tags", type: "tags" }
],
bundled_action_ids: ["approve", "reject"]
},
flag
],
bundled_actions: [
{
id: "approve",
action_ids: ["approve"]
},
{
id: "reject",
action_ids: ["reject"]
}
],
actions: [
{
id: "approve",
label: "Approve",
icon: "far-thumbs-up"
},
{
id: "reject",
label: "Reject",
icon: "far-thumbs-down"
}
],
reviewable_scores: [{ id: 1 }, { id: 2 }],
users: [{ id: 1, username: "eviltrout" }],
meta: {
total_rows_reviewables: 2,
types: {
created_by: "user",
target_created_by: "user"
}
},
__rest_serializer: "1"
});
});
this.get("/review/topics", () => {
return response(200, {
reviewable_topics: [{ id: 1234, title: "Cool topic" }]
});
});
this.get("/review/:id", () => {
return response(200, {
reviewable: flag
});
});
this.put("/review/:id/perform/:actionId", request => {
return response(200, {
reviewable_perform_result: {
success: true,
remove_reviewable_ids: [parseInt(request.params.id)]
}
});
});
this.put("/review/:id", request => {
let result = { payload: {} };
Object.entries(JSON.parse(request.requestBody).reviewable).forEach(t => {
Ember.set(result, t[0], t[1]);
});
return response(200, result);
});
}

View File

@@ -78,32 +78,18 @@ widgetTest("staff menu - admin", {
}
});
widgetTest("queued posts", {
widgetTest("reviewable content", {
template: '{{mount-widget widget="hamburger-menu"}}',
beforeEach() {
this.currentUser.setProperties({
staff: true,
show_queued_posts: true,
post_queue_new_count: 5
reviewable_count: 5
});
},
test(assert) {
assert.ok(find(".queued-posts-link").length);
assert.equal(find(".queued-posts").text(), "5");
}
});
widgetTest("queued posts - disabled", {
template: '{{mount-widget widget="hamburger-menu"}}',
beforeEach() {
this.currentUser.setProperties({ staff: true, show_queued_posts: false });
},
test(assert) {
assert.ok(!find(".queued-posts-link").length);
assert.equal(this.$(".reviewables").text(), "5");
}
});