diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
index 0d06a8d0adc..266fdbd02c4 100644
--- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
@@ -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'));
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
new file mode 100644
index 00000000000..9f1aef916f8
--- /dev/null
+++ b/app/assets/javascripts/admin/controllers/modals/admin-silence-user.js.es6
@@ -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));
+ }
+ }
+});
diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6
index a162c0f319f..e3f1530badf 100644
--- a/app/assets/javascripts/admin/models/admin-user.js.es6
+++ b/app/assets/javascripts/admin/models/admin-user.js.es6
@@ -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}`
});
- };
-
- 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" });
+ bootbox.alert(error);
+ }).finally(() => {
+ this.set('silencingUser', false);
+ });
},
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,
});
diff --git a/app/assets/javascripts/admin/services/admin-tools.js.es6 b/app/assets/javascripts/admin/services/admin-tools.js.es6
index 9f855772a22..987c7c3d645 100644
--- a/app/assets/javascripts/admin/services/admin-tools.js.es6
+++ b/app/assets/javascripts/admin/services/admin-tools.js.es6
@@ -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(() => {
diff --git a/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
new file mode 100644
index 00000000000..9bc92171160
--- /dev/null
+++ b/app/assets/javascripts/admin/templates/modal/admin-silence-user.hbs
@@ -0,0 +1,50 @@
+{{#d-modal-body title="admin.user.silence_modal_title"}}
+ {{#conditional-loading-spinner condition=loadingUser}}
+
+
+
+
+
+
+
+
+
+
+
+ {{/conditional-loading-spinner}}
+
+{{/d-modal-body}}
+
+
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs
index 9eea948704a..a0c7741475e 100644
--- a/app/assets/javascripts/admin/templates/user-index.hbs
+++ b/app/assets/javascripts/admin/templates/user-index.hbs
@@ -349,19 +349,49 @@
{{i18n 'admin.user.silenced'}}
-
{{i18n-yes-no model.silenced}}
+
+ {{i18n-yes-no model.silenced}}
+ {{#if model.isSilenced}}
+ {{#unless model.silencedForever}}
+ {{i18n "admin.user.suspended_until" until=model.silencedTillDate}}
+ {{/unless}}
+ {{/if}}
+
{{#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}}
+
+ {{#if model.isSilenced}}
+
+
{{i18n 'admin.user.silenced_by'}}
+
+ {{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
+ {{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}}
+
+
+ {{i18n 'admin.user.silence_reason'}}:
+ {{model.silence_reason}}
+
+
+ {{/if}}
+
{{#if currentUser.admin}}
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index cc32dda9271..9925d4798a0 100644
--- a/app/assets/javascripts/discourse/models/user.js.es6
+++ b/app/assets/javascripts/discourse/models/user.js.es6
@@ -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`), {
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 3228bb4e3a6..95a7016b3be 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -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
diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb
index 9354f4288d8..1697792b64a 100644
--- a/app/jobs/regular/export_csv_file.rb
+++ b/app/jobs/regular/export_csv_file.rb
@@ -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)
diff --git a/app/jobs/scheduled/grant_anniversary_badges.rb b/app/jobs/scheduled/grant_anniversary_badges.rb
index 7a4da06d30a..6d0e24bd12d 100644
--- a/app/jobs/scheduled/grant_anniversary_badges.rb
+++ b/app/jobs/scheduled/grant_anniversary_badges.rb
@@ -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
diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb
index e1d24b37c6a..236acc0dabd 100644
--- a/app/mailers/user_notifications.rb
+++ b/app/mailers/user_notifications.rb
@@ -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 ||= {}
diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb
index 584ce6a7097..8bcf2bbd065 100644
--- a/app/models/directory_item.rb
+++ b/app/models/directory_item.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 7e3fe29d1e8..f7398f6374e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb
index 6df084c7274..e3e488415ce 100644
--- a/app/serializers/admin_detailed_user_serializer.rb
+++ b/app/serializers/admin_detailed_user_serializer.rb
@@ -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
diff --git a/app/serializers/admin_user_list_serializer.rb b/app/serializers/admin_user_list_serializer.rb
index 22d2e07879c..251ba21035e 100644
--- a/app/serializers/admin_user_list_serializer.rb
+++ b/app/serializers/admin_user_list_serializer.rb
@@ -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
diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb
index 7232490b3c4..0dc4543774d 100644
--- a/app/services/staff_action_logger.rb
+++ b/app/services/staff_action_logger.rb
@@ -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 = {})
diff --git a/app/services/user_silencer.rb b/app/services/user_silencer.rb
index 63061f8b4bd..ee7981b5f10 100644
--- a/app/services/user_silencer.rb
+++ b/app/services/user_silencer.rb
@@ -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
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 80a49840fd9..1114bf2ba62 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -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"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index cb518bac1bf..140726dca0a 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -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"
diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb
index ae6588ce471..2f49f9d3148 100644
--- a/db/fixtures/009_users.rb
+++ b/db/fixtures/009_users.rb
@@ -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|
diff --git a/db/migrate/20171113175414_add_silenced_till_to_users.rb b/db/migrate/20171113175414_add_silenced_till_to_users.rb
new file mode 100644
index 00000000000..33552a38987
--- /dev/null
+++ b/db/migrate/20171113175414_add_silenced_till_to_users.rb
@@ -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
diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb
index 5878293a6da..c2ce008150a 100644
--- a/lib/badge_queries.rb
+++ b/lib/badge_queries.rb
@@ -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
diff --git a/spec/components/admin_user_index_query_spec.rb b/spec/components/admin_user_index_query_spec.rb
index de124a5be8e..6604b48d52a 100644
--- a/spec/components/admin_user_index_query_spec.rb
+++ b/spec/components/admin_user_index_query_spec.rb
@@ -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')
diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb
index 39451cb49bf..cf36f2d8e78 100644
--- a/spec/components/email/receiver_spec.rb
+++ b/spec/components/email/receiver_spec.rb
@@ -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
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 277e2f408d3..16bd0378f5c 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -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
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 629f12c80ab..78f298a4642 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -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
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index eda2fa09c86..1981d15bf68 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -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
diff --git a/spec/jobs/grant_anniversary_badges_spec.rb b/spec/jobs/grant_anniversary_badges_spec.rb
index d1664eaa2cc..a73a4b719d0 100644
--- a/spec/jobs/grant_anniversary_badges_spec.rb
+++ b/spec/jobs/grant_anniversary_badges_spec.rb
@@ -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({})
diff --git a/spec/jobs/notify_mailing_list_subscribers_spec.rb b/spec/jobs/notify_mailing_list_subscribers_spec.rb
index 131dd780c26..f6252b9120a 100644
--- a/spec/jobs/notify_mailing_list_subscribers_spec.rb
+++ b/spec/jobs/notify_mailing_list_subscribers_spec.rb
@@ -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
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index 0e527e92eb2..0d85b51daa6 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4f4071bffa0..700b77fb160 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -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
diff --git a/spec/services/auto_silence_spec.rb b/spec/services/auto_silence_spec.rb
index 3ee3269e4c6..4a21a55f135 100644
--- a/spec/services/auto_silence_spec.rb
+++ b/spec/services/auto_silence_spec.rb
@@ -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
diff --git a/spec/services/user_silencer_spec.rb b/spec/services/user_silencer_spec.rb
index 9b230575633..188bd0435fd 100644
--- a/spec/services/user_silencer_spec.rb
+++ b/spec/services/user_silencer_spec.rb
@@ -7,8 +7,8 @@ describe UserSilencer do
end
describe 'silence' do
- let(:user) { stub_everything(save: true) }
- let(:silencer) { UserSilencer.new(user) }
+ let(:user) { Fabricate(:user) }
+ let(:silencer) { UserSilencer.new(user) }
subject(:silence_user) { silencer.silence }
it 'silences the user' do
@@ -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