mirror of
https://github.com/discourse/discourse.git
synced 2025-06-21 00:11:39 +08:00

These lines of code would output "Period - Site name" as the page title for the crawlier view only for Top routes. This is too specific and unnecessary, since the top route isn't highly prioritized for crawlers. Without this, the page title defaults to "Site name" for these routes. One major advantage of this change is that when the homepage is set to the Top route, the page title for crawlers on the homepage is now "Site name", which is a lot better.
531 lines
17 KiB
Ruby
531 lines
17 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class ListController < ApplicationController
|
|
include TopicListResponder
|
|
include TopicQueryParams
|
|
|
|
skip_before_action :check_xhr
|
|
|
|
before_action :set_category,
|
|
only: [
|
|
:category_default,
|
|
# filtered topics lists
|
|
Discourse.filters.map { |f| :"category_#{f}" },
|
|
Discourse.filters.map { |f| :"category_none_#{f}" },
|
|
# top summaries
|
|
:category_top,
|
|
:category_none_top,
|
|
# top pages (ie. with a period)
|
|
TopTopic.periods.map { |p| :"category_top_#{p}" },
|
|
TopTopic.periods.map { |p| :"category_none_top_#{p}" },
|
|
# category feeds
|
|
:category_feed,
|
|
].flatten
|
|
|
|
before_action :ensure_logged_in,
|
|
except: [
|
|
:topics_by,
|
|
# anonymous filters
|
|
Discourse.anonymous_filters,
|
|
Discourse.anonymous_filters.map { |f| "#{f}_feed" },
|
|
# anonymous categorized filters
|
|
:category_default,
|
|
Discourse.anonymous_filters.map { |f| :"category_#{f}" },
|
|
Discourse.anonymous_filters.map { |f| :"category_none_#{f}" },
|
|
# category feeds
|
|
:category_feed,
|
|
# user topics feed
|
|
:user_topics_feed,
|
|
# top summaries
|
|
:top,
|
|
:category_top,
|
|
:category_none_top,
|
|
# top pages (ie. with a period)
|
|
TopTopic.periods.map { |p| :"top_#{p}" },
|
|
TopTopic.periods.map { |p| :"top_#{p}_feed" },
|
|
TopTopic.periods.map { |p| :"category_top_#{p}" },
|
|
TopTopic.periods.map { |p| :"category_none_top_#{p}" },
|
|
:group_topics,
|
|
:filter,
|
|
].flatten
|
|
|
|
rescue_from ActionController::Redirecting::UnsafeRedirectError do
|
|
rescue_discourse_actions(:not_found, 404)
|
|
end
|
|
|
|
# Create our filters
|
|
Discourse.filters.each do |filter|
|
|
define_method(filter) do |options = nil|
|
|
list_opts = build_topic_list_options
|
|
list_opts.merge!(options) if options
|
|
user = list_target_user
|
|
if params[:category].blank? && filter == :latest &&
|
|
!SiteSetting.show_category_definitions_in_topic_lists
|
|
list_opts[:no_definitions] = true
|
|
end
|
|
|
|
list = TopicQuery.new(user, list_opts).public_send("list_#{filter}")
|
|
|
|
if guardian.can_create_shared_draft? && @category.present?
|
|
if @category.id == SiteSetting.shared_drafts_category.to_i
|
|
# On shared drafts, show the destination category
|
|
list.topics.each { |t| t.includes_destination_category = t.shared_draft.present? }
|
|
else
|
|
# When viewing a non-shared draft category, find topics whose
|
|
# destination are this category
|
|
shared_drafts =
|
|
TopicQuery.new(
|
|
user,
|
|
category: SiteSetting.shared_drafts_category,
|
|
destination_category_id: list_opts[:category],
|
|
).list_latest
|
|
|
|
if shared_drafts.present? && shared_drafts.topics.present?
|
|
list.shared_drafts = shared_drafts.topics
|
|
end
|
|
end
|
|
end
|
|
|
|
list.more_topics_url = construct_url_with(:next, list_opts)
|
|
list.prev_topics_url = construct_url_with(:prev, list_opts)
|
|
|
|
if Discourse.anonymous_filters.include?(filter)
|
|
@description = SiteSetting.site_description
|
|
@rss = filter
|
|
@rss_description = filter
|
|
|
|
# Note the first is the default and we don't add a title
|
|
if (filter.to_s != current_homepage) && use_crawler_layout?
|
|
filter_title = I18n.t("js.filters.#{filter}.title", count: 0)
|
|
|
|
if list_opts[:category] && @category
|
|
@title =
|
|
I18n.t("js.filters.with_category", filter: filter_title, category: @category.name)
|
|
else
|
|
@title = I18n.t("js.filters.with_topics", filter: filter_title)
|
|
end
|
|
|
|
@title << " - #{SiteSetting.title}"
|
|
elsif @category.blank? && (filter.to_s == current_homepage) &&
|
|
SiteSetting.short_site_description.present?
|
|
@title = "#{SiteSetting.title} - #{SiteSetting.short_site_description}"
|
|
end
|
|
end
|
|
|
|
respond_with_list(list)
|
|
end
|
|
|
|
define_method("category_#{filter}") do
|
|
canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}"
|
|
self.public_send(filter, category: @category.id)
|
|
end
|
|
|
|
define_method("category_none_#{filter}") do
|
|
self.public_send(filter, category: @category.id, no_subcategories: true)
|
|
end
|
|
end
|
|
|
|
def filter
|
|
topic_query_opts = { no_definitions: !SiteSetting.show_category_definitions_in_topic_lists }
|
|
|
|
%i[page q].each do |key|
|
|
if params.key?(key.to_s)
|
|
value = params[key]
|
|
raise Discourse::InvalidParameters.new(key) if !TopicQuery.validate?(key, value)
|
|
topic_query_opts[key] = value
|
|
end
|
|
end
|
|
|
|
user = list_target_user
|
|
list = TopicQuery.new(user, topic_query_opts).list_filter
|
|
list.more_topics_url = construct_url_with(:next, topic_query_opts)
|
|
list.prev_topics_url = construct_url_with(:prev, topic_query_opts)
|
|
|
|
respond_with_list(list)
|
|
end
|
|
|
|
def category_default
|
|
canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}"
|
|
view_method = @category.default_view
|
|
view_method = "latest" if %w[latest top].exclude?(view_method)
|
|
|
|
self.public_send(view_method, category: @category.id)
|
|
end
|
|
|
|
def topics_by
|
|
list_opts = build_topic_list_options
|
|
target_user =
|
|
fetch_user_from_params(
|
|
{
|
|
include_inactive:
|
|
current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts),
|
|
},
|
|
%i[user_stat user_option],
|
|
)
|
|
ensure_can_see_profile!(target_user)
|
|
|
|
list = generate_list_for("topics_by", target_user, list_opts)
|
|
list.more_topics_url = construct_url_with(:next, list_opts)
|
|
list.prev_topics_url = construct_url_with(:prev, list_opts)
|
|
respond_with_list(list)
|
|
end
|
|
|
|
def group_topics
|
|
group = Group.find_by(name: params[:group_name])
|
|
raise Discourse::NotFound unless group
|
|
guardian.ensure_can_see_group!(group)
|
|
guardian.ensure_can_see_group_members!(group)
|
|
|
|
list_opts = build_topic_list_options
|
|
list = generate_list_for("group_topics", group, list_opts)
|
|
list.more_topics_url = construct_url_with(:next, list_opts)
|
|
list.prev_topics_url = construct_url_with(:prev, list_opts)
|
|
respond_with_list(list)
|
|
end
|
|
|
|
def self.generate_message_route(action)
|
|
define_method action do
|
|
message_route(action)
|
|
end
|
|
end
|
|
|
|
def message_route(action)
|
|
target_user =
|
|
fetch_user_from_params(
|
|
{ include_inactive: current_user.try(:staff?) },
|
|
%i[user_stat user_option],
|
|
)
|
|
|
|
case action
|
|
when :private_messages_unread, :private_messages_new, :private_messages_group_new,
|
|
:private_messages_group_unread
|
|
raise Discourse::NotFound if target_user.id != current_user.id
|
|
when :private_messages_tag
|
|
raise Discourse::NotFound if target_user.id != current_user.id
|
|
raise Discourse::NotFound if !guardian.can_tag_pms?
|
|
when :private_messages_warnings
|
|
guardian.ensure_can_see_warnings!(target_user)
|
|
when :private_messages_group, :private_messages_group_archive
|
|
group = Group.find_by("LOWER(name) = ?", params[:group_name].downcase)
|
|
raise Discourse::NotFound if !group
|
|
raise Discourse::NotFound unless guardian.can_see_group_messages?(group)
|
|
else
|
|
guardian.ensure_can_see_private_messages!(target_user.id)
|
|
end
|
|
|
|
list_opts = build_topic_list_options
|
|
list = generate_list_for(action.to_s, target_user, list_opts)
|
|
url_prefix = "topics"
|
|
list.more_topics_url = construct_url_with(:next, list_opts, url_prefix)
|
|
list.prev_topics_url = construct_url_with(:prev, list_opts, url_prefix)
|
|
respond_with_list(list)
|
|
end
|
|
|
|
%i[
|
|
private_messages
|
|
private_messages_sent
|
|
private_messages_unread
|
|
private_messages_new
|
|
private_messages_archive
|
|
private_messages_group
|
|
private_messages_group_new
|
|
private_messages_group_unread
|
|
private_messages_group_archive
|
|
private_messages_warnings
|
|
private_messages_tag
|
|
].each { |action| generate_message_route(action) }
|
|
|
|
def latest_feed
|
|
discourse_expires_in 1.minute
|
|
|
|
options = { order: "created" }.merge(build_topic_list_options)
|
|
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.latest")}"
|
|
@link = "#{Discourse.base_url}/latest"
|
|
@atom_link = "#{Discourse.base_url}/latest.rss"
|
|
@description = I18n.t("rss_description.latest")
|
|
@topic_list = TopicQuery.new(nil, options).list_latest
|
|
|
|
render "list", formats: [:rss]
|
|
end
|
|
|
|
def top_feed
|
|
discourse_expires_in 1.minute
|
|
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.top")}"
|
|
@link = "#{Discourse.base_url}/top"
|
|
@atom_link = "#{Discourse.base_url}/top.rss"
|
|
@description = I18n.t("rss_description.top")
|
|
period = params[:period] || SiteSetting.top_page_default_timeframe.to_sym
|
|
TopTopic.validate_period(period)
|
|
|
|
@topic_list = TopicQuery.new(nil).list_top_for(period)
|
|
|
|
render "list", formats: [:rss]
|
|
end
|
|
|
|
def hot_feed
|
|
discourse_expires_in 1.minute
|
|
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.hot")}"
|
|
@link = "#{Discourse.base_url}/hot"
|
|
@atom_link = "#{Discourse.base_url}/hot.rss"
|
|
@description = I18n.t("rss_description.hot")
|
|
|
|
@topic_list = TopicQuery.new(nil).list_hot
|
|
|
|
render "list", formats: [:rss]
|
|
end
|
|
|
|
def category_feed
|
|
guardian.ensure_can_see!(@category)
|
|
discourse_expires_in 1.minute
|
|
|
|
@title = "#{@category.name} - #{SiteSetting.title}"
|
|
@link = "#{Discourse.base_url_no_prefix}#{@category.url}"
|
|
@atom_link = "#{Discourse.base_url_no_prefix}#{@category.url}.rss"
|
|
@description =
|
|
"#{I18n.t("topics_in_category", category: @category.name)} #{@category.description}"
|
|
@topic_list = TopicQuery.new(current_user).list_new_in_category(@category)
|
|
|
|
render "list", formats: [:rss]
|
|
end
|
|
|
|
def user_topics_feed
|
|
discourse_expires_in 1.minute
|
|
target_user = fetch_user_from_params
|
|
ensure_can_see_profile!(target_user)
|
|
|
|
@title =
|
|
"#{SiteSetting.title} - #{I18n.t("rss_description.user_topics", username: target_user.username)}"
|
|
@link = "#{target_user.full_url}/activity/topics"
|
|
@atom_link = "#{target_user.full_url}/activity/topics.rss"
|
|
@description = I18n.t("rss_description.user_topics", username: target_user.username)
|
|
|
|
@topic_list = TopicQuery.new(nil, order: "created").public_send("list_topics_by", target_user)
|
|
|
|
render "list", formats: [:rss]
|
|
end
|
|
|
|
def top(options = nil)
|
|
options ||= {}
|
|
period = params[:period]
|
|
period ||=
|
|
ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])
|
|
TopTopic.validate_period(period)
|
|
public_send("top_#{period}", options)
|
|
end
|
|
|
|
def category_top
|
|
top(category: @category.id)
|
|
end
|
|
|
|
def category_none_top
|
|
top(category: @category.id, no_subcategories: true)
|
|
end
|
|
|
|
TopTopic.periods.each do |period|
|
|
define_method("top_#{period}") do |options = nil|
|
|
top_options = build_topic_list_options
|
|
top_options.merge!(options) if options
|
|
top_options[:per_page] = top_options[:per_page].presence ||
|
|
SiteSetting.topics_per_period_in_top_page
|
|
|
|
user = list_target_user
|
|
list = TopicQuery.new(user, top_options).list_top_for(period)
|
|
list.for_period = period
|
|
list.more_topics_url = construct_url_with(:next, top_options)
|
|
list.prev_topics_url = construct_url_with(:prev, top_options)
|
|
@rss = "top"
|
|
@params = { period: period }
|
|
@rss_description = "top_#{period}"
|
|
|
|
respond_with_list(list)
|
|
end
|
|
|
|
define_method("category_top_#{period}") do
|
|
self.public_send("top_#{period}", category: @category.id)
|
|
end
|
|
|
|
define_method("category_none_top_#{period}") do
|
|
self.public_send("top_#{period}", category: @category.id, no_subcategories: true)
|
|
end
|
|
|
|
# rss feed
|
|
define_method("top_#{period}_feed") do |options = nil|
|
|
discourse_expires_in 1.minute
|
|
|
|
@description = I18n.t("rss_description.top_#{period}")
|
|
@title = "#{SiteSetting.title} - #{@description}"
|
|
@link = "#{Discourse.base_url}/top?period=#{period}"
|
|
@atom_link = "#{Discourse.base_url}/top.rss?period=#{period}"
|
|
@topic_list = TopicQuery.new(nil).list_top_for(period)
|
|
|
|
render "list", formats: [:rss]
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def next_page_params
|
|
page_params.merge(page: params[:page].to_i + 1)
|
|
end
|
|
|
|
def prev_page_params
|
|
pg = params[:page].to_i
|
|
if pg > 1
|
|
page_params.merge(page: pg - 1)
|
|
else
|
|
page_params.merge(page: nil)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def page_params
|
|
route_params = { format: "json" }
|
|
|
|
if @category.present?
|
|
slug_path = @category.slug_path
|
|
|
|
route_params[:category_slug_path_with_id] = (slug_path + [@category.id.to_s]).join("/")
|
|
end
|
|
|
|
route_params[:username] = UrlHelper.encode_component(params[:username]) if params[
|
|
:username
|
|
].present?
|
|
route_params[:period] = params[:period] if params[:period].present?
|
|
route_params
|
|
end
|
|
|
|
def set_category
|
|
category_slug_path_with_id = params.require(:category_slug_path_with_id)
|
|
|
|
@category = Category.find_by_slug_path_with_id(category_slug_path_with_id)
|
|
raise Discourse::NotFound.new("category not found", check_permalinks: true) if @category.nil?
|
|
|
|
params[:category] = @category.id.to_s
|
|
|
|
if !guardian.can_see?(@category)
|
|
if SiteSetting.detailed_404
|
|
raise Discourse::InvalidAccess
|
|
else
|
|
raise Discourse::NotFound
|
|
end
|
|
end
|
|
|
|
# Check if the category slug is incorrect and redirect to a link containing
|
|
# the correct one.
|
|
current_slug = category_slug_path_with_id
|
|
if SiteSetting.slug_generation_method == "encoded"
|
|
current_slug = current_slug.split("/").map { |slug| CGI.escape(slug) }.join("/")
|
|
end
|
|
real_slug = @category.full_slug("/")
|
|
if CGI.unescape(current_slug) != CGI.unescape(real_slug)
|
|
url = CGI.unescape(request.fullpath).gsub(current_slug, real_slug)
|
|
if ActionController::Base.config.relative_url_root
|
|
url = url.sub(ActionController::Base.config.relative_url_root, "")
|
|
end
|
|
|
|
return redirect_to path(url), status: 301
|
|
end
|
|
|
|
@description_meta =
|
|
if @category.uncategorized?
|
|
I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale)
|
|
elsif @category.description_text.present?
|
|
@category.description_text
|
|
else
|
|
SiteSetting.site_description
|
|
end
|
|
|
|
if use_crawler_layout?
|
|
@subcategories = @category.subcategories.select { |c| guardian.can_see?(c) }
|
|
end
|
|
end
|
|
|
|
def list_target_user
|
|
if params[:user_id] && guardian.is_staff?
|
|
User.find(params[:user_id].to_i)
|
|
else
|
|
current_user
|
|
end
|
|
end
|
|
|
|
def generate_list_for(action, target_user, opts)
|
|
TopicQuery.new(current_user, opts).public_send("list_#{action}", target_user)
|
|
end
|
|
|
|
def construct_url_with(action, opts, url_prefix = nil)
|
|
method = url_prefix.blank? ? "#{action_name}_path" : "#{url_prefix}_#{action_name}_path"
|
|
|
|
page_params =
|
|
case action
|
|
when :prev
|
|
prev_page_params
|
|
when :next
|
|
next_page_params
|
|
else
|
|
raise "unreachable"
|
|
end
|
|
|
|
opts = opts.dup
|
|
|
|
if SiteSetting.unicode_usernames && opts[:group_name]
|
|
opts[:group_name] = UrlHelper.encode_component(opts[:group_name])
|
|
end
|
|
|
|
opts.delete(:category) if page_params.include?(:category_slug_path_with_id)
|
|
|
|
url = public_send(method, opts.merge(page_params)).sub(".json?", "?")
|
|
|
|
# Unicode usernames need to be encoded when calling Rails' path helper. However, it means that the already
|
|
# encoded username are encoded again which we do not want. As such, we unencode the url once when unicode usernames
|
|
# have been enabled.
|
|
url = UrlHelper.unencode(url) if SiteSetting.unicode_usernames
|
|
|
|
url
|
|
end
|
|
|
|
def ensure_can_see_profile!(target_user = nil)
|
|
raise Discourse::NotFound unless guardian.can_see_profile?(target_user)
|
|
end
|
|
|
|
def self.best_period_for(previous_visit_at, category_id = nil)
|
|
default_period =
|
|
(
|
|
(category_id && Category.where(id: category_id).pick(:default_top_period)) ||
|
|
SiteSetting.top_page_default_timeframe
|
|
).to_sym
|
|
|
|
best_period_with_topics_for(previous_visit_at, category_id, default_period) || default_period
|
|
end
|
|
|
|
def self.best_period_with_topics_for(
|
|
previous_visit_at,
|
|
category_id = nil,
|
|
default_period = SiteSetting.top_page_default_timeframe
|
|
)
|
|
best_periods_for(previous_visit_at, default_period.to_sym).find do |period|
|
|
top_topics = TopTopic.where("#{period}_score > 0")
|
|
top_topics =
|
|
top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id
|
|
top_topics = top_topics.limit(SiteSetting.topics_per_period_in_top_page)
|
|
top_topics.count == SiteSetting.topics_per_period_in_top_page
|
|
end
|
|
end
|
|
|
|
def self.best_periods_for(date, default_period = :all)
|
|
return [default_period, :all].uniq unless date
|
|
|
|
periods = []
|
|
periods << :daily if date > (1.week + 1.day).ago
|
|
periods << :weekly if date > (1.month + 1.week).ago
|
|
periods << :monthly if date > (3.months + 3.weeks).ago
|
|
periods << :quarterly if date > (1.year + 1.month).ago
|
|
periods << :yearly if date > 3.years.ago
|
|
periods << :all
|
|
periods
|
|
end
|
|
end
|