FEATURE: list category moderators on the about page (#7916)

FEATURE: list category moderators on the about page (#7916)

Category Group Review/Moderation - announcements - Discourse Meta

diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6
index 019a233..3b19492 100644
--- a/app/assets/javascripts/discourse/routes/about.js.es6
+++ b/app/assets/javascripts/discourse/routes/about.js.es6
@@ -16,6 +16,14 @@ export default Discourse.Route.extend({
       });
       result.about.admins = activeAdmins;
       result.about.moderators = activeModerators;
+
+      const { category_moderators: categoryModerators } = result.about;
+      if (categoryModerators && categoryModerators.length) {
+        categoryModerators.forEach((obj, index) => {
+          const category = this.site.categories.findBy("id", obj.category_id);
+          result.about.category_moderators[index].category = category;
+        });
+      }
       return result.about;
     });
   },
diff --git a/app/assets/javascripts/discourse/templates/about.hbs b/app/assets/javascripts/discourse/templates/about.hbs
index 495a7a3..5abd436 100644
--- a/app/assets/javascripts/discourse/templates/about.hbs
+++ b/app/assets/javascripts/discourse/templates/about.hbs
@@ -57,6 +57,19 @@
                     connectorTagName='section'
                     args=(hash model=model)}}
 
+      {{#if model.category_moderators.length}}
+        {{#each model.category_moderators as |cm|}}
+          <section class='about category-moderators'>
+            <h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3>
+            <div class='users'>
+              {{#each cm.moderators as |m|}}
+                {{user-info user=m}}
+              {{/each}}
+            </div>
+            <div class='clearfix'></div>
+          </section>
+        {{/each}}
+      {{/if}}
       <section class='about stats'>
         <h3>{{d-icon "far-chart-bar"}}  {{i18n 'about.stats'}}</h3>
 
diff --git a/app/assets/stylesheets/common/base/faqs.scss b/app/assets/stylesheets/common/base/faqs.scss
index acda62d..9102cc6 100644
--- a/app/assets/stylesheets/common/base/faqs.scss
+++ b/app/assets/stylesheets/common/base/faqs.scss
@@ -4,9 +4,14 @@
   max-width: 700px;
   .about-page & {
     max-width: unset;
-    section:not(.admins):not(.moderators) {
+    section:not(.admins):not(.moderators):not(.category-moderators) {
       max-width: 700px;
     }
+    .about.category-moderators {
+      .badge-wrapper.bullet .badge-category {
+        color: $primary;
+      }
+    }
   }
   .mobile-view & {
     font-size: $font-0;
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 03336bd..8856a37 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -11,7 +11,7 @@ class AboutController < ApplicationController
   def index
     return redirect_to path('/login') if SiteSetting.login_required? && current_user.nil?
 
-    @about = About.new
+    @about = About.new(current_user)
     @title = "#{I18n.t("js.about.simple_title")} - #{SiteSetting.title}"
     respond_to do |format|
       format.html do
diff --git a/app/models/about.rb b/app/models/about.rb
index 783c682..b24a62c 100644
--- a/app/models/about.rb
+++ b/app/models/about.rb
@@ -1,6 +1,16 @@
 # frozen_string_literal: true
 
 class About
+  class CategoryMods
+    include ActiveModel::Serialization
+    attr_reader :category_id, :moderators
+
+    def initialize(category_id, moderators)
+      @category_id = category_id
+      @moderators = moderators
+    end
+  end
+
   include ActiveModel::Serialization
   include StatsCacheable
 
@@ -15,6 +25,10 @@ class About
     About.new.stats
   end
 
+  def initialize(user = nil)
+    @user = user
+  end
+
   def version
     Discourse::VERSION::STRING
   end
@@ -66,4 +80,24 @@ class About
     }
   end
 
+  def category_moderators
+    category_ids = Guardian.new(@user).allowed_category_ids
+    return [] if category_ids.blank?
+    results = DB.query(<<~SQL, category_ids: category_ids)
+      SELECT c.id category_id, array_agg(gu.user_id) user_ids
+      FROM categories c
+      JOIN group_users gu
+      ON gu.group_id = reviewable_by_group_id
+      WHERE c.id IN (:category_ids)
+      GROUP BY c.id
+    SQL
+    moderators = {}
+    User.where(id: results.map(&:user_ids).flatten).each do |user|
+      moderators[user.id] = user
+    end
+    moderators
+    results.map do |row|
+      CategoryMods.new(row.category_id, row.user_ids.map { |id| moderators[id] })
+    end
+  end
 end
diff --git a/app/serializers/about_serializer.rb b/app/serializers/about_serializer.rb
index af78559..308f990 100644
--- a/app/serializers/about_serializer.rb
+++ b/app/serializers/about_serializer.rb
@@ -6,8 +6,15 @@ class AboutSerializer < ApplicationSerializer
     attributes :title, :last_seen_at
   end
 
+  class AboutCategoryModsSerializer < ApplicationSerializer
+    attributes :category_id
+
+    has_many :moderators, serializer: UserAboutSerializer, embed: :objects
+  end
+
   has_many :moderators, serializer: UserAboutSerializer, embed: :objects
   has_many :admins, serializer: UserAboutSerializer, embed: :objects
+  has_many :category_moderators, serializer: AboutCategoryModsSerializer, embed: :objects
 
   attributes :stats,
              :description,
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 4bafe97..3f8b850 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -274,6 +274,7 @@ en:
       stats: "Site Statistics"
       our_admins: "Our Admins"
       our_moderators: "Our Moderators"
+      moderators: "Moderators"
       stat:
         all_time: "All Time"
         last_7_days: "Last 7"
diff --git a/spec/models/about_spec.rb b/spec/models/about_spec.rb
index ea93c79..c4ab5ab 100644
--- a/spec/models/about_spec.rb
+++ b/spec/models/about_spec.rb
@@ -8,4 +8,46 @@ describe About do
     include_examples 'stats cachable'
   end
 
+  describe "#category_moderators" do
+    let(:user) { Fabricate(:user) }
+    let(:public_cat_moderator) { Fabricate(:user) }
+    let(:private_cat_moderator) { Fabricate(:user) }
+    let(:common_moderator) { Fabricate(:user) }
+
+    let(:public_group) do
+      group = Fabricate(:public_group)
+      group.add(public_cat_moderator)
+      group.add(common_moderator)
+      group
+    end
+
+    let(:private_group) do
+      group = Fabricate(:group)
+      group.add(private_cat_moderator)
+      group.add(common_moderator)
+      group
+    end
+
+    let!(:public_cat) { Fabricate(:category, reviewable_by_group: public_group) }
+    let!(:private_cat) { Fabricate(:private_category, group: private_group, reviewable_by_group: private_group) }
+
+    it "lists moderators of the category that the current user can see" do
+      results = About.new(private_cat_moderator).category_moderators
+      expect(results.map(&:category_id)).to contain_exactly(public_cat.id, private_cat.id)
+      expect(results.map(&:moderators).flatten.map(&:id).uniq).to contain_exactly(
+        public_cat_moderator.id,
+        common_moderator.id,
+        private_cat_moderator.id
+      )
+
+      [public_cat_moderator, user, nil].each do |u|
+        results = About.new(u).category_moderators
+        expect(results.map(&:category_id)).to contain_exactly(public_cat.id)
+        expect(results.map(&:moderators).flatten.map(&:id)).to contain_exactly(
+          public_cat_moderator.id,
+          common_moderator.id
+        )
+      end
+    end
+  end
 end

GitHub sha: 13e74151

1 Like