From be1ab8b275fbc6d4a0032cfffadb66c6e9843d41 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Mon, 6 May 2013 14:49:56 +1000
Subject: [PATCH] automatic group infrustructure

---
 app/controllers/admin/users_controller.rb     | 10 +--
 app/models/group.rb                           | 82 +++++++++++++++++++
 app/models/user.rb                            | 34 +++++++-
 .../20130506020935_add_automatic_to_groups.rb | 14 ++++
 lib/promotion.rb                              |  6 +-
 spec/models/group_spec.rb                     | 48 +++++++++++
 6 files changed, 183 insertions(+), 11 deletions(-)
 create mode 100644 db/migrate/20130506020935_add_automatic_to_groups.rb

diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index a68cada91..6d3df8a18 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -63,30 +63,28 @@ class Admin::UsersController < Admin::AdminController
   def revoke_admin
     @admin = User.where(id: params[:user_id]).first
     guardian.ensure_can_revoke_admin!(@admin)
-    @admin.update_column(:admin, false)
+    @admin.revoke_admin!
     render nothing: true
   end
 
   def grant_admin
     @user = User.where(id: params[:user_id]).first
     guardian.ensure_can_grant_admin!(@user)
-    @user.update_column(:admin, true)
+    @user.grant_admin!
     render_serialized(@user, AdminUserSerializer)
   end
 
   def revoke_moderation
     @moderator = User.where(id: params[:user_id]).first
     guardian.ensure_can_revoke_moderation!(@moderator)
-    @moderator.moderator = false
-    @moderator.save
+    @moderator.revoke_moderation!
     render nothing: true
   end
 
   def grant_moderation
     @user = User.where(id: params[:user_id]).first
     guardian.ensure_can_grant_moderation!(@user)
-    @user.moderator = true
-    @user.save
+    @user.grant_moderation!
     render_serialized(@user, AdminUserSerializer)
   end
 
diff --git a/app/models/group.rb b/app/models/group.rb
index 0c5c70b36..69657be45 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -5,6 +5,88 @@ class Group < ActiveRecord::Base
   has_many :categories, through: :category_groups
   has_many :users, through: :group_users
 
+  AUTO_GROUPS = {
+    :admins => 1,
+    :moderators => 2,
+    :staff => 3,
+    :trust_level_1 => 11,
+    :trust_level_2 => 12,
+    :trust_level_3 => 13,
+    :trust_level_4 => 14,
+    :trust_level_5 => 15
+  }
+
+  def self.trust_group_ids
+    (10..19).to_a
+  end
+
+  def self.refresh_automatic_group!(name)
+
+    id = AUTO_GROUPS[name]
+
+    unless group = self[name]
+      group = Group.new(name: name.to_s, automatic: true)
+      group.id = id
+      group.save!
+    end
+
+
+    real_ids = case name
+               when :admins
+                 "SELECT u.id FROM users u WHERE u.admin = 't'"
+               when :moderators
+                 "SELECT u.id FROM users u WHERE u.moderator = 't'"
+               when :staff
+                 "SELECT u.id FROM users u WHERE u.moderator = 't' OR u.admin = 't'"
+               when :trust_level_1, :trust_level_2, :trust_level_3, :trust_level_4, :trust_level_5
+                 "SELECT u.id FROM users u WHERE u.trust_level = #{id-10}"
+               end
+
+
+    extra_users = group.users.where("users.id NOT IN (#{real_ids})").select('users.id')
+    missing_users = GroupUser.joins("RIGHT JOIN (#{real_ids}) X ON X.id = user_id AND group_id = #{group.id}")
+      .where("user_id IS NULL")
+      .select("X.id")
+
+    group.group_users.where("user_id IN (#{extra_users.to_sql})").delete_all
+
+    missing_users.each do |u|
+      group.group_users.build(user_id: u.id)
+    end
+
+    group.save!
+  end
+
+  def self.refresh_automatic_groups!(*args)
+    args.each do |group|
+      refresh_automatic_group!(group)
+    end
+  end
+
+  def self.[](name)
+    raise ArgumentError, "unknown group" unless id = AUTO_GROUPS[name]
+
+    Group.where(id: id).first
+  end
+
+
+  def self.user_trust_level_change!(user_id, trust_level)
+    name = "trust_level_#{trust_level}".to_sym
+
+    GroupUser.where(group_id: trust_group_ids, user_id: user_id).delete_all
+
+    if group = Group[name]
+      group_users.build(user_id: user_id)
+      group_users.save!
+    else
+      refresh_automatic_group!(name)
+    end
+  end
+
+  def user_ids
+    users.select('users.id').map(&:id)
+  end
+
   def self.builtin
     Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2)
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index e590937fd..1d70b5036 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -177,6 +177,34 @@ class User < ActiveRecord::Base
     where("username_lower = :user or lower(username) = :user or email = :email or lower(name) = :user", user: lower_user, email: lower_email)
   end
 
+
+  def save_and_refresh_staff_groups!
+    transaction do
+      self.save!
+      Group.refresh_automatic_groups!(:admins,:moderators,:staff)
+    end
+  end
+
+  def grant_moderation!
+    self.moderator = true
+    save_and_refresh_staff_groups!
+  end
+
+  def revoke_moderation!
+    self.moderator = false
+    save_and_refresh_staff_groups!
+  end
+
+  def grant_admin!
+    self.admin = true
+    save_and_refresh_staff_groups!
+  end
+
+  def revoke_admin!
+    self.admin = false
+    save_and_refresh_staff_groups!
+  end
+
   def enqueue_welcome_message(message_type)
     return unless SiteSetting.send_welcome_message?
     Jobs.enqueue(:send_system_message, user_id: id, message_type: message_type)
@@ -439,9 +467,13 @@ class User < ActiveRecord::Base
     admin
   end
 
-  def change_trust_level(level)
+  def change_trust_level!(level)
     raise "Invalid trust level #{level}" unless TrustLevel.valid_level?(level)
     self.trust_level = TrustLevel.levels[level]
+    transaction do
+      self.save!
+      Group.user_trust_level_change!(self.id, self.trust_level)
+    end
   end
 
   def guardian
diff --git a/db/migrate/20130506020935_add_automatic_to_groups.rb b/db/migrate/20130506020935_add_automatic_to_groups.rb
new file mode 100644
index 000000000..e62081c7b
--- /dev/null
+++ b/db/migrate/20130506020935_add_automatic_to_groups.rb
@@ -0,0 +1,14 @@
+class AddAutomaticToGroups < ActiveRecord::Migration
+  def up
+    add_column :groups, :automatic, :boolean, default: false, null: false
+
+    # all numbers below 100 are reserved for automatic
+    execute <<SQL
+    ALTER SEQUENCE groups_id_seq START WITH 100
+SQL
+  end
+
+  def down
+    remove_column :groups, :automatic
+  end
+end
diff --git a/lib/promotion.rb b/lib/promotion.rb
index 39b315ff7..921cbc5c6 100644
--- a/lib/promotion.rb
+++ b/lib/promotion.rb
@@ -26,8 +26,7 @@ class Promotion
     return false if @user.posts_read_count < SiteSetting.basic_requires_read_posts
     return false if (@user.time_read / 60) < SiteSetting.basic_requires_time_spent_mins
 
-    @user.trust_level = TrustLevel.levels[:basic]
-    @user.save
+    @user.change_trust_level!(:basic)
 
     true
   end
@@ -41,8 +40,7 @@ class Promotion
     return false if @user.likes_given < SiteSetting.regular_requires_likes_given
     return false if @user.topic_reply_count < SiteSetting.regular_requires_topic_reply_count
 
-    @user.trust_level = TrustLevel.levels[:regular]
-    @user.save
+    @user.change_trust_level!(:regular)
   end
 
 end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 32135450e..253532fae 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1,4 +1,52 @@
 require 'spec_helper'
 
 describe Group do
+
+  it "Can update moderator/staff/admin groups correctly" do
+    admin = Fabricate(:admin)
+    moderator = Fabricate(:moderator)
+
+    Group.refresh_automatic_groups!(:admins, :staff, :moderators)
+
+    Group[:admins].user_ids.should == [admin.id]
+    Group[:moderators].user_ids.should == [moderator.id]
+    Group[:staff].user_ids.sort.should == [moderator.id,admin.id].sort
+
+    admin.admin = false
+    admin.save
+
+    Group.refresh_automatic_group!(:admins)
+    Group[:admins].user_ids.should == []
+
+    moderator.revoke_moderation!
+
+    admin.grant_admin!
+    Group[:admins].user_ids.should == [admin.id]
+    Group[:staff].user_ids.should == [admin.id]
+
+    admin.revoke_admin!
+    Group[:admins].user_ids.should == []
+    Group[:staff].user_ids.should == []
+
+    admin.grant_moderation!
+    Group[:moderators].user_ids.should == [admin.id]
+    Group[:staff].user_ids.should == [admin.id]
+
+    admin.revoke_moderation!
+    Group[:admins].user_ids.should == []
+    Group[:staff].user_ids.should == []
+  end
+
+  it "Correctly updates automatic trust level groups" do
+    user = Fabricate(:user)
+    user.change_trust_level!(:basic)
+
+    Group[:trust_level_1].user_ids.should == [user.id]
+
+    user.change_trust_level!(:regular)
+
+    Group[:trust_level_1].user_ids.should == []
+    Group[:trust_level_2].user_ids.should == [user.id]
+  end
+
 end