discourse/app/models/about.rb
Andrei Prigorshnev d91456fd53
DEV: Ability to collect stats without exposing them via API (#23933)
This adds the ability to collect stats without exposing them 
among other stats via API.

The most important thing I wanted to achieve is to provide 
an API where stats are not exposed by default, and a developer 
has to explicitly specify that they should be 
exposed (`expose_via_api: true`). Implementing an opposite 
solution would be simpler, but that's less safe in terms of 
potential security issues. 

When working on this, I had to refactor the current solution. 
I would go even further with the refactoring, but the next steps 
seem to be going too far in changing the solution we have, 
and that would also take more time. Two things that can be 
improved in the future:
1. Data structures for holding stats can be further improved
2. Core stats are hard-coded in the About template (it's hard 
to fix it without correcting data structures first, see point 1):
    63a0700d45/app/views/about/index.html.erb (L61-L101)

The most significant refactorings are:
1. Introducing the `Stat` model
2. Aligning the way the core and the plugin stats' are registered
2023-11-10 00:44:05 +04:00

103 lines
2.3 KiB
Ruby

# frozen_string_literal: true
class About
def self.displayed_plugin_stat_groups
DiscoursePluginRegistry.stats.select { |stat| stat.show_in_ui }.map { |stat| stat.name }
end
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
attr_accessor :moderators, :admins
def self.stats_cache_key
"about-stats"
end
def self.fetch_stats
Stat.api_stats
end
def initialize(user = nil)
@user = user
end
def version
Discourse::VERSION::STRING
end
def https
SiteSetting.force_https
end
def title
SiteSetting.title
end
def locale
SiteSetting.default_locale
end
def description
SiteSetting.site_description
end
def moderators
@moderators ||= User.where(moderator: true, admin: false).human_users.order("last_seen_at DESC")
end
def admins
@admins ||= User.where(admin: true).human_users.order("last_seen_at DESC")
end
def stats
@stats ||= About.fetch_stats
end
def category_moderators
allowed_cats = Guardian.new(@user).allowed_category_ids
return [] if allowed_cats.blank?
cats_with_mods = Category.where.not(reviewable_by_group_id: nil).pluck(:id)
category_ids = cats_with_mods & allowed_cats
return [] if category_ids.blank?
per_cat_limit = category_mods_limit / category_ids.size
per_cat_limit = 1 if per_cat_limit < 1
results = DB.query(<<~SQL, category_ids: category_ids)
SELECT c.id category_id
, (ARRAY_AGG(u.id ORDER BY u.last_seen_at DESC))[:#{per_cat_limit}] user_ids
FROM categories c
JOIN group_users gu ON gu.group_id = c.reviewable_by_group_id
JOIN users u ON u.id = gu.user_id
WHERE c.id IN (:category_ids)
GROUP BY c.id
ORDER BY c.position
SQL
mods = User.where(id: results.map(&:user_ids).flatten.uniq).index_by(&:id)
results.map { |row| CategoryMods.new(row.category_id, mods.values_at(*row.user_ids)) }
end
def category_mods_limit
@category_mods_limit || 100
end
def category_mods_limit=(number)
@category_mods_limit = number
end
end