mirror of
https://github.com/discourse/discourse.git
synced 2025-04-29 21:14:38 +08:00

A lot of work has been put in the select kits used for selecting categories: CategorySelector, CategoryChooser, CategoryDrop, however they still do not work as expected when these selectors already have values set, because the category were still looked up in the list of categories stored on the client-side Categrories.list(). This PR fixes that by looking up the categories when the selector is initialized. This required altering the /categories/find.json endpoint to accept a list of IDs that need to be looked up. The API is called using Category.asyncFindByIds on the client-side. CategorySelector was also updated to receive a list of category IDs as attribute, instead of the list of categories, because the list of categories may have not been loaded. During this development, I noticed that SiteCategorySerializer did not serializer all fields (such as permission and notification_level) which are not a property of category, but a property of the relationship between users and categories. To make this more efficient, the preload_user_fields! method was implemented that can be used to preload these attributes for a user and a list of categories.
554 lines
17 KiB
Ruby
554 lines
17 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class CategoriesController < ApplicationController
|
|
include TopicQueryParams
|
|
|
|
requires_login except: %i[
|
|
index
|
|
categories_and_latest
|
|
categories_and_top
|
|
show
|
|
redirect
|
|
find_by_slug
|
|
visible_groups
|
|
find
|
|
search
|
|
]
|
|
|
|
before_action :fetch_category, only: %i[show update destroy visible_groups]
|
|
before_action :initialize_staff_action_logger, only: %i[create update destroy]
|
|
skip_before_action :check_xhr, only: %i[index categories_and_latest categories_and_top redirect]
|
|
|
|
SYMMETRICAL_CATEGORIES_TO_TOPICS_FACTOR = 1.5
|
|
MIN_CATEGORIES_TOPICS = 5
|
|
MAX_CATEGORIES_LIMIT = 25
|
|
|
|
def redirect
|
|
return if handle_permalink("/category/#{params[:path]}")
|
|
redirect_to path("/c/#{params[:path]}")
|
|
end
|
|
|
|
def index
|
|
discourse_expires_in 1.minute
|
|
|
|
@description = SiteSetting.site_description
|
|
|
|
parent_category =
|
|
Category.find_by_slug(params[:parent_category_id]) ||
|
|
Category.find_by(id: params[:parent_category_id].to_i)
|
|
|
|
include_subcategories =
|
|
SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" ||
|
|
params[:include_subcategories] == "true"
|
|
|
|
category_options = {
|
|
is_homepage: current_homepage == "categories",
|
|
parent_category_id: params[:parent_category_id],
|
|
include_topics: include_topics(parent_category),
|
|
include_subcategories: include_subcategories,
|
|
tag: params[:tag],
|
|
}
|
|
|
|
@category_list = CategoryList.new(guardian, category_options)
|
|
|
|
if category_options[:is_homepage] && SiteSetting.short_site_description.present?
|
|
@title = "#{SiteSetting.title} - #{SiteSetting.short_site_description}"
|
|
elsif !category_options[:is_homepage]
|
|
@title = "#{I18n.t("js.filters.categories.title")} - #{SiteSetting.title}"
|
|
end
|
|
|
|
respond_to do |format|
|
|
format.html do
|
|
store_preloaded(
|
|
@category_list.preload_key,
|
|
MultiJson.dump(CategoryListSerializer.new(@category_list, scope: guardian)),
|
|
)
|
|
|
|
style = SiteSetting.desktop_category_page_style
|
|
topic_options = { per_page: CategoriesController.topics_per_page, no_definitions: true }
|
|
|
|
if style == "categories_and_latest_topics_created_date"
|
|
topic_options[:order] = "created"
|
|
@topic_list = TopicQuery.new(current_user, topic_options).list_latest
|
|
@topic_list.more_topics_url = url_for(public_send("latest_path", sort: :created))
|
|
elsif style == "categories_and_latest_topics"
|
|
@topic_list = TopicQuery.new(current_user, topic_options).list_latest
|
|
@topic_list.more_topics_url = url_for(public_send("latest_path"))
|
|
elsif style == "categories_and_top_topics"
|
|
@topic_list =
|
|
TopicQuery.new(current_user, topic_options).list_top_for(
|
|
SiteSetting.top_page_default_timeframe.to_sym,
|
|
)
|
|
@topic_list.more_topics_url = url_for(public_send("top_path"))
|
|
end
|
|
|
|
if @topic_list.present? && @topic_list.topics.present?
|
|
store_preloaded(
|
|
@topic_list.preload_key,
|
|
MultiJson.dump(TopicListSerializer.new(@topic_list, scope: guardian)),
|
|
)
|
|
end
|
|
|
|
render
|
|
end
|
|
|
|
format.json { render_serialized(@category_list, CategoryListSerializer) }
|
|
end
|
|
end
|
|
|
|
def categories_and_latest
|
|
categories_and_topics(:latest)
|
|
end
|
|
|
|
def categories_and_top
|
|
categories_and_topics(:top)
|
|
end
|
|
|
|
def move
|
|
guardian.ensure_can_create_category!
|
|
|
|
params.require("category_id")
|
|
params.require("position")
|
|
|
|
if category = Category.find(params["category_id"])
|
|
category.move_to(params["position"].to_i)
|
|
render json: success_json
|
|
else
|
|
render status: 500, json: failed_json
|
|
end
|
|
end
|
|
|
|
def reorder
|
|
guardian.ensure_can_create_category!
|
|
|
|
params.require(:mapping)
|
|
change_requests = MultiJson.load(params[:mapping])
|
|
by_category = Hash[change_requests.map { |cat, pos| [Category.find(cat.to_i), pos] }]
|
|
|
|
unless guardian.is_admin?
|
|
unless by_category.keys.all? { |c| guardian.can_see_category? c }
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
end
|
|
|
|
by_category.each do |cat, pos|
|
|
cat.position = pos
|
|
cat.save! if cat.will_save_change_to_position?
|
|
end
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def show
|
|
guardian.ensure_can_see!(@category)
|
|
|
|
if Category.topic_create_allowed(guardian).where(id: @category.id).exists?
|
|
@category.permission = CategoryGroup.permission_types[:full]
|
|
end
|
|
|
|
render_serialized(@category, CategorySerializer)
|
|
end
|
|
|
|
def create
|
|
guardian.ensure_can_create!(Category)
|
|
position = category_params.delete(:position)
|
|
|
|
@category =
|
|
begin
|
|
Category.new(required_create_params.merge(user: current_user))
|
|
rescue ArgumentError => e
|
|
return render json: { errors: [e.message] }, status: 422
|
|
end
|
|
|
|
if @category.save
|
|
@category.move_to(position.to_i) if position
|
|
|
|
Scheduler::Defer.later "Log staff action create category" do
|
|
@staff_action_logger.log_category_creation(@category)
|
|
end
|
|
|
|
render_serialized(@category, CategorySerializer)
|
|
else
|
|
render_json_error(@category)
|
|
end
|
|
end
|
|
|
|
def update
|
|
guardian.ensure_can_edit!(@category)
|
|
|
|
json_result(@category, serializer: CategorySerializer) do |cat|
|
|
old_category_params = category_params.dup
|
|
|
|
cat.move_to(category_params[:position].to_i) if category_params[:position]
|
|
category_params.delete(:position)
|
|
|
|
old_custom_fields = cat.custom_fields.dup
|
|
if category_params[:custom_fields]
|
|
category_params[:custom_fields].each do |key, value|
|
|
if value.present?
|
|
cat.custom_fields[key] = value
|
|
else
|
|
cat.custom_fields.delete(key)
|
|
end
|
|
end
|
|
end
|
|
category_params.delete(:custom_fields)
|
|
|
|
# properly null the value so the database constraint doesn't catch us
|
|
category_params[:email_in] = nil if category_params[:email_in]&.blank?
|
|
category_params[:minimum_required_tags] = 0 if category_params[:minimum_required_tags]&.blank?
|
|
|
|
old_permissions = cat.permissions_params
|
|
old_permissions = { "everyone" => 1 } if old_permissions.empty?
|
|
|
|
if result = cat.update(category_params)
|
|
Scheduler::Defer.later "Log staff action change category settings" do
|
|
@staff_action_logger.log_category_settings_change(
|
|
@category,
|
|
old_category_params,
|
|
old_permissions: old_permissions,
|
|
old_custom_fields: old_custom_fields,
|
|
)
|
|
end
|
|
end
|
|
|
|
DiscourseEvent.trigger(:category_updated, cat) if result
|
|
|
|
result
|
|
end
|
|
end
|
|
|
|
def update_slug
|
|
@category = Category.find(params[:category_id].to_i)
|
|
guardian.ensure_can_edit!(@category)
|
|
|
|
custom_slug = params[:slug].to_s
|
|
|
|
if custom_slug.blank?
|
|
error = @category.errors.full_message(:slug, I18n.t("errors.messages.blank"))
|
|
render_json_error(error)
|
|
elsif @category.update(slug: custom_slug)
|
|
render json: success_json
|
|
else
|
|
render_json_error(@category)
|
|
end
|
|
end
|
|
|
|
def set_notifications
|
|
category_id = params[:category_id].to_i
|
|
notification_level = params[:notification_level].to_i
|
|
|
|
CategoryUser.set_notification_level_for_category(current_user, notification_level, category_id)
|
|
render json:
|
|
success_json.merge(
|
|
{
|
|
indirectly_muted_category_ids:
|
|
CategoryUser.indirectly_muted_category_ids(current_user),
|
|
},
|
|
)
|
|
end
|
|
|
|
def destroy
|
|
guardian.ensure_can_delete!(@category)
|
|
@category.destroy
|
|
|
|
Scheduler::Defer.later "Log staff action delete category" do
|
|
@staff_action_logger.log_category_deletion(@category)
|
|
end
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def find_by_slug
|
|
params.require(:category_slug)
|
|
@category =
|
|
Category.includes(:category_setting).find_by_slug_path(params[:category_slug].split("/"))
|
|
|
|
raise Discourse::NotFound unless @category.present?
|
|
|
|
if !guardian.can_see?(@category)
|
|
if SiteSetting.detailed_404 && group = @category.access_category_via_group
|
|
raise Discourse::InvalidAccess.new(
|
|
"not in group",
|
|
@category,
|
|
custom_message: "not_in_group.title_category",
|
|
custom_message_params: {
|
|
group: group.name,
|
|
},
|
|
group: group,
|
|
)
|
|
else
|
|
raise Discourse::NotFound
|
|
end
|
|
end
|
|
|
|
@category.permission = CategoryGroup.permission_types[:full] if Category
|
|
.topic_create_allowed(guardian)
|
|
.where(id: @category.id)
|
|
.exists?
|
|
render_serialized(@category, CategorySerializer)
|
|
end
|
|
|
|
def visible_groups
|
|
@guardian.ensure_can_see!(@category)
|
|
|
|
groups =
|
|
if !@category.groups.exists?(id: Group::AUTO_GROUPS[:everyone])
|
|
@category.groups.merge(Group.visible_groups(current_user)).pluck("name")
|
|
end
|
|
|
|
render json: success_json.merge(groups: groups || [])
|
|
end
|
|
|
|
def find
|
|
categories = []
|
|
|
|
if params[:ids].present?
|
|
categories = Category.secured(guardian).where(id: params[:ids])
|
|
elsif params[:slug_path_with_id].present?
|
|
category = Category.find_by_slug_path_with_id(params[:slug_path_with_id])
|
|
raise Discourse::NotFound if category.blank?
|
|
guardian.ensure_can_see!(category)
|
|
|
|
ancestors = Category.secured(guardian).with_ancestors(category.id).where.not(id: category.id)
|
|
categories = [*ancestors, category]
|
|
end
|
|
|
|
raise Discourse::NotFound if categories.blank?
|
|
|
|
Category.preload_user_fields!(guardian, categories)
|
|
|
|
render_serialized(categories, SiteCategorySerializer, root: :categories, scope: guardian)
|
|
end
|
|
|
|
def search
|
|
term = params[:term].to_s.strip
|
|
parent_category_id = params[:parent_category_id].to_i if params[:parent_category_id].present?
|
|
include_uncategorized =
|
|
(
|
|
if params[:include_uncategorized].present?
|
|
ActiveModel::Type::Boolean.new.cast(params[:include_uncategorized])
|
|
else
|
|
true
|
|
end
|
|
)
|
|
select_category_ids = params[:select_category_ids].presence
|
|
reject_category_ids = params[:reject_category_ids].presence
|
|
include_subcategories =
|
|
if params[:include_subcategories].present?
|
|
ActiveModel::Type::Boolean.new.cast(params[:include_subcategories])
|
|
else
|
|
true
|
|
end
|
|
prioritized_category_id = params[:prioritized_category_id].to_i if params[
|
|
:prioritized_category_id
|
|
].present?
|
|
limit = params[:limit].to_i.clamp(1, MAX_CATEGORIES_LIMIT) if params[:limit].present?
|
|
|
|
categories = Category.secured(guardian)
|
|
|
|
categories =
|
|
categories
|
|
.includes(:category_search_data)
|
|
.references(:category_search_data)
|
|
.where(
|
|
"category_search_data.search_data @@ #{Search.ts_query(term: term)}",
|
|
) if term.present?
|
|
|
|
categories =
|
|
(
|
|
if parent_category_id != -1
|
|
categories.where(parent_category_id: parent_category_id)
|
|
else
|
|
categories.where(parent_category_id: nil)
|
|
end
|
|
) if parent_category_id.present?
|
|
|
|
categories =
|
|
categories.where.not(id: SiteSetting.uncategorized_category_id) if !include_uncategorized
|
|
|
|
categories = categories.where(id: select_category_ids) if select_category_ids
|
|
|
|
categories = categories.where.not(id: reject_category_ids) if reject_category_ids
|
|
|
|
categories = categories.where(parent_category_id: nil) if !include_subcategories
|
|
|
|
categories = categories.limit(limit || MAX_CATEGORIES_LIMIT)
|
|
|
|
categories = categories.order(<<~SQL) if prioritized_category_id.present?
|
|
CASE
|
|
WHEN id = #{prioritized_category_id} THEN 1
|
|
WHEN parent_category_id = #{prioritized_category_id} THEN 2
|
|
ELSE 3
|
|
END
|
|
SQL
|
|
|
|
categories = categories.order(:id)
|
|
|
|
Category.preload_user_fields!(guardian, categories)
|
|
|
|
render_serialized(categories, SiteCategorySerializer, root: :categories, scope: guardian)
|
|
end
|
|
|
|
private
|
|
|
|
def self.topics_per_page
|
|
return SiteSetting.categories_topics if SiteSetting.categories_topics > 0
|
|
|
|
count = Category.where(parent_category: nil).count
|
|
count = (SYMMETRICAL_CATEGORIES_TO_TOPICS_FACTOR * count).to_i
|
|
count > MIN_CATEGORIES_TOPICS ? count : MIN_CATEGORIES_TOPICS
|
|
end
|
|
|
|
def categories_and_topics(topics_filter)
|
|
discourse_expires_in 1.minute
|
|
|
|
category_options = {
|
|
is_homepage: current_homepage == "categories",
|
|
parent_category_id: params[:parent_category_id],
|
|
include_topics: false,
|
|
}
|
|
|
|
topic_options = { per_page: CategoriesController.topics_per_page, no_definitions: true }
|
|
|
|
topic_options.merge!(build_topic_list_options)
|
|
style = SiteSetting.desktop_category_page_style
|
|
topic_options[:order] = "created" if style == "categories_and_latest_topics_created_date"
|
|
|
|
result = CategoryAndTopicLists.new
|
|
result.category_list = CategoryList.new(guardian, category_options)
|
|
|
|
if topics_filter == :latest
|
|
result.topic_list = TopicQuery.new(current_user, topic_options).list_latest
|
|
result.topic_list.more_topics_url =
|
|
url_for(
|
|
public_send("latest_path", sort: topic_options[:order] == "created" ? :created : nil),
|
|
)
|
|
elsif topics_filter == :top
|
|
result.topic_list =
|
|
TopicQuery.new(current_user, topic_options).list_top_for(
|
|
SiteSetting.top_page_default_timeframe.to_sym,
|
|
)
|
|
result.topic_list.more_topics_url = url_for(public_send("top_path"))
|
|
end
|
|
|
|
render_serialized(result, CategoryAndTopicListsSerializer, root: false)
|
|
end
|
|
|
|
def required_param_keys
|
|
[:name]
|
|
end
|
|
|
|
def required_create_params
|
|
required_param_keys.each { |key| params.require(key) }
|
|
category_params
|
|
end
|
|
|
|
def category_params
|
|
@category_params ||=
|
|
begin
|
|
if p = params[:permissions]
|
|
p.each { |k, v| p[k] = v.to_i }
|
|
end
|
|
|
|
if SiteSetting.tagging_enabled
|
|
params[:allowed_tags] = params[:allowed_tags].presence || [] if params[:allowed_tags]
|
|
params[:allowed_tag_groups] = params[:allowed_tag_groups].presence || [] if params[
|
|
:allowed_tag_groups
|
|
]
|
|
params[:required_tag_groups] = params[:required_tag_groups].presence || [] if params[
|
|
:required_tag_groups
|
|
]
|
|
end
|
|
|
|
if SiteSetting.enable_category_group_moderation?
|
|
params[:reviewable_by_group_id] = Group.where(
|
|
name: params[:reviewable_by_group_name],
|
|
).pick(:id) if params[:reviewable_by_group_name]
|
|
end
|
|
|
|
result =
|
|
params.permit(
|
|
*required_param_keys,
|
|
:position,
|
|
:name,
|
|
:color,
|
|
:text_color,
|
|
:email_in,
|
|
:email_in_allow_strangers,
|
|
:mailinglist_mirror,
|
|
:all_topics_wiki,
|
|
:allow_unlimited_owner_edits_on_first_post,
|
|
:default_slow_mode_seconds,
|
|
:parent_category_id,
|
|
:auto_close_hours,
|
|
:auto_close_based_on_last_post,
|
|
:uploaded_logo_id,
|
|
:uploaded_logo_dark_id,
|
|
:uploaded_background_id,
|
|
:uploaded_background_dark_id,
|
|
:slug,
|
|
:allow_badges,
|
|
:topic_template,
|
|
:sort_order,
|
|
:sort_ascending,
|
|
:topic_featured_link_allowed,
|
|
:show_subcategory_list,
|
|
:num_featured_topics,
|
|
:default_view,
|
|
:subcategory_list_style,
|
|
:default_top_period,
|
|
:minimum_required_tags,
|
|
:navigate_to_first_post_after_read,
|
|
:search_priority,
|
|
:allow_global_tags,
|
|
:read_only_banner,
|
|
:default_list_filter,
|
|
:reviewable_by_group_id,
|
|
category_setting_attributes: %i[
|
|
auto_bump_cooldown_days
|
|
num_auto_bump_daily
|
|
require_reply_approval
|
|
require_topic_approval
|
|
],
|
|
custom_fields: [custom_field_params],
|
|
permissions: [*p.try(:keys)],
|
|
allowed_tags: [],
|
|
allowed_tag_groups: [],
|
|
required_tag_groups: %i[name min_count],
|
|
form_template_ids: [],
|
|
)
|
|
|
|
if result[:required_tag_groups] && !result[:required_tag_groups].is_a?(Array)
|
|
raise Discourse::InvalidParameters.new(:required_tag_groups)
|
|
end
|
|
|
|
result
|
|
end
|
|
end
|
|
|
|
def custom_field_params
|
|
keys = params[:custom_fields].try(:keys)
|
|
return if keys.blank?
|
|
|
|
keys.map { |key| params[:custom_fields][key].is_a?(Array) ? { key => [] } : key }
|
|
end
|
|
|
|
def fetch_category
|
|
@category = Category.find_by_slug(params[:id]) || Category.find_by(id: params[:id].to_i)
|
|
raise Discourse::NotFound if @category.blank?
|
|
end
|
|
|
|
def initialize_staff_action_logger
|
|
@staff_action_logger = StaffActionLogger.new(current_user)
|
|
end
|
|
|
|
def include_topics(parent_category = nil)
|
|
style = SiteSetting.desktop_category_page_style
|
|
view_context.mobile_view? || params[:include_topics] ||
|
|
(parent_category && parent_category.subcategory_list_includes_topics?) ||
|
|
style == "categories_with_featured_topics" || style == "subcategories_with_featured_topics" ||
|
|
style == "categories_boxes_with_topics" || style == "categories_with_top_topics"
|
|
end
|
|
end
|