FEATURE: Penalty history improvements (#13359)

* FEATURE: add penalty history when silencing a user

Display penalty history (last 6 months) when silencing/suspending a user

* FEATURE: allow default penalty values to be chosen

Adds a site setting that designates default penalty values in hours.

Silence/suspend modals will auto-fill in the default values, but otherwise
will still allow moderators to pick and overwrite values as normal.

First silence/suspend: first value
Second silence/suspend: second value
etc.

Penalty counts are forgiven at the same rate as tl3 promotion requirements do.

Co-authored-by: jjaffeux <j.jaffeux@gmail.com>
This commit is contained in:
Jeff Wong 2021-07-12 08:36:56 -10:00 committed by GitHub
parent 729a9856f8
commit d87a0216bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 4 deletions

View File

@ -0,0 +1,22 @@
import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["penalty-history"],
@discourseComputed("user.penalty_counts.suspended")
suspendedCountClass(count) {
if (count > 0) {
return "danger";
}
return "";
},
@discourseComputed("user.penalty_counts.silenced")
silencedCountClass(count) {
if (count > 0) {
return "danger";
}
return "";
},
});

View File

@ -12,6 +12,10 @@ export default Controller.extend(PenaltyController, {
this.setProperties({ silenceUntil: null, silencing: false }); this.setProperties({ silenceUntil: null, silencing: false });
}, },
finishedSetup() {
this.set("silenceUntil", this.user?.next_penalty);
},
@discourseComputed("silenceUntil", "reason", "silencing") @discourseComputed("silenceUntil", "reason", "silencing")
submitDisabled(silenceUntil, reason, silencing) { submitDisabled(silenceUntil, reason, silencing) {
return silencing || isEmpty(silenceUntil) || !reason || reason.length < 1; return silencing || isEmpty(silenceUntil) || !reason || reason.length < 1;

View File

@ -12,6 +12,10 @@ export default Controller.extend(PenaltyController, {
this.setProperties({ suspendUntil: null, suspending: false }); this.setProperties({ suspendUntil: null, suspending: false });
}, },
finishedSetup() {
this.set("suspendUntil", this.user?.next_penalty);
},
@discourseComputed("suspendUntil", "reason", "suspending") @discourseComputed("suspendUntil", "reason", "suspending")
submitDisabled(suspendUntil, reason, suspending) { submitDisabled(suspendUntil, reason, suspending) {
return suspending || isEmpty(suspendUntil) || !reason || reason.length < 1; return suspending || isEmpty(suspendUntil) || !reason || reason.length < 1;

View File

@ -48,7 +48,6 @@ export default Service.extend({
_showControlModal(type, user, opts) { _showControlModal(type, user, opts) {
opts = opts || {}; opts = opts || {};
let controller = showModal(`admin-${type}-user`, { let controller = showModal(`admin-${type}-user`, {
admin: true, admin: true,
modalClass: `${type}-user-modal`, modalClass: `${type}-user-modal`,
@ -65,6 +64,8 @@ export default Service.extend({
before: opts.before, before: opts.before,
successCallback: opts.successCallback, successCallback: opts.successCallback,
}); });
controller.finishedSetup();
}); });
}, },

View File

@ -0,0 +1,8 @@
<div class="suspended-count {{suspendedCountClass}}" title={{i18n "admin.user.last_six_months"}}>
<label>{{i18n "admin.user.suspended_count"}}</label>
<span>{{user.penalty_counts.suspended}}</span>
</div>
<div class="silenced-count {{silencedCountClass}}" title={{i18n "admin.user.last_six_months"}}>
<label>{{i18n "admin.user.silenced_count"}}</label>
<span>{{user.penalty_counts.silenced}}</span>
</div>

View File

@ -5,6 +5,8 @@
<div class="alert alert-error">{{errorMessage}}</div> <div class="alert alert-error">{{errorMessage}}</div>
{{/if}} {{/if}}
{{admin-penalty-history user=user}}
<div class="until-controls"> <div class="until-controls">
<label> <label>
{{future-date-input {{future-date-input

View File

@ -6,6 +6,8 @@
{{/if}} {{/if}}
{{#if user.canSuspend}} {{#if user.canSuspend}}
{{admin-penalty-history user=user}}
<div class="until-controls"> <div class="until-controls">
<label> <label>
{{future-date-input {{future-date-input

View File

@ -14,9 +14,10 @@ export default Component.extend({
displayLabel: null, displayLabel: null,
labelClasses: null, labelClasses: null,
timeInputDisabled: empty("date"),
init() { init() {
this._super(...arguments); this._super(...arguments);
if (this.input) { if (this.input) {
const datetime = moment(this.input); const datetime = moment(this.input);
this.setProperties({ this.setProperties({
@ -27,8 +28,6 @@ export default Component.extend({
} }
}, },
timeInputDisabled: empty("date"),
@observes("date", "time") @observes("date", "time")
_updateInput() { _updateInput() {
if (!this.date) { if (!this.date) {

View File

@ -35,4 +35,24 @@
height: 10em; height: 10em;
} }
} }
.penalty-history {
margin-bottom: 1em;
padding-bottom: 0.5em;
border-bottom: 1px solid var(--primary-low);
display: flex;
& > * {
flex-basis: 100%;
text-align: center;
padding: 1em 0;
font-weight: 600;
label {
font-weight: 600;
justify-content: center;
}
}
.danger {
background-color: var(--danger);
color: var(--secondary);
}
}
} }

View File

@ -22,6 +22,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:full_suspend_reason, :full_suspend_reason,
:suspended_till, :suspended_till,
:silence_reason, :silence_reason,
:penalty_counts,
:next_penalty,
:primary_group_id, :primary_group_id,
:badge_count, :badge_count,
:warnings_received_count, :warnings_received_count,
@ -96,6 +98,20 @@ class AdminDetailedUserSerializer < AdminUserSerializer
object.silence_reason object.silence_reason
end end
def penalty_counts
TrustLevel3Requirements.new(object).penalty_counts
end
def next_penalty
step_number = penalty_counts.total
steps = SiteSetting.penalty_step_hours.split('|')
step_number = [step_number, steps.length].min
penalty_hours = steps[step_number]
Integer(penalty_hours, 10).hours.from_now
rescue
nil
end
def silenced_by def silenced_by
object.silenced_record.try(:acting_user) object.silenced_record.try(:acting_user)
end end

View File

@ -4858,6 +4858,7 @@ en:
penalty_post_edit: "Edit the post" penalty_post_edit: "Edit the post"
penalty_post_none: "Do nothing" penalty_post_none: "Do nothing"
penalty_count: "Penalty Count" penalty_count: "Penalty Count"
penalty_history: "Penalty History"
clear_penalty_history: clear_penalty_history:
title: "Clear Penalty History" title: "Clear Penalty History"
description: "users with penalties cannot reach TL3" description: "users with penalties cannot reach TL3"
@ -4997,6 +4998,9 @@ en:
trust_level_unlocked_tip: "trust level is unlocked, system will may promote or demote user" trust_level_unlocked_tip: "trust level is unlocked, system will may promote or demote user"
lock_trust_level: "Lock Trust Level" lock_trust_level: "Lock Trust Level"
unlock_trust_level: "Unlock Trust Level" unlock_trust_level: "Unlock Trust Level"
silenced_count: "Silenced"
suspended_count: "Suspended"
last_six_months: "Last 6 months"
tl3_requirements: tl3_requirements:
title: "Requirements for Trust Level 3" title: "Requirements for Trust Level 3"
table_title: table_title:

View File

@ -2160,6 +2160,7 @@ en:
share_anonymized_statistics: "Share anonymized usage statistics." share_anonymized_statistics: "Share anonymized usage statistics."
auto_handle_queued_age: "Automatically handle records that are waiting to be reviewed after this many days. Flags will be ignored. Queued posts and users will be rejected. Set to 0 to disable this feature." auto_handle_queued_age: "Automatically handle records that are waiting to be reviewed after this many days. Flags will be ignored. Queued posts and users will be rejected. Set to 0 to disable this feature."
penalty_step_hours: "Default penalties for silencing or suspending users in hours. First offense defaults to the first value, second offense defaults to the second value, etc."
svg_icon_subset: "Add additional FontAwesome 5 icons that you would like to include in your assets. Use prefix 'fa-' for solid icons, 'far-' for regular icons and 'fab-' for brand icons." svg_icon_subset: "Add additional FontAwesome 5 icons that you would like to include in your assets. Use prefix 'fa-' for solid icons, 'far-' for regular icons and 'fab-' for brand icons."
max_prints_per_hour_per_user: "Maximum number of /print page impressions (set to 0 to disable)" max_prints_per_hour_per_user: "Maximum number of /print page impressions (set to 0 to disable)"

View File

@ -2270,6 +2270,11 @@ uncategorized:
default: 60 default: 60
min: 0 min: 0
penalty_step_hours:
default: "24|72|168|720"
type: "list"
list_type: "compact"
svg_icon_subset: svg_icon_subset:
default: "" default: ""
type: "list" type: "list"