discourse/app/models/category_list.rb
Sérgio Saquetim abf86271ff
DEV: Extract the query code from CategoryList.find_relevant_topics into a separate method (#26390)
## Why this change?
The previous implementation of the method generated the query to find the relevant topics and iterated over the results, processing them.

This behavior made difficult reusing or changing the query logic in classes extending `CategoryList`.

This commit extracts the query logic into another method called `relevant_topics_query ` which can be reused or overwritten in descendant classes.
2024-03-27 16:32:45 -03:00

266 lines
8.4 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 relevant_topics_query
@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)
end
def find_relevant_topics
featured_topics_by_category_id = Hash.new { |h, k| h[k] = [] }
relevant_topics_query.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? && @options[:parent_category_id].blank?
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? && @options[:parent_category_id].blank?
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
Category.preload_user_fields!(@guardian, categories_with_descendants)
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