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,38 +0,0 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor(store, type, findArgs) {
let args = _.merge({ rest_api: true }, findArgs);
delete args.filter;
return `/admin/flags/${findArgs.filter}.json?${$.param(args)}`;
},
afterFindAll(results, helper) {
results.forEach(flag => {
let conversations = [];
flag.post_actions.forEach(pa => {
if (pa.conversation) {
let conversation = {
permalink: pa.permalink,
hasMore: pa.conversation.has_more,
response: {
excerpt: pa.conversation.response.excerpt,
user: helper.lookup("user", pa.conversation.response.user_id)
}
};
if (pa.conversation.reply) {
conversation.reply = {
excerpt: pa.conversation.reply.excerpt,
user: helper.lookup("user", pa.conversation.reply.user_id)
};
}
conversations.push(conversation);
}
});
flag.set("conversations", conversations);
});
return results;
}
});

View File

@ -1,3 +0,0 @@
export default Ember.Component.extend({
classNames: ["flagged-post-response"]
});

View File

@ -1,3 +0,0 @@
export default Ember.Component.extend({
tagName: "h3"
});

View File

@ -1,58 +0,0 @@
import showModal from "discourse/lib/show-modal";
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
adminTools: Ember.inject.service(),
expanded: false,
tagName: "div",
classNameBindings: [
":flagged-post",
"flaggedPost.hidden:hidden-post",
"flaggedPost.deleted"
],
canAct: Ember.computed.alias("actableFilter"),
@computed("filter")
actableFilter(filter) {
return filter === "active";
},
removeAfter(promise) {
return promise.then(() => this.attrs.removePost());
},
_spawnModal(name, model, modalClass) {
let controller = showModal(name, { model, admin: true, modalClass });
controller.removeAfter = p => this.removeAfter(p);
},
actions: {
removeAfter(promise) {
return this.removeAfter(promise);
},
disagree() {
this.removeAfter(this.get("flaggedPost").disagreeFlags());
},
defer() {
this.removeAfter(this.get("flaggedPost").deferFlags());
},
expand() {
this.get("flaggedPost")
.expandHidden()
.then(() => {
this.set("expanded", true);
});
},
showModerationHistory() {
this.get("adminTools").showModerationHistory({
filter: "post",
post_id: this.get("flaggedPost.id")
});
}
}
});

View File

@ -2,6 +2,7 @@ import computed from "ember-addons/ember-computed-decorators";
const ACTIONS = ["delete", "edit", "none"];
export default Ember.Component.extend({
postId: null,
postAction: null,
postEdit: null,

View File

@ -9,7 +9,7 @@ export default Ember.Component.extend({
},
// We do a little logic to choose which icon to display and which text
@computed("user.flags_agreed", "user.flags_disagreed", "user.flags_ignored")
@computed("agreed", "disagreed", "ignored")
percentage(agreed, disagreed, ignored) {
let total = agreed + disagreed + ignored;
let result = { total };

View File

@ -13,7 +13,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
availableGroups: null,
userTitleValue: null,
showApproval: setting("must_approve_users"),
showBadges: setting("enable_badges"),
hasLockedTrustLevel: Ember.computed.notEmpty(
"model.manual_locked_trust_level"
@ -215,9 +214,6 @@ export default Ember.Controller.extend(CanCheckEmails, {
target_user: this.get("model.username")
});
},
showFlagsReceived() {
this.get("adminTools").showFlagsReceived(this.get("model"));
},
showSuspendModal() {
this.get("adminTools").showSuspendModal(this.get("model"));
},

View File

@ -12,31 +12,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
refreshing: false,
listFilter: null,
selectAll: false,
queryNew: Ember.computed.equal("query", "new"),
queryPending: Ember.computed.equal("query", "pending"),
queryHasApproval: Ember.computed.or("queryNew", "queryPending"),
showApproval: Ember.computed.and(
"siteSettings.must_approve_users",
"queryHasApproval"
),
searchHint: i18n("search_hint"),
hasSelection: Ember.computed.gt("selectedCount", 0),
selectedCount: function() {
var model = this.get("model");
if (!model || !model.length) return 0;
return model.filterBy("selected").length;
}.property("model.@each.selected"),
selectAllChanged: function() {
var val = this.get("selectAll");
this.get("model").forEach(function(user) {
if (user.get("can_approve")) {
user.set("selected", val);
}
});
}.observes("selectAll"),
title: function() {
return I18n.t("admin.users.titles." + this.get("query"));
@ -60,34 +36,8 @@ export default Ember.Controller.extend(CanCheckEmails, {
},
actions: {
approveUsers: function() {
AdminUser.bulkApprove(this.get("model").filterBy("selected"));
this._refreshUsers();
},
rejectUsers: function() {
var maxPostAge = this.siteSettings.delete_user_max_post_age;
var controller = this;
AdminUser.bulkReject(this.get("model").filterBy("selected")).then(
function(result) {
var message = I18n.t("admin.users.reject_successful", {
count: result.success
});
if (result.failed > 0) {
message +=
" " +
I18n.t("admin.users.reject_failures", { count: result.failed });
message +=
" " +
I18n.t("admin.user.delete_forbidden", { count: maxPostAge });
}
bootbox.alert(message);
controller._refreshUsers();
}
);
},
toggleEmailVisibility: function() {
toggleEmailVisibility() {
this.toggleProperty("showEmails");
this._refreshUsers();
}

View File

@ -1,17 +0,0 @@
export default Ember.Controller.extend({
loadingFlags: null,
user: null,
onShow() {
this.set("loadingFlags", true);
this.store
.findAll("flagged-post", {
filter: "without_custom",
user_id: this.get("model.id")
})
.then(result => {
this.set("loadingFlags", false);
this.set("flaggedPosts", result);
});
}
});

View File

@ -1,21 +0,0 @@
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Ember.Controller.extend(ModalFunctionality, {
loading: null,
historyTarget: null,
history: null,
onShow() {
this.set("loading", true);
this.set("history", null);
},
loadHistory(target) {
this.store
.findAll("moderation-history", target)
.then(result => {
this.set("history", result);
})
.finally(() => this.set("loading", false));
}
});

View File

@ -29,7 +29,7 @@ export default Ember.Controller.extend(PenaltyController, {
silenced_till: this.get("silenceUntil"),
reason: this.get("reason"),
message: this.get("message"),
post_id: this.get("post.id"),
post_id: this.get("postId"),
post_action: this.get("postAction"),
post_edit: this.get("postEdit")
});

View File

@ -30,7 +30,7 @@ export default Ember.Controller.extend(PenaltyController, {
suspend_until: this.get("suspendUntil"),
reason: this.get("reason"),
message: this.get("message"),
post_id: this.get("post.id"),
post_id: this.get("postId"),
post_action: this.get("postAction"),
post_edit: this.get("postEdit")
});

View File

@ -7,7 +7,7 @@ export default Ember.Mixin.create(ModalFunctionality, {
postEdit: null,
postAction: null,
user: null,
post: null,
postId: null,
successCallback: null,
resetModal() {
@ -15,7 +15,7 @@ export default Ember.Mixin.create(ModalFunctionality, {
reason: null,
message: null,
loadingUser: true,
post: null,
postId: null,
postEdit: null,
postAction: "delete",
before: null,

View File

@ -573,36 +573,6 @@ const AdminUser = Discourse.User.extend({
});
AdminUser.reopenClass({
bulkApprove(users) {
users.forEach(user => {
user.setProperties({
approved: true,
can_approve: false,
selected: false
});
});
return ajax("/admin/users/approve-bulk", {
type: "PUT",
data: { users: users.map(u => u.id) }
}).finally(() => bootbox.alert(I18n.t("admin.user.approve_bulk_success")));
},
bulkReject(users) {
users.forEach(user => {
user.set("can_approve", false);
user.set("selected", false);
});
return ajax("/admin/users/reject-bulk", {
type: "DELETE",
data: {
users: users.map(u => u.id),
context: window.location.pathname
}
});
},
find(user_id) {
return ajax("/admin/users/" + user_id + ".json").then(result => {
result.loadedDetails = true;

View File

@ -1,166 +0,0 @@
import { ajax } from "discourse/lib/ajax";
import Post from "discourse/models/post";
import computed from "ember-addons/ember-computed-decorators";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default Post.extend({
@computed
summary() {
return _(this.post_actions)
.groupBy(function(a) {
return a.post_action_type_id;
})
.map(function(v, k) {
return I18n.t("admin.flags.summary.action_type_" + k, {
count: v.length
});
})
.join(",");
},
@computed("last_revised_at", "post_actions.@each.created_at")
wasEdited(lastRevisedAt) {
if (Ember.isEmpty(this.get("last_revised_at"))) {
return false;
}
lastRevisedAt = Date.parse(lastRevisedAt);
const postActions = this.get("post_actions") || [];
return postActions.some(postAction => {
return Date.parse(postAction.created_at) < lastRevisedAt;
});
},
@computed("post_actions")
hasDisposedBy() {
return this.get("post_actions").some(action => action.disposed_by);
},
@computed("post_actions.@each.name_key")
flaggedForSpam() {
return this.get("post_actions").every(action => action.name_key === "spam");
},
@computed("post_actions.@each.targets_topic")
topicFlagged() {
return _.any(this.get("post_actions"), function(action) {
return action.targets_topic;
});
},
@computed("post_actions.@each.targets_topic")
postAuthorFlagged() {
return _.any(this.get("post_actions"), function(action) {
return !action.targets_topic;
});
},
@computed("flaggedForSpam")
canDeleteAsSpammer(flaggedForSpam) {
return (
flaggedForSpam &&
this.get("user.can_delete_all_posts") &&
this.get("user.can_be_deleted")
);
},
deletePost() {
if (this.get("post_number") === 1) {
return ajax("/t/" + this.topic_id, { type: "DELETE", cache: false });
} else {
return ajax("/posts/" + this.id, { type: "DELETE", cache: false });
}
},
disagreeFlags() {
return ajax("/admin/flags/disagree/" + this.id, {
type: "POST",
cache: false
}).catch(popupAjaxError);
},
deferFlags(deletePost) {
const action = () => {
return ajax("/admin/flags/defer/" + this.id, {
type: "POST",
cache: false,
data: { delete_post: deletePost }
});
};
if (deletePost && this._hasDeletableReplies()) {
return this._actOnFlagAndDeleteReplies(action);
} else {
return action().catch(popupAjaxError);
}
},
agreeFlags(actionOnPost) {
const action = () => {
return ajax("/admin/flags/agree/" + this.id, {
type: "POST",
cache: false,
data: { action_on_post: actionOnPost }
});
};
if (actionOnPost === "delete" && this._hasDeletableReplies()) {
return this._actOnFlagAndDeleteReplies(action);
} else {
return action().catch(popupAjaxError);
}
},
_hasDeletableReplies() {
return this.get("post_number") > 1 && this.get("reply_count") > 0;
},
_actOnFlagAndDeleteReplies(action) {
return new Ember.RSVP.Promise((resolve, reject) => {
return ajax(`/posts/${this.id}/reply-ids/all.json`)
.then(replies => {
const buttons = [];
buttons.push({
label: I18n.t("no_value"),
callback() {
action()
.then(resolve)
.catch(error => {
popupAjaxError(error);
reject();
});
}
});
buttons.push({
label: I18n.t("yes_value"),
class: "btn-danger",
callback() {
Post.deleteMany(replies.map(r => r.id), {
agreeWithFirstReplyFlag: false
})
.then(action)
.then(resolve)
.catch(error => {
popupAjaxError(error);
reject();
});
}
});
bootbox.dialog(
I18n.t("admin.flags.delete_replies", { count: replies.length }),
buttons
);
})
.catch(error => {
popupAjaxError(error);
reject();
});
});
},
postHidden: Ember.computed.alias("hidden"),
deleted: Ember.computed.or("deleted_at", "topic_deleted_at")
});

View File

@ -1,8 +0,0 @@
export default Discourse.Route.extend({
redirect() {
let segment = this.siteSettings.flags_default_topics
? "topics"
: "postsActive";
this.replaceWith(`adminFlags.${segment}`);
}
});

View File

@ -1,20 +0,0 @@
import { loadTopicView } from "discourse/models/topic";
export default Ember.Route.extend({
model(params) {
let topicRecord = this.store.createRecord("topic", { id: params.id });
let topic = loadTopicView(topicRecord).then(() => topicRecord);
return Ember.RSVP.hash({
topic,
flaggedPosts: this.store.findAll("flagged-post", {
filter: "active",
topic_id: params.id
})
});
},
setupController(controller, hash) {
controller.setProperties(hash);
}
});

View File

@ -120,18 +120,6 @@ export default function() {
}
);
this.route(
"adminFlags",
{ path: "/flags", resetNamespace: true },
function() {
this.route("postsActive", { path: "active" });
this.route("postsOld", { path: "old" });
this.route("topics", { path: "topics" }, function() {
this.route("show", { path: ":id" });
});
}
);
this.route(
"adminLogs",
{ path: "/logs", resetNamespace: true },

View File

@ -26,10 +26,6 @@ export default Ember.Service.extend({
});
},
showFlagsReceived(user) {
showModal(`admin-flags-received`, { admin: true, model: user });
},
checkSpammer(userId) {
return AdminUser.find(userId).then(au => this.spammerDetails(au));
},
@ -53,12 +49,7 @@ export default Ember.Service.extend({
admin: true,
modalClass: `${type}-user-modal`
});
if (opts.post) {
controller.setProperties({
post: opts.post,
postEdit: opts.post.get("raw")
});
}
controller.setProperties({ postId: opts.postId, postEdit: opts.postEdit });
return (user.adminUserView
? Ember.RSVP.resolve(user)
@ -81,11 +72,6 @@ export default Ember.Service.extend({
this._showControlModal("suspend", user, opts);
},
showModerationHistory(target) {
let controller = showModal("admin-moderation-history", { admin: true });
controller.loadHistory(target);
},
_deleteSpammer(adminUser) {
// Try loading the email if the site supports it
let tryEmail = this.siteSettings.moderators_view_emails

View File

@ -19,7 +19,6 @@
{{#if currentUser.admin}}
{{nav-item route='adminEmail' label='admin.email.title'}}
{{/if}}
{{nav-item route='adminFlags' label='admin.flags.title'}}
{{nav-item route='adminLogs' label='admin.logs.title'}}
{{#if currentUser.admin}}
{{nav-item route='adminCustomize' label='admin.customize.title'}}

View File

@ -8,7 +8,10 @@
<div class='flagger-flag-type'>
{{post-action-title postAction.post_action_type_id postAction.name_key}}
</div>
{{user-flag-percentage user=postAction.user}}
{{user-flag-percentage
agreed=postAction.user.flags_agreed
disagreed=postAction.user.flags_disagreed
ignored=postAction.user.flags_ignored}}
{{/flag-user}}
{{/each}}
</div>

View File

@ -1,7 +0,0 @@
{{#link-to 'adminUser' response.user.id response.user.username class="response-avatar"}}
{{avatar response.user imageSize="small"}}
{{/link-to}}
<div class='excerpt'>{{{response.excerpt}}}</div>
{{#if hasMore}}
<a href={{permalink}} class="has-more">{{i18n 'admin.flags.more'}}</a>
{{/if}}

View File

@ -1,8 +0,0 @@
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>
{{#if flaggedPost.reply_count}}
<span class="flagged-post-reply-count">{{i18n 'admin.flags.replies' count=flaggedPost.reply_count}}</span>
{{/if}}

View File

@ -1,116 +0,0 @@
<div class='flagged-post-details'>
<div class="flagged-post-avatar">
{{#if flaggedPost.postAuthorFlagged}}
{{#if flaggedPost.user}}
{{#link-to 'adminUser' flaggedPost.user.id flaggedPost.user.username}}
{{avatar flaggedPost.user imageSize="large"}}
{{/link-to}}
{{#if flaggedPost.wasEdited}}
<div class='edited-after'>
{{d-icon "pencil-alt" title="admin.flags.was_edited"}}
</div>
{{/if}}
{{/if}}
{{/if}}
{{#if canAct}}
{{#if flaggedPost.previous_flags_count}}
<span title="{{i18n 'admin.flags.previous_flags_count' count=flaggedPost.previous_flags_count}}" class="badge-notification previous-flagged-posts">{{flaggedPost.previous_flags_count}}</span>
{{/if}}
{{/if}}
</div>
<div class="flagged-post-contents">
<div class='flagged-post-user-details'>
<a class='username' href={{user.path}} data-user-card={{flaggedPost.user.username}}>{{format-username flaggedPost.user.username}}</a>
{{plugin-outlet
name="flagged-post-controls"
tagName=""
args=(hash flaggedPost=flaggedPost actableFilter=actableFilter topic=topic)}}
</div>
<div class='flagged-post-excerpt'>
{{#unless hideTitle}}
{{flagged-post-title flaggedPost=flaggedPost}}
{{/unless}}
{{#if flaggedPost.postAuthorFlagged}}
{{#if expanded}}
{{{flaggedPost.cooked}}}
{{else}}
<p>
{{{flaggedPost.excerpt}}}
<a href {{action "expand"}}>{{i18n "admin.flags.show_full"}}</a>
</p>
{{/if}}
{{/if}}
</div>
{{#if flaggedPost.topicFlagged}}
<div class='flagged-post-message'>
<span class='text'>{{{i18n 'admin.flags.topic_flagged'}}}</span>
<a href={{flaggedPost.url}} class="btn">{{i18n 'admin.flags.visit_topic'}}</a>
</div>
{{/if}}
{{#each flaggedPost.conversations as |c|}}
<div class='flag-conversation'>
{{#if c.response}}
{{flagged-post-response response=c.response}}
{{#if c.reply}}
{{flagged-post-response response=c.reply hasMore=c.hasMore permalink=c.permalink}}
{{/if}}
<a href={{c.permalink}} class="btn reply-conversation btn-small">
{{d-icon "reply"}}
{{i18n "admin.flags.reply_message"}}
</a>
{{/if}}
</div>
{{/each}}
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=showResolvedBy}}
<div class='flagged-post-controls'>
{{#if canAct}}
{{admin-agree-flag-dropdown
post=flaggedPost
removeAfter=(action "removeAfter") }}
{{#if flaggedPost.postHidden}}
{{d-button
title="admin.flags.disagree_flag_unhide_post_title"
class="btn-default disagree-flag"
action=(action "disagree")
icon="thumbs-o-down"
label="admin.flags.disagree_flag_unhide_post"}}
{{else}}
{{d-button
title="admin.flags.disagree_flag_title"
class="btn-default disagree-flag"
action=(action "disagree")
icon="thumbs-o-down"
label="admin.flags.disagree_flag"}}
{{/if}}
{{d-button
class="btn-default defer-flag"
title="admin.flags.ignore_flag_title"
action=(action "defer")
icon="external-link-alt"
label="admin.flags.ignore_flag"}}
{{admin-delete-flag-dropdown
post=flaggedPost
removeAfter=(action "removeAfter")}}
{{/if}}
{{d-button
class="btn-default"
icon="list"
label="admin.flags.moderation_history"
action=(action "showModerationHistory")}}
</div>
{{plugin-outlet
name="flagged-post-below-controls"
tagName=""
args=(hash flaggedPost=flaggedPost canAct=canAct actableFilter=actableFilter)}}
</div>
</div>

View File

@ -1,17 +0,0 @@
{{#if flaggedPosts}}
{{#load-more selector=".flagged-post" action=(action "loadMore")}}
<div class='flagged-posts'>
{{#each flaggedPosts as |flaggedPost|}}
{{flagged-post
flaggedPost=flaggedPost
filter=filter
topic=topic
showResolvedBy=showResolvedBy
removePost=(action "removePost" flaggedPost)
hideTitle=topic}}
{{/each}}
</div>
{{/load-more}}
{{else}}
<p>{{i18n 'admin.flags.no_results'}}</p>
{{/if}}

View File

@ -1,5 +0,0 @@
{{#each users as |u|}}
{{#link-to 'adminUser' u.id u.username class="flagged-topic-user"}}
{{avatar u imageSize="small"}}
{{/link-to}}
{{/each}}

View File

@ -1,17 +0,0 @@
<td class='date'>
{{format-date item.created_at}}
</td>
<td class='history-item-action'>
<div class='action-name'>
{{i18n (concat "admin.moderation_history.actions." item.action_name)}}
</div>
<div class='action-details'>{{item.details}}</div>
</td>
<td class='history-item-actor'>
{{#if item.acting_user}}
{{#user-link user=item.acting_user}}
{{avatar item.acting_user imageSize="small"}}
<span>{{format-username item.acting_user.username}}</span>
{{/user-link}}
{{/if}}
</td>

View File

@ -1 +0,0 @@
{{flagged-posts flaggedPosts=model filter="active"}}

View File

@ -1 +0,0 @@
{{flagged-posts flaggedPosts=model filter="old"}}

View File

@ -1,54 +0,0 @@
{{plugin-outlet name="flagged-topics-before" noTags=true args=(hash flaggedTopics=flaggedTopics)}}
{{#if flaggedTopics}}
<table class='flagged-topics grid'>
<thead>
{{plugin-outlet name="flagged-topic-header-row" noTags=true}}
<th>{{i18n "admin.flags.flagged_topics.topic"}} </th>
<th>{{i18n "admin.flags.flagged_topics.type"}}</th>
<th>{{I18n "admin.flags.flagged_topics.users"}}</th>
<th>{{i18n "admin.flags.flagged_topics.last_flagged"}}</th>
<th></th>
</thead>
<tbody>
{{#each flaggedTopics as |ft|}}
<tr class='flagged-topic'>
{{plugin-outlet name="flagged-topic-row" noTags=true args=(hash topic=ft.topic)}}
<td class="topic-title">
<div class='combined-title'>
{{topic-status topic=ft.topic}}
<a href={{ft.topic.relative_url}} target="_blank">{{replace-emoji ft.topic.fancy_title}}</a>
</div>
</td>
<td class="flag-counts">
{{#each ft.flag_counts as |fc|}}
<div class='flag-counts'>
<span class='type-name'>{{post-action-title fc.post_action_type_id fc.name_key}}</span>
<span class='type-count'>x{{fc.count}}</span>
</div>
{{/each}}
</td>
<td class='flagged-topic-users'>
{{flagged-topic-users users=ft.users tagName=""}}
</td>
<td class="last-flagged">
{{format-age ft.last_flag_at}}
</td>
<td class="flag-details">
{{#link-to
"adminFlags.topics.show"
ft.id
class="btn d-button no-text btn-small btn-primary show-details"
title=(i18n "admin.flags.show_details")}}
{{d-icon "list"}}
{{i18n "admin.flags.details"}}
{{/link-to}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
{{i18n "admin.flags.flagged_topics.no_results"}}
{{/if}}

View File

@ -1,19 +0,0 @@
<div class='flagged-topic-details'>
<div class='topic-title'>
<h1>
{{topic-status topic=topic}}
{{#link-to 'topic' topic target="_blank"}}
{{{topic.fancyTitle}}}
{{/link-to}}
</h1>
</div>
{{plugin-outlet name="flagged-topic-details-header" args=(hash topic=topic)}}
</div>
<div class='topic-flags'>
{{flagged-posts
flaggedPosts=flaggedPosts
filter="active"
topic=topic}}
</div>

View File

@ -1,15 +0,0 @@
{{#admin-nav}}
{{#if siteSettings.flags_default_topics}}
{{nav-item route='adminFlags.topics' label='admin.flags.topics'}}
{{nav-item route='adminFlags.postsActive' label='admin.flags.active_posts'}}
{{else}}
{{nav-item route='adminFlags.postsActive' label='admin.flags.active_posts'}}
{{nav-item route='adminFlags.topics' label='admin.flags.topics'}}
{{/if}}
{{nav-item route='adminFlags.postsOld' label='admin.flags.old_posts' class='right'}}
{{/admin-nav}}
<div class="admin-container">
{{outlet}}
</div>

View File

@ -1,14 +0,0 @@
{{#d-modal-body rawTitle=(i18n "admin.user.flags_received_by" username=model.username)}}
{{#conditional-loading-spinner condition=loadingFlags}}
{{#each flaggedPosts as |flaggedPost|}}
<div class='received-flag flagged-post'>
<div class='flagged-post-excerpt'>
{{flagged-post-title flaggedPost=flaggedPost}}
</div>
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=flaggedPost.hasDisposedBy}}
</div>
{{else}}
{{i18n "admin.user.flags_received_none"}}
{{/each}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}

View File

@ -1,23 +0,0 @@
{{#d-modal-body title="admin.flags.moderation_history"}}
{{#conditional-loading-spinner condition=loading}}
{{#if history}}
<table class='moderation-history'>
<tr>
<th>{{i18n "admin.logs.created_at"}}</th>
<th>{{i18n "admin.logs.action"}}</th>
<th>{{i18n "admin.moderation_history.performed_by"}}</th>
</tr>
{{#each history as |item|}}
{{moderation-history-item item=item}}
{{/each}}
</table>
{{else}}
<div class='no-results'>
{{i18n "admin.moderation_history.no_results"}}
</div>
{{/if}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action=(route-action "closeModal") label="close"}}
</div>

View File

@ -13,9 +13,9 @@
</div>
{{silence-details reason=reason message=message}}
{{#if post}}
{{#if postId}}
{{penalty-post-action
post=post
postId=postId
postAction=postAction
postEdit=postEdit}}
{{/if}}

View File

@ -14,9 +14,9 @@
</div>
{{suspension-details reason=reason message=message}}
{{#if post}}
{{#if postId}}
{{penalty-post-action
post=post
postId=postId
postAction=postAction
postEdit=postEdit}}
{{/if}}

View File

@ -221,7 +221,7 @@
<section class="details">
<h1>{{i18n "admin.user.permissions"}}</h1>
{{#if showApproval}}
{{#if siteSettings.must_approve_users}}
<div class="display-row">
<div class="field">{{i18n "admin.users.approved"}}</div>
<div class="value">
@ -614,12 +614,9 @@
</div>
<div class="controls">
{{#if model.flags_received_count}}
{{d-button
class="btn-default"
action=(action "showFlagsReceived")
label="admin.user.show_flags_received"
icon="flag"
}}
{{#link-to 'review' (query-params username=model.username type="ReviewableFlaggedPost" status="all") class="btn"}}
{{i18n "admin.user.show_flags_received"}}
{{/link-to}}
{{/if}}
</div>
</div>

View File

@ -1,10 +1,3 @@
{{#if hasSelection}}
<div id='selected-controls'>
<button {{action "approveUsers"}} class='btn'>{{count-i18n key="admin.users.approved_selected" count=selectedCount}}</button>
<button {{action "rejectUsers"}} class='btn btn-danger'>{{count-i18n key="admin.users.reject_selected" count=selectedCount}}</button>
</div>
{{/if}}
<div class="admin-title">
<h2>{{title}}</h2>
{{#if canCheckEmails}}
@ -24,9 +17,6 @@
{{#if model}}
<table class='table users-list grid'>
<thead>
{{#if showApproval}}
<th>{{input type="checkbox" checked=selectAll}}</th>
{{/if}}
{{admin-directory-toggle field="username" i18nKey='username' order=order ascending=ascending}}
{{admin-directory-toggle field="email" i18nKey='email' order=order ascending=ascending}}
{{admin-directory-toggle field="last_emailed" i18nKey='admin.users.last_emailed' order=order ascending=ascending}}
@ -35,7 +25,7 @@
{{admin-directory-toggle field="posts_read" i18nKey="admin.user.posts_read_count" order=order ascending=ascending}}
{{admin-directory-toggle field="read_time" i18nKey="admin.user.time_read" order=order ascending=ascending}}
{{admin-directory-toggle field="created" i18nKey="created" order=order ascending=ascending}}
{{#if showApproval}}
{{#if siteSettings.must_approve_users}}
<th>{{i18n 'admin.users.approved'}}</th>
{{/if}}
<th>&nbsp;</th>
@ -43,13 +33,6 @@
<tbody>
{{#each model as |user|}}
<tr class="user {{user.selected}} {{unless user.active 'not-activated'}}">
{{#if showApproval}}
<td class="approval">
{{#if user.can_approve}}
{{input type="checkbox" checked=user.selected}}
{{/if}}
</td>
{{/if}}
<td class="username">
<a href="{{unbound user.path}}" data-user-card="{{unbound user.username}}">
{{avatar user imageSize="small"}}
@ -88,15 +71,10 @@
<div>{{{format-duration user.created_at_age}}}</div>
</td>
{{#if showApproval}}
<td>
{{#if user.approved}}
{{i18n 'yes_value'}}
{{else}}
{{i18n 'no_value'}}
{{/if}}
</td>
{{#if siteSettings.must_approve_users}}
<td>{{i18n-yes-no user.approved}}</td>
{{/if}}
<td class="user-status">
{{#if user.admin}}
{{d-icon "shield-alt" title="admin.title" }}

View File

@ -3,9 +3,6 @@
<ul class="nav nav-pills">
{{nav-item route='adminUsersList.show' routeParam='active' label='admin.users.nav.active'}}
{{nav-item route='adminUsersList.show' routeParam='new' label='admin.users.nav.new'}}
{{#if siteSettings.must_approve_users}}
{{nav-item route='adminUsersList.show' routeParam='pending' label='admin.users.nav.pending'}}
{{/if}}
{{nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}}
{{nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}}
{{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}}

View File

@ -7,8 +7,7 @@ const ADMIN_MODELS = [
"embeddable-host",
"web-hook",
"web-hook-event",
"flagged-topic",
"moderation-history"
"flagged-topic"
];
export function Result(payload, responseJson) {

View File

@ -0,0 +1,7 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
pathFor() {
return "/review/topics";
}
});

View File

@ -0,0 +1,9 @@
import RestAdapter from "discourse/adapters/rest";
export default RestAdapter.extend({
jsonMode: true,
pathFor(store, type, findArgs) {
return this.appendQueryParams("/review", findArgs);
}
});

View File

@ -1,18 +0,0 @@
export default Ember.Component.extend({
canAct: Ember.computed.equal("filter", "active"),
showResolvedBy: Ember.computed.equal("filter", "old"),
allLoaded: false,
actions: {
removePost(flaggedPost) {
this.get("flaggedPosts").removeObject(flaggedPost);
},
loadMore() {
const flaggedPosts = this.get("flaggedPosts");
if (flaggedPosts.get("canLoadMore")) {
flaggedPosts.loadMore();
}
}
}
});

View File

@ -1,114 +0,0 @@
import { propertyEqual } from "discourse/lib/computed";
import { bufferedProperty } from "discourse/mixins/buffered-content";
import { popupAjaxError } from "discourse/lib/ajax-error";
function updateState(state, opts) {
opts = opts || {};
return function() {
const post = this.get("post");
const args = { state };
if (opts.deleteUser) {
args.delete_user = true;
}
post
.update(args)
.then(() => {
this.removePost(post);
})
.catch(popupAjaxError);
};
}
export default Ember.Component.extend(bufferedProperty("editables"), {
editing: propertyEqual("post", "currentlyEditing"),
editables: null,
_confirmDelete: updateState("rejected", { deleteUser: true }),
_initEditables: function() {
const post = this.get("post");
const postOptions = post.get("post_options");
this.set("editables", {});
this.set("editables.raw", post.get("raw"));
this.set("editables.category", post.get("category"));
this.set("editables.category_id", post.get("category.id"));
this.set("editables.title", postOptions.title);
this.set("editables.tags", postOptions.tags);
}.on("init"),
_categoryChanged: function() {
this.set(
"buffered.category",
Discourse.Category.findById(this.get("buffered.category_id"))
);
}.observes("buffered.category_id"),
editTitleAndCategory: function() {
return this.get("editing") && !this.get("post.topic");
}.property("editing"),
tags: function() {
return this.get("editables.tags") || this.get("post.topic.tags") || [];
}.property("editables.tags"),
showTags: function() {
return (
this.siteSettings.tagging_enabled &&
!this.get("editing") &&
this.get("tags").length > 0
);
}.property("editing", "tags"),
editTags: function() {
return (
this.siteSettings.tagging_enabled &&
this.get("editing") &&
!this.get("post.topic")
);
}.property("editing"),
actions: {
approve: updateState("approved"),
reject: updateState("rejected"),
deleteUser() {
bootbox.confirm(
I18n.t("queue.delete_prompt", {
username: this.get("post.user.username")
}),
confirmed => {
if (confirmed) {
this._confirmDelete();
}
}
);
},
edit() {
// This is stupid but pagedown cannot be on the screen twice or it will break
this.set("currentlyEditing", null);
Ember.run.scheduleOnce("afterRender", () =>
this.set("currentlyEditing", this.get("post"))
);
},
confirmEdit() {
const buffered = this.get("buffered");
this.get("post")
.update(buffered.getProperties("raw", "title", "tags", "category_id"))
.then(() => {
this.commitBuffer();
this.set("currentlyEditing", null);
});
},
cancelEdit() {
this.rollbackBuffer();
this.set("currentlyEditing", null);
}
}
});

View File

@ -0,0 +1,18 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Component.extend({
tagName: "",
multiple: Ember.computed.gt("bundle.actions.length", 1),
first: Ember.computed.alias("bundle.actions.firstObject"),
actions: {
performById(id) {
this.attrs.performAction(this.get("bundle.actions").findBy("id", id));
},
perform(action) {
this.attrs.performAction(action);
}
}
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
filteredHistories: Ember.computed.filterBy("histories", "created", false)
});

View File

@ -0,0 +1,153 @@
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import computed from "ember-addons/ember-computed-decorators";
import Category from "discourse/models/category";
import optionalService from "discourse/lib/optional-service";
let _components = {};
export default Ember.Component.extend({
adminTools: optionalService(),
tagName: "",
updating: null,
editing: false,
_updates: null,
@computed("reviewable.type")
customClass(type) {
return type.dasherize();
},
// Find a component to render, if one exists. For example:
// `ReviewableUser` will return `reviewable-user`
@computed("reviewable.type")
reviewableComponent(type) {
if (_components[type] !== undefined) {
return _components[type];
}
let dasherized = Ember.String.dasherize(type);
let templatePath = `components/${dasherized}`;
let template =
Ember.TEMPLATES[`${templatePath}`] ||
Ember.TEMPLATES[`javascripts/${templatePath}`];
_components[type] = template ? dasherized : null;
return _components[type];
},
_performConfirmed(action) {
let reviewable = this.get("reviewable");
let performAction = () => {
let version = reviewable.get("version");
this.set("updating", true);
return ajax(
`/review/${reviewable.id}/perform/${action.id}?version=${version}`,
{
method: "PUT"
}
)
.then(result => {
this.attrs.remove(
result.reviewable_perform_result.remove_reviewable_ids
);
})
.catch(popupAjaxError)
.finally(() => this.set("updating", false));
};
if (action.client_action) {
let actionMethod = this[`client${action.client_action.classify()}`];
if (actionMethod) {
return actionMethod.call(this, reviewable, performAction);
} else {
// eslint-disable-next-line no-console
console.error(`No handler for ${action.client_action} found`);
return;
}
return;
} else {
return performAction();
}
},
clientSuspend(reviewable, performAction) {
this._penalize("showSuspendModal", reviewable, performAction);
},
clientSilence(reviewable, performAction) {
this._penalize("showSilenceModal", reviewable, performAction);
},
_penalize(adminToolMethod, reviewable, performAction) {
let adminTools = this.get("adminTools");
if (adminTools) {
let createdBy = reviewable.get("target_created_by");
let postId = reviewable.get("post_id");
let postEdit = reviewable.get("raw");
return adminTools[adminToolMethod](createdBy, {
postId,
postEdit,
before: performAction
});
}
},
actions: {
edit() {
this.set("editing", true);
this._updates = { payload: {} };
},
cancelEdit() {
this.set("editing", false);
},
saveEdit() {
let updates = this._updates;
// Remove empty objects
Object.keys(updates).forEach(name => {
let attr = updates[name];
if (typeof attr === "object" && Object.keys(attr).length === 0) {
delete updates[name];
}
});
this.set("updating", true);
return this.get("reviewable")
.update(updates)
.then(() => this.set("editing", false))
.catch(popupAjaxError)
.finally(() => this.set("updating", false));
},
categoryChanged(category) {
if (!category) {
category = Category.findUncategorized();
}
this._updates.category_id = category.id;
},
valueChanged(fieldId, event) {
Ember.set(this._updates, fieldId, event.target.value);
},
perform(action) {
if (this.get("updating")) {
return;
}
let msg = action.get("confirm_message");
if (msg) {
bootbox.confirm(msg, answer => {
if (answer) {
return this._performConfirmed(action);
}
});
} else {
return this._performConfirmed(action);
}
}
}
});

View File

@ -3,7 +3,6 @@ import MountWidget from "discourse/components/mount-widget";
import { cloak, uncloak } from "discourse/widgets/post-stream";
import { isWorkaroundActive } from "discourse/lib/safari-hacks";
import offsetCalculator from "discourse/lib/offset-calculator";
import optionalService from "discourse/lib/optional-service";
function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
if (max < min) {
@ -26,7 +25,6 @@ function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
}
export default MountWidget.extend({
adminTools: optionalService(),
widget: "post-stream",
_topVisible: null,
_bottomVisible: null,
@ -329,12 +327,5 @@ export default MountWidget.extend({
this.$().off("mouseleave.post-stream");
this.appEvents.off("post-stream:refresh", this, "_refresh");
this.appEvents.off("post-stream:posted", this, "_posted");
},
showModerationHistory(post) {
this.get("adminTools").showModerationHistory({
filter: "post",
post_id: post.id
});
}
});

View File

@ -423,8 +423,7 @@ function applyFlaggedProperties() {
});
}
addFlagProperty("currentUser.site_flagged_posts_count");
addFlagProperty("currentUser.post_queue_new_count");
addFlagProperty("currentUser.reviewable_count");
export { addFlagProperty, applyFlaggedProperties };

View File

@ -1,20 +1,11 @@
import MountWidget from "discourse/components/mount-widget";
import optionalService from "discourse/lib/optional-service";
export default MountWidget.extend({
classNames: "topic-admin-menu-button-container",
tagName: "span",
widget: "topic-admin-menu-button",
adminTools: optionalService(),
buildArgs() {
return this.getProperties("topic", "fixed", "openUpwards", "rightSide");
},
showModerationHistory() {
this.get("adminTools").showModerationHistory({
filter: "topic",
topic_id: this.get("topic.id")
});
}
});

View File

@ -93,12 +93,5 @@ export default MountWidget.extend(Docking, {
}
this.dispatch("topic:current-post-scrolled", "timeline-scrollarea");
},
showModerationHistory() {
this.get("adminTools").showModerationHistory({
filter: "topic",
topic_id: this.get("topic.id")
});
}
});

View File

@ -56,9 +56,11 @@ export default Ember.Component.extend(LoadMore, {
actions: {
removeBookmark(userAction) {
const stream = this.get("stream");
Post.updateBookmark(userAction.get("post_id"), false).then(() => {
stream.remove(userAction);
});
Post.updateBookmark(userAction.get("post_id"), false)
.then(() => {
stream.remove(userAction);
})
.catch(popupAjaxError);
},
resumeDraft(item) {

View File

@ -1,9 +0,0 @@
import { addFlagProperty as realAddFlagProperty } from "discourse/components/site-header";
import deprecated from "discourse-common/lib/deprecated";
export function addFlagProperty(prop) {
deprecated(
"importing `addFlagProperty` is deprecated. Use the PluginAPI instead"
);
realAddFlagProperty(prop);
}

View File

@ -1,8 +0,0 @@
export default Ember.Controller.extend({
description: Ember.computed("model.reason", function() {
const reason = this.get("model.reason");
return reason
? I18n.t("queue_reason." + reason + ".description")
: I18n.t("queue.approval.description");
})
});

View File

@ -0,0 +1,98 @@
import computed from "ember-addons/ember-computed-decorators";
export default Ember.Controller.extend({
queryParams: [
"min_score",
"type",
"status",
"category_id",
"topic_id",
"username"
],
type: null,
status: "pending",
min_score: null,
category_id: null,
reviewables: null,
topic_id: null,
filtersExpanded: false,
username: "",
init(...args) {
this._super(...args);
this.set("min_score", this.siteSettings.min_score_default_visibility);
this.set("filtersExpanded", !this.site.mobileView);
},
@computed("reviewableTypes")
allTypes() {
return (this.get("reviewableTypes") || []).map(type => {
return {
id: type,
name: I18n.t(`review.types.${type.underscore()}.title`)
};
});
},
@computed
statuses() {
return [
"pending",
"approved",
"rejected",
"ignored",
"reviewed",
"all"
].map(id => {
return { id, name: I18n.t(`review.statuses.${id}.title`) };
});
},
@computed("filtersExpanded")
toggleFiltersIcon(filtersExpanded) {
return filtersExpanded ? "chevron-up" : "chevron-down";
},
actions: {
remove(ids) {
if (!ids) {
return;
}
let newList = this.get("reviewables").reject(reviewable => {
return ids.indexOf(reviewable.id) !== -1;
});
this.set("reviewables", newList);
},
resetTopic() {
this.set("topic_id", null);
this.send("refreshRoute");
},
refresh() {
// If filterScore is blank use the default
let filterScore = this.get("filterScore");
if (!filterScore || filterScore.length === 0) {
filterScore = this.siteSettings.min_score_default_visibility;
}
this.setProperties({
type: this.get("filterType"),
min_score: filterScore,
status: this.get("filterStatus"),
category_id: this.get("filterCategoryId"),
username: this.get("filterUsername")
});
this.send("refreshRoute");
},
loadMore() {
return this.get("reviewables").loadMore();
},
toggleFilters() {
this.toggleProperty("filtersExpanded");
}
}
});

View File

@ -0,0 +1,5 @@
function dasherize([value]) {
return (value || "").replace(".", "-").dasherize();
}
export default Ember.Helper.helper(dasherize);

View File

@ -0,0 +1,17 @@
export function formatCurrency([reviewable, fieldId]) {
// The field `category_id` corresponds to `category`
if (fieldId === "category_id") {
fieldId = "category.id";
}
let value = Ember.get(reviewable, fieldId);
// If it's an array, say tags, make a copy so we aren't mutating the original
if (Array.isArray(value)) {
value = value.slice(0);
}
return value;
}
export default Ember.Helper.helper(formatCurrency);

View File

@ -0,0 +1,5 @@
import { registerUnbound } from "discourse-common/lib/helpers";
registerUnbound("format-score", function(score) {
return I18n.toNumber(score || 0, { precision: 1 });
});

View File

@ -0,0 +1,12 @@
import { htmlHelper } from "discourse-common/lib/helpers";
import { htmlStatus } from "discourse/helpers/reviewable-status";
import { EDITED } from "discourse/models/reviewable-history";
export default htmlHelper(function(rh) {
switch (rh.reviewable_history_type) {
case EDITED:
return I18n.t("review.history.edited");
default:
return htmlStatus(rh.status);
}
});

View File

@ -0,0 +1,31 @@
import { htmlHelper } from "discourse-common/lib/helpers";
import { iconHTML } from "discourse-common/lib/icon-library";
import {
PENDING,
APPROVED,
REJECTED,
IGNORED,
DELETED
} from "discourse/models/reviewable";
export function htmlStatus(status) {
switch (status) {
case PENDING:
return I18n.t("review.statuses.pending.title");
case APPROVED:
return `${iconHTML("check")} ${I18n.t("review.statuses.approved.title")}`;
case REJECTED:
return `${iconHTML("times")} ${I18n.t("review.statuses.rejected.title")}`;
case IGNORED:
return `${iconHTML("external-link-alt")} ${I18n.t(
"review.statuses.ignored.title"
)}`;
case DELETED:
return `${iconHTML("trash")} ${I18n.t("review.statuses.deleted.title")}`;
}
}
export default htmlHelper(status => {
return htmlStatus(status);
});

View File

@ -21,18 +21,12 @@ export default {
const appEvents = container.lookup("app-events:main");
if (user) {
if (user.get("staff")) {
bus.subscribe("/flagged_counts", data => {
user.set("site_flagged_posts_count", data.total);
});
bus.subscribe("/queue_counts", data => {
user.set("post_queue_new_count", data.post_queue_new_count);
if (data.post_queue_new_count > 0) {
user.set("show_queued_posts", 1);
}
});
}
bus.subscribe("/reviewable_counts", data => {
user.set("reviewable_count", data.reviewable_count);
if (data.reviewable_count > 0) {
user.set("show_reviewables", 1);
}
});
bus.subscribe(
`/notification/${user.get("id")}`,
data => {

View File

@ -10,6 +10,7 @@ export default Ember.ArrayProxy.extend({
findArgs: null,
store: null,
__type: null,
resultSetMeta: null,
canLoadMore: function() {
return this.get("length") < this.get("totalRows");

View File

@ -0,0 +1,9 @@
import RestModel from "discourse/models/rest";
export const CREATED = 0;
export const TRANSITIONED_TO = 1;
export const EDITED = 2;
export default RestModel.extend({
created: Ember.computed.equal("reviewable_history_type", CREATED)
});

View File

@ -0,0 +1,50 @@
import { ajax } from "discourse/lib/ajax";
import RestModel from "discourse/models/rest";
import computed from "ember-addons/ember-computed-decorators";
import Category from "discourse/models/category";
export const PENDING = 0;
export const APPROVED = 1;
export const REJECTED = 2;
export const IGNORED = 3;
export const DELETED = 4;
export default RestModel.extend({
pending: Ember.computed.equal("status", PENDING),
approved: Ember.computed.equal("status", APPROVED),
rejected: Ember.computed.equal("status", REJECTED),
ignored: Ember.computed.equal("status", IGNORED),
@computed("type")
humanType(type) {
return I18n.t(`review.types.${type.underscore()}.title`, {
defaultValue: ""
});
},
update(updates) {
// If no changes, do nothing
if (Object.keys(updates).length === 0) {
return Ember.RSVP.resolve();
}
let adapter = this.store.adapterFor("reviewable");
return ajax(
`/review/${this.get("id")}?version=${this.get("version")}`,
adapter.getPayload("PUT", { reviewable: updates })
).then(updated => {
updated.payload = Object.assign(
{},
this.get("payload") || {},
updated.payload || {}
);
if (updated.category_id) {
updated.category = Category.findById(updated.category_id);
delete updated.category_id;
}
this.setProperties(updated);
});
}
});

View File

@ -48,7 +48,7 @@ export default Ember.Object.extend({
_plurals: {
"post-reply": "post-replies",
"post-reply-history": "post_reply_histories",
"moderation-history": "moderation_history"
reviewable_history: "reviewable_histories"
},
init() {
@ -223,6 +223,7 @@ export default Ember.Object.extend({
totalRows: pageTarget["total_rows_" + typeName] || content.length,
loadMoreUrl: pageTarget["load_more_" + typeName],
refreshUrl: pageTarget["refresh_" + typeName],
resultSetMeta: result.meta,
store: this,
__type: type
};
@ -328,8 +329,6 @@ export default Ember.Object.extend({
root = root || obj;
// Experimental: If serialized with a certain option we'll wire up embedded objects
// automatically.
if (root.__rest_serializer === "1") {
this._hydrateEmbedded(type, obj, root);
}

View File

@ -126,7 +126,6 @@ export default function() {
);
this.route("badges");
this.route("flaggedPosts", { path: "/flagged-posts" });
this.route("deletedPosts", { path: "/deleted-posts" });
this.route(
@ -170,6 +169,11 @@ export default function() {
}
);
this.route("review", { path: "/review" }, function() {
this.route("show", { path: "/:reviewable_id" });
this.route("index", { path: "/" });
this.route("topics", { path: "/topics" });
});
this.route("signup", { path: "/signup" });
this.route("login", { path: "/login" });
this.route("login-preferences");
@ -188,8 +192,6 @@ export default function() {
this.route("show", { path: "/:id/:slug" });
});
this.route("queued-posts", { path: "/queued-posts", resetNamespace: true });
this.route("full-page-search", { path: "/search" });
this.route("tags", { resetNamespace: true }, function() {

View File

@ -52,10 +52,10 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
},
postWasEnqueued(details) {
const title = details.reason
? "queue_reason." + details.reason + ".title"
: "queue.approval.title";
showModal("post-enqueued", { model: details, title });
showModal("post-enqueued", {
model: details,
title: "review.approval.title"
});
},
composePrivateMessage(user, post) {

View File

@ -1,17 +0,0 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
model() {
return this.store.find("queuedPost", { status: "new" });
},
actions: {
removePost(post) {
this.modelFor("queued-posts").removeObject(post);
},
refresh() {
this.modelFor("queued-posts").refresh();
}
}
});

View File

@ -0,0 +1,31 @@
export default Discourse.Route.extend({
model(params) {
// `0` is a valid query param
if (params.min_score != null) {
params.min_score = params.min_score.toString();
}
return this.store.findAll("reviewable", params);
},
setupController(controller, model) {
let meta = model.resultSetMeta;
controller.setProperties({
reviewables: model,
type: meta.type,
filterType: meta.type,
filterStatus: meta.status,
filterTopic: meta.topic_id,
filterCategoryId: meta.category_id,
min_score: meta.min_score,
filterScore: meta.min_score,
reviewableTypes: meta.reviewable_types,
filterUsername: meta.username
});
},
actions: {
refreshRoute() {
this.refresh();
}
}
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
setupController(controller, model) {
controller.set("reviewable", model);
}
});

View File

@ -0,0 +1,9 @@
export default Discourse.Route.extend({
model() {
return this.store.findAll("reviewable-topic");
},
setupController(controller, model) {
controller.set("reviewableTopics", model);
}
});

View File

@ -0,0 +1,5 @@
export default Discourse.Route.extend({
titleToken() {
return I18n.t("review.title");
}
});

View File

@ -1,3 +0,0 @@
import createAdminUserPostsRoute from "discourse/routes/build-admin-user-posts-route";
export default createAdminUserPostsRoute("flagged");

View File

@ -39,7 +39,14 @@
</div>
{{conditional-loading-spinner condition=loading}}
{{textarea autocomplete="discourse" tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated disabled=disabled}}
{{textarea
autocomplete="discourse"
tabindex=tabindex
value=value
class="d-editor-input"
placeholder=placeholderTranslated
disabled=disabled
change=change}}
{{popup-input-tip validation=validation}}
{{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
</div>

View File

@ -1,96 +0,0 @@
<div class='queued-post'>
<div class='poster'>
{{#user-link user=post.user}}
{{avatar post.user imageSize="large"}}
{{/user-link}}
</div>
<div class='cooked'>
<div class='names'>
<span class="username">
{{#user-link user=post.user}}
{{post.user.username}}
{{/user-link}}
{{#if post.user.silenced}}
{{d-icon "ban" title="user.silenced_tooltip"}}
{{/if}}
</span>
</div>
<div class='post-info'>
<span class='post-date'>{{age-with-tooltip post.created_at}}</span>
</div>
<div class='clearfix'></div>
{{#if editTitleAndCategory}}
<span class="edit-title">
{{text-field value=buffered.title maxlength=siteSettings.max_topic_title_length}}
</span>
{{category-chooser value=buffered.category_id}}
{{else}}
<span class='post-title'>
{{i18n "queue.topic"}}
{{#if post.topic}}
{{topic-link post.topic}}
{{else}}
{{editables.title}}
{{/if}}
{{category-badge editables.category}}
</span>
{{/if}}
<div class='body'>
{{#if editing}}
{{d-editor value=buffered.raw}}
{{else}}
{{cook-text editables.raw}}
{{/if}}
</div>
{{#if showTags}}
<div class="list-tags">
{{#each tags as |t|}}
{{discourse-tag t}}
{{/each}}
</div>
{{else if editTags}}
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}
{{/if}}
<div class='queue-controls'>
{{#if editing}}
{{d-button action=(action "confirmEdit")
label="queue.confirm"
disabled=post.isSaving
class="btn-primary confirm"}}
{{d-button action=(action "cancelEdit")
label="queue.cancel"
icon="times"
disabled=post.isSaving
class="btn-danger cancel"}}
{{else}}
{{d-button action=(action "approve")
disabled=post.isSaving
label="queue.approve"
icon="check"
class="btn-primary approve"}}
{{d-button action=(action "reject")
disabled=post.isSaving
label="queue.reject"
icon="times"
class="btn-danger reject"}}
{{#if post.can_delete_user}}
{{d-button action=(action "deleteUser")
disabled=post.isSaving
label="queue.delete_user"
icon="trash-alt"
class="btn-danger delete-user"}}
{{/if}}
{{d-button action=(action "edit")
disabled=post.isSaving
label="queue.edit"
icon="pencil-alt"
class="edit"}}
{{/if}}
</div>
</div>
<div class='clearfix'></div>
</div>

View File

@ -0,0 +1,17 @@
{{#if multiple}}
{{dropdown-select-box
headerIcon=bundle.icon
class="reviewable-action-dropdown"
nameProperty="label"
title=bundle.label
content=bundle.actions
onSelect=(action "performById")
disabled=reviewableUpdating}}
{{else}}
{{d-button
class=(concat "reviewable-action " (dasherize first.id))
icon=first.icon
action=(action "perform" first)
translatedLabel=first.label
disabled=reviewableUpdating}}
{{/if}}

View File

@ -0,0 +1,5 @@
{{#if post}}
<div class='reviewable-conversation-post'>
{{#link-to 'user' post.user class="username"}}@{{post.user.username}}{{/link-to}} {{{post.excerpt}}}
</div>
{{/if}}

View File

@ -0,0 +1 @@
{{category-chooser value=value onChooseCategory=categoryChanged}}

View File

@ -0,0 +1 @@
{{d-editor value=value change=valueChanged}}

View File

@ -0,0 +1 @@
{{mini-tag-chooser tags=value onChangeTags=valueChanged}}

View File

@ -0,0 +1 @@
{{input value=value change=valueChanged class='reviewable-input-text'}}

View File

@ -0,0 +1 @@
{{textarea value=value change=valueChanged class="reviewable-input-textarea"}}

View File

@ -0,0 +1,23 @@
<div class='created-by'>
{{#user-link user=reviewable.target_created_by}}
{{avatar reviewable.target_created_by imageSize="large"}}
{{/user-link}}
</div>
<div class='post-contents'>
<div class='names'>
<span class="username">
{{#user-link user=reviewable.target_created_by}}
{{reviewable.target_created_by.username}}
{{/user-link}}
</span>
</div>
{{reviewable-topic-link topic=reviewable.topic}}
<div class='post-body'>
{{{reviewable.cooked}}}
</div>
{{yield}}
</div>

View File

@ -0,0 +1,23 @@
{{#if filteredHistories}}
<table class='reviewable-histories'>
<tr>
<th colspan="3">{{i18n "review.history.title"}}</th>
</tr>
{{#each filteredHistories as |rh|}}
{{#unless rh.created}}
<tr>
<td>{{format-date rh.created_at format="medium"}}</td>
<td>
{{#user-link user=rs.user}}
{{avatar rh.created_by imageSize="tiny"}}
{{rh.created_by.username}}
{{/user-link}}
</td>
<td>
{{reviewable-history-description rh}}
</td>
</tr>
{{/unless}}
{{/each}}
</table>
{{/if}}

View File

@ -0,0 +1,79 @@
<div class='reviewable-item {{customClass}}' data-reviewable-id={{reviewable.id}}>
<div class='reviewable-meta-data'>
<div class='reviewable-type'>{{reviewable.humanType}}</div>
<div class='reviewable-category'>
{{category-badge reviewable.category}}
</div>
<div class='created-at'>
{{#link-to 'review.show' reviewable.id}}{{age-with-tooltip reviewable.created_at}}{{/link-to}}
</div>
<div class='score' title={{i18n "review.scores.score"}}>{{format-score reviewable.score}}</div>
<div class='status'>
{{#if reviewable.approved}}
{{d-icon "check"}} {{i18n "review.statuses.approved.title"}}
{{else if reviewable.rejected}}
{{d-icon "times"}} {{i18n "review.statuses.rejected.title"}}
{{else if reviewable.ignored}}
{{d-icon "external-link-alt"}} {{i18n "review.statuses.ignored.title"}}
{{/if}}
</div>
</div>
<div class='reviewable-contents'>
{{#if editing}}
<div class='editable-fields'>
{{#each reviewable.editable_fields as |f|}}
<div class='editable-field {{dasherize f.id}}'>
{{component
(concat "reviewable-field-" f.type)
tagName=''
value=(editable-value reviewable f.id)
tagCategoryId=reviewable.category.id
valueChanged=(action "valueChanged" f.id)
categoryChanged=(action "categoryChanged")}}
</div>
{{/each}}
</div>
{{else}}
{{#component reviewableComponent reviewable=reviewable tagName=''}}
{{reviewable-scores scores=reviewable.reviewable_scores}}
{{reviewable-histories histories=reviewable.reviewable_histories}}
{{/component}}
{{/if}}
</div>
<div class='reviewable-actions'>
{{#if editing}}
{{d-button
class="btn-primary reviewable-action save-edit"
disabled=updating
icon="check"
action=(action "saveEdit")
label="review.save"}}
{{d-button
class="btn-danger reviewable-action cancel-edit"
disabled=updating
icon="times"
action=(action "cancelEdit")
label="review.cancel"}}
{{else}}
{{#each reviewable.bundled_actions as |bundle|}}
{{reviewable-bundled-action
bundle=bundle
performAction=(action "perform")
reviewableUpdating=updating}}
{{/each}}
{{#if reviewable.can_edit}}
{{d-button
class="reviewable-action edit"
disabled=updating
icon="pencil-alt"
action=(action "edit")
label="review.edit"}}
{{/if}}
{{/if}}
</div>
</div>

View File

@ -0,0 +1,37 @@
<div class='created-by'>
{{#user-link user=reviewable.created_by}}
{{avatar reviewable.created_by imageSize="large"}}
{{/user-link}}
</div>
<div class='post-contents'>
<div class='names'>
<span class="username">
{{#user-link user=reviewable.created_by}}
{{reviewable.created_by.username}}
{{/user-link}}
{{#if reviewable.created_by.silenced}}
{{d-icon "ban" title="user.silenced_tooltip"}}
{{/if}}
</span>
</div>
{{#reviewable-topic-link topic=reviewable.topic}}
{{i18n "review.new_topic"}}
{{reviewable.payload.title}}
{{/reviewable-topic-link}}
<div class='post-body'>
{{cook-text reviewable.payload.raw}}
</div>
{{#if reviewable.payload.tags}}
<div class="list-tags">
{{#each reviewable.payload.tags as |t|}}
{{discourse-tag t}}
{{/each}}
</div>
{{/if}}
{{yield}}
</div>

View File

@ -0,0 +1,41 @@
{{#if scores}}
<table class='reviewable-scores'>
<tr>
<th class='user'>{{i18n "review.scores.submitted_by"}}</th>
<th>{{i18n "review.scores.description"}}</th>
<th>{{i18n "review.scores.score"}}</th>
</tr>
{{#each scores as |rs|}}
<tr class='reviewable-score'>
<td class='user'>
{{#user-link user=rs.user}}
{{avatar rs.user imageSize="tiny"}}
{{rs.user.username}}
{{/user-link}}
{{user-flag-percentage
agreed=rs.agree_stats.agreed
disagreed=rs.agree_stats.disagreed
ignored=rs.agree_stats.ignored}}
</td>
<td>{{rs.score_type.title}}</td>
<td>{{format-score rs.score}}</td>
</tr>
{{#if rs.reviewable_conversation}}
<tr>
<td colspan='3'>
<div class='reviewable-conversation'>
{{#each rs.reviewable_conversation.conversation_posts as |p|}}
{{reviewable-conversation-post post=p}}
{{/each}}
<div class='controls'>
<a href={{rs.reviewable_conversation.permalink}} class='btn btn-small'>
{{i18n "review.conversation.view_full"}}
</a>
</div>
</div>
</td>
</tr>
{{/if}}
{{/each}}
</table>
{{/if}}

View File

@ -0,0 +1,10 @@
<div class='post-topic'>
{{#if topic}}
{{i18n "review.topic"}}
{{topic-status topic=topic}}
{{topic-link topic}}
{{i18n "review.topic_replies" count=topic.reply_count}}
{{else}}
{{yield}}
{{/if}}
</div>

View File

@ -0,0 +1,8 @@
<div class='reviewable-user-info'>
<div class='reviewable-user-details'>
{{reviewable.username}}
{{reviewable.email}}
</div>
{{yield}}
</div>

View File

@ -1,8 +1,8 @@
{{#d-modal-body}}
<p>{{{description}}}</p>
<p>{{i18n "review.approval.description"}}</p>
<p>{{{i18n "queue.approval.pending_posts" count=model.pending_count}}}</p>
<p>{{{i18n "review.approval.pending_posts" count=model.pending_count}}}</p>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action=(route-action "closeModal") class="btn-primary" label="queue.approval.ok"}}
{{d-button action=(route-action "closeModal") class="btn-primary" label="review.approval.ok"}}
</div>

View File

@ -1,11 +0,0 @@
<div class='container'>
<div class='queued-posts'>
{{#each model as |post|}}
{{queued-post post=post currentlyEditing=editing removePost=(route-action "removePost" post)}}
{{else}}
<p>{{i18n "queue.none"}}</p>
{{/each}}
{{d-button action=(route-action "refresh") label="refresh" icon="sync" disabled=model.refreshing class="btn-default" id='refresh-queued'}}
</div>
</div>

View File

@ -0,0 +1,83 @@
<div class="reviewable">
<ul class="nav nav-pills reviewable-title">
{{nav-item route='review.index' label='review.view_all'}}
{{nav-item route='review.topics' label='review.grouped_by_topic'}}
</ul>
<div class="reviewable-container">
<div class="reviewable-list">
{{#if reviewables}}
{{#load-more selector=".reviewable-item" action=(action "loadMore")}}
<div class='reviewables'>
{{#each reviewables as |r|}}
{{reviewable-item reviewable=r remove=(action "remove")}}
{{/each}}
</div>
{{/load-more}}
{{conditional-loading-spinner condition=reviewables.loadingMore}}
{{else}}
<div class="no-review">
{{i18n "review.none"}}
</div>
{{/if}}
</div>
<div class='reviewable-filters'>
<div class='reviewable-filter'>
<label class='filter-label'>{{i18n "review.filters.status"}}</label>
{{combo-box value=filterStatus content=statuses}}
</div>
{{#if filtersExpanded}}
<div class='reviewable-filter'>
<label class='filter-label'>{{i18n "review.filters.type.title"}}</label>
{{combo-box value=filterType content=allTypes none="review.filters.type.all"}}
</div>
<div class='reviewable-filter'>
<label class='filter-label'>{{i18n "review.filters.minimum_score"}}</label>
{{input value=filterScore class="score-filter"}}
</div>
<div class='reviewable-filter'>
<label class='filter-label'>{{i18n "review.filters.category"}}</label>
{{category-chooser none="category.all" value=filterCategoryId}}
</div>
<div class='reviewable-filter topic-filter'>
{{i18n "review.filtered_user"}}
{{user-selector
excludeCurrentUser=false
usernames=filterUsername
fullWidthWrap="true"
class="user-selector"
single="true"
canReceiveUpdates="true"}}
</div>
{{#if filterTopic}}
<div class='reviewable-filter topic-filter'>
{{i18n "review.filtered_topic"}}
{{d-button label="review.show_all_topics" icon="times" action=(action "resetTopic")}}
</div>
{{/if}}
{{/if}}
<div class='reviewable-filters-actions'>
{{d-button
icon="sync"
label="review.filters.refresh"
class="btn-primary refresh" action=(action "refresh")}}
{{#if site.mobileView}}
{{d-button
label="show_help"
icon=toggleFiltersIcon
class="btn-default expand-secondary-filters"
action=(action "toggleFilters")}}
{{/if}}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
{{reviewable-item reviewable=reviewable}}

View File

@ -0,0 +1,45 @@
<div class="reviewable">
<ul class="nav nav-pills reviewable-title">
{{nav-item route='review.index' routeParam=(query-params topic_id=null) label='review.view_all'}}
{{nav-item route='review.topics' label='review.grouped_by_topic'}}
</ul>
{{#if reviewableTopics}}
<table class='reviewable-topics'>
<thead>
<th>{{i18n "review.topics.topic"}} </th>
<th>{{i18n "review.topics.reviewable_count"}}</th>
<th>{{i18n "review.topics.reported_by"}}</th>
<th></th>
</thead>
<tbody>
{{#each reviewableTopics as |rt|}}
<tr class='reviewable-topic'>
<td class="topic-title">
<div class='combined-title'>
{{topic-status topic=rt}}
<a href={{rt.relative_url}} target="_blank">{{replace-emoji rt.fancy_title}}</a>
</div>
</td>
<td class="reviewable-count">
{{rt.stats.count}}
</td>
<td class="reported-by">
{{i18n "review.topics.unique_users" count=rt.stats.unique_users}}
</td>
<td class="reviewable-details">
{{#link-to "review.index" (query-params topic_id=rt.id) class="btn btn-primary btn-small"}}
{{d-icon "list"}}
<span>{{i18n "review.topics.details"}}</span>
{{/link-to}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{else}}
<div class="no-review">
{{i18n "review.none"}}
</div>
{{/if}}
</div>

View File

@ -0,0 +1 @@
{{outlet}}

View File

@ -213,14 +213,13 @@
{{#if model.pending_posts_count}}
<div class="has-pending-posts">
{{{i18n "queue.has_pending_posts" count=model.pending_posts_count}}}
<div>
{{{i18n "review.topic_has_pending" count=model.pending_posts_count}}}
</div>
{{#if currentUser.show_queued_posts}}
{{#link-to "queued-posts"}}
{{d-icon "check"}}
{{i18n "queue.view_pending"}}
{{/link-to}}
{{/if}}
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
{{i18n "review.view_pending"}}
{{/link-to}}
</div>
{{/if}}

View File

@ -10,9 +10,9 @@
{{/if}}
{{#if model.number_of_flagged_posts}}
<div>
{{#link-to 'user.flaggedPosts' model}}
{{#link-to 'review' (query-params username=model.username status='all' type='ReviewableFlaggedPost')}}
<span class="flagged-posts">{{model.number_of_flagged_posts}}</span>{{i18n 'user.staff_counters.flagged_posts'}}
{{/link-to}}
{{/link-to}}
</div>
{{/if}}
{{#if model.number_of_deleted_posts}}

View File

@ -1,6 +1,7 @@
import { createWidget } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
import { h } from "virtual-dom";
import DiscourseURL from "discourse/lib/url";
export const ButtonClass = {
tagName: "button.widget-button.btn",
@ -83,6 +84,10 @@ export const ButtonClass = {
this.sendWidgetAction(attrs.secondaryAction);
}
if (attrs.url) {
return DiscourseURL.routeTo(attrs.url);
}
if (attrs.sendActionEvent) {
return this.sendWidgetAction(attrs.action, e);
}

View File

@ -46,8 +46,7 @@ export default createWidget("hamburger-menu", {
},
adminLinks() {
const { currentUser, siteSettings } = this;
let flagsPath = siteSettings.flags_default_topics ? "topics" : "active";
const { currentUser } = this;
const links = [
{
@ -55,27 +54,16 @@ export default createWidget("hamburger-menu", {
className: "admin-link",
icon: "wrench",
label: "admin_title"
},
{
href: `/admin/flags/${flagsPath}`,
className: "flagged-posts-link",
icon: "flag",
label: "flags_title",
badgeClass: "flagged-posts",
badgeTitle: "notifications.total_flagged",
badgeCount: "site_flagged_posts_count"
}
];
if (currentUser.show_queued_posts) {
links.push({
route: "queued-posts",
className: "queued-posts-link",
label: "queue.title",
badgeCount: "post_queue_new_count",
badgeClass: "queued-posts"
});
}
links.push({
route: "review",
className: "review",
label: "review.title",
badgeCount: "reviewable_count",
badgeClass: "reviewables"
});
if (currentUser.admin) {
links.push({

View File

@ -6,10 +6,7 @@ createWidget(
"post-admin-menu-button",
jQuery.extend(ButtonClass, {
tagName: "li.btn",
click() {
this.sendWidgetAction("closeAdminMenu");
return this.sendWidgetAction(this.attrs.action);
}
secondaryAction: "closeAdminMenu"
})
);
@ -23,8 +20,8 @@ export function buildManageButtons(attrs, currentUser, siteSettings) {
contents.push({
icon: "list",
className: "btn-default",
label: "admin.flags.moderation_history",
action: "showModerationHistory"
label: "review.moderation_history",
url: `/review?topic_id=${attrs.topicId}&status=all`
});
}

Some files were not shown because too many files have changed in this diff Show More