Add Suspend User to flags page

This commit is contained in:
Robin Ward 2017-09-14 14:10:39 -04:00
parent 079f108ceb
commit 09ed2ed749
17 changed files with 177 additions and 55 deletions

View File

@ -1,7 +1,10 @@
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,
suspended: false,
tagName: 'div',
classNameBindings: [
@ -10,6 +13,11 @@ export default Ember.Component.extend({
'flaggedPost.deleted'
],
@computed('filter')
canAct(filter) {
return filter === 'active';
},
removeAfter(promise) {
return promise.then(() => {
this.attrs.removePost();
@ -44,6 +52,18 @@ export default Ember.Component.extend({
this.get('flaggedPost').expandHidden().then(() => {
this.set('expanded', true);
});
},
showSuspendModal() {
let post = this.get('flaggedPost');
let user = post.get('user');
this.get('adminTools').showSuspendModal(
user,
{
post,
successCallback: result => this.set('suspended', result.suspended)
}
);
}
}
});

View File

@ -6,34 +6,45 @@ export default Ember.Controller.extend(ModalFunctionality, {
suspendUntil: null,
reason: null,
message: null,
loading: false,
suspending: false,
user: null,
post: null,
successCallback: null,
onShow() {
this.setProperties({
suspendUntil: null,
reason: null,
message: null,
loading: false
suspending: false,
loadingUser: true,
post: null,
successCallback: null,
});
},
@computed('suspendUntil', 'reason', 'loading')
submitDisabled(suspendUntil, reason, loading) {
return (loading || Ember.isEmpty(suspendUntil) || !reason || reason.length < 1);
@computed('suspendUntil', 'reason', 'suspending')
submitDisabled(suspendUntil, reason, suspending) {
return (suspending || Ember.isEmpty(suspendUntil) || !reason || reason.length < 1);
},
actions: {
suspend() {
if (this.get('submitDisabled')) { return; }
this.set('loading', true);
this.get('model').suspend({
this.set('suspending', true);
this.get('user').suspend({
suspend_until: this.get('suspendUntil'),
reason: this.get('reason'),
message: this.get('message')
}).then(() => {
message: this.get('message'),
post_id: this.get('post.id')
}).then(result => {
this.send('closeModal');
}).catch(popupAjaxError).finally(() => this.set('loading', false));
let callback = this.get('successCallback');
if (callback) {
callback(result);
}
}).catch(popupAjaxError).finally(() => this.set('suspending', false));
}
}

View File

@ -9,7 +9,7 @@ import TL3Requirements from 'admin/models/tl3-requirements';
import { userPath } from 'discourse/lib/url';
const AdminUser = Discourse.User.extend({
adminUserView: true,
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
automaticGroups: Ember.computed.filter("groups", g => g.automatic && Group.create(g)),

View File

@ -20,12 +20,28 @@ export default Ember.Service.extend({
};
},
showSuspendModal(user) {
showModal('admin-suspend-user', {
model: user,
showSuspendModal(user, opts) {
opts = opts || {};
let controller = showModal('admin-suspend-user', {
admin: true,
modalClass: 'suspend-user-modal'
});
if (opts.post) {
controller.set('post', opts.post);
}
let promise = user.adminUserView ?
Ember.RSVP.resolve(user) :
AdminUser.find(user.get('id'));
promise.then(loadedUser => {
controller.setProperties({
user: loadedUser,
loadingUser: false,
successCallback: opts.successCallback
});
});
},
_deleteSpammer(adminUser) {

View File

@ -97,6 +97,12 @@
{{/if}}
</div>
{{#if suspended}}
<div class='suspended-message'>
The user was suspended for this post.
</div>
{{/if}}
{{#if canAct}}
<div class='flagged-post-controls'>
{{d-button
@ -136,6 +142,15 @@
action="showDeleteFlagModal"
icon="trash-o"
label="admin.flags.delete"}}
{{#unless suspended}}
{{d-button
class="btn-danger suspend-user"
icon="ban"
label="admin.flags.suspend_user"
title="admin.flags.suspend_user_title"
action=(action "showSuspendModal")}}
{{/unless}}
</div>
{{/if}}
</div>

View File

@ -4,7 +4,7 @@
{{#each flaggedPosts as |flaggedPost|}}
{{flagged-post
flaggedPost=flaggedPost
canAct=canAct
filter=filter
showResolvedBy=showResolvedBy
removePost=(action "removePost" flaggedPost)
hideTitle=topic}}

View File

@ -1,39 +1,49 @@
{{#d-modal-body title="admin.user.suspend_modal_title"}}
<div class='until-controls'>
<label>
{{future-date-input
class="suspend-until"
label="admin.user.suspend_duration"
input=suspendUntil}}
</label>
</div>
{{#conditional-loading-spinner condition=loadingUser}}
<div class='reason-controls'>
<label>
<div class='suspend-reason-label'>
{{#if siteSettings.hide_suspension_reasons}}
{{{i18n 'admin.user.suspend_reason_hidden_label'}}}
{{else}}
{{{i18n 'admin.user.suspend_reason_label'}}}
{{/if}}
{{#if user.canSuspend}}
<div class='until-controls'>
<label>
{{future-date-input
class="suspend-until"
label="admin.user.suspend_duration"
input=suspendUntil}}
</label>
</div>
{{text-field
value=reason
class="suspend-reason"
placeholderKey="admin.user.suspend_reason_placeholder"}}
</label>
</div>
<div class='reason-controls'>
<label>
<div class='suspend-reason-label'>
{{#if siteSettings.hide_suspension_reasons}}
{{{i18n 'admin.user.suspend_reason_hidden_label'}}}
{{else}}
{{{i18n 'admin.user.suspend_reason_label'}}}
{{/if}}
</div>
<label>
<div class='suspend-message-label'>
{{i18n "admin.user.suspend_message"}}
</div>
{{textarea
value=message
class="suspend-message"
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
</label>
{{text-field
value=reason
class="suspend-reason"
placeholderKey="admin.user.suspend_reason_placeholder"}}
</label>
</div>
<label>
<div class='suspend-message-label'>
{{i18n "admin.user.suspend_message"}}
</div>
{{textarea
value=message
class="suspend-message"
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
</label>
{{else}}
<div class='cant-suspend'>
{{i18n "admin.user.cant_suspend"}}
</div>
{{/if}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}

View File

@ -47,4 +47,5 @@
icon="exclamation-triangle"
label="flagging.delete_spammer"}}
{{/if}}
</div>

View File

@ -98,9 +98,16 @@
}
}
.suspended-message {
padding: 0.5em;
background-color: $danger;
margin-bottom: 1em;
color: $secondary;
}
.flagged-post-message {
padding: 0.5em 0 0.5em 4em;
margin-bottom: 0.5em;
padding: 0.5em 1em;
margin: 0.5em 0;
background-color: $highlight-medium;
.text {

View File

@ -67,7 +67,8 @@ class Admin::UsersController < Admin::AdminController
user_history = StaffActionLogger.new(current_user).log_user_suspend(
@user,
params[:reason],
context: message
context: message,
post_id: params[:post_id]
)
end
@user.logged_out

View File

@ -132,7 +132,6 @@ module HasCustomFields
if @preloaded.key?(key)
@preloaded[key]
else
raise "#{@preloaded.inspect} -> #{key.inspect}"
# for now you can not mix preload an non preload, it better just to fail
raise StandardError, "Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries."
end

View File

@ -169,9 +169,13 @@ class StaffActionLogger
def log_user_suspend(user, reason, opts = {})
raise Discourse::InvalidParameters.new(:user) unless user
UserHistory.create(params(opts).merge(action: UserHistory.actions[:suspend_user],
target_user_id: user.id,
details: reason))
args = params(opts).merge(
action: UserHistory.actions[:suspend_user],
target_user_id: user.id,
details: reason
)
args[:post_id] = opts[:post_id] if opts[:post_id]
UserHistory.create(args)
end
def log_user_unsuspend(user, opts = {})

View File

@ -2627,6 +2627,8 @@ en:
clear_topic_flags: "Done"
clear_topic_flags_title: "The topic has been investigated and issues have been resolved. Click Done to remove the flags."
more: "(more replies...)"
suspend_user: "Suspend User"
suspend_user_title: "Suspend user for this post"
dispositions:
agreed: "agreed"
@ -3273,6 +3275,7 @@ en:
suspend_message_placeholder: "Optionally, provide more information about the suspension and it will be emailed to the user."
suspended_by: "Suspended by"
suspended_until: "(until %{until})"
cant_suspend: "This user cannot be suspended."
delete_all_posts: "Delete all posts"
# keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details

View File

@ -122,9 +122,9 @@ describe Admin::UsersController do
context '.suspend' do
let(:user) { Fabricate(:evil_trout) }
let!(:api_key) { Fabricate(:api_key, user: user) }
it "works properly" do
Fabricate(:api_key, user: user)
put(
:suspend,
user_id: user.id,
@ -144,6 +144,24 @@ describe Admin::UsersController do
expect(log.details).to match(/because I said so/)
end
it "can have an associated post" do
post = Fabricate(:post)
put(
:suspend,
user_id: user.id,
suspend_until: 5.hours.from_now,
reason: "because of this post",
post_id: post.id,
format: :json
)
expect(response).to be_success
log = UserHistory.where(target_user_id: user.id).order('id desc').first
expect(log).to be_present
expect(log.post_id).to eq(post.id)
end
it "can send a message to the user" do
Jobs.expects(:enqueue).with(
:critical_user_email,

View File

@ -107,6 +107,14 @@ QUnit.test("flagged posts - delete + deleteSpammer", assert => {
});
});
QUnit.test("flagged posts - suspend", assert => {
visit("/admin/flags/active");
click('.suspend-user');
andThen(() => {
assert.equal(find('.suspend-user-modal:visible').length, 1);
assert.equal(find('.suspend-user-modal .cant-suspend').length, 1);
});
});
QUnit.test("topics with flags", assert => {
visit("/admin/flags/topics");

View File

@ -63,3 +63,4 @@ QUnit.test("suspend, then unsuspend a user", assert => {
assert.ok(!exists('.suspension-info'));
});
});

View File

@ -350,13 +350,21 @@ export default function() {
this.get('/tag_groups', () => response(200, {tag_groups: []}));
this.get('/admin/users/1234.json', request => {
this.get('/admin/users/1234.json', () => {
return response(200, {
id: 1234,
username: 'regular',
});
});
this.get('/admin/users/2.json', () => {
return response(200, {
id: 2,
username: 'sam',
admin: true
});
});
this.post('/admin/users/:user_id/generate_api_key', success);
this.delete('/admin/users/:user_id/revoke_api_key', success);
this.delete('/admin/users/:user_id.json', () => response(200, { deleted: true }));