diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6
index aaa4759e827..8aff3d2a2d1 100644
--- a/app/assets/javascripts/admin/components/flagged-post.js.es6
+++ b/app/assets/javascripts/admin/components/flagged-post.js.es6
@@ -4,8 +4,6 @@ import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
adminTools: Ember.inject.service(),
expanded: false,
- suspended: false,
-
tagName: 'div',
classNameBindings: [
':flagged-post',
@@ -21,12 +19,7 @@ export default Ember.Component.extend({
},
removeAfter(promise) {
- return promise.then(() => {
- this.attrs.removePost();
- }).catch(error => {
- if (error._discourse_displayed) { return; }
- bootbox.alert(I18n.t("admin.flags.error"));
- });
+ return promise.then(() => this.attrs.removePost());
},
_spawnModal(name, model, modalClass) {
@@ -36,7 +29,7 @@ export default Ember.Component.extend({
actions: {
removeAfter(promise) {
- this.removeAfter(promise);
+ return this.removeAfter(promise);
},
disagree() {
@@ -58,18 +51,6 @@ export default Ember.Component.extend({
filter: 'post',
post_id: this.get('flaggedPost.id')
});
- },
-
- 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/components/penalty-post-action.js.es6 b/app/assets/javascripts/admin/components/penalty-post-action.js.es6
new file mode 100644
index 00000000000..d89c69a32d0
--- /dev/null
+++ b/app/assets/javascripts/admin/components/penalty-post-action.js.es6
@@ -0,0 +1,32 @@
+import computed from 'ember-addons/ember-computed-decorators';
+
+const ACTIONS = ['delete', 'edit', 'none'];
+export default Ember.Component.extend({
+ postAction: null,
+ postEdit: null,
+
+ @computed
+ penaltyActions() {
+ return ACTIONS.map(id => {
+ return { id, name: I18n.t(`admin.user.penalty_post_${id}`) };
+ });
+ },
+
+ editing: Ember.computed.equal('postAction', 'edit'),
+
+ actions: {
+ penaltyChanged() {
+ let postAction = this.get('postAction');
+
+ // If we switch to edit mode, jump to the edit textarea
+ if (postAction === 'edit') {
+ Ember.run.scheduleOnce('afterRender', () => {
+ let $elem = this.$();
+ let body = $elem.closest('.modal-body');
+ body.scrollTop(body.height());
+ $elem.find('.post-editor').focus();
+ });
+ }
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6
index 9f1aef916f8..2e12a2196d3 100644
--- a/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6
+++ b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6
@@ -1,26 +1,14 @@
-import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
+import PenaltyController from 'admin/mixins/penalty-controller';
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Ember.Controller.extend(PenaltyController, {
silenceUntil: null,
- reason: null,
- message: null,
silencing: false,
- user: null,
- post: null,
- successCallback: null,
onShow() {
- this.setProperties({
- silenceUntil: null,
- reason: null,
- message: null,
- silencing: false,
- loadingUser: true,
- post: null,
- successCallback: null,
- });
+ this.resetModal();
+ this.setProperties({ silenceUntil: null, silencing: false });
},
@computed('silenceUntil', 'reason', 'silencing')
@@ -33,18 +21,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.get('submitDisabled')) { return; }
this.set('silencing', true);
- this.get('user').silence({
- silenced_till: this.get('silenceUntil'),
- reason: this.get('reason'),
- message: this.get('message'),
- post_id: this.get('post.id')
- }).then(result => {
- this.send('closeModal');
- let callback = this.get('successCallback');
- if (callback) {
- callback(result);
- }
- }).catch(popupAjaxError).finally(() => this.set('silencing', false));
+ this.penalize(() => {
+ return this.get('user').silence({
+ silenced_till: this.get('silenceUntil'),
+ reason: this.get('reason'),
+ message: this.get('message'),
+ post_id: this.get('post.id'),
+ post_action: this.get('postAction'),
+ post_edit: this.get('postEdit')
+ });
+ }).finally(() => this.set('silencing', false));
}
}
});
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 efcd1426700..f66a1eec694 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
@@ -1,26 +1,13 @@
-import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
-import { popupAjaxError } from 'discourse/lib/ajax-error';
+import PenaltyController from 'admin/mixins/penalty-controller';
-export default Ember.Controller.extend(ModalFunctionality, {
+export default Ember.Controller.extend(PenaltyController, {
suspendUntil: null,
- reason: null,
- message: null,
suspending: false,
- user: null,
- post: null,
- successCallback: null,
onShow() {
- this.setProperties({
- suspendUntil: null,
- reason: null,
- message: null,
- suspending: false,
- loadingUser: true,
- post: null,
- successCallback: null,
- });
+ this.resetModal();
+ this.setProperties({ suspendUntil: null, suspending: false });
},
@computed('suspendUntil', 'reason', 'suspending')
@@ -33,19 +20,17 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.get('submitDisabled')) { return; }
this.set('suspending', true);
- this.get('user').suspend({
- suspend_until: this.get('suspendUntil'),
- reason: this.get('reason'),
- message: this.get('message'),
- post_id: this.get('post.id')
- }).then(result => {
- this.send('closeModal');
- let callback = this.get('successCallback');
- if (callback) {
- callback(result);
- }
- }).catch(popupAjaxError).finally(() => this.set('suspending', false));
+
+ this.penalize(() => {
+ return this.get('user').suspend({
+ suspend_until: this.get('suspendUntil'),
+ reason: this.get('reason'),
+ message: this.get('message'),
+ post_id: this.get('post.id'),
+ post_action: this.get('postAction'),
+ post_edit: this.get('postEdit')
+ });
+ }).finally(() => this.set('suspending', false));
}
}
-
});
diff --git a/app/assets/javascripts/admin/mixins/penalty-controller.js.es6 b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6
new file mode 100644
index 00000000000..8b5bbb06297
--- /dev/null
+++ b/app/assets/javascripts/admin/mixins/penalty-controller.js.es6
@@ -0,0 +1,41 @@
+import ModalFunctionality from 'discourse/mixins/modal-functionality';
+import { popupAjaxError } from 'discourse/lib/ajax-error';
+
+export default Ember.Mixin.create(ModalFunctionality, {
+ reason: null,
+ message: null,
+ postEdit: null,
+ postAction: null,
+ user: null,
+ post: null,
+ successCallback: null,
+
+ resetModal() {
+ this.setProperties({
+ reason: null,
+ message: null,
+ loadingUser: true,
+ post: null,
+ postEdit: null,
+ postAction: 'delete',
+ before: null,
+ successCallback: null
+ });
+ },
+
+ penalize(cb) {
+ let before = this.get('before');
+ let promise = before ? before() : Ember.RSVP.resolve();
+
+ return promise
+ .then(() => cb())
+ .then(result => {
+ this.send('closeModal');
+ let callback = this.get('successCallback');
+ if (callback) {
+ callback(result);
+ }
+ })
+ .catch(popupAjaxError);
+ }
+});
diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6
index a6a56d0f145..9f9bec8b036 100644
--- a/app/assets/javascripts/admin/services/admin-tools.js.es6
+++ b/app/assets/javascripts/admin/services/admin-tools.js.es6
@@ -52,7 +52,10 @@ export default Ember.Service.extend({
modalClass: `${type}-user-modal`
});
if (opts.post) {
- controller.set('post', opts.post);
+ controller.setProperties({
+ post: opts.post,
+ postEdit: opts.post.get('raw')
+ });
}
return (user.adminUserView ?
@@ -62,6 +65,7 @@ export default Ember.Service.extend({
controller.setProperties({
user: loadedUser,
loadingUser: false,
+ before: opts.before,
successCallback: opts.successCallback
});
});
diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs
index 04b953c451f..37631f1442f 100644
--- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs
+++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs
@@ -68,12 +68,6 @@
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=showResolvedBy}}
- {{#if suspended}}
-
- {{i18n "admin.flags.suspended_for_post"}}
-
- {{/if}}
-
{{#if canAct}}
{{admin-agree-flag-dropdown
@@ -106,15 +100,6 @@
{{admin-delete-flag-dropdown
post=flaggedPost
removeAfter=(action "removeAfter")}}
-
- {{#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}}
{{d-button
diff --git a/app/assets/javascripts/admin/templates/components/penalty-post-action.hbs b/app/assets/javascripts/admin/templates/components/penalty-post-action.hbs
new file mode 100644
index 00000000000..1c8ffe56d8e
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/components/penalty-post-action.hbs
@@ -0,0 +1,16 @@
+
+
+ {{combo-box value=postAction content=penaltyActions onSelect=(action "penaltyChanged")}}
+
+
+{{#if editing}}
+
+ {{textarea
+ value=postEdit
+ class="post-editor"}}
+
+{{/if}}
diff --git a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
index 6b3b899ac83..b31251a91d5 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
@@ -12,6 +12,12 @@
{{silence-details reason=reason message=message}}
+ {{#if post}}
+ {{penalty-post-action
+ post=post
+ postAction=postAction
+ postEdit=postEdit}}
+ {{/if}}
{{/conditional-loading-spinner}}
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 4c0af11512e..6c2e8e1636a 100644
--- a/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
+++ b/app/assets/javascripts/admin/templates/modal/admin-suspend-user.hbs
@@ -13,6 +13,13 @@
{{suspension-details reason=reason message=message}}
+ {{#if post}}
+ {{penalty-post-action
+ post=post
+ postAction=postAction
+ postEdit=postEdit}}
+ {{/if}}
+
{{else}}
{{i18n "admin.user.cant_suspend"}}
diff --git a/app/assets/javascripts/discourse/lib/ajax-error.js.es6 b/app/assets/javascripts/discourse/lib/ajax-error.js.es6
index cb155ec2581..9666d9c378f 100644
--- a/app/assets/javascripts/discourse/lib/ajax-error.js.es6
+++ b/app/assets/javascripts/discourse/lib/ajax-error.js.es6
@@ -54,9 +54,11 @@ export function throwAjaxError(undoCallback) {
}
export function popupAjaxError(error) {
+ if (error && error._discourse_displayed) { return; }
bootbox.alert(extractError(error));
error._discourse_displayed = true;
+
// We re-throw in a catch to not swallow the exception
throw error;
}
diff --git a/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6
index 55708f10eaa..3020529ef65 100644
--- a/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6
+++ b/app/assets/javascripts/select-kit/components/admin-agree-flag-dropdown.js.es6
@@ -55,6 +55,22 @@ export default DropdownSelectBox.extend({
label: I18n.t("admin.flags.agree_flag"),
});
+ content.push({
+ icon: 'ban',
+ id: 'confirm-agree-suspend',
+ description: I18n.t('admin.flags.agree_flag_suspend_title'),
+ action: () => this.send("showSuspendModal"),
+ label: I18n.t("admin.flags.agree_flag_suspend"),
+ });
+
+ content.push({
+ icon: 'microphone-slash',
+ id: 'confirm-agree-silence',
+ description: I18n.t('admin.flags.agree_flag_silence_title'),
+ action: () => this.send("showSilenceModal"),
+ label: I18n.t("admin.flags.agree_flag_silence"),
+ });
+
if (canDeleteSpammer) {
content.push({
title: I18n.t("admin.flags.delete_spammer_title"),
@@ -79,6 +95,28 @@ export default DropdownSelectBox.extend({
this.attrs.removeAfter(spammerDetails.deleteUser());
},
+ showSuspendModal() {
+ let post = this.get('post');
+ let user = post.get('user');
+ this.get('adminTools').showSuspendModal(user, {
+ post,
+ before: () => {
+ return this.attrs.removeAfter(post.agreeFlags('suspended'));
+ }
+ });
+ },
+
+ showSilenceModal() {
+ let post = this.get('post');
+ let user = post.get('user');
+ this.get('adminTools').showSilenceModal(user, {
+ post,
+ before: () => {
+ return this.attrs.removeAfter(post.agreeFlags('silenced'));
+ }
+ });
+ },
+
perform(action) {
let flaggedPost = this.get("post");
this.attrs.removeAfter(flaggedPost.agreeFlags(action));
diff --git a/app/assets/stylesheets/common/admin/suspend.scss b/app/assets/stylesheets/common/admin/suspend.scss
index efdfb68f791..8e2ab667e1f 100644
--- a/app/assets/stylesheets/common/admin/suspend.scss
+++ b/app/assets/stylesheets/common/admin/suspend.scss
@@ -21,3 +21,13 @@
float: right;
}
}
+
+.modal-body {
+ .penalty-post-edit {
+ margin-top: 1em;
+
+ textarea {
+ height: 10em;
+ }
+ }
+}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 2095903ab6e..13aa89d3ca5 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -93,6 +93,8 @@ class Admin::UsersController < Admin::AdminController
suspended_at: DateTime.now
)
+ perform_post_action
+
render_json_dump(
suspension: {
suspended: true,
@@ -297,6 +299,7 @@ class Admin::UsersController < Admin::AdminController
user_history_id: silencer.user_history.id
)
end
+ perform_post_action
render_json_dump(
silence: {
@@ -467,6 +470,27 @@ class Admin::UsersController < Admin::AdminController
private
+ def perform_post_action
+ return unless params[:post_id].present? &&
+ params[:post_action].present?
+
+ if post = Post.where(id: params[:post_id]).first
+ case params[:post_action]
+ when 'delete'
+ PostDestroyer.new(current_user, post).destroy
+ when 'edit'
+ revisor = PostRevisor.new(post)
+
+ # Take what the moderator edited in as gospel
+ revisor.revise!(
+ current_user,
+ { raw: params[:post_edit] },
+ skip_validations: true, skip_revision: true
+ )
+ end
+ end
+ end
+
def fetch_user
@user = User.find_by(id: params[:user_id])
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index cbd757d0397..7784265fcfa 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -2695,7 +2695,12 @@ en:
agree_flag_hide_post_title: "Hide this post and automatically send the user a message urging them to edit it."
agree_flag_restore_post: "Agree and Restore Post"
agree_flag_restore_post_title: "Restore the post so that all users can see it."
- agree_flag: "Agree and Keep Post"
+ agree_flag_suspend: "Suspend User"
+ agree_flag_suspend_title: "Agree with flag and suspend the user."
+ agree_flag_silence: "Silence User"
+ agree_flag_silence_title: "Agree with flag and silence the user."
+
+ agree_flag: "Keep Post"
agree_flag_title: "Agree with flag and keep the post unchanged."
ignore_flag: "Ignore"
ignore_flag_title: "Remove this flag; it requires no action at this time."
@@ -2729,7 +2734,6 @@ en:
system: "System"
error: "Something went wrong"
reply_message: "Reply"
- suspended_for_post: "The user was suspended for this post."
no_results: "There are no flagged posts."
topic_flagged: "This topic has been flagged."
show_full: "show full post"
@@ -3392,6 +3396,10 @@ en:
suspended_until: "(until %{until})"
cant_suspend: "This user cannot be suspended."
delete_all_posts: "Delete all posts"
+ penalty_post_actions: "What would you like to do with the associated post?"
+ penalty_post_delete: "Delete the post"
+ penalty_post_edit: "Edit the post"
+ penalty_post_none: "Do nothing"
# keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details
delete_all_posts_confirm_MF: "You are about to delete {POSTS, plural, one {1 post} other {# posts}} and {TOPICS, plural, one {1 topic} other {# topics}}. Are you sure?"
diff --git a/lib/flag_query.rb b/lib/flag_query.rb
index 15b1591d3f7..cda69b8f913 100644
--- a/lib/flag_query.rb
+++ b/lib/flag_query.rb
@@ -31,6 +31,7 @@ module FlagQuery
posts = SqlBuilder.new("
SELECT p.id,
p.cooked,
+ p.raw,
p.user_id,
p.topic_id,
p.post_number,
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 80f9a865a00..e56fa93b3e6 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -148,24 +148,42 @@ 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,
- params: {
- user_id: user.id,
+ context "with an associated post" do
+ let(:post) { Fabricate(:post) }
+ let(:suspend_params) do
+ { 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
+ format: :json }
+ end
- 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)
+ it "can have an associated post" do
+ put(:suspend, params: suspend_params)
+ 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 delete an associated post" do
+ put(:suspend, params: suspend_params.merge(post_action: 'delete'))
+ post.reload
+ expect(post.deleted_at).to be_present
+ expect(response).to be_success
+ end
+
+ it "can edit an associated post" do
+ put(:suspend, params: suspend_params.merge(
+ post_action: 'edit',
+ post_edit: 'this is the edited content'
+ ))
+ post.reload
+ expect(post.deleted_at).to be_blank
+ expect(post.raw).to eq("this is the edited content")
+ expect(response).to be_success
+ end
end
it "can send a message to the user" do
diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6
index e64e823039e..3f38df94b18 100644
--- a/test/javascripts/acceptance/admin-flags-test.js.es6
+++ b/test/javascripts/acceptance/admin-flags-test.js.es6
@@ -104,15 +104,6 @@ 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");
andThen(() => {