From 09ed2ed749beade1f1480fc112e73759e25e0cc4 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 14 Sep 2017 14:10:39 -0400 Subject: [PATCH] Add Suspend User to flags page --- .../admin/components/flagged-post.js.es6 | 20 ++++++ .../modals/admin-suspend-user.js.es6 | 31 +++++--- .../admin/models/admin-user.js.es6 | 2 +- .../admin/services/admin-tools.js.es6 | 22 +++++- .../templates/components/flagged-post.hbs | 15 ++++ .../templates/components/flagged-posts.hbs | 2 +- .../templates/modal/admin-suspend-user.hbs | 72 +++++++++++-------- .../discourse/templates/modal/flag.hbs | 1 + .../stylesheets/common/admin/flagging.scss | 11 ++- app/controllers/admin/users_controller.rb | 3 +- app/models/concerns/has_custom_fields.rb | 1 - app/services/staff_action_logger.rb | 10 ++- config/locales/client.en.yml | 3 + .../admin/users_controller_spec.rb | 20 +++++- .../acceptance/admin-flags-test.js.es6 | 8 +++ .../acceptance/admin-suspend-user-test.js.es6 | 1 + .../helpers/create-pretender.js.es6 | 10 ++- 17 files changed, 177 insertions(+), 55 deletions(-) diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index e1cbed8fc8a..5bf1f9ab22e 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -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) + } + ); } } }); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 index 8d93aea3c29..efcd1426700 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 @@ -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)); } } diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 4bbe46f4464..893631b7858 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -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)), diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6 index a65b3bdde1c..9f855772a22 100644 --- a/app/assets/javascripts/admin/services/admin-tools.js.es6 +++ b/app/assets/javascripts/admin/services/admin-tools.js.es6 @@ -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) { diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 7d04159a816..39350d12d15 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -97,6 +97,12 @@ {{/if}} + {{#if suspended}} +
+ The user was suspended for this post. +
+ {{/if}} + {{#if canAct}}
{{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}}
{{/if}} diff --git a/app/assets/javascripts/admin/templates/components/flagged-posts.hbs b/app/assets/javascripts/admin/templates/components/flagged-posts.hbs index fd8d0b182ce..a2f60c25875 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-posts.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-posts.hbs @@ -4,7 +4,7 @@ {{#each flaggedPosts as |flaggedPost|}} {{flagged-post flaggedPost=flaggedPost - canAct=canAct + filter=filter showResolvedBy=showResolvedBy removePost=(action "removePost" flaggedPost) hideTitle=topic}} diff --git a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs index c1e03a6aca6..6de8afdd3f6 100644 --- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs @@ -1,39 +1,49 @@ {{#d-modal-body title="admin.user.suspend_modal_title"}} -
- -
+ {{#conditional-loading-spinner condition=loadingUser}} -
-
diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index 60a233a4bb9..59b4e8e820b 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -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 { diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 889e54edc26..06b3db551fe 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -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 diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 00f5cd9fb14..25a6422b0e5 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -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 diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index b58ff9093ed..1a80f93250f 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -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 = {}) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8a8385855ed..ff07e664573 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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 diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index d072b1cbcec..4a8e0faebfc 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -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, diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index ab77a827066..bd01fd0d369 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -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"); diff --git a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 index 11659f33a5a..b122e5a8267 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -63,3 +63,4 @@ QUnit.test("suspend, then unsuspend a user", assert => { assert.ok(!exists('.suspension-info')); }); }); + diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 610dc40c0ea..6c70dad01f1 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -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 }));