mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 22:43:33 +08:00
Add Suspend User to flags page
This commit is contained in:
@ -1,7 +1,10 @@
|
|||||||
import showModal from 'discourse/lib/show-modal';
|
import showModal from 'discourse/lib/show-modal';
|
||||||
|
import computed from 'ember-addons/ember-computed-decorators';
|
||||||
|
|
||||||
export default Ember.Component.extend({
|
export default Ember.Component.extend({
|
||||||
|
adminTools: Ember.inject.service(),
|
||||||
expanded: false,
|
expanded: false,
|
||||||
|
suspended: false,
|
||||||
|
|
||||||
tagName: 'div',
|
tagName: 'div',
|
||||||
classNameBindings: [
|
classNameBindings: [
|
||||||
@ -10,6 +13,11 @@ export default Ember.Component.extend({
|
|||||||
'flaggedPost.deleted'
|
'flaggedPost.deleted'
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@computed('filter')
|
||||||
|
canAct(filter) {
|
||||||
|
return filter === 'active';
|
||||||
|
},
|
||||||
|
|
||||||
removeAfter(promise) {
|
removeAfter(promise) {
|
||||||
return promise.then(() => {
|
return promise.then(() => {
|
||||||
this.attrs.removePost();
|
this.attrs.removePost();
|
||||||
@ -44,6 +52,18 @@ export default Ember.Component.extend({
|
|||||||
this.get('flaggedPost').expandHidden().then(() => {
|
this.get('flaggedPost').expandHidden().then(() => {
|
||||||
this.set('expanded', true);
|
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,
|
suspendUntil: null,
|
||||||
reason: null,
|
reason: null,
|
||||||
message: null,
|
message: null,
|
||||||
loading: false,
|
suspending: false,
|
||||||
|
user: null,
|
||||||
|
post: null,
|
||||||
|
successCallback: null,
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
suspendUntil: null,
|
suspendUntil: null,
|
||||||
reason: null,
|
reason: null,
|
||||||
message: null,
|
message: null,
|
||||||
loading: false
|
suspending: false,
|
||||||
|
loadingUser: true,
|
||||||
|
post: null,
|
||||||
|
successCallback: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@computed('suspendUntil', 'reason', 'loading')
|
@computed('suspendUntil', 'reason', 'suspending')
|
||||||
submitDisabled(suspendUntil, reason, loading) {
|
submitDisabled(suspendUntil, reason, suspending) {
|
||||||
return (loading || Ember.isEmpty(suspendUntil) || !reason || reason.length < 1);
|
return (suspending || Ember.isEmpty(suspendUntil) || !reason || reason.length < 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
suspend() {
|
suspend() {
|
||||||
if (this.get('submitDisabled')) { return; }
|
if (this.get('submitDisabled')) { return; }
|
||||||
|
|
||||||
this.set('loading', true);
|
this.set('suspending', true);
|
||||||
this.get('model').suspend({
|
this.get('user').suspend({
|
||||||
suspend_until: this.get('suspendUntil'),
|
suspend_until: this.get('suspendUntil'),
|
||||||
reason: this.get('reason'),
|
reason: this.get('reason'),
|
||||||
message: this.get('message')
|
message: this.get('message'),
|
||||||
}).then(() => {
|
post_id: this.get('post.id')
|
||||||
|
}).then(result => {
|
||||||
this.send('closeModal');
|
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';
|
import { userPath } from 'discourse/lib/url';
|
||||||
|
|
||||||
const AdminUser = Discourse.User.extend({
|
const AdminUser = Discourse.User.extend({
|
||||||
|
adminUserView: true,
|
||||||
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
|
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
|
||||||
automaticGroups: 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) {
|
showSuspendModal(user, opts) {
|
||||||
showModal('admin-suspend-user', {
|
opts = opts || {};
|
||||||
model: user,
|
|
||||||
|
let controller = showModal('admin-suspend-user', {
|
||||||
admin: true,
|
admin: true,
|
||||||
modalClass: 'suspend-user-modal'
|
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) {
|
_deleteSpammer(adminUser) {
|
||||||
|
@ -97,6 +97,12 @@
|
|||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{#if suspended}}
|
||||||
|
<div class='suspended-message'>
|
||||||
|
The user was suspended for this post.
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if canAct}}
|
{{#if canAct}}
|
||||||
<div class='flagged-post-controls'>
|
<div class='flagged-post-controls'>
|
||||||
{{d-button
|
{{d-button
|
||||||
@ -136,6 +142,15 @@
|
|||||||
action="showDeleteFlagModal"
|
action="showDeleteFlagModal"
|
||||||
icon="trash-o"
|
icon="trash-o"
|
||||||
label="admin.flags.delete"}}
|
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>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
{{#each flaggedPosts as |flaggedPost|}}
|
{{#each flaggedPosts as |flaggedPost|}}
|
||||||
{{flagged-post
|
{{flagged-post
|
||||||
flaggedPost=flaggedPost
|
flaggedPost=flaggedPost
|
||||||
canAct=canAct
|
filter=filter
|
||||||
showResolvedBy=showResolvedBy
|
showResolvedBy=showResolvedBy
|
||||||
removePost=(action "removePost" flaggedPost)
|
removePost=(action "removePost" flaggedPost)
|
||||||
hideTitle=topic}}
|
hideTitle=topic}}
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
{{#d-modal-body title="admin.user.suspend_modal_title"}}
|
{{#d-modal-body title="admin.user.suspend_modal_title"}}
|
||||||
|
{{#conditional-loading-spinner condition=loadingUser}}
|
||||||
|
|
||||||
|
{{#if user.canSuspend}}
|
||||||
<div class='until-controls'>
|
<div class='until-controls'>
|
||||||
<label>
|
<label>
|
||||||
{{future-date-input
|
{{future-date-input
|
||||||
@ -34,6 +37,13 @@
|
|||||||
class="suspend-message"
|
class="suspend-message"
|
||||||
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
|
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
|
||||||
</label>
|
</label>
|
||||||
|
{{else}}
|
||||||
|
<div class='cant-suspend'>
|
||||||
|
{{i18n "admin.user.cant_suspend"}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{/conditional-loading-spinner}}
|
||||||
|
|
||||||
{{/d-modal-body}}
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
@ -47,4 +47,5 @@
|
|||||||
icon="exclamation-triangle"
|
icon="exclamation-triangle"
|
||||||
label="flagging.delete_spammer"}}
|
label="flagging.delete_spammer"}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,9 +98,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.suspended-message {
|
||||||
|
padding: 0.5em;
|
||||||
|
background-color: $danger;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
color: $secondary;
|
||||||
|
}
|
||||||
|
|
||||||
.flagged-post-message {
|
.flagged-post-message {
|
||||||
padding: 0.5em 0 0.5em 4em;
|
padding: 0.5em 1em;
|
||||||
margin-bottom: 0.5em;
|
margin: 0.5em 0;
|
||||||
background-color: $highlight-medium;
|
background-color: $highlight-medium;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
@ -67,7 +67,8 @@ class Admin::UsersController < Admin::AdminController
|
|||||||
user_history = StaffActionLogger.new(current_user).log_user_suspend(
|
user_history = StaffActionLogger.new(current_user).log_user_suspend(
|
||||||
@user,
|
@user,
|
||||||
params[:reason],
|
params[:reason],
|
||||||
context: message
|
context: message,
|
||||||
|
post_id: params[:post_id]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@user.logged_out
|
@user.logged_out
|
||||||
|
@ -132,7 +132,6 @@ module HasCustomFields
|
|||||||
if @preloaded.key?(key)
|
if @preloaded.key?(key)
|
||||||
@preloaded[key]
|
@preloaded[key]
|
||||||
else
|
else
|
||||||
raise "#{@preloaded.inspect} -> #{key.inspect}"
|
|
||||||
# for now you can not mix preload an non preload, it better just to fail
|
# 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."
|
raise StandardError, "Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries."
|
||||||
end
|
end
|
||||||
|
@ -169,9 +169,13 @@ class StaffActionLogger
|
|||||||
|
|
||||||
def log_user_suspend(user, reason, opts = {})
|
def log_user_suspend(user, reason, opts = {})
|
||||||
raise Discourse::InvalidParameters.new(:user) unless user
|
raise Discourse::InvalidParameters.new(:user) unless user
|
||||||
UserHistory.create(params(opts).merge(action: UserHistory.actions[:suspend_user],
|
args = params(opts).merge(
|
||||||
|
action: UserHistory.actions[:suspend_user],
|
||||||
target_user_id: user.id,
|
target_user_id: user.id,
|
||||||
details: reason))
|
details: reason
|
||||||
|
)
|
||||||
|
args[:post_id] = opts[:post_id] if opts[:post_id]
|
||||||
|
UserHistory.create(args)
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_user_unsuspend(user, opts = {})
|
def log_user_unsuspend(user, opts = {})
|
||||||
|
@ -2627,6 +2627,8 @@ en:
|
|||||||
clear_topic_flags: "Done"
|
clear_topic_flags: "Done"
|
||||||
clear_topic_flags_title: "The topic has been investigated and issues have been resolved. Click Done to remove the flags."
|
clear_topic_flags_title: "The topic has been investigated and issues have been resolved. Click Done to remove the flags."
|
||||||
more: "(more replies...)"
|
more: "(more replies...)"
|
||||||
|
suspend_user: "Suspend User"
|
||||||
|
suspend_user_title: "Suspend user for this post"
|
||||||
|
|
||||||
dispositions:
|
dispositions:
|
||||||
agreed: "agreed"
|
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."
|
suspend_message_placeholder: "Optionally, provide more information about the suspension and it will be emailed to the user."
|
||||||
suspended_by: "Suspended by"
|
suspended_by: "Suspended by"
|
||||||
suspended_until: "(until %{until})"
|
suspended_until: "(until %{until})"
|
||||||
|
cant_suspend: "This user cannot be suspended."
|
||||||
delete_all_posts: "Delete all posts"
|
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
|
# 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
|
context '.suspend' do
|
||||||
let(:user) { Fabricate(:evil_trout) }
|
let(:user) { Fabricate(:evil_trout) }
|
||||||
let!(:api_key) { Fabricate(:api_key, user: user) }
|
|
||||||
|
|
||||||
it "works properly" do
|
it "works properly" do
|
||||||
|
Fabricate(:api_key, user: user)
|
||||||
put(
|
put(
|
||||||
:suspend,
|
:suspend,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
@ -144,6 +144,24 @@ describe Admin::UsersController do
|
|||||||
expect(log.details).to match(/because I said so/)
|
expect(log.details).to match(/because I said so/)
|
||||||
end
|
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
|
it "can send a message to the user" do
|
||||||
Jobs.expects(:enqueue).with(
|
Jobs.expects(:enqueue).with(
|
||||||
:critical_user_email,
|
: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 => {
|
QUnit.test("topics with flags", assert => {
|
||||||
visit("/admin/flags/topics");
|
visit("/admin/flags/topics");
|
||||||
|
@ -63,3 +63,4 @@ QUnit.test("suspend, then unsuspend a user", assert => {
|
|||||||
assert.ok(!exists('.suspension-info'));
|
assert.ok(!exists('.suspension-info'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -350,13 +350,21 @@ export default function() {
|
|||||||
|
|
||||||
this.get('/tag_groups', () => response(200, {tag_groups: []}));
|
this.get('/tag_groups', () => response(200, {tag_groups: []}));
|
||||||
|
|
||||||
this.get('/admin/users/1234.json', request => {
|
this.get('/admin/users/1234.json', () => {
|
||||||
return response(200, {
|
return response(200, {
|
||||||
id: 1234,
|
id: 1234,
|
||||||
username: 'regular',
|
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.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/revoke_api_key', success);
|
||||||
this.delete('/admin/users/:user_id.json', () => response(200, { deleted: true }));
|
this.delete('/admin/users/:user_id.json', () => response(200, { deleted: true }));
|
||||||
|
Reference in New Issue
Block a user