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 cbba833b659..0d2ae55508e 100644
--- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
+++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6
@@ -103,6 +103,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
anonymize() { return this.get('model').anonymize(); },
disableSecondFactor() { return this.get('model').disableSecondFactor(); },
+
+ clearPenaltyHistory() {
+ let user = this.get('model');
+ return ajax(`/admin/users/${user.get('id')}/penalty_history`, {
+ type: 'DELETE'
+ }).then(() => {
+ user.set('tl3_requirements.penalty_counts.total', 0);
+ }).catch(popupAjaxError);
+ },
+
destroy() {
const postCount = this.get('model.post_count');
if (postCount <= 5) {
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs
index aec1b986f20..e36f7cd4b5c 100644
--- a/app/assets/javascripts/admin/templates/user-index.hbs
+++ b/app/assets/javascripts/admin/templates/user-index.hbs
@@ -408,6 +408,21 @@
{{/if}}
+ {{#if model.tl3_requirements.penalty_counts.total}}
+
+
{{i18n 'admin.user.penalty_count'}}
+
{{model.tl3_requirements.penalty_counts.total}}
+ {{#if currentUser.admin}}
+
+ {{d-button label="admin.user.clear_penalty_history.title"
+ icon="times"
+ action=(action "clearPenaltyHistory")}}
+ {{i18n "admin.user.clear_penalty_history.description"}}
+
+ {{/if}}
+
+ {{/if}}
+
{{#if currentUser.admin}}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index c43a3e8f05f..7b7f42096c0 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -54,6 +54,46 @@ class Admin::UsersController < Admin::AdminController
end
end
+ # DELETE action to delete penalty history for a user
+ def penalty_history
+
+ # We don't delete any history, we merely remove the action type
+ # with a removed type. It can still be viewed in the logs but
+ # will not affect TL3 promotions.
+ sql = <<~SQL
+ UPDATE user_histories
+ SET action = CASE
+ WHEN action = :silence_user THEN :removed_silence_user
+ WHEN action = :unsilence_user THEN :removed_unsilence_user
+ WHEN action = :suspend_user THEN :removed_suspend_user
+ WHEN action = :unsuspend_user THEN :removed_unsuspend_user
+ END
+ WHERE target_user_id = :user_id
+ AND action IN (
+ :silence_user,
+ :suspend_user,
+ :unsilence_user,
+ :unsuspend_user
+ )
+ SQL
+
+ UserHistory.exec_sql(
+ sql,
+ UserHistory.actions.slice(
+ :silence_user,
+ :suspend_user,
+ :unsilence_user,
+ :unsuspend_user,
+ :removed_silence_user,
+ :removed_unsilence_user,
+ :removed_suspend_user,
+ :removed_unsuspend_user
+ ).merge(user_id: params[:user_id].to_i)
+ )
+
+ render json: success_json
+ end
+
def suspend
guardian.ensure_can_suspend!(@user)
@user.suspended_till = params[:suspend_until]
diff --git a/app/models/user_history.rb b/app/models/user_history.rb
index 0a247331157..f500d831793 100644
--- a/app/models/user_history.rb
+++ b/app/models/user_history.rb
@@ -76,6 +76,10 @@ class UserHistory < ActiveRecord::Base
create_badge: 57,
change_badge: 58,
delete_badge: 59,
+ removed_silence_user: 60,
+ removed_suspend_user: 61,
+ removed_unsilence_user: 62,
+ removed_unsuspend_user: 63,
)
end
@@ -90,6 +94,8 @@ class UserHistory < ActiveRecord::Base
:change_site_text,
:suspend_user,
:unsuspend_user,
+ :removed_suspend_user,
+ :removed_unsuspend_user,
:grant_badge,
:revoke_badge,
:check_email,
@@ -106,6 +112,8 @@ class UserHistory < ActiveRecord::Base
:create_category,
:silence_user,
:unsilence_user,
+ :removed_silence_user,
+ :removed_unsilence_user,
:grant_admin,
:revoke_admin,
:grant_moderation,
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index e936696bf8d..7e88bdbade5 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3365,6 +3365,8 @@ en:
change_site_text: "change site text"
suspend_user: "suspend user"
unsuspend_user: "unsuspend user"
+ removed_suspend_user: "suspend user (removed)"
+ removed_unsuspend_user: "unsuspend user (removed)"
grant_badge: "grant badge"
revoke_badge: "revoke badge"
check_email: "check email"
@@ -3379,6 +3381,8 @@ en:
create_category: "create category"
silence_user: "silence user"
unsilence_user: "unsilence user"
+ removed_silence_user: "silence user (removed)"
+ removed_unsilence_user: "unsilence user (removed)"
grant_admin: "grant admin"
revoke_admin: "revoke admin"
grant_moderation: "grant moderation"
@@ -3563,6 +3567,10 @@ en:
penalty_post_delete: "Delete the post"
penalty_post_edit: "Edit the post"
penalty_post_none: "Do nothing"
+ penalty_count: "Penalty Count"
+ clear_penalty_history:
+ title: "Clear Penalty History"
+ description: "users with penalties cannot reach TL3"
# 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/config/routes.rb b/config/routes.rb
index 71bcbaab242..8765881d63d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -103,6 +103,7 @@ Discourse::Application.routes.draw do
put "approve-bulk" => "users#approve_bulk"
delete "reject-bulk" => "users#reject_bulk"
end
+ delete "penalty_history", constraints: AdminConstraint.new
put "suspend"
put "delete_all_posts"
put "unsuspend"
diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb
index 8d665aea2ea..e13942e7792 100644
--- a/spec/requests/admin/users_controller_spec.rb
+++ b/spec/requests/admin/users_controller_spec.rb
@@ -47,4 +47,42 @@ RSpec.describe Admin::UsersController do
end
end
end
+
+ describe "#penalty_history" do
+ let(:moderator) { Fabricate(:moderator) }
+ let(:logger) { StaffActionLogger.new(admin) }
+
+ it "doesn't allow moderators to clear a user's history" do
+ sign_in(moderator)
+ delete "/admin/users/#{user.id}/penalty_history.json"
+ expect(response.code).to eq("404")
+ end
+
+ def find_logs(action)
+ UserHistory.where(target_user_id: user.id, action: UserHistory.actions[action])
+ end
+
+ it "allows admins to clear a user's history" do
+ logger.log_user_suspend(user, "suspend reason")
+ logger.log_user_unsuspend(user)
+ logger.log_unsilence_user(user)
+ logger.log_silence_user(user)
+
+ sign_in(admin)
+ delete "/admin/users/#{user.id}/penalty_history.json"
+ expect(response.code).to eq("200")
+
+ expect(find_logs(:suspend_user)).to be_blank
+ expect(find_logs(:unsuspend_user)).to be_blank
+ expect(find_logs(:silence_user)).to be_blank
+ expect(find_logs(:unsilence_user)).to be_blank
+
+ expect(find_logs(:removed_suspend_user)).to be_present
+ expect(find_logs(:removed_unsuspend_user)).to be_present
+ expect(find_logs(:removed_silence_user)).to be_present
+ expect(find_logs(:removed_unsilence_user)).to be_present
+ end
+
+ end
+
end