FEATURE: Support an end date for user silencing

This commit is contained in:
Robin Ward 2017-11-13 13:41:36 -05:00
parent 52480d554a
commit 971e302ff2
33 changed files with 456 additions and 114 deletions

View File

@ -70,7 +70,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
unsuspend() {
this.get("model").unsuspend().catch(popupAjaxError);
},
showSilenceModal() {
this.get('adminTools').showSilenceModal(this.get('model'));
},
toggleUsernameEdit() {
this.set('userUsernameValue', this.get('model.username'));

View File

@ -0,0 +1,50 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend(ModalFunctionality, {
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,
});
},
@computed('silenceUntil', 'reason', 'silencing')
submitDisabled(silenceUntil, reason, silencing) {
return (silencing || Ember.isEmpty(silenceUntil) || !reason || reason.length < 1);
},
actions: {
silence() {
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));
}
}
});

View File

@ -8,6 +8,8 @@ import Group from 'discourse/models/group';
import TL3Requirements from 'admin/models/tl3-requirements';
import { userPath } from 'discourse/lib/url';
const wrapAdmin = user => user ? AdminUser.create(user) : null;
const AdminUser = Discourse.User.extend({
adminUserView: true,
customGroups: Ember.computed.filter("groups", g => !g.automatic && Group.create(g)),
@ -232,6 +234,7 @@ const AdminUser = Discourse.User.extend({
}.property('trust_level'),
isSuspended: Em.computed.equal('suspended', true),
isSilenced: Ember.computed.equal('silenced', true),
canSuspend: Em.computed.not('staff'),
suspendDuration: function() {
@ -301,44 +304,36 @@ const AdminUser = Discourse.User.extend({
unsilence() {
this.set('silencingUser', true);
return ajax('/admin/users/' + this.id + '/unsilence', {
return ajax(`/admin/users/${this.id}/unsilence`, {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.unsilence_failed', { error: "http: " + e.status + " - " + e.body });
}).then(result => {
this.setProperties(result.unsilence);
}).catch(e => {
let error = I18n.t('admin.user.unsilence_failed', {
error: `http: ${e.status} - ${e.body}`
});
bootbox.alert(error);
}).finally(() => {
this.set('silencingUser', false);
});
},
silence() {
const user = this,
message = I18n.t("admin.user.silence_confirm");
const performSilence = function() {
user.set('silencingUser', true);
return ajax('/admin/users/' + user.id + '/silence', {
type: 'PUT'
}).then(function() {
window.location.reload();
}).catch(function(e) {
var error = I18n.t('admin.user.silence_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
user.set('silencingUser', false);
silence(data) {
this.set('silencingUser', true);
return ajax(`/admin/users/${this.id}/silence`, {
type: 'PUT',
data
}).then(result => {
this.setProperties(result.silence);
}).catch(e => {
let error = I18n.t('admin.user.silence_failed', {
error: `http: ${e.status} - ${e.body}`
});
bootbox.alert(error);
}).finally(() => {
this.set('silencingUser', false);
});
};
const buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
}, {
"label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.silence_accept'),
"class": "btn btn-danger",
"callback": function() { performSilence(); }
}];
bootbox.dialog(message, buttons, { "classes": "delete-user-modal" });
},
sendActivationEmail() {
@ -475,17 +470,14 @@ const AdminUser = Discourse.User.extend({
}
}.property('tl3_requirements'),
suspendedBy: function() {
if (this.get('suspended_by')) {
return AdminUser.create(this.get('suspended_by'));
}
}.property('suspended_by'),
@computed('suspended_by')
suspendedBy: wrapAdmin,
approvedBy: function() {
if (this.get('approved_by')) {
return AdminUser.create(this.get('approved_by'));
}
}.property('approved_by')
@computed('silenced_by')
silencedBy: wrapAdmin,
@computed('approved_by')
approvedBy: wrapAdmin,
});

View File

@ -20,12 +20,12 @@ export default Ember.Service.extend({
};
},
showSuspendModal(user, opts) {
_showControlModal(type, user, opts) {
opts = opts || {};
let controller = showModal('admin-suspend-user', {
let controller = showModal(`admin-${type}-user`, {
admin: true,
modalClass: 'suspend-user-modal'
modalClass: `${type}-user-modal`
});
if (opts.post) {
controller.set('post', opts.post);
@ -44,6 +44,14 @@ export default Ember.Service.extend({
});
},
showSilenceModal(user, opts) {
this._showControlModal('silence', user, opts);
},
showSuspendModal(user, opts) {
this._showControlModal('suspend', user, opts);
},
_deleteSpammer(adminUser) {
return adminUser.checkEmail().then(() => {

View File

@ -0,0 +1,50 @@
{{#d-modal-body title="admin.user.silence_modal_title"}}
{{#conditional-loading-spinner condition=loadingUser}}
<div class='until-controls'>
<label>
{{future-date-input
class="silence-until"
label="admin.user.silence_duration"
includeFarFuture=true
input=silenceUntil}}
</label>
</div>
<div class='reason-controls'>
<label>
<div class='silence-reason-label'>
{{{i18n 'admin.user.silence_reason_label'}}}
</div>
{{text-field
value=reason
class="silence-reason"
placeholderKey="admin.user.silence_reason_placeholder"}}
</label>
</div>
<label>
<div class='silence-message-label'>
{{i18n "admin.user.silence_message"}}
</div>
{{textarea
value=message
class="silence-message"
placeholder=(i18n "admin.user.silence_message_placeholder")}}
</label>
{{/conditional-loading-spinner}}
{{/d-modal-body}}
<div class="modal-footer">
{{d-button
class="btn-danger perform-silence"
action="silence"
disabled=submitDisabled
icon="microphone-slash"
label="admin.user.silence"}}
{{d-modal-cancel close=(action "closeModal")}}
{{conditional-loading-spinner condition=loading size="small"}}
</div>

View File

@ -349,19 +349,49 @@
<div class="display-row {{if model.silenced 'highlight-danger'}}">
<div class='field'>{{i18n 'admin.user.silenced'}}</div>
<div class='value'>{{i18n-yes-no model.silenced}}</div>
<div class='value'>
{{i18n-yes-no model.silenced}}
{{#if model.isSilenced}}
{{#unless model.silencedForever}}
{{i18n "admin.user.suspended_until" until=model.silencedTillDate}}
{{/unless}}
{{/if}}
</div>
<div class='controls'>
{{#conditional-loading-spinner size="small" condition=model.silencingUser}}
{{#if model.silenced}}
{{d-button action="unsilence" icon="thumbs-o-up" label="admin.user.unsilence"}}
{{d-button
class="btn-danger unsilence-user"
action="unsilence"
icon="microphone-slash"
label="admin.user.unsilence"}}
{{i18n 'admin.user.silence_explanation'}}
{{else}}
{{d-button action="silence" icon="ban" label="admin.user.silence"}}
{{d-button
class="btn-danger silence-user"
action=(action "showSilenceModal")
icon="microphone-slash"
label="admin.user.silence"}}
{{i18n 'admin.user.silence_explanation'}}
{{/if}}
{{/conditional-loading-spinner}}
</div>
</div>
{{#if model.isSilenced}}
<div class='display-row highlight-danger silence-info'>
<div class='field'>{{i18n 'admin.user.silenced_by'}}</div>
<div class='value'>
{{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
{{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}}
</div>
<div class='controls'>
<b>{{i18n 'admin.user.silence_reason'}}</b>:
{{model.silence_reason}}
</div>
</div>
{{/if}}
</section>
{{#if currentUser.admin}}

View File

@ -16,6 +16,8 @@ import PreloadStore from 'preload-store';
import { defaultHomepage } from 'discourse/lib/utilities';
import { userPath } from 'discourse/lib/url';
const isForever = dt => moment().diff(dt, 'years') < -500;
const User = RestModel.extend({
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
@ -178,14 +180,16 @@ const User = RestModel.extend({
},
@computed("suspended_till")
suspendedForever(suspendedTill) {
return moment().diff(suspendedTill, 'years') < -500;
},
suspendedForever: isForever,
@computed("silenced_till")
silencedForever: isForever,
@computed("suspended_till")
suspendedTillDate(suspendedTill) {
return longDate(suspendedTill);
},
suspendedTillDate: longDate,
@computed("silenced_till")
silencedTillDate: longDate,
changeUsername(new_username) {
return ajax(userPath(`${this.get('username_lower')}/preferences/username`), {

View File

@ -274,14 +274,48 @@ class Admin::UsersController < Admin::AdminController
def silence
guardian.ensure_can_silence_user! @user
UserSilencer.silence(@user, current_user, keep_posts: true)
render body: nil
message = params[:message]
silencer = UserSilencer.new(
@user,
current_user,
silenced_till: params[:silenced_till],
reason: params[:reason],
context: message,
keep_posts: true
)
if silencer.silence && message.present?
Jobs.enqueue(
:critical_user_email,
type: :account_silenced,
user_id: @user.id,
user_history_id: silencer.user_history.id
)
end
render_json_dump(
silence: {
silenced: true,
silence_reason: params[:reason],
silenced_till: @user.silenced_till,
suspended_at: @user.silenced_at
}
)
end
def unsilence
guardian.ensure_can_unsilence_user! @user
UserSilencer.unsilence(@user, current_user)
render body: nil
render_json_dump(
unsilence: {
silenced: false,
silence_reason: nil,
silenced_till: nil,
suspended_at: nil
}
)
end
def reject_bulk

View File

@ -10,7 +10,7 @@ module Jobs
HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new(
user_archive: ['topic_title', 'category', 'sub_category', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'],
user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'silenced', 'active', 'admin', 'moderator', 'ip_address', 'staged'],
user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'silenced_till', 'active', 'admin', 'moderator', 'ip_address', 'staged'],
user_stats: ['topics_entered', 'posts_read_count', 'time_read', 'topic_count', 'post_count', 'likes_given', 'likes_received'],
user_profile: ['location', 'website', 'views'],
user_sso: ['external_id', 'external_email', 'external_username', 'external_name', 'external_avatar_url'],
@ -181,7 +181,7 @@ module Jobs
def get_base_user_array(user)
user_array = []
user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views)
user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced_till, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views)
end
def add_single_sign_on(user, user_info_array)

View File

@ -22,7 +22,7 @@ module Jobs
ub.badge_id = #{Badge::Anniversary} AND
ub.granted_at BETWEEN '#{fmt_start_date}' AND '#{fmt_end_date}'
WHERE u.active AND
NOT u.silenced AND
u.silenced_till IS NULL AND
NOT p.hidden AND
p.deleted_at IS NULL AND
t.visible AND

View File

@ -68,6 +68,21 @@ class UserNotifications < ActionMailer::Base
email_token: opts[:email_token])
end
def account_silenced(user, opts = nil)
opts ||= {}
return unless user_history = opts[:user_history]
build_email(
user.email,
template: "user_notifications.account_silenced",
locale: user_locale(user),
reason: user_history.details,
message: user_history.context,
silenced_till: I18n.l(user.silenced_till, format: :long)
)
end
def account_suspended(user, opts = nil)
opts ||= {}

View File

@ -82,7 +82,7 @@ class DirectoryItem < ActiveRecord::Base
LEFT OUTER JOIN posts AS p ON ua.target_post_id = p.id
LEFT OUTER JOIN categories AS c ON t.category_id = c.id
WHERE u.active
AND NOT u.silenced
AND u.silenced_till IS NULL
AND t.deleted_at IS NULL
AND COALESCE(t.visible, true)
AND p.deleted_at IS NULL

View File

@ -147,8 +147,8 @@ class User < ActiveRecord::Base
# TODO-PERF: There is no indexes on any of these
# and NotifyMailingListSubscribers does a select-all-and-loop
# may want to create an index on (active, silence, suspended_till)?
scope :silenced, -> { where(silenced: true) }
scope :not_silenced, -> { where(silenced: false) }
scope :silenced, -> { where("silenced_till IS NOT NULL AND silenced_till > ?", Time.zone.now) }
scope :not_silenced, -> { where("silenced_till IS NULL OR silenced_till <= ?", Time.zone.now) }
scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) }
scope :not_suspended, -> { where('suspended_till IS NULL OR suspended_till <= ?', Time.zone.now) }
scope :activated, -> { where(active: true) }
@ -660,6 +660,22 @@ class User < ActiveRecord::Base
!!(suspended_till && suspended_till > DateTime.now)
end
def silenced?
!!(silenced_till && silenced_till > DateTime.now)
end
def silenced_record
UserHistory.for(self, :silence_user).order('id DESC').first
end
def silence_reason
silenced_record.try(:details) if silenced?
end
def silenced_at
silenced_record.try(:created_at) if silenced?
end
def suspend_record
UserHistory.for(self, :suspend_user).order('id DESC').first
end

View File

@ -18,6 +18,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:can_be_anonymized,
:suspend_reason,
:suspended_till,
:silence_reason,
:primary_group_id,
:badge_count,
:warnings_received_count,
@ -29,6 +30,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
has_one :api_key, serializer: ApiKeySerializer, embed: :objects
has_one :suspended_by, serializer: BasicUserSerializer, embed: :objects
has_one :silenced_by, serializer: BasicUserSerializer, embed: :objects
has_one :tl3_requirements, serializer: TrustLevel3RequirementsSerializer, embed: :objects
has_many :groups, embed: :object, serializer: BasicGroupSerializer
@ -72,6 +74,14 @@ class AdminDetailedUserSerializer < AdminUserSerializer
object.suspend_record.try(:acting_user)
end
def silence_reason
object.silence_reason
end
def silenced_by
object.silenced_record.try(:acting_user)
end
def include_tl3_requirements?
object.has_trust_level?(TrustLevel[2])
end

View File

@ -23,6 +23,7 @@ class AdminUserListSerializer < BasicUserSerializer
:suspended_till,
:suspended,
:silenced,
:silenced_till,
:time_read,
:staged
@ -40,6 +41,22 @@ class AdminUserListSerializer < BasicUserSerializer
alias_method :include_associated_accounts?, :include_email?
def silenced
object.silenced?
end
def include_silenced?
object.silenced?
end
def silenced_till
object.silenced_till
end
def include_silenced_till?
object.silenced_till?
end
def suspended
object.suspended?
end

View File

@ -275,8 +275,13 @@ class StaffActionLogger
def log_silence_user(user, opts = {})
raise Discourse::InvalidParameters.new(:user) unless user
UserHistory.create(params(opts).merge(action: UserHistory.actions[:silence_user],
target_user_id: user.id))
UserHistory.create(
params(opts).merge(
action: UserHistory.actions[:silence_user],
target_user_id: user.id,
details: opts[:details]
)
)
end
def log_unsilence_user(user, opts = {})

View File

@ -1,5 +1,7 @@
class UserSilencer
attr_reader :user_history
def initialize(user, by_user = nil, opts = {})
@user, @by_user, @opts = user, by_user, opts
end
@ -14,14 +16,26 @@ class UserSilencer
def silence
hide_posts unless @opts[:keep_posts]
unless @user.silenced?
@user.silenced = true
unless @user.silenced_till.present?
@user.silenced_till = @opts[:silenced_till] || 1000.years.from_now
if @user.save
message_type = @opts[:message] || :silenced_by_staff
post = SystemMessage.create(@user, message_type)
if post && @by_user
StaffActionLogger.new(@by_user).log_silence_user(@user, context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}")
if @opts[:context].present?
context = @opts[:context]
else
context = "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}"
SystemMessage.create(@user, message_type)
end
if @by_user
@user_history = StaffActionLogger.new(@by_user).log_silence_user(
@user,
context: context,
details: @opts[:reason]
)
end
return true
end
else
false
@ -37,7 +51,7 @@ class UserSilencer
end
def unsilence
@user.silenced = false
@user.silenced_till = nil
if @user.save
SystemMessage.create(@user, :unsilenced)
StaffActionLogger.new(@by_user).log_unsilence_user(@user) if @by_user

View File

@ -3281,6 +3281,14 @@ en:
suspend_message: "Email Message"
suspend_message_placeholder: "Optionally, provide more information about the suspension and it will be emailed to the user."
suspended_by: "Suspended by"
silence_reason: "Reason"
silenced_by: "Silenced By"
silence_modal_title: "Silence User"
silence_duration: "How long will the user be silenced for?"
silence_reason_label: "Why are you silencing this user?"
silence_reason_placeholder: "Silence Reason"
silence_message: "Email Message"
silence_message_placeholder: "(leave blank to send default message)"
suspended_until: "(until %{until})"
cant_suspend: "This user cannot be suspended."
delete_all_posts: "Delete all posts"

View File

@ -2660,6 +2660,17 @@ en:
%{message}
account_silenced:
title: "Account Silenced"
subject_template: "[%{email_prefix}] Your account has been silenced"
text_body_template: |
You have been silenced from the forum until %{silenced_till}.
%{reason}
%{message}
account_exists:
title: "Account already exists"
subject_template: "[%{email_prefix}] Account already exists"

View File

@ -72,6 +72,17 @@ ColumnDropper.drop(
}
)
ColumnDropper.drop(
table: 'users',
after_migration: 'AddSilencedTillToUsers',
columns: %w[
silenced
],
on_drop: ->() {
STDERR.puts 'Removing user silenced column!'
}
)
# User for the smoke tests
if ENV["SMOKE"] == "1"
UserEmail.seed do |ue|

View File

@ -0,0 +1,14 @@
class AddSilencedTillToUsers < ActiveRecord::Migration[5.1]
def up
add_column :users, :silenced_till, :timestamp, null: true
execute <<~SQL
UPDATE users
SET silenced_till = CURRENT_TIMESTAMP + INTERVAL '1000 YEAR'
WHERE silenced
SQL
end
def down
add_column :users, :silenced_till
end
end

View File

@ -141,10 +141,10 @@ SQL
SELECT invited_by_id
FROM invites i
JOIN users u2 ON u2.id = i.user_id
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.silenced
WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND u2.silenced_till IS NULL
GROUP BY invited_by_id
HAVING COUNT(*) >= #{count.to_i}
) AND u.active AND NOT u.silenced AND u.id > 0 AND
) AND u.active AND u.silenced_till IS NULL AND u.id > 0 AND
(:backfill OR u.id IN (:user_ids) )
"
end

View File

@ -167,7 +167,7 @@ describe AdminUserIndexQuery do
describe "with a silenced user" do
let!(:user) { Fabricate(:user, silenced: true) }
let!(:user) { Fabricate(:user, silenced_till: 1.year.from_now) }
it "finds the silenced user" do
query = ::AdminUserIndexQuery.new(query: 'silenced')

View File

@ -57,7 +57,7 @@ describe Email::Receiver do
end
it "raises a SilencedUserError when the sender has been silenced" do
Fabricate(:user, email: "silenced@bar.com", silenced: true)
Fabricate(:user, email: "silenced@bar.com", silenced_till: 1.year.from_now)
expect { process(:silenced_sender) }.to raise_error(Email::Receiver::SilencedUserError)
end

View File

@ -195,7 +195,7 @@ describe Guardian do
context "author is silenced" do
before do
user.silenced = true
user.silenced_till = 1.year.from_now
user.save
end
@ -853,13 +853,13 @@ describe Guardian do
end
it "allows new posts from silenced users included in the pm" do
user.update_attribute(:silenced, true)
user.update_attribute(:silenced_till, 1.year.from_now)
private_message.topic_allowed_users.create!(user_id: user.id)
expect(Guardian.new(user).can_create?(Post, private_message)).to be_truthy
end
it "doesn't allow new posts from silenced users not invited to the pm" do
user.update_attribute(:silenced, true)
user.update_attribute(:silenced_till, 1.year.from_now)
expect(Guardian.new(user).can_create?(Post, private_message)).to be_falsey
end
end
@ -1376,7 +1376,7 @@ describe Guardian do
context 'when user is silenced' do
it 'returns false' do
user.toggle!(:silenced)
user.update_column(:silenced_till, 1.year.from_now)
expect(Guardian.new(user).can_moderate?(post)).to be(false)
expect(Guardian.new(user).can_moderate?(topic)).to be(false)
end

View File

@ -495,15 +495,7 @@ describe Admin::UsersController do
end
context '.destroy' do
before do
@delete_me = Fabricate(:user)
end
it "raises an error when the user doesn't have permission" do
Guardian.any_instance.expects(:can_delete_user?).with(@delete_me).returns(false)
delete :destroy, params: { id: @delete_me.id }, format: :json
expect(response).to be_forbidden
end
let(:delete_me) { Fabricate(:user) }
it "returns a 403 if the user doesn't exist" do
delete :destroy, params: { id: 123123 }, format: :json
@ -511,31 +503,26 @@ describe Admin::UsersController do
end
context "user has post" do
let(:topic) { create_topic(user: delete_me) }
before do
@user = Fabricate(:user)
topic = create_topic(user: @user)
_post = create_post(topic: topic, user: @user)
@user.stubs(:first_post_created_at).returns(Time.zone.now)
User.expects(:find_by).with(id: @delete_me.id).returns(@user)
_post = create_post(topic: topic, user: delete_me)
end
it "returns an error" do
delete :destroy, params: { id: @delete_me.id }, format: :json
delete :destroy, params: { id: delete_me.id }, format: :json
expect(response).to be_forbidden
end
it "doesn't return an error if delete_posts == true" do
UserDestroyer.any_instance.expects(:destroy).with(@user, has_entry('delete_posts' => true)).returns(true)
delete :destroy, params: { id: @delete_me.id, delete_posts: true }, format: :json
delete :destroy, params: { id: delete_me.id, delete_posts: true }, format: :json
expect(response).to be_success
end
end
it "deletes the user record" do
UserDestroyer.any_instance.expects(:destroy).returns(true)
delete :destroy, params: { id: @delete_me.id }, format: :json
delete :destroy, params: { id: delete_me.id }, format: :json
end
end
@ -590,9 +577,10 @@ describe Admin::UsersController do
it "raises an error when the user doesn't have permission" do
Guardian.any_instance.expects(:can_silence_user?).with(@reg_user).returns(false)
UserSilencer.expects(:silence).never
put :silence, params: { user_id: @reg_user.id }, format: :json
expect(response).to be_forbidden
@reg_user.reload
expect(@reg_user).not_to be_silenced
end
it "returns a 403 if the user doesn't exist" do
@ -601,8 +589,43 @@ describe Admin::UsersController do
end
it "punishes the user for spamming" do
UserSilencer.expects(:silence).with(@reg_user, @user, anything)
put :silence, params: { user_id: @reg_user.id }, format: :json
expect(response).to be_success
@reg_user.reload
expect(@reg_user).to be_silenced
end
it "will set a length of time if provided" do
future_date = 1.month.from_now.to_date
put(
:silence,
params: {
user_id: @reg_user.id,
silenced_till: future_date
},
format: :json
)
@reg_user.reload
expect(@reg_user.silenced_till).to eq(future_date)
end
it "will send a message if provided" do
Jobs.expects(:enqueue).with(
:critical_user_email,
has_entries(
type: :account_silenced,
user_id: @reg_user.id
)
)
put(
:silence,
params: {
user_id: @reg_user.id,
message: "Email this to the user"
},
format: :json
)
end
end

View File

@ -710,7 +710,7 @@ describe PostsController do
expect(parsed["action"]).to eq("enqueued")
user.reload
expect(user.silenced).to eq(true)
expect(user).to be_silenced
qp = QueuedPost.first
@ -718,7 +718,7 @@ describe PostsController do
qp.approve!(mod)
user.reload
expect(user.silenced).to eq(false)
expect(user).not_to be_silenced
end
it "doesn't enqueue replies when the topic is closed" do
@ -763,7 +763,7 @@ describe PostsController do
expect(parsed["action"]).to eq("enqueued")
user.reload
expect(user.silenced).to eq(true)
expect(user).to be_silenced
end
it "can send a message to a group" do

View File

@ -24,7 +24,7 @@ describe Jobs::GrantAnniversaryBadges do
end
it "doesn't award to a silenced user" do
user = Fabricate(:user, created_at: 400.days.ago, silenced: true)
user = Fabricate(:user, created_at: 400.days.ago, silenced_till: 1.year.from_now)
Fabricate(:post, user: user, created_at: 1.week.ago)
granter.execute({})

View File

@ -66,7 +66,7 @@ describe Jobs::NotifyMailingListSubscribers do
end
context "to a silenced user" do
before { mailing_list_user.update(silenced: true) }
before { mailing_list_user.update(silenced_till: 1.year.from_now) }
include_examples "no emails"
end

View File

@ -677,7 +677,7 @@ describe Topic do
context "when moderator post fails to be created" do
before do
user.toggle!(:silenced)
user.update_column(:silenced_till, 1.year.from_now)
end
it "should not increment moderator_posts_count" do
@ -833,7 +833,7 @@ describe Topic do
it_should_behave_like 'a status that closes a topic'
context 'topic was set to close when it was created' do
it 'puts the autoclose duration in the moderator post' do
it 'includes the autoclose duration in the moderator post' do
freeze_time(Time.new(2000, 1, 1))
@topic.created_at = 3.days.ago
@topic.update_status(status, true, @user)
@ -842,7 +842,7 @@ describe Topic do
end
context 'topic was set to close after it was created' do
it 'puts the autoclose duration in the moderator post' do
it 'includes the autoclose duration in the moderator post' do
freeze_time(Time.new(2000, 1, 1))
@topic.created_at = 7.days.ago

View File

@ -1532,4 +1532,32 @@ describe User do
])
end
end
describe "silenced?" do
it "is not silenced by default" do
expect(Fabricate(:user)).not_to be_silenced
end
it "is not silenced with a date in the past" do
expect(Fabricate(:user, silenced_till: 1.month.ago)).not_to be_silenced
end
it "is is silenced with a date in the future" do
expect(Fabricate(:user, silenced_till: 1.month.from_now)).to be_silenced
end
context "finders" do
let!(:user0) { Fabricate(:user, silenced_till: 1.month.ago) }
let!(:user1) { Fabricate(:user, silenced_till: 1.month.from_now) }
it "doesn't return old silenced records" do
expect(User.silenced).to_not include(user0)
expect(User.silenced).to include(user1)
expect(User.not_silenced).to include(user0)
expect(User.not_silenced).to_not include(user1)
end
end
end
end

View File

@ -275,7 +275,7 @@ describe SpamRule::AutoSilence do
end
context "silenced, but has higher trust level now" do
let(:user) { Fabricate(:user, silenced: true, trust_level: TrustLevel[1]) }
let(:user) { Fabricate(:user, silenced_till: 1.year.from_now, trust_level: TrustLevel[1]) }
subject { described_class.new(user) }
it 'returns false' do

View File

@ -7,7 +7,7 @@ describe UserSilencer do
end
describe 'silence' do
let(:user) { stub_everything(save: true) }
let(:user) { Fabricate(:user) }
let(:silencer) { UserSilencer.new(user) }
subject(:silence_user) { silencer.silence }
@ -53,7 +53,7 @@ describe UserSilencer do
end
it "doesn't send a pm if the user is already silenced" do
user.stubs(:silenced?).returns(true)
user.silenced_till = 1.year.from_now
SystemMessage.unstub(:create)
SystemMessage.expects(:create).never
expect(silence_user).to eq(false)
@ -73,7 +73,7 @@ describe UserSilencer do
subject(:unsilence_user) { UserSilencer.unsilence(user, Fabricate.build(:admin)) }
it 'unsilences the user' do
u = Fabricate(:user, silenced: true)
u = Fabricate(:user, silenced_till: 1.year.from_now)
expect { UserSilencer.unsilence(u) }.to change { u.reload.silenced? }
end