FEATURE: Allow admins to lock users from TL3 promotion/demotion

Also, update the display logic for the leader promotion screen to
account for the demotion grace period.
This commit is contained in:
riking 2014-09-13 13:55:26 -07:00 committed by Sam
parent 41d53c7222
commit c8111ada6e
12 changed files with 121 additions and 18 deletions

View file

@ -164,6 +164,24 @@ Discourse.AdminUser = Discourse.User.extend({
this.set('trustLevel.id', this.get('originalTrustLevel')); this.set('trustLevel.id', this.get('originalTrustLevel'));
}, },
lockTrustLevel: function(locked) {
Discourse.ajax("/admin/users/" + this.id + "/trust_level_lock", {
type: 'PUT',
data: { locked: !!locked }
}).then(function() {
// succeeded
window.location.reload();
}, function(e) {
// failure
var error;
if (e.responseJSON && e.responseJSON.errors) {
error = e.responseJSON.errors[0];
}
error = error || I18n.t('admin.user.trust_level_change_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
},
isSuspended: Em.computed.equal('suspended', true), isSuspended: Em.computed.equal('suspended', true),
canSuspend: Em.computed.not('staff'), canSuspend: Em.computed.not('staff'),

View file

@ -18,7 +18,8 @@ Discourse.LeaderRequirements = Discourse.Model.extend({
flagged_posts: this.get('num_flagged_posts') <= this.get('max_flagged_posts'), flagged_posts: this.get('num_flagged_posts') <= this.get('max_flagged_posts'),
flagged_by_users: this.get('num_flagged_by_users') <= this.get('max_flagged_by_users'), flagged_by_users: this.get('num_flagged_by_users') <= this.get('max_flagged_by_users'),
likes_given: this.get('num_likes_given') >= this.get('min_likes_given'), likes_given: this.get('num_likes_given') >= this.get('min_likes_given'),
likes_received: this.get('num_likes_received') >= this.get('min_likes_received') likes_received: this.get('num_likes_received') >= this.get('min_likes_received'),
level_locked: this.get('trust_level_locked')
}; };
}.property('days_visited', 'min_days_visited', }.property('days_visited', 'min_days_visited',
'num_topics_replied_to', 'min_topics_replied_to', 'num_topics_replied_to', 'min_topics_replied_to',
@ -29,5 +30,6 @@ Discourse.LeaderRequirements = Discourse.Model.extend({
'posts_read_all_time', 'min_posts_read_all_time', 'posts_read_all_time', 'min_posts_read_all_time',
'num_flagged_by_users', 'max_flagged_by_users', 'num_flagged_by_users', 'max_flagged_by_users',
'num_likes_given', 'min_likes_given', 'num_likes_given', 'min_likes_given',
'num_likes_received', 'min_likes_received') 'num_likes_received', 'min_likes_received',
'trust_level_locked')
}); });

View file

@ -10,5 +10,15 @@
Discourse.AdminUserLeaderRequirementsRoute = Discourse.Route.extend({ Discourse.AdminUserLeaderRequirementsRoute = Discourse.Route.extend({
model: function() { model: function() {
return this.modelFor('adminUser'); return this.modelFor('adminUser');
},
actions: {
lock_trust_level: function() {
this.modelFor('adminUser').lockTrustLevel(true);
},
unlock_trust_level: function() {
this.modelFor('adminUser').lockTrustLevel(false);
}
} }
}); });

View file

@ -85,21 +85,50 @@
<td>{{num_likes_received}}</td> <td>{{num_likes_received}}</td>
<td>{{min_likes_received}}</td> <td>{{min_likes_received}}</td>
</tr> </tr>
<tr>
<th>{{i18n admin.user.tl3_requirements.trust_level_locked}}</th>
<td><i {{bind-attr class=":fa met.level_locked:fa-lock:fa-unlock"}}></i></td>
{{#if trust_level_locked}}
<td>{{i18n yes_value}}</td>
<td><a class="btn" {{action unlock_trust_level}}>{{i18n admin.user.tl3_requirements.unlock_tl}}</a></td>
{{else}}
<td>{{i18n no_value}}</td>
<td><a class="btn" {{action lock_trust_level}}>{{i18n admin.user.tl3_requirements.lock_tl}}</a></td>
{{/if}}
</tr>
</tbody> </tbody>
</table> </table>
{{/with}} {{/with}}
<br/> <br/>
<p> <p>
{{#if leaderRequirements.requirements_met}} {{#if isLeader}}
<i class="fa fa-check"></i> {{i18n admin.user.tl3_requirements.qualifies}} {{#if leaderRequirements.requirements_lost}}
{{#unless isLeader}} {{! tl implicitly not locked }}
{{i18n admin.user.tl3_requirements.will_be_promoted}} {{#if leaderRequirements.on_grace_period}}
{{/unless}} <i class="fa fa-times"></i> {{i18n admin.user.tl3_requirements.on_grace_period}}
{{else}} {{else}} {{! not on grace period }}
<i class="fa fa-times"></i> {{i18n admin.user.tl3_requirements.does_not_qualify}} <i class="fa fa-times"></i> {{i18n admin.user.tl3_requirements.does_not_qualify}}
{{#if suspended}} {{i18n admin.user.tl3_requirements.will_be_demoted}}
{{i18n user.suspended_notice date="suspendedTillDate"}} {{/if}}
{{else}} {{! requirements not lost - remains leader }}
{{#if leaderRequirements.trust_level_locked}}
<i class="fa fa-lock"></i> {{i18n admin.user.tl3_requirements.locked_will_not_be_demoted}}
{{else}} {{! tl not locked }}
<i class="fa fa-check"></i> {{i18n admin.user.tl3_requirements.qualifies}}
{{/if}}
{{/if}}
{{else}} {{! is not leader }}
{{#if leaderRequirements.requirements_met}}
{{! met & not leader - will be promoted}}
<i class="fa fa-check"></i> {{i18n admin.user.tl3_requirements.qualifies}}
{{i18n admin.user.tl3_requirements.will_be_promoted}}
{{else}} {{! requirements not met - remains regular }}
{{#if leaderRequirements.trust_level_locked}}
<i class="fa fa-lock"></i> {{i18n admin.user.tl3_requirements.locked_will_not_be_promoted}}
{{else}}
<i class="fa fa-times"></i> {{i18n admin.user.tl3_requirements.does_not_qualify}}
{{/if}}
{{/if}} {{/if}}
{{/if}} {{/if}}
</p> </p>

View file

@ -17,6 +17,7 @@ class Admin::UsersController < Admin::AdminController
:block, :block,
:unblock, :unblock,
:trust_level, :trust_level,
:trust_level_lock,
:add_group, :add_group,
:remove_group, :remove_group,
:primary_group, :primary_group,
@ -134,6 +135,19 @@ class Admin::UsersController < Admin::AdminController
render_json_error(e.message) render_json_error(e.message)
end end
def trust_level_lock
guardian.ensure_can_change_trust_level!(@user)
new_lock = params[:locked]
unless new_lock =~ /t|f|true|false/
return render_json_error I18n.t('errors.invalid_boolaen')
end
@user.trust_level_locked = !!(new_lock =~ /t|true/)
@user.save
render nothing: true
end
def approve def approve
guardian.ensure_can_approve!(@user) guardian.ensure_can_approve!(@user)
@user.approve(current_user) @user.approve(current_user)

View file

@ -8,11 +8,7 @@ module Jobs
demoted_user_ids = [] demoted_user_ids = []
User.real.where(trust_level: TrustLevel[3]).find_each do |u| User.real.where(trust_level: TrustLevel[3]).find_each do |u|
# Don't demote too soon after being promoted # Don't demote too soon after being promoted
next if UserHistory.for(u, :auto_trust_level_change) next if user.on_leader_grace_period?
.where('created_at >= ?', SiteSetting.tl3_promotion_min_duration.to_i.days.ago)
.where(previous_value: TrustLevel[2].to_s)
.where(new_value: TrustLevel[3].to_s)
.exists?
if Promotion.tl3_lost?(u) if Promotion.tl3_lost?(u)
demoted_user_ids << u.id demoted_user_ids << u.id

View file

@ -16,13 +16,15 @@ class TrustLevel3Requirements
:posts_read_all_time, :min_posts_read_all_time, :posts_read_all_time, :min_posts_read_all_time,
:num_flagged_posts, :max_flagged_posts, :num_flagged_posts, :max_flagged_posts,
:num_likes_given, :min_likes_given, :num_likes_given, :min_likes_given,
:num_likes_received, :min_likes_received :num_likes_received, :min_likes_received,
:trust_level_locked, :on_grace_period
def initialize(user) def initialize(user)
@user = user @user = user
end end
def requirements_met? def requirements_met?
return false if trust_level_locked
!@user.suspended? && !@user.suspended? &&
days_visited >= min_days_visited && days_visited >= min_days_visited &&
num_topics_replied_to >= min_topics_replied_to && num_topics_replied_to >= min_topics_replied_to &&
@ -37,6 +39,7 @@ class TrustLevel3Requirements
end end
def requirements_lost? def requirements_lost?
return false if trust_level_locked
@user.suspended? || @user.suspended? ||
days_visited < min_days_visited * LOW_WATER_MARK || days_visited < min_days_visited * LOW_WATER_MARK ||
num_topics_replied_to < min_topics_replied_to * LOW_WATER_MARK || num_topics_replied_to < min_topics_replied_to * LOW_WATER_MARK ||
@ -50,6 +53,14 @@ class TrustLevel3Requirements
num_likes_received < min_likes_received * LOW_WATER_MARK num_likes_received < min_likes_received * LOW_WATER_MARK
end end
def trust_level_locked
@user.trust_level_locked
end
def on_grace_period
@user.on_leader_grace_period?
end
def days_visited def days_visited
@user.user_visits.where("visited_at > ? and posts_read > 0", TIME_PERIOD.days.ago).count @user.user_visits.where("visited_at > ? and posts_read > 0", TIME_PERIOD.days.ago).count
end end

View file

@ -574,6 +574,14 @@ class User < ActiveRecord::Base
@lq ||= TrustLevel3Requirements.new(self) @lq ||= TrustLevel3Requirements.new(self)
end end
def on_leader_grace_period?
UserHistory.for(self, :auto_trust_level_change)
.where('created_at >= ?', SiteSetting.tl3_promotion_min_duration.to_i.days.ago)
.where(previous_value: TrustLevel[2].to_s)
.where(new_value: TrustLevel[3].to_s)
.exists?
end
def should_be_redirected_to_top def should_be_redirected_to_top
redirected_to_top_reason.present? redirected_to_top_reason.present?
end end

View file

@ -1,6 +1,8 @@
class TrustLevel3RequirementsSerializer < ApplicationSerializer class TrustLevel3RequirementsSerializer < ApplicationSerializer
attributes :time_period, attributes :time_period,
:requirements_met, :requirements_met,
:requirements_lost,
:trust_level_locked, :on_grace_period,
:days_visited, :min_days_visited, :days_visited, :min_days_visited,
:num_topics_replied_to, :min_topics_replied_to, :num_topics_replied_to, :min_topics_replied_to,
:topics_viewed, :min_topics_viewed, :topics_viewed, :min_topics_viewed,
@ -19,4 +21,8 @@ class TrustLevel3RequirementsSerializer < ApplicationSerializer
def requirements_met def requirements_met
object.requirements_met? object.requirements_met?
end end
def requirements_lost
object.requirements_lost?
end
end end

View file

@ -1975,9 +1975,16 @@ en:
flagged_by_users: "Users Who Flagged" flagged_by_users: "Users Who Flagged"
likes_given: "Likes Given" likes_given: "Likes Given"
likes_received: "Likes Received" likes_received: "Likes Received"
trust_level_locked: "Trust Level Locked"
lock_tl: "Lock"
unlock_tl: "Unlock"
qualifies: "Qualifies for trust level 3." qualifies: "Qualifies for trust level 3."
will_be_promoted: "Will be promoted within 24 hours."
does_not_qualify: "Doesn't qualify for trust level 3." does_not_qualify: "Doesn't qualify for trust level 3."
will_be_promoted: "Will be promoted within 24 hours."
will_be_demoted: "Will be demoted within 24 hours."
on_grace_period: "Currently in promotion grace period, will not be demoted."
locked_will_not_be_promoted: "Trust level locked. Will never be promoted."
locked_will_not_be_demoted: "Trust level locked. Will never be demoted."
sso: sso:
title: "Single Sign On" title: "Single Sign On"
external_id: "External ID" external_id: "External ID"

View file

@ -41,6 +41,7 @@ en:
errors: errors:
messages: messages:
too_long_validation: "is limited to %{max} characters; you entered %{length}." too_long_validation: "is limited to %{max} characters; you entered %{length}."
invalid_boolean: "Invalid boolean."
embed: embed:
load_from_remote: "There was an error loading that post." load_from_remote: "There was an error loading that post."

View file

@ -72,6 +72,7 @@ Discourse::Application.routes.draw do
put "block" put "block"
put "unblock" put "unblock"
put "trust_level" put "trust_level"
put "trust_level_lock"
put "primary_group" put "primary_group"
post "groups" => "users#add_group", constraints: AdminConstraint.new post "groups" => "users#add_group", constraints: AdminConstraint.new
delete "groups/:group_id" => "users#remove_group", constraints: AdminConstraint.new delete "groups/:group_id" => "users#remove_group", constraints: AdminConstraint.new