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 }));