diff --git a/app/assets/javascripts/admin/models/admin_user.js b/app/assets/javascripts/admin/models/admin_user.js index 2e68f50a128..ab7aa34ffe0 100644 --- a/app/assets/javascripts/admin/models/admin_user.js +++ b/app/assets/javascripts/admin/models/admin_user.js @@ -67,6 +67,37 @@ Discourse.AdminUser = Discourse.User.extend({ return site.get('trust_levels').findProperty('id', this.get('trust_level')); }.property('trust_level'), + setOriginalTrustLevel: function() { + this.set('originalTrustLevel', this.get('trust_level')); + }, + + trustLevels: function() { + var site = Discourse.Site.instance(); + return site.get('trust_levels'); + }.property('trust_level'), + + dirty: function() { + return this.get('originalTrustLevel') !== parseInt(this.get('trustLevel.id'), 10); + }.property('originalTrustLevel', 'trustLevel.id'), + + saveTrustLevel: function() { + Discourse.ajax("/admin/users/" + this.id + "/trust_level", { + type: 'PUT', + data: {level: this.get('trustLevel.id')} + }).then(function () { + // succeeded + window.location.reload(); + }, function(e) { + // failure + var error = Em.String.i18n('admin.user.trust_level_change_failed', { error: "http: " + e.status + " - " + e.body }); + bootbox.alert(error); + }); + }, + + restoreTrustLevel: function() { + this.set('trustLevel.id', this.get('originalTrustLevel')); + }, + isBanned: (function() { return this.get('is_banned') === true; }).property('is_banned'), diff --git a/app/assets/javascripts/admin/routes/admin_user_route.js b/app/assets/javascripts/admin/routes/admin_user_route.js index ab38ba41921..da0063e9999 100644 --- a/app/assets/javascripts/admin/routes/admin_user_route.js +++ b/app/assets/javascripts/admin/routes/admin_user_route.js @@ -16,6 +16,11 @@ Discourse.AdminUserRoute = Discourse.Route.extend(Discourse.ModelReady, { return Discourse.AdminUser.find(Em.get(params, 'username').toLowerCase()); }, + setupController: function(controller, model) { + controller.set('model', model); + model.setOriginalTrustLevel(); + }, + renderTemplate: function() { this.render({into: 'admin/templates/admin'}); }, diff --git a/app/assets/javascripts/admin/templates/user.js.handlebars b/app/assets/javascripts/admin/templates/user.js.handlebars index 2e953e7f369..60db63a606e 100644 --- a/app/assets/javascripts/admin/templates/user.js.handlebars +++ b/app/assets/javascripts/admin/templates/user.js.handlebars @@ -161,10 +161,22 @@ +
{{i18n trust_level}}
-
{{trustLevel.name}}
+
+ {{combobox content=trustLevels value=trustLevel.id }} +
+
+ {{#if dirty}} +
+ + +
+ {{/if}} +
+
{{i18n admin.user.banned}}
{{isBanned}}
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 1c51293b656..6c1e1de5ca7 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,9 +1,10 @@ require_dependency 'user_destroyer' require_dependency 'admin_user_index_query' +require_dependency 'boost_trust_level' class Admin::UsersController < Admin::AdminController - before_filter :fetch_user, only: [:ban, :unban, :refresh_browsers, :revoke_admin, :grant_admin, :revoke_moderation, :grant_moderation, :approve, :activate, :deactivate, :block, :unblock] + before_filter :fetch_user, only: [:ban, :unban, :refresh_browsers, :revoke_admin, :grant_admin, :revoke_moderation, :grant_moderation, :approve, :activate, :deactivate, :block, :unblock, :trust_level] def index query = ::AdminUserIndexQuery.new(params) @@ -69,6 +70,12 @@ class Admin::UsersController < Admin::AdminController render_serialized(@user, AdminUserSerializer) end + def trust_level + guardian.ensure_can_change_trust_level!(@user) + BoostTrustLevel.new(@user, params[:level]).save! + render_serialized(@user, AdminUserSerializer) + end + def approve guardian.ensure_can_approve!(@user) @user.approve(current_user) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 960ef4ed45f..2b6ebb96f8e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1207,7 +1207,7 @@ en: deactivate_explanation: "A deactivated user must re-validate their email." banned_explanation: "A banned user can't log in." block_explanation: "A blocked user can't post or start topics." - + trust_level_change_failed: "There was a problem changing the user's trust level." site_content: none: "Choose a type of content to begin editing." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 10e53bfa530..17b2630eedf 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -134,6 +134,8 @@ en: title: "leader" elder: title: "elder" + change_failed_explanation: "You attempted to demote %{user_name} to '%{new_trust_level}'. However their trust level is already '%{current_trust_level}'. %{user_name} will remain at '%{current_trust_level}'" + rate_limiter: too_many_requests: "You're doing that too often. Please wait %{time_left} before trying again." diff --git a/config/routes.rb b/config/routes.rb index 556ea294eb4..7cb1d650f51 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,7 @@ Discourse::Application.routes.draw do put 'deactivate' put 'block' put 'unblock' + put 'trust_level' end resources :impersonate, constraints: AdminConstraint.new diff --git a/lib/boost_trust_level.rb b/lib/boost_trust_level.rb new file mode 100644 index 00000000000..597f380866e --- /dev/null +++ b/lib/boost_trust_level.rb @@ -0,0 +1,40 @@ +require_dependency 'promotion' + +class BoostTrustLevel + + def initialize(user, level) + @user = user + @level = level.to_i + @promotion = Promotion.new(@user) + @trust_levels = TrustLevel.levels + end + + def save! + if @level < @user.trust_level + demote! + else + @user.update_attributes!(trust_level: @level) + end + end + + protected + + def demote! + current_trust_level = @user.trust_level + @user.update_attributes!(trust_level: @level) + if @promotion.review + @user.update_attributes!(trust_level: current_trust_level) + raise Discourse::InvalidAccess.new, I18n.t('trust_levels.change_failed_explanation', + user_name: @user.name, + new_trust_level: trust_level_lookup(@level), + current_trust_level: trust_level_lookup(current_trust_level)) + else + true + end + end + + def trust_level_lookup(level) + @trust_levels.key(level).id2name + end + +end diff --git a/lib/guardian.rb b/lib/guardian.rb index ed49762376b..4a7589f875a 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -137,6 +137,10 @@ class Guardian user && is_staff? end + def can_change_trust_level?(user) + can_administer?(user) + end + def can_block_user?(user) user && is_staff? && not(user.staff?) end diff --git a/spec/components/boost_trust_level_spec.rb b/spec/components/boost_trust_level_spec.rb new file mode 100644 index 00000000000..26412079337 --- /dev/null +++ b/spec/components/boost_trust_level_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' +require 'boost_trust_level' + +describe BoostTrustLevel do + + let(:user) { Fabricate(:user) } + + it "should upgrade the trust level of a user" do + boostr = BoostTrustLevel.new(user, TrustLevel.levels[:basic]) + boostr.save!.should be_true + user.trust_level.should == TrustLevel.levels[:basic] + end + + describe "demotions" do + before { user.update_attributes(trust_level: TrustLevel.levels[:newuser]) } + + context "for a user that has not done the requisite things to attain their trust level" do + + before do + # scenario: admin mistakenly promotes user's trust level + user.update_attributes(trust_level: TrustLevel.levels[:basic]) + end + + it "should demote the user" do + boostr = BoostTrustLevel.new(user, TrustLevel.levels[:newuser]) + boostr.save!.should be_true + user.trust_level.should == TrustLevel.levels[:newuser] + end + end + + context "for a user that has done the requisite things to attain their trust level" do + + before do + user.topics_entered = SiteSetting.basic_requires_topics_entered + 1 + user.posts_read_count = SiteSetting.basic_requires_read_posts + 1 + user.time_read = SiteSetting.basic_requires_time_spent_mins * 60 + user.save! + user.update_attributes(trust_level: TrustLevel.levels[:basic]) + end + + it "should not demote the user" do + boostr = BoostTrustLevel.new(user, TrustLevel.levels[:newuser]) + expect { boostr.save! }.to raise_error(Discourse::InvalidAccess, "You attempted to demote #{user.name} to 'newuser'. However their trust level is already 'basic'. #{user.name} will remain at 'basic'") + user.trust_level.should == TrustLevel.levels[:basic] + end + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 8467594137e..35fbaab5887 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -120,6 +120,39 @@ describe Admin::UsersController do end end + context '.trust_level' do + before do + @another_user = Fabricate(:coding_horror) + end + + it "raises an error when the user doesn't have permission" do + Guardian.any_instance.expects(:can_change_trust_level?).with(@another_user).returns(false) + xhr :put, :trust_level, user_id: @another_user.id + response.should be_forbidden + end + + it "returns a 404 if the username doesn't exist" do + xhr :put, :trust_level, user_id: 123123 + response.should be_forbidden + end + + it "upgrades the user's trust level" do + xhr :put, :trust_level, user_id: @another_user.id, level: 2 + @another_user.reload + @another_user.trust_level.should == 2 + end + + it "raises an error when demoting a user below their current trust level" do + @another_user.topics_entered = SiteSetting.basic_requires_topics_entered + 1 + @another_user.posts_read_count = SiteSetting.basic_requires_read_posts + 1 + @another_user.time_read = SiteSetting.basic_requires_time_spent_mins * 60 + @another_user.save! + @another_user.update_attributes(trust_level: TrustLevel.levels[:basic]) + xhr :put, :trust_level, user_id: @another_user.id, level: TrustLevel.levels[:newuser] + response.should be_forbidden + end + end + describe '.revoke_moderation' do before do @moderator = Fabricate(:moderator)