Add Suspend User to flags page
This commit is contained in:
parent
079f108ceb
commit
09ed2ed749
|
@ -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)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)),
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{{#each flaggedPosts as |flaggedPost|}}
|
||||
{{flagged-post
|
||||
flaggedPost=flaggedPost
|
||||
canAct=canAct
|
||||
filter=filter
|
||||
showResolvedBy=showResolvedBy
|
||||
removePost=(action "removePost" flaggedPost)
|
||||
hideTitle=topic}}
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -47,4 +47,5 @@
|
|||
icon="exclamation-triangle"
|
||||
label="flagging.delete_spammer"}}
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -63,3 +63,4 @@ QUnit.test("suspend, then unsuspend a user", assert => {
|
|||
assert.ok(!exists('.suspension-info'));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 }));
|
||||
|
|
Loading…
Reference in New Issue