2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-03-30 11:42:26 -04:00
|
|
|
class CategoryList
|
2024-01-17 10:18:01 -05:00
|
|
|
CATEGORIES_PER_PAGE = 20
|
|
|
|
SUBCATEGORIES_PER_CATEGORY = 5
|
2023-12-11 10:58:45 -05:00
|
|
|
|
2021-03-30 11:42:26 -04:00
|
|
|
include ActiveModel::Serialization
|
|
|
|
|
2017-08-01 04:26:03 -04:00
|
|
|
cattr_accessor :preloaded_topic_custom_fields
|
|
|
|
self.preloaded_topic_custom_fields = Set.new
|
|
|
|
|
2013-05-28 21:15:30 -04:00
|
|
|
attr_accessor :categories, :uncategorized
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2023-03-23 13:39:38 -04:00
|
|
|
def self.register_included_association(association)
|
|
|
|
@included_assocations ||= []
|
|
|
|
@included_assocations << association if !@included_assocations.include?(association)
|
2023-03-22 16:12:08 -04:00
|
|
|
end
|
|
|
|
|
2023-03-23 13:39:38 -04:00
|
|
|
def self.included_associations
|
|
|
|
[
|
|
|
|
:uploaded_background,
|
2023-10-20 08:48:06 -04:00
|
|
|
:uploaded_background_dark,
|
2023-03-23 13:39:38 -04:00
|
|
|
:uploaded_logo,
|
|
|
|
:uploaded_logo_dark,
|
|
|
|
:topic_only_relative_url,
|
|
|
|
subcategories: [:topic_only_relative_url],
|
|
|
|
].concat(@included_assocations || [])
|
2023-03-22 16:12:08 -04:00
|
|
|
end
|
|
|
|
|
2016-08-17 17:23:16 -04:00
|
|
|
def initialize(guardian = nil, options = {})
|
2013-05-28 14:54:00 -04:00
|
|
|
@guardian = guardian || Guardian.new
|
2013-10-17 02:44:56 -04:00
|
|
|
@options = options
|
2013-05-28 14:54:00 -04:00
|
|
|
|
|
|
|
find_categories
|
2024-02-28 23:19:04 -05:00
|
|
|
find_relevant_topics if options[:include_topics]
|
2016-08-18 19:47:00 -04:00
|
|
|
|
|
|
|
prune_empty
|
|
|
|
find_user_data
|
|
|
|
sort_unpinned
|
|
|
|
trim_results
|
2020-03-17 16:33:15 -04:00
|
|
|
demote_muted
|
2017-08-01 04:26:03 -04:00
|
|
|
|
|
|
|
if preloaded_topic_custom_fields.present?
|
2017-08-01 04:57:26 -04:00
|
|
|
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
|
2017-08-01 04:26:03 -04:00
|
|
|
end
|
2016-08-17 17:23:16 -04:00
|
|
|
end
|
2013-05-28 15:56:46 -04:00
|
|
|
|
2016-08-17 17:23:16 -04:00
|
|
|
def preload_key
|
2020-04-30 02:48:34 -04:00
|
|
|
"categories_list"
|
2013-05-28 14:54:00 -04:00
|
|
|
end
|
|
|
|
|
2019-04-25 05:07:19 -04:00
|
|
|
def self.order_categories(categories)
|
|
|
|
if SiteSetting.fixed_category_positions
|
|
|
|
categories.order(:position, :id)
|
|
|
|
else
|
|
|
|
categories
|
|
|
|
.left_outer_joins(:featured_topics)
|
2024-01-08 19:19:37 -05:00
|
|
|
.where("topics.category_id IS NULL OR topics.category_id IN (?)", categories.select(:id))
|
2019-04-25 05:07:19 -04:00
|
|
|
.group("categories.id")
|
|
|
|
.order("max(topics.bumped_at) DESC NULLS LAST")
|
|
|
|
.order("categories.id ASC")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-05-28 14:54:00 -04:00
|
|
|
private
|
|
|
|
|
2024-03-27 15:32:45 -04:00
|
|
|
def relevant_topics_query
|
2022-12-22 15:29:49 -05:00
|
|
|
@all_topics =
|
2024-02-28 23:19:04 -05:00
|
|
|
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")
|
2021-10-29 10:52:23 -04:00
|
|
|
|
2022-08-17 12:51:02 -04:00
|
|
|
@all_topics = @all_topics.joins(:tags).where(tags: { name: @options[:tag] }) if @options[
|
|
|
|
:tag
|
|
|
|
].present?
|
|
|
|
|
2021-10-29 10:52:23 -04:00
|
|
|
if @guardian.authenticated?
|
|
|
|
@all_topics =
|
2023-06-28 21:25:58 -04:00
|
|
|
@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],
|
|
|
|
)
|
2021-10-29 10:52:23 -04:00
|
|
|
end
|
|
|
|
|
2022-08-17 12:51:02 -04:00
|
|
|
@all_topics = TopicQuery.remove_muted_tags(@all_topics, @guardian.user).includes(:last_poster)
|
2024-03-27 15:32:45 -04:00
|
|
|
end
|
2024-02-28 23:19:04 -05:00
|
|
|
|
2024-03-27 15:32:45 -04:00
|
|
|
def find_relevant_topics
|
2024-02-28 23:19:04 -05:00
|
|
|
featured_topics_by_category_id = Hash.new { |h, k| h[k] = [] }
|
|
|
|
|
2024-03-27 15:32:45 -04:00
|
|
|
relevant_topics_query.each do |t|
|
2016-08-22 17:01:43 -04:00
|
|
|
# hint for the serializer
|
2022-08-17 12:51:02 -04:00
|
|
|
t.include_last_poster = true
|
2021-03-04 17:04:19 -05:00
|
|
|
t.dismissed = dismissed_topic?(t)
|
2024-02-28 23:19:04 -05:00
|
|
|
featured_topics_by_category_id[t.category_featured_topic_category_id] << t
|
2016-08-22 17:01:43 -04:00
|
|
|
end
|
2016-08-18 19:47:00 -04:00
|
|
|
|
2024-02-28 23:19:04 -05:00
|
|
|
categories_with_descendants.each do |category|
|
|
|
|
category.displayable_topics = featured_topics_by_category_id[category.id]
|
2016-08-18 19:47:00 -04:00
|
|
|
end
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2016-08-18 19:47:00 -04:00
|
|
|
|
2021-03-04 17:04:19 -05:00
|
|
|
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
|
|
|
|
|
2013-05-28 14:54:00 -04:00
|
|
|
def find_categories
|
2023-05-30 17:41:50 -04:00
|
|
|
query = Category.includes(CategoryList.included_associations).secured(@guardian)
|
2023-01-09 07:20:10 -05:00
|
|
|
|
2023-05-30 17:41:50 -04:00
|
|
|
query =
|
|
|
|
query.where(
|
2016-08-22 17:01:43 -04:00
|
|
|
"categories.parent_category_id = ?",
|
|
|
|
@options[:parent_category_id].to_i,
|
|
|
|
) if @options[:parent_category_id].present?
|
2018-06-07 01:28:18 -04:00
|
|
|
|
2023-05-30 17:41:50 -04:00
|
|
|
query = self.class.order_categories(query)
|
2023-12-11 10:58:45 -05:00
|
|
|
|
2024-03-28 12:19:09 -04:00
|
|
|
page = [1, @options[:page].to_i].max
|
2024-03-21 15:39:14 -04:00
|
|
|
if @guardian.can_lazy_load_categories? && @options[:parent_category_id].blank?
|
2024-01-17 10:18:01 -05:00
|
|
|
query =
|
|
|
|
query
|
|
|
|
.where(parent_category_id: nil)
|
|
|
|
.limit(CATEGORIES_PER_PAGE)
|
|
|
|
.offset((page - 1) * CATEGORIES_PER_PAGE)
|
2024-03-28 12:19:09 -04:00
|
|
|
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
|
2023-12-11 10:58:45 -05:00
|
|
|
end
|
|
|
|
|
2023-05-30 17:41:50 -04:00
|
|
|
query =
|
|
|
|
DiscoursePluginRegistry.apply_modifier(:category_list_find_categories_query, query, self)
|
2013-05-28 14:54:00 -04:00
|
|
|
|
2023-05-30 17:41:50 -04:00
|
|
|
@categories = query.to_a
|
2015-09-07 12:52:53 -04:00
|
|
|
|
2024-03-21 15:39:14 -04:00
|
|
|
if @guardian.can_lazy_load_categories? && @options[:parent_category_id].blank?
|
2024-01-17 10:18:01 -05:00
|
|
|
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 +=
|
2024-02-09 12:48:26 -05:00
|
|
|
Category.includes(CategoryList.included_associations).where(
|
2024-01-17 10:18:01 -05:00
|
|
|
"id IN (WITH cte AS (#{categories_with_rownum.to_sql}) SELECT id FROM cte WHERE rownum <= ?)",
|
|
|
|
SUBCATEGORIES_PER_CATEGORY,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2023-11-09 11:23:24 -05:00
|
|
|
if Site.preloaded_category_custom_fields.any?
|
|
|
|
Category.preload_custom_fields(@categories, Site.preloaded_category_custom_fields)
|
2023-10-26 09:34:23 -04:00
|
|
|
end
|
|
|
|
|
2021-10-05 14:12:31 -04:00
|
|
|
include_subcategories = @options[:include_subcategories] == true
|
|
|
|
|
2024-01-17 13:26:51 -05:00
|
|
|
if @guardian.can_lazy_load_categories?
|
2023-12-18 09:46:09 -05:00
|
|
|
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?
|
2021-10-05 14:12:31 -04:00
|
|
|
subcategory_ids = {}
|
|
|
|
subcategory_list = {}
|
2016-08-22 17:01:43 -04:00
|
|
|
to_delete = Set.new
|
|
|
|
@categories.each do |c|
|
|
|
|
if c.parent_category_id.present?
|
2021-10-05 14:12:31 -04:00
|
|
|
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
|
2016-08-22 17:01:43 -04:00
|
|
|
to_delete << c
|
2014-01-15 14:11:19 -05:00
|
|
|
end
|
2013-05-28 14:54:00 -04:00
|
|
|
end
|
2021-10-05 14:12:31 -04:00
|
|
|
@categories.each do |c|
|
|
|
|
c.subcategory_ids = subcategory_ids[c.id] || []
|
|
|
|
c.subcategory_list = subcategory_list[c.id] || [] if include_subcategories
|
|
|
|
end
|
2016-08-22 17:01:43 -04:00
|
|
|
@categories.delete_if { |c| to_delete.include?(c) }
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2013-05-28 14:54:00 -04:00
|
|
|
|
2024-03-25 12:56:32 -04:00
|
|
|
Category.preload_user_fields!(@guardian, categories_with_descendants)
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2016-08-18 19:47:00 -04:00
|
|
|
|
|
|
|
def prune_empty
|
|
|
|
return if SiteSetting.allow_uncategorized_topics
|
2021-05-03 23:05:08 -04:00
|
|
|
@categories.delete_if { |c| c.uncategorized? }
|
2016-08-18 19:47:00 -04:00
|
|
|
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
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2016-08-18 19:47:00 -04:00
|
|
|
|
|
|
|
# Put unpinned topics at the end of the list
|
|
|
|
def sort_unpinned
|
|
|
|
if @guardian.current_user && @all_topics.present?
|
2022-03-04 16:11:59 -05:00
|
|
|
categories_with_descendants.each do |c|
|
2017-03-01 12:03:12 -05:00
|
|
|
next if c.displayable_topics.blank? || c.displayable_topics.size <= c.num_featured_topics
|
2016-08-18 19:47:00 -04:00
|
|
|
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
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2016-08-18 19:47:00 -04:00
|
|
|
|
2020-03-17 16:33:15 -04:00
|
|
|
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
|
|
|
|
|
2016-08-18 19:47:00 -04:00
|
|
|
def trim_results
|
2022-03-04 16:11:59 -05:00
|
|
|
categories_with_descendants.each do |c|
|
2016-08-18 19:47:00 -04:00
|
|
|
next if c.displayable_topics.blank?
|
2017-03-01 12:03:12 -05:00
|
|
|
c.displayable_topics = c.displayable_topics[0, c.num_featured_topics]
|
2016-08-18 19:47:00 -04:00
|
|
|
end
|
2018-06-07 01:28:18 -04:00
|
|
|
end
|
2016-08-18 19:47:00 -04:00
|
|
|
|
2022-03-04 16:11:59 -05:00
|
|
|
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
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|