discourse/app/controllers/categories_controller.rb
Ted Johansson 25a226279a
DEV: Replace #pluck_first freedom patch with AR #pick in core (#19893)
The #pluck_first freedom patch, first introduced by @danielwaterworth has served us well, and is used widely throughout both core and plugins. It seems to have been a common enough use case that Rails 6 introduced it's own method #pick with the exact same implementation. This allows us to retire the freedom patch and switch over to the built-in ActiveRecord method.

There is no replacement for #pluck_first!, but a quick search shows we are using this in a very limited capacity, and in some cases incorrectly (by assuming a nil return rather than an exception), which can quite easily be replaced with #pick plus some extra handling.
2023-02-13 12:39:45 +08:00

447 lines
14 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
]
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
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.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
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
elsif topics_filter == :top
result.topic_list =
TopicQuery.new(current_user, topic_options).list_top_for(
SiteSetting.top_page_default_timeframe.to_sym,
)
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,
: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,
custom_fields: [custom_field_params],
permissions: [*p.try(:keys)],
allowed_tags: [],
allowed_tag_groups: [],
required_tag_groups: %i[name min_count],
)
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