mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 09:26:54 -06:00
FEATURE: Show a button to Staff for "Moderation History" on posts/topics
When clicked, it pops up a modal showing a history of moderation actions taken on the post or topic.
This commit is contained in:
parent
85a59c632d
commit
410994b7f5
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'tr',
|
||||
});
|
@ -0,0 +1,18 @@
|
||||
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));
|
||||
}
|
||||
});
|
@ -60,6 +60,11 @@ 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
|
||||
|
@ -0,0 +1,17 @@
|
||||
<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>
|
@ -0,0 +1,23 @@
|
||||
{{#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=(action "closeModal") label="close"}}
|
||||
</div>
|
@ -7,7 +7,8 @@ const ADMIN_MODELS = [
|
||||
'embeddable-host',
|
||||
'web-hook',
|
||||
'web-hook-event',
|
||||
'flagged-topic'
|
||||
'flagged-topic',
|
||||
'moderation-history'
|
||||
];
|
||||
|
||||
export function Result(payload, responseJson) {
|
||||
|
@ -3,6 +3,7 @@ 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) { return min; }
|
||||
@ -23,6 +24,7 @@ function findTopView($posts, viewportTop, postsWrapperTop, min, max) {
|
||||
}
|
||||
|
||||
export default MountWidget.extend({
|
||||
adminTools: optionalService(),
|
||||
widget: 'post-stream',
|
||||
_topVisible: null,
|
||||
_bottomVisible: null,
|
||||
@ -271,6 +273,9 @@ export default MountWidget.extend({
|
||||
this.$().off('mouseleave.post-stream');
|
||||
this.appEvents.off('post-stream:refresh');
|
||||
this.appEvents.off('post-stream:posted');
|
||||
}
|
||||
},
|
||||
|
||||
showModerationHistory(post) {
|
||||
this.get('adminTools').showModerationHistory({ filter: 'post', post_id: post.id });
|
||||
}
|
||||
});
|
||||
|
@ -1,11 +1,20 @@
|
||||
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')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -17,7 +17,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
selected: null,
|
||||
spammerDetails: null
|
||||
spammerDetails: null,
|
||||
});
|
||||
|
||||
let adminTools = this.get('adminTools');
|
||||
|
@ -40,7 +40,8 @@ flushMap();
|
||||
|
||||
export default Ember.Object.extend({
|
||||
_plurals: {'post-reply': 'post-replies',
|
||||
'post-reply-history': 'post_reply_histories'},
|
||||
'post-reply-history': 'post_reply_histories',
|
||||
'moderation-history': 'moderation_history'},
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
@ -48,5 +48,4 @@
|
||||
icon="exclamation-triangle"
|
||||
label="flagging.delete_spammer"}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
@ -16,6 +16,14 @@ export function buildManageButtons(attrs, currentUser) {
|
||||
}
|
||||
|
||||
let contents = [];
|
||||
if (attrs.canManage) {
|
||||
contents.push({
|
||||
icon: 'list',
|
||||
label: 'admin.flags.moderation_history',
|
||||
action: 'showModerationHistory',
|
||||
});
|
||||
}
|
||||
|
||||
if (!attrs.isWhisper && currentUser.staff) {
|
||||
const buttonAtts = {
|
||||
action: 'togglePostType',
|
||||
|
@ -10,7 +10,7 @@ createWidget('admin-menu-button', {
|
||||
className,
|
||||
action: attrs.action,
|
||||
icon: attrs.icon,
|
||||
label: `topic.${attrs.label}`,
|
||||
label: attrs.fullLabel || `topic.${attrs.label}`,
|
||||
secondaryAction: 'hideAdminMenu'
|
||||
}));
|
||||
}
|
||||
@ -114,6 +114,7 @@ export default createWidget('topic-admin-menu', {
|
||||
|
||||
const topic = attrs.topic;
|
||||
const details = topic.get('details');
|
||||
|
||||
if (details.get('can_delete')) {
|
||||
buttons.push({ className: 'topic-admin-delete',
|
||||
buttonClass: 'btn-danger',
|
||||
@ -184,6 +185,12 @@ export default createWidget('topic-admin-menu', {
|
||||
label: isPrivateMessage ? 'actions.make_public' : 'actions.make_private' });
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
action: 'showModerationHistory',
|
||||
icon: 'list',
|
||||
fullLabel: 'admin.flags.moderation_history'
|
||||
});
|
||||
|
||||
const extraButtons = applyDecorators(this, 'adminMenuButtons', this.attrs, this.state);
|
||||
|
||||
return [ h('h3', I18n.t('admin_title')),
|
||||
|
@ -310,7 +310,7 @@ export default class Widget {
|
||||
view.sendAction(method, param);
|
||||
promise = Ember.RSVP.resolve();
|
||||
} else {
|
||||
const target = view.get('targetObject');
|
||||
const target = view.get('targetObject') || view;
|
||||
promise = method.call(target, param);
|
||||
if (!promise || !promise.then) {
|
||||
promise = Ember.RSVP.resolve(promise);
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
@import "common/admin/customize";
|
||||
@import "common/admin/flagging";
|
||||
@import "common/admin/moderation_history";
|
||||
@import "common/admin/suspend";
|
||||
|
||||
$mobile-breakpoint: 700px;
|
||||
|
32
app/assets/stylesheets/common/admin/moderation_history.scss
Normal file
32
app/assets/stylesheets/common/admin/moderation_history.scss
Normal file
@ -0,0 +1,32 @@
|
||||
.moderation-history {
|
||||
width: 100%;
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
td.date {
|
||||
padding-right: 1em;
|
||||
}
|
||||
td, th {
|
||||
padding-bottom: 0.5em;
|
||||
vertical-align: top;
|
||||
}
|
||||
.history-item-action {
|
||||
.action-details {
|
||||
margin: 1em 0;
|
||||
color: $primary-medium;
|
||||
white-space: pre-wrap;
|
||||
line-height: 1em;
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.history-item-actor {
|
||||
a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
span {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
app/controllers/admin/moderation_history_controller.rb
Normal file
40
app/controllers/admin/moderation_history_controller.rb
Normal file
@ -0,0 +1,40 @@
|
||||
class Admin::ModerationHistoryController < Admin::AdminController
|
||||
|
||||
def index
|
||||
history_filter = params[:filter]
|
||||
raise Discourse::NotFound unless ['post', 'topic'].include?(history_filter)
|
||||
|
||||
query = UserHistory.where(
|
||||
action: UserHistory.actions.only(
|
||||
:delete_user,
|
||||
:suspend_user,
|
||||
:silence_user,
|
||||
:delete_post,
|
||||
:delete_topic
|
||||
).values
|
||||
)
|
||||
|
||||
case history_filter
|
||||
when 'post'
|
||||
raise Discourse::NotFound if params[:post_id].blank?
|
||||
query = query.where(post_id: params[:post_id])
|
||||
when 'topic'
|
||||
raise Discourse::NotFound if params[:topic_id].blank?
|
||||
query = query.where(
|
||||
"topic_id = ? OR post_id IN (?)",
|
||||
params[:topic_id],
|
||||
Post.with_deleted.where(topic_id: params[:topic_id]).pluck(:id)
|
||||
)
|
||||
end
|
||||
query = query.includes(:acting_user)
|
||||
query = query.order(:created_at)
|
||||
|
||||
render_serialized(
|
||||
query,
|
||||
UserHistorySerializer,
|
||||
root: 'moderation_history',
|
||||
rest_serializer: true
|
||||
)
|
||||
end
|
||||
|
||||
end
|
@ -2646,6 +2646,7 @@ en:
|
||||
active_posts: "Flagged Posts"
|
||||
old_posts: "Old Flagged Posts"
|
||||
topics: "Flagged Topics"
|
||||
moderation_history: "Moderation History"
|
||||
|
||||
agree: "Agree"
|
||||
agree_title: "Confirm this flag as valid and correct"
|
||||
@ -3112,6 +3113,16 @@ en:
|
||||
reply_key_placeholder: "reply key"
|
||||
skipped_reason_placeholder: "reason"
|
||||
|
||||
moderation_history:
|
||||
performed_by: "Performed By"
|
||||
no_results: "There is no moderation history available."
|
||||
actions:
|
||||
delete_user: "User Deleted"
|
||||
suspend_user: "User Suspended"
|
||||
silence_user: "User Silenced"
|
||||
delete_post: "Post Deleted"
|
||||
delete_topic: "Topic Deleted"
|
||||
|
||||
logs:
|
||||
title: "Logs"
|
||||
action: "Action"
|
||||
|
@ -93,6 +93,8 @@ Discourse::Application.routes.draw do
|
||||
get "groups/:type" => "groups#show", constraints: AdminConstraint.new
|
||||
get "groups/:type/:id" => "groups#show", constraints: AdminConstraint.new
|
||||
|
||||
get "moderation_history" => "moderation_history#index"
|
||||
|
||||
resources :users, id: USERNAME_ROUTE_FORMAT, except: [:show] do
|
||||
collection do
|
||||
get "list" => "users#index"
|
||||
|
55
spec/requests/admin/moderation_history_controller_spec.rb
Normal file
55
spec/requests/admin/moderation_history_controller_spec.rb
Normal file
@ -0,0 +1,55 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Admin::BackupsController do
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
describe "parameters" do
|
||||
it "returns 404 without a valid filter" do
|
||||
get "/admin/moderation_history.json"
|
||||
expect(response).not_to be_success
|
||||
end
|
||||
|
||||
it "returns 404 without a valid id" do
|
||||
get "/admin/moderation_history.json?filter=topic"
|
||||
expect(response).not_to be_success
|
||||
end
|
||||
end
|
||||
|
||||
describe "for a post" do
|
||||
it "returns an empty array when the post doesn't exist" do
|
||||
get "/admin/moderation_history.json?filter=post&post_id=99999999"
|
||||
expect(response).to be_success
|
||||
expect(::JSON.parse(response.body)['moderation_history']).to be_blank
|
||||
end
|
||||
|
||||
it "returns a history when the post exists" do
|
||||
p = Fabricate(:post)
|
||||
p = Fabricate(:post, topic_id: p.topic_id)
|
||||
PostDestroyer.new(Discourse.system_user, p).destroy
|
||||
get "/admin/moderation_history.json?filter=post&post_id=#{p.id}"
|
||||
expect(response).to be_success
|
||||
expect(::JSON.parse(response.body)['moderation_history']).to be_present
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "for a topic" do
|
||||
it "returns empty history when the topic doesn't exist" do
|
||||
get "/admin/moderation_history.json?filter=topic&topic_id=1234"
|
||||
expect(response).to be_success
|
||||
expect(::JSON.parse(response.body)['moderation_history']).to be_blank
|
||||
end
|
||||
|
||||
it "returns a history when the topic exists" do
|
||||
p = Fabricate(:post)
|
||||
PostDestroyer.new(Discourse.system_user, p).destroy
|
||||
get "/admin/moderation_history.json?filter=topic&topic_id=#{p.topic_id}"
|
||||
expect(response).to be_success
|
||||
expect(::JSON.parse(response.body)['moderation_history']).to be_present
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user