mirror of
https://github.com/discourse/discourse.git
synced 2024-11-26 19:00:32 -06:00
3b9e9354d6
Pagination is enabled only when "lazy load categories" is enabled. For those cases when it is not, the first page should return all the results.
270 lines
8.6 KiB
Ruby
270 lines
8.6 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)
|
|
|
|
page = [1, @options[:page].to_i].max
|
|
if @guardian.can_lazy_load_categories? && @options[:parent_category_id].blank?
|
|
query =
|
|
query
|
|
.where(parent_category_id: nil)
|
|
.limit(CATEGORIES_PER_PAGE)
|
|
.offset((page - 1) * CATEGORIES_PER_PAGE)
|
|
elsif page > 1
|
|
# Pagination is supported only when lazy load is enabled. If it is not,
|
|
# everything is returned on page 1.
|
|
query = query.none
|
|
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
|