mirror of
https://github.com/discourse/discourse.git
synced 2024-12-01 13:09:33 -06:00
f562da3150
Why this change? Prior to this change, the `CategoryList#find_relevant_topics` method was loading and allocating all `CategoryFeaturedTopic` records in the database to eventually only just use its `category_id` and `topic_id` column. On a site with many `CategoryFeaturedTopic` records, the loading of the ActiveRecord objects is a source of bottleneck. The other problem with the `CategoryList#find_relevant_topics` method is that it is unconditionally loading all records from the database even if the user does not have access to the category. This again is wasteful. What does this change do? This commit makes it such that `CategoryList#find_relevant_topics` is called only after `CategoryList#find_categories` in the `CategoryList#initialize` method so that we can filter featured topics against categories that the user has access to. The second change is that Instead of loading `CategoryFeaturedTopic` records, we make an inner join agains the `topics` table instead and skip any allocation of `CatgoryFeaturedTopic` ActiveRecord objects.
272 lines
8.7 KiB
Ruby
272 lines
8.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class CategoryList
|
|
CATEGORIES_PER_PAGE = 20
|
|
SUBCATEGORIES_PER_CATEGORY = 5
|
|
|
|
include ActiveModel::Serialization
|
|
|
|
cattr_accessor :preloaded_topic_custom_fields
|
|
self.preloaded_topic_custom_fields = Set.new
|
|
|
|
attr_accessor :categories, :uncategorized
|
|
|
|
def self.register_included_association(association)
|
|
@included_assocations ||= []
|
|
@included_assocations << association if !@included_assocations.include?(association)
|
|
end
|
|
|
|
def self.included_associations
|
|
[
|
|
:uploaded_background,
|
|
:uploaded_background_dark,
|
|
:uploaded_logo,
|
|
:uploaded_logo_dark,
|
|
:topic_only_relative_url,
|
|
subcategories: [:topic_only_relative_url],
|
|
].concat(@included_assocations || [])
|
|
end
|
|
|
|
def initialize(guardian = nil, options = {})
|
|
@guardian = guardian || Guardian.new
|
|
@options = options
|
|
|
|
find_categories
|
|
find_relevant_topics if options[:include_topics]
|
|
|
|
prune_empty
|
|
find_user_data
|
|
sort_unpinned
|
|
trim_results
|
|
demote_muted
|
|
|
|
if preloaded_topic_custom_fields.present?
|
|
displayable_topics = @categories.map(&:displayable_topics)
|
|
displayable_topics.flatten!
|
|
displayable_topics.compact!
|
|
|
|
if displayable_topics.present?
|
|
Topic.preload_custom_fields(displayable_topics, preloaded_topic_custom_fields)
|
|
end
|
|
end
|
|
end
|
|
|
|
def preload_key
|
|
"categories_list"
|
|
end
|
|
|
|
def self.order_categories(categories)
|
|
if SiteSetting.fixed_category_positions
|
|
categories.order(:position, :id)
|
|
else
|
|
categories
|
|
.left_outer_joins(:featured_topics)
|
|
.where("topics.category_id IS NULL OR topics.category_id IN (?)", categories.select(:id))
|
|
.group("categories.id")
|
|
.order("max(topics.bumped_at) DESC NULLS LAST")
|
|
.order("categories.id ASC")
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def find_relevant_topics
|
|
@all_topics =
|
|
Topic
|
|
.secured(@guardian)
|
|
.joins(
|
|
"INNER JOIN category_featured_topics ON topics.id = category_featured_topics.topic_id",
|
|
)
|
|
.where("category_featured_topics.category_id IN (?)", categories_with_descendants.map(&:id))
|
|
.select(
|
|
"topics.*, category_featured_topics.category_id AS category_featured_topic_category_id",
|
|
)
|
|
.includes(:shared_draft, :category, { topic_thumbnails: %i[optimized_image upload] })
|
|
.order("category_featured_topics.rank")
|
|
|
|
@all_topics = @all_topics.joins(:tags).where(tags: { name: @options[:tag] }) if @options[
|
|
:tag
|
|
].present?
|
|
|
|
if @guardian.authenticated?
|
|
@all_topics =
|
|
@all_topics
|
|
.joins(
|
|
"LEFT JOIN topic_users tu ON topics.id = tu.topic_id AND tu.user_id = #{@guardian.user.id.to_i}",
|
|
)
|
|
.joins(
|
|
"LEFT JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{@guardian.user.id}",
|
|
)
|
|
.where(
|
|
"COALESCE(tu.notification_level,1) > :muted",
|
|
muted: TopicUser.notification_levels[:muted],
|
|
)
|
|
end
|
|
|
|
@all_topics = TopicQuery.remove_muted_tags(@all_topics, @guardian.user).includes(:last_poster)
|
|
|
|
featured_topics_by_category_id = Hash.new { |h, k| h[k] = [] }
|
|
|
|
@all_topics.each do |t|
|
|
# hint for the serializer
|
|
t.include_last_poster = true
|
|
t.dismissed = dismissed_topic?(t)
|
|
featured_topics_by_category_id[t.category_featured_topic_category_id] << t
|
|
end
|
|
|
|
categories_with_descendants.each do |category|
|
|
category.displayable_topics = featured_topics_by_category_id[category.id]
|
|
end
|
|
end
|
|
|
|
def dismissed_topic?(topic)
|
|
if @guardian.current_user
|
|
@dismissed_topic_users_lookup ||=
|
|
DismissedTopicUser.lookup_for(@guardian.current_user, @all_topics)
|
|
@dismissed_topic_users_lookup.include?(topic.id)
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def find_categories
|
|
query = Category.includes(CategoryList.included_associations).secured(@guardian)
|
|
|
|
query =
|
|
query.where(
|
|
"categories.parent_category_id = ?",
|
|
@options[:parent_category_id].to_i,
|
|
) if @options[:parent_category_id].present?
|
|
|
|
query = self.class.order_categories(query)
|
|
|
|
if @guardian.can_lazy_load_categories?
|
|
page = [1, @options[:page].to_i].max
|
|
query =
|
|
query
|
|
.where(parent_category_id: nil)
|
|
.limit(CATEGORIES_PER_PAGE)
|
|
.offset((page - 1) * CATEGORIES_PER_PAGE)
|
|
end
|
|
|
|
query =
|
|
DiscoursePluginRegistry.apply_modifier(:category_list_find_categories_query, query, self)
|
|
|
|
@categories = query.to_a
|
|
|
|
if @guardian.can_lazy_load_categories?
|
|
categories_with_rownum =
|
|
Category
|
|
.secured(@guardian)
|
|
.select(:id, "ROW_NUMBER() OVER (PARTITION BY parent_category_id) rownum")
|
|
.where(parent_category_id: @categories.map { |c| c.id })
|
|
|
|
@categories +=
|
|
Category.includes(CategoryList.included_associations).where(
|
|
"id IN (WITH cte AS (#{categories_with_rownum.to_sql}) SELECT id FROM cte WHERE rownum <= ?)",
|
|
SUBCATEGORIES_PER_CATEGORY,
|
|
)
|
|
end
|
|
|
|
if Site.preloaded_category_custom_fields.any?
|
|
Category.preload_custom_fields(@categories, Site.preloaded_category_custom_fields)
|
|
end
|
|
|
|
include_subcategories = @options[:include_subcategories] == true
|
|
|
|
notification_levels = CategoryUser.notification_levels_for(@guardian.user)
|
|
default_notification_level = CategoryUser.default_notification_level
|
|
|
|
if @guardian.can_lazy_load_categories?
|
|
subcategory_ids = {}
|
|
Category
|
|
.secured(@guardian)
|
|
.where(parent_category_id: @categories.map(&:id))
|
|
.pluck(:id, :parent_category_id)
|
|
.each { |id, parent_id| (subcategory_ids[parent_id] ||= []) << id }
|
|
@categories.each { |c| c.subcategory_ids = subcategory_ids[c.id] || [] }
|
|
elsif @options[:parent_category_id].blank?
|
|
subcategory_ids = {}
|
|
subcategory_list = {}
|
|
to_delete = Set.new
|
|
@categories.each do |c|
|
|
if c.parent_category_id.present?
|
|
subcategory_ids[c.parent_category_id] ||= []
|
|
subcategory_ids[c.parent_category_id] << c.id
|
|
if include_subcategories
|
|
subcategory_list[c.parent_category_id] ||= []
|
|
subcategory_list[c.parent_category_id] << c
|
|
end
|
|
to_delete << c
|
|
end
|
|
end
|
|
@categories.each do |c|
|
|
c.subcategory_ids = subcategory_ids[c.id] || []
|
|
c.subcategory_list = subcategory_list[c.id] || [] if include_subcategories
|
|
end
|
|
@categories.delete_if { |c| to_delete.include?(c) }
|
|
end
|
|
|
|
allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id))
|
|
|
|
categories_with_descendants.each do |category|
|
|
category.notification_level = notification_levels[category.id] || default_notification_level
|
|
category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(
|
|
category.id,
|
|
)
|
|
category.has_children = category.subcategories.present?
|
|
end
|
|
end
|
|
|
|
def prune_empty
|
|
return if SiteSetting.allow_uncategorized_topics
|
|
@categories.delete_if { |c| c.uncategorized? }
|
|
end
|
|
|
|
# Attach some data for serialization to each topic
|
|
def find_user_data
|
|
if @guardian.current_user && @all_topics.present?
|
|
topic_lookup = TopicUser.lookup_for(@guardian.current_user, @all_topics)
|
|
@all_topics.each { |ft| ft.user_data = topic_lookup[ft.id] }
|
|
end
|
|
end
|
|
|
|
# Put unpinned topics at the end of the list
|
|
def sort_unpinned
|
|
if @guardian.current_user && @all_topics.present?
|
|
categories_with_descendants.each do |c|
|
|
next if c.displayable_topics.blank? || c.displayable_topics.size <= c.num_featured_topics
|
|
unpinned = []
|
|
c.displayable_topics.each do |t|
|
|
unpinned << t if t.pinned_at && PinnedCheck.unpinned?(t, t.user_data)
|
|
end
|
|
c.displayable_topics = (c.displayable_topics - unpinned) + unpinned unless unpinned.empty?
|
|
end
|
|
end
|
|
end
|
|
|
|
def demote_muted
|
|
muted_categories = @categories.select { |category| category.notification_level == 0 }
|
|
@categories = @categories.reject { |category| category.notification_level == 0 }
|
|
@categories.concat muted_categories
|
|
end
|
|
|
|
def trim_results
|
|
categories_with_descendants.each do |c|
|
|
next if c.displayable_topics.blank?
|
|
c.displayable_topics = c.displayable_topics[0, c.num_featured_topics]
|
|
end
|
|
end
|
|
|
|
def categories_with_descendants(categories = @categories)
|
|
return @categories_with_children if @categories_with_children && (categories == @categories)
|
|
return nil if categories.nil?
|
|
|
|
result = categories.flat_map { |c| [c, *categories_with_descendants(c.subcategory_list)] }
|
|
|
|
@categories_with_children = result if categories == @categories
|
|
|
|
result
|
|
end
|
|
end
|