2019-05-03 06:17:27 +08:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
class TopicView
|
2018-07-13 05:00:53 +08:00
|
|
|
MEGA_TOPIC_POSTS_COUNT = 10_000
|
2019-07-19 23:15:38 +08:00
|
|
|
MIN_POST_READ_TIME = 4.0
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2021-05-25 00:46:57 +08:00
|
|
|
def self.on_preload(&blk)
|
|
|
|
(@preload ||= Set.new) << blk
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.cancel_preload(&blk)
|
|
|
|
if @preload
|
|
|
|
@preload.delete blk
|
|
|
|
@preload = nil if @preload.length == 0
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.preload(topic_view)
|
|
|
|
@preload.each { |preload| preload.call(topic_view) } if @preload
|
|
|
|
end
|
|
|
|
|
2019-04-12 21:55:27 +08:00
|
|
|
attr_reader(
|
|
|
|
:topic,
|
|
|
|
:posts,
|
|
|
|
:guardian,
|
|
|
|
:filtered_posts,
|
|
|
|
:chunk_size,
|
|
|
|
:print,
|
|
|
|
:message_bus_last_id,
|
|
|
|
:queued_posts_enabled,
|
2019-05-04 02:26:37 +08:00
|
|
|
:personal_message,
|
2022-03-15 17:17:06 +08:00
|
|
|
:can_review_topic,
|
2022-12-06 23:10:36 +08:00
|
|
|
:page,
|
2019-04-12 21:55:27 +08:00
|
|
|
)
|
2021-12-03 01:03:43 +08:00
|
|
|
alias queued_posts_enabled? queued_posts_enabled
|
2019-04-12 21:55:27 +08:00
|
|
|
|
|
|
|
attr_accessor(
|
|
|
|
:draft,
|
|
|
|
:draft_key,
|
|
|
|
:draft_sequence,
|
|
|
|
:user_custom_fields,
|
|
|
|
:post_custom_fields,
|
|
|
|
:post_number,
|
2024-08-23 16:10:50 +08:00
|
|
|
:include_suggested,
|
|
|
|
:include_related,
|
2019-04-12 21:55:27 +08:00
|
|
|
)
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2021-12-03 01:03:43 +08:00
|
|
|
delegate :category, to: :topic, allow_nil: true, private: true
|
|
|
|
delegate :require_reply_approval?, to: :category, prefix: true, allow_nil: true, private: true
|
|
|
|
|
2016-08-01 12:45:05 +08:00
|
|
|
def self.print_chunk_size
|
|
|
|
1000
|
|
|
|
end
|
|
|
|
|
2024-08-23 16:10:50 +08:00
|
|
|
CHUNK_SIZE = 20
|
|
|
|
|
2014-12-15 07:57:34 +08:00
|
|
|
def self.chunk_size
|
2024-08-23 16:10:50 +08:00
|
|
|
CHUNK_SIZE
|
2014-12-15 07:57:34 +08:00
|
|
|
end
|
|
|
|
|
2016-01-11 19:42:06 +08:00
|
|
|
def self.default_post_custom_fields
|
2021-11-08 11:32:17 +08:00
|
|
|
@default_post_custom_fields ||= [Post::NOTICE, "action_code_who", "action_code_path"]
|
2016-01-11 19:42:06 +08:00
|
|
|
end
|
|
|
|
|
2020-07-27 08:23:54 +08:00
|
|
|
def self.post_custom_fields_allowlisters
|
|
|
|
@post_custom_fields_allowlisters ||= Set.new
|
2017-09-29 23:04:05 +08:00
|
|
|
end
|
2015-04-24 01:33:29 +08:00
|
|
|
|
2020-07-27 08:23:54 +08:00
|
|
|
def self.add_post_custom_fields_allowlister(&block)
|
|
|
|
post_custom_fields_allowlisters << block
|
2015-04-24 01:33:29 +08:00
|
|
|
end
|
|
|
|
|
2021-10-22 10:22:09 +08:00
|
|
|
def self.allowed_post_custom_fields(user, topic)
|
|
|
|
wpcf =
|
|
|
|
default_post_custom_fields + post_custom_fields_allowlisters.map { |w| w.call(user, topic) }
|
2016-01-11 19:42:06 +08:00
|
|
|
wpcf.flatten.uniq
|
2015-04-24 01:33:29 +08:00
|
|
|
end
|
|
|
|
|
2021-05-10 06:57:58 +08:00
|
|
|
def self.add_custom_filter(key, &blk)
|
2024-04-29 21:56:52 +08:00
|
|
|
custom_filters[key] = blk
|
2021-05-10 06:57:58 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def self.custom_filters
|
2024-04-29 21:56:52 +08:00
|
|
|
@custom_filters ||= {}
|
2021-05-10 06:57:58 +08:00
|
|
|
end
|
|
|
|
|
2021-11-24 16:40:58 +08:00
|
|
|
# Configure a default scope to be applied to @filtered_posts.
|
|
|
|
# The registered block is called with @filtered_posts and an instance of
|
|
|
|
# `TopicView`.
|
|
|
|
#
|
|
|
|
# This API should be considered experimental until it is exposed in
|
|
|
|
# `Plugin::Instance`.
|
|
|
|
def self.apply_custom_default_scope(&block)
|
|
|
|
custom_default_scopes << block
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.custom_default_scopes
|
|
|
|
@custom_default_scopes ||= []
|
|
|
|
end
|
|
|
|
|
|
|
|
# For testing
|
|
|
|
def self.reset_custom_default_scopes
|
|
|
|
@custom_default_scopes = nil
|
|
|
|
end
|
|
|
|
|
2018-05-28 17:06:47 +08:00
|
|
|
def initialize(topic_or_topic_id, user = nil, options = {})
|
|
|
|
@topic = find_topic(topic_or_topic_id)
|
2013-07-12 04:38:46 +08:00
|
|
|
@user = user
|
|
|
|
@guardian = Guardian.new(@user)
|
2018-03-24 09:44:39 +08:00
|
|
|
|
2020-09-02 08:10:42 +08:00
|
|
|
check_and_raise_exceptions(options[:skip_staff_action])
|
2013-06-21 05:20:08 +08:00
|
|
|
|
2018-05-28 17:06:47 +08:00
|
|
|
@message_bus_last_id = MessageBus.last_id("/topic/#{@topic.id}")
|
|
|
|
|
2013-10-28 14:12:07 +08:00
|
|
|
options.each { |key, value| self.instance_variable_set("@#{key}".to_sym, value) }
|
2013-08-13 22:29:25 +08:00
|
|
|
|
2018-03-24 09:44:39 +08:00
|
|
|
@post_number = [@post_number.to_i, 1].max
|
|
|
|
|
2019-02-22 07:37:18 +08:00
|
|
|
@include_suggested = options.fetch(:include_suggested) { true }
|
|
|
|
@include_related = options.fetch(:include_related) { true }
|
|
|
|
|
2016-08-01 12:45:05 +08:00
|
|
|
@chunk_size =
|
|
|
|
case
|
2016-08-05 13:12:35 +08:00
|
|
|
when @print
|
|
|
|
TopicView.print_chunk_size
|
2016-08-01 12:45:05 +08:00
|
|
|
else
|
|
|
|
TopicView.chunk_size
|
|
|
|
end
|
2017-07-28 09:20:09 +08:00
|
|
|
|
2014-12-13 00:47:20 +08:00
|
|
|
@limit ||= @chunk_size
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2020-10-19 14:11:49 +08:00
|
|
|
@page = @page.to_i > 1 ? @page.to_i : calculate_page
|
|
|
|
|
2013-07-12 16:33:45 +08:00
|
|
|
setup_filtered_posts
|
2021-11-24 16:40:58 +08:00
|
|
|
@filtered_posts = apply_default_scope(@filtered_posts)
|
2013-02-11 02:50:26 +08:00
|
|
|
filter_posts(options)
|
|
|
|
|
2018-06-28 14:54:54 +08:00
|
|
|
if @posts && !@skip_custom_fields
|
2020-07-27 08:23:54 +08:00
|
|
|
if (added_fields = User.allowed_user_custom_fields(@guardian)).present?
|
2023-01-03 08:17:52 +08:00
|
|
|
@user_custom_fields = User.custom_fields_for_ids(@posts.map(&:user_id), added_fields)
|
2016-03-12 04:52:18 +08:00
|
|
|
end
|
2015-03-03 14:51:01 +08:00
|
|
|
|
2021-10-22 10:22:09 +08:00
|
|
|
if (allowed_fields = TopicView.allowed_post_custom_fields(@user, @topic)).present?
|
2023-01-03 08:17:52 +08:00
|
|
|
@post_custom_fields = Post.custom_fields_for_ids(@posts.map(&:id), allowed_fields)
|
2017-08-12 10:18:04 +08:00
|
|
|
end
|
2015-04-24 01:33:29 +08:00
|
|
|
end
|
|
|
|
|
2021-05-25 00:46:57 +08:00
|
|
|
TopicView.preload(self)
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
@draft_key = @topic.draft_key
|
2013-07-12 04:38:46 +08:00
|
|
|
@draft_sequence = DraftSequence.current(@user, @draft_key)
|
2019-04-12 21:55:27 +08:00
|
|
|
|
2019-05-04 02:26:37 +08:00
|
|
|
@can_review_topic = @guardian.can_review_topic?(@topic)
|
2021-12-03 01:03:43 +08:00
|
|
|
@queued_posts_enabled = NewPostManager.queue_enabled? || category_require_reply_approval?
|
2019-04-12 21:55:27 +08:00
|
|
|
@personal_message = @topic.private_message?
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2024-10-17 22:16:16 +08:00
|
|
|
def user_badges(badge_names)
|
|
|
|
return if !badge_names.present?
|
|
|
|
|
|
|
|
user_ids = Set.new
|
|
|
|
posts.each { |post| user_ids << post.user_id if post.user_id }
|
|
|
|
|
|
|
|
return if !user_ids.present?
|
|
|
|
|
|
|
|
badges =
|
|
|
|
Badge.where("LOWER(name) IN (?)", badge_names.map(&:downcase)).where(enabled: true).to_a
|
|
|
|
|
|
|
|
sql = <<~SQL
|
|
|
|
SELECT user_id, badge_id
|
|
|
|
FROM user_badges
|
|
|
|
WHERE user_id IN (:user_ids) AND badge_id IN (:badge_ids)
|
|
|
|
GROUP BY user_id, badge_id
|
|
|
|
ORDER BY user_id, badge_id
|
|
|
|
SQL
|
|
|
|
|
|
|
|
user_badges = DB.query(sql, user_ids: user_ids, badge_ids: badges.map(&:id))
|
|
|
|
|
|
|
|
user_badge_mapping = {}
|
|
|
|
user_badges.each do |user_badge|
|
|
|
|
user_badge_mapping[user_badge.user_id] ||= []
|
|
|
|
user_badge_mapping[user_badge.user_id] << user_badge.badge_id
|
|
|
|
end
|
|
|
|
|
|
|
|
indexed_badges = {}
|
|
|
|
|
|
|
|
badges.each do |badge|
|
|
|
|
indexed_badges[badge.id] = {
|
|
|
|
id: badge.id,
|
|
|
|
name: badge.name,
|
|
|
|
slug: badge.slug,
|
|
|
|
description: badge.description,
|
|
|
|
icon: badge.icon,
|
|
|
|
image_url: badge.image_url,
|
|
|
|
badge_grouping_id: badge.badge_grouping_id,
|
|
|
|
badge_type_id: badge.badge_type_id,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
user_badge_mapping =
|
|
|
|
user_badge_mapping
|
|
|
|
.map { |user_id, badge_ids| [user_id, { id: user_id, badge_ids: badge_ids }] }
|
|
|
|
.to_h
|
|
|
|
|
|
|
|
{ users: user_badge_mapping, badges: indexed_badges }
|
|
|
|
end
|
|
|
|
|
2024-12-03 10:43:27 +08:00
|
|
|
def post_user_badges
|
|
|
|
return [] unless SiteSetting.enable_badges && SiteSetting.show_badges_in_post_header
|
|
|
|
|
|
|
|
@post_user_badges ||=
|
|
|
|
begin
|
|
|
|
UserBadge
|
|
|
|
.for_post_header_badges(@posts)
|
|
|
|
.reduce({}) do |hash, user_badge|
|
|
|
|
hash[user_badge.post_id] ||= []
|
|
|
|
hash[user_badge.post_id] << user_badge
|
|
|
|
hash
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return [] unless @post_user_badges
|
|
|
|
|
|
|
|
@post_user_badges
|
|
|
|
end
|
|
|
|
|
2019-08-27 20:09:00 +08:00
|
|
|
def show_read_indicator?
|
2020-11-14 01:13:37 +08:00
|
|
|
return false if !@user || !topic.private_message?
|
2019-08-27 20:09:00 +08:00
|
|
|
|
|
|
|
topic.allowed_groups.any? { |group| group.publish_read_state? && group.users.include?(@user) }
|
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def canonical_path
|
2020-03-09 22:31:24 +08:00
|
|
|
if SiteSetting.embed_set_canonical_url
|
|
|
|
topic_embed = topic.topic_embed
|
|
|
|
return topic_embed.embed_url if topic_embed
|
|
|
|
end
|
FEATURE: Simplify crawler content for non-canonical post URLs (#26324)
When crawlers visit a post-specific URL like `/t/-/{topic-id}/{post-number}`, we use the canonical to direct them to the appropriate crawler-optimised paginated view (e.g. `?page=3`).
However, analysis of google results shows that the post-specific URLs are still being included in the index. Google doesn't tell us exactly why this is happening. However, as a general rule, 'A large portion of the duplicate page's content should be present on the canonical version'.
In our previous implementation, this wasn't 100% true all the time. That's because a request for a post-specific URL would include posts 'surrounding' that post, and won't exactly conform to the page boundaries which are used in the canonical version of the page. Essentially: in some cases, the content of the post-specific pages would include many posts which were not present on the canonical paginated version.
This commit aims to resolve that problem by simplifying the implementation. Instead of rendering posts surrounding the target post_number, we will only render the target post, and include a link to 'show post in topic'. With this new implementation, 100% of the post-specific page content will be present on the canonical paginated version, which will hopefully mean google reduces their indexing of the non-canonical post-specific pages.
2024-03-26 23:18:46 +08:00
|
|
|
current_page_path
|
|
|
|
end
|
|
|
|
|
|
|
|
def current_page_path
|
|
|
|
if @page > 1
|
|
|
|
"#{relative_url}?page=#{@page}"
|
|
|
|
else
|
|
|
|
relative_url
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2018-06-26 12:54:14 +08:00
|
|
|
def contains_gaps?
|
|
|
|
@contains_gaps
|
|
|
|
end
|
|
|
|
|
2013-12-05 04:56:09 +08:00
|
|
|
def gaps
|
|
|
|
return unless @contains_gaps
|
2018-07-10 16:23:53 +08:00
|
|
|
|
|
|
|
@gaps ||=
|
|
|
|
begin
|
|
|
|
if is_mega_topic?
|
|
|
|
nil
|
|
|
|
else
|
2021-11-24 16:40:58 +08:00
|
|
|
Gaps.new(filtered_post_ids, apply_default_scope(unfiltered_posts).pluck(:id))
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2018-07-10 16:23:53 +08:00
|
|
|
end
|
2013-12-05 04:56:09 +08:00
|
|
|
end
|
|
|
|
|
2013-07-06 02:45:54 +08:00
|
|
|
def last_post
|
|
|
|
return nil if @posts.blank?
|
|
|
|
@last_post ||= @posts.last
|
|
|
|
end
|
|
|
|
|
2014-03-04 01:56:37 +08:00
|
|
|
def prev_page
|
2018-03-24 09:44:39 +08:00
|
|
|
@page > 1 && posts.size > 0 ? @page - 1 : nil
|
2014-03-04 01:56:37 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def next_page
|
2013-07-06 02:45:54 +08:00
|
|
|
@next_page ||=
|
|
|
|
begin
|
2019-12-04 20:52:24 +08:00
|
|
|
if last_post && highest_post_number && (highest_post_number > last_post.post_number)
|
2013-07-06 02:45:54 +08:00
|
|
|
@page + 1
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2013-07-06 02:45:54 +08:00
|
|
|
end
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
|
|
|
|
2014-03-04 01:56:37 +08:00
|
|
|
def prev_page_path
|
|
|
|
if prev_page > 1
|
2016-08-04 12:15:22 +08:00
|
|
|
"#{relative_url}?page=#{prev_page}"
|
2014-03-04 01:56:37 +08:00
|
|
|
else
|
2016-08-04 12:15:22 +08:00
|
|
|
relative_url
|
2014-03-04 01:56:37 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def next_page_path
|
2016-08-04 12:15:22 +08:00
|
|
|
"#{relative_url}?page=#{next_page}"
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
|
|
|
|
2013-03-08 06:31:06 +08:00
|
|
|
def absolute_url
|
2018-08-28 06:05:08 +08:00
|
|
|
"#{Discourse.base_url_no_prefix}#{relative_url}"
|
2013-03-08 06:31:06 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def relative_url
|
2016-08-09 11:53:08 +08:00
|
|
|
"#{@topic.relative_url}#{@print ? "/print" : ""}"
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
|
|
|
|
2015-07-22 08:26:58 +08:00
|
|
|
def page_title
|
|
|
|
title = @topic.title
|
2020-12-17 08:19:13 +08:00
|
|
|
if @post_number > 1
|
|
|
|
title += " - "
|
|
|
|
post = @topic.posts.find_by(post_number: @post_number)
|
|
|
|
author = post&.user
|
|
|
|
if author && @guardian.can_see_post?(post)
|
|
|
|
title +=
|
|
|
|
I18n.t(
|
|
|
|
"inline_oneboxer.topic_page_title_post_number_by_user",
|
|
|
|
post_number: @post_number,
|
|
|
|
username: author.username,
|
|
|
|
)
|
|
|
|
else
|
|
|
|
title += I18n.t("inline_oneboxer.topic_page_title_post_number", post_number: @post_number)
|
|
|
|
end
|
2024-03-26 23:19:00 +08:00
|
|
|
elsif @page > 1
|
|
|
|
title += " - #{I18n.t("page_num", num: @page)}"
|
2020-12-17 08:19:13 +08:00
|
|
|
end
|
2024-03-26 23:19:00 +08:00
|
|
|
|
2017-02-08 05:55:42 +08:00
|
|
|
if SiteSetting.topic_page_title_includes_category
|
|
|
|
if @topic.category_id != SiteSetting.uncategorized_category_id && @topic.category_id &&
|
|
|
|
@topic.category
|
|
|
|
title += " - #{@topic.category.name}"
|
2023-02-22 23:01:32 +08:00
|
|
|
elsif SiteSetting.tagging_enabled && visible_tags.exists?
|
2023-01-20 09:50:24 +08:00
|
|
|
title +=
|
2023-02-22 23:01:32 +08:00
|
|
|
" - #{visible_tags.order("tags.#{Tag.topic_count_column(@guardian)} DESC").first.name}"
|
2017-02-08 05:55:42 +08:00
|
|
|
end
|
2015-07-22 08:26:58 +08:00
|
|
|
end
|
|
|
|
title
|
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def title
|
|
|
|
@topic.title
|
|
|
|
end
|
|
|
|
|
2013-07-09 00:21:39 +08:00
|
|
|
def desired_post
|
|
|
|
return @desired_post if @desired_post.present?
|
2013-03-08 06:31:06 +08:00
|
|
|
return nil if posts.blank?
|
2013-07-09 00:21:39 +08:00
|
|
|
|
2018-03-24 09:44:39 +08:00
|
|
|
@desired_post = posts.detect { |p| p.post_number == @post_number }
|
2013-07-09 00:21:39 +08:00
|
|
|
@desired_post ||= posts.first
|
|
|
|
@desired_post
|
|
|
|
end
|
|
|
|
|
FEATURE: Simplify crawler content for non-canonical post URLs (#26324)
When crawlers visit a post-specific URL like `/t/-/{topic-id}/{post-number}`, we use the canonical to direct them to the appropriate crawler-optimised paginated view (e.g. `?page=3`).
However, analysis of google results shows that the post-specific URLs are still being included in the index. Google doesn't tell us exactly why this is happening. However, as a general rule, 'A large portion of the duplicate page's content should be present on the canonical version'.
In our previous implementation, this wasn't 100% true all the time. That's because a request for a post-specific URL would include posts 'surrounding' that post, and won't exactly conform to the page boundaries which are used in the canonical version of the page. Essentially: in some cases, the content of the post-specific pages would include many posts which were not present on the canonical paginated version.
This commit aims to resolve that problem by simplifying the implementation. Instead of rendering posts surrounding the target post_number, we will only render the target post, and include a link to 'show post in topic'. With this new implementation, 100% of the post-specific page content will be present on the canonical paginated version, which will hopefully mean google reduces their indexing of the non-canonical post-specific pages.
2024-03-26 23:18:46 +08:00
|
|
|
def crawler_posts
|
|
|
|
if single_post_request?
|
|
|
|
[desired_post]
|
|
|
|
else
|
|
|
|
posts
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def single_post_request?
|
|
|
|
@post_number && @post_number != 1
|
|
|
|
end
|
|
|
|
|
2017-11-28 19:27:43 +08:00
|
|
|
def summary(opts = {})
|
2013-07-09 00:21:39 +08:00
|
|
|
return nil if desired_post.blank?
|
2013-09-05 07:33:30 +08:00
|
|
|
# TODO, this is actually quite slow, should be cached in the post table
|
2017-11-28 19:27:43 +08:00
|
|
|
excerpt = desired_post.excerpt(500, opts.merge(strip_links: true, text_entities: true))
|
2014-12-08 07:23:53 +08:00
|
|
|
(excerpt || "").gsub(/\n/, " ").strip
|
2013-03-08 06:31:06 +08:00
|
|
|
end
|
|
|
|
|
2015-12-28 20:52:31 +08:00
|
|
|
def read_time
|
2018-03-24 09:44:39 +08:00
|
|
|
return nil if @post_number > 1 # only show for topic URLs
|
2019-07-19 23:15:38 +08:00
|
|
|
|
|
|
|
if @topic.word_count && SiteSetting.read_time_word_count > 0
|
|
|
|
[
|
|
|
|
@topic.word_count / SiteSetting.read_time_word_count,
|
|
|
|
@topic.posts_count * MIN_POST_READ_TIME / 60,
|
|
|
|
].max.ceil
|
|
|
|
end
|
2015-12-28 20:52:31 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def like_count
|
2018-03-24 09:44:39 +08:00
|
|
|
return nil if @post_number > 1 # only show for topic URLs
|
2015-12-28 20:52:31 +08:00
|
|
|
@topic.like_count
|
|
|
|
end
|
|
|
|
|
2018-07-30 18:52:51 +08:00
|
|
|
def published_time
|
|
|
|
return nil if desired_post.blank?
|
2018-07-30 22:56:40 +08:00
|
|
|
if desired_post.wiki && desired_post.post_number == 1 && desired_post.revisions.size > 0
|
|
|
|
desired_post.revisions.last.updated_at.strftime("%FT%T%:z")
|
|
|
|
else
|
|
|
|
desired_post.created_at.strftime("%FT%T%:z")
|
|
|
|
end
|
2018-07-30 18:52:51 +08:00
|
|
|
end
|
|
|
|
|
2013-03-09 04:58:37 +08:00
|
|
|
def image_url
|
2022-10-25 01:54:02 +08:00
|
|
|
return @topic.image_url if @post_number == 1
|
|
|
|
desired_post&.image_url
|
2013-03-09 04:58:37 +08:00
|
|
|
end
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def filter_posts(opts = {})
|
2021-10-15 10:22:49 +08:00
|
|
|
if opts[:post_number].present?
|
|
|
|
filter_posts_near(opts[:post_number].to_i)
|
|
|
|
elsif opts[:post_ids].present?
|
|
|
|
filter_posts_by_ids(opts[:post_ids])
|
|
|
|
elsif opts[:filter_post_number].present?
|
2021-11-24 16:40:58 +08:00
|
|
|
# Only used for megatopics where we do not load the entire post stream
|
2021-10-15 10:22:49 +08:00
|
|
|
filter_posts_by_post_number(opts[:filter_post_number], opts[:asc])
|
|
|
|
elsif opts[:best].present?
|
2021-11-24 16:40:58 +08:00
|
|
|
# Only used for wordpress
|
2021-10-15 10:22:49 +08:00
|
|
|
filter_best(opts[:best], opts)
|
|
|
|
else
|
|
|
|
filter_posts_paged(@page)
|
2018-07-11 15:41:26 +08:00
|
|
|
end
|
2013-03-26 23:58:49 +08:00
|
|
|
end
|
|
|
|
|
2014-02-11 05:59:36 +08:00
|
|
|
def primary_group_names
|
|
|
|
return @group_names if @group_names
|
|
|
|
|
|
|
|
primary_group_ids = Set.new
|
|
|
|
@posts.each do |p|
|
|
|
|
primary_group_ids << p.user.primary_group_id if p.user.try(:primary_group_id)
|
|
|
|
end
|
|
|
|
|
|
|
|
result = {}
|
|
|
|
unless primary_group_ids.empty?
|
|
|
|
Group.where(id: primary_group_ids.to_a).pluck(:id, :name).each { |g| result[g[0]] = g[1] }
|
|
|
|
end
|
2015-09-28 14:38:34 +08:00
|
|
|
|
|
|
|
@group_names = result
|
2014-02-11 05:59:36 +08:00
|
|
|
end
|
2013-03-28 13:53:11 +08:00
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
# Filter to all posts near a particular post number
|
|
|
|
def filter_posts_near(post_number)
|
2018-07-10 15:42:48 +08:00
|
|
|
posts_before = (@limit.to_f / 4).floor
|
|
|
|
posts_before = 1 if posts_before.zero?
|
2018-07-13 14:25:12 +08:00
|
|
|
sort_order = get_sort_order(post_number)
|
2018-07-10 15:42:48 +08:00
|
|
|
|
2021-10-19 10:37:46 +08:00
|
|
|
before_post_ids =
|
|
|
|
@filtered_posts
|
|
|
|
.reverse_order
|
2018-07-13 14:25:12 +08:00
|
|
|
.where("posts.sort_order < ?", sort_order)
|
2018-07-10 15:42:48 +08:00
|
|
|
.limit(posts_before)
|
|
|
|
.pluck(:id)
|
|
|
|
|
2021-10-19 10:37:46 +08:00
|
|
|
post_ids =
|
|
|
|
before_post_ids +
|
|
|
|
@filtered_posts
|
2018-07-13 14:25:12 +08:00
|
|
|
.where("posts.sort_order >= ?", sort_order)
|
2018-07-10 15:42:48 +08:00
|
|
|
.limit(@limit - before_post_ids.length)
|
|
|
|
.pluck(:id)
|
|
|
|
|
|
|
|
if post_ids.length < @limit
|
2021-10-19 10:37:46 +08:00
|
|
|
post_ids =
|
|
|
|
post_ids +
|
|
|
|
@filtered_posts
|
|
|
|
.reverse_order
|
2018-07-13 14:25:12 +08:00
|
|
|
.where("posts.sort_order < ?", sort_order)
|
2018-07-10 15:42:48 +08:00
|
|
|
.offset(before_post_ids.length)
|
|
|
|
.limit(@limit - post_ids.length)
|
|
|
|
.pluck(:id)
|
|
|
|
end
|
|
|
|
|
|
|
|
filter_posts_by_ids(post_ids)
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
|
|
|
def filter_posts_paged(page)
|
2013-04-04 06:12:27 +08:00
|
|
|
page = [page, 1].max
|
2014-01-04 01:52:24 +08:00
|
|
|
min = @limit * (page - 1)
|
|
|
|
|
|
|
|
# Sometimes we don't care about the OP, for example when embedding comments
|
|
|
|
min = 1 if min == 0 && @exclude_first
|
|
|
|
|
2021-10-15 15:49:22 +08:00
|
|
|
filter_posts_by_ids(@filtered_posts.offset(min).limit(@limit).pluck(:id))
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-07-01 19:29:45 +08:00
|
|
|
def filter_best(max, opts = {})
|
2013-07-12 16:33:45 +08:00
|
|
|
filter = FilterBestPosts.new(@topic, @filtered_posts, max, opts)
|
|
|
|
@posts = filter.posts
|
|
|
|
@filtered_posts = filter.filtered_posts
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def read?(post_number)
|
2014-06-03 09:48:52 +08:00
|
|
|
return true unless @user
|
2013-02-06 03:16:51 +08:00
|
|
|
read_posts_set.include?(post_number)
|
|
|
|
end
|
|
|
|
|
2014-07-16 05:02:43 +08:00
|
|
|
def has_deleted?
|
2014-08-04 23:29:01 +08:00
|
|
|
@predelete_filtered_posts
|
|
|
|
.with_deleted
|
|
|
|
.where("posts.deleted_at IS NOT NULL")
|
2014-08-08 01:12:35 +08:00
|
|
|
.where("posts.post_number > 1")
|
2014-08-04 23:29:01 +08:00
|
|
|
.exists?
|
2014-07-16 05:02:43 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def topic_user
|
|
|
|
@topic_user ||=
|
|
|
|
begin
|
|
|
|
return nil if @user.blank?
|
2014-05-06 21:41:59 +08:00
|
|
|
@topic.topic_users.find_by(user_id: @user.id)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-12 13:20:56 +08:00
|
|
|
def has_bookmarks?
|
2021-09-21 06:45:47 +08:00
|
|
|
bookmarks.any?
|
2020-03-12 13:20:56 +08:00
|
|
|
end
|
|
|
|
|
2021-09-21 06:45:47 +08:00
|
|
|
def bookmarks
|
2023-01-03 10:00:36 +08:00
|
|
|
return [] if @user.blank?
|
|
|
|
return [] if @topic.trashed?
|
|
|
|
|
2022-05-23 08:07:15 +08:00
|
|
|
@bookmarks ||=
|
|
|
|
Bookmark.for_user_in_topic(@user, @topic.id).select(
|
|
|
|
:id,
|
|
|
|
:bookmarkable_id,
|
|
|
|
:bookmarkable_type,
|
|
|
|
:reminder_at,
|
|
|
|
:name,
|
|
|
|
:auto_delete_preference,
|
|
|
|
)
|
2020-04-16 07:20:44 +08:00
|
|
|
end
|
|
|
|
|
2017-12-13 14:19:42 +08:00
|
|
|
MAX_PARTICIPANTS = 24
|
|
|
|
|
2013-03-27 02:06:24 +08:00
|
|
|
def post_counts_by_user
|
2017-09-13 23:14:03 +08:00
|
|
|
@post_counts_by_user ||=
|
|
|
|
begin
|
2018-06-21 09:09:45 +08:00
|
|
|
if is_mega_topic?
|
|
|
|
{}
|
|
|
|
else
|
|
|
|
sql = <<~SQL
|
2019-02-27 21:49:07 +08:00
|
|
|
SELECT user_id, count(*) AS count_all
|
|
|
|
FROM posts
|
2020-07-14 09:42:09 +08:00
|
|
|
WHERE topic_id = :topic_id
|
|
|
|
AND post_type IN (:post_types)
|
2019-02-27 21:49:07 +08:00
|
|
|
AND user_id IS NOT NULL
|
2020-07-14 09:42:09 +08:00
|
|
|
AND posts.deleted_at IS NULL
|
2020-08-05 09:51:28 +08:00
|
|
|
AND action_code IS NULL
|
2019-02-27 21:49:07 +08:00
|
|
|
GROUP BY user_id
|
|
|
|
ORDER BY count_all DESC
|
|
|
|
LIMIT #{MAX_PARTICIPANTS}
|
2018-06-21 09:09:45 +08:00
|
|
|
SQL
|
2017-09-12 14:34:43 +08:00
|
|
|
|
2020-08-05 09:51:28 +08:00
|
|
|
Hash[
|
|
|
|
*DB.query_single(
|
|
|
|
sql,
|
|
|
|
topic_id: @topic.id,
|
|
|
|
post_types: Topic.visible_post_types(@guardian&.user),
|
2023-01-09 20:10:19 +08:00
|
|
|
)
|
2020-08-05 09:51:28 +08:00
|
|
|
]
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2018-06-21 09:09:45 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2018-06-20 16:11:39 +08:00
|
|
|
# if a topic has more that N posts no longer attempt to
|
|
|
|
# get accurate participant count, instead grab cached count
|
|
|
|
# from topic
|
|
|
|
MAX_POSTS_COUNT_PARTICIPANTS = 500
|
|
|
|
|
2017-12-13 14:19:42 +08:00
|
|
|
def participant_count
|
|
|
|
@participant_count ||=
|
|
|
|
begin
|
|
|
|
if participants.size == MAX_PARTICIPANTS
|
2018-06-27 17:18:47 +08:00
|
|
|
if @topic.posts_count > MAX_POSTS_COUNT_PARTICIPANTS
|
2018-06-20 16:11:39 +08:00
|
|
|
@topic.participant_count
|
|
|
|
else
|
|
|
|
sql = <<~SQL
|
|
|
|
SELECT COUNT(DISTINCT user_id)
|
|
|
|
FROM posts
|
|
|
|
WHERE id IN (:post_ids)
|
|
|
|
AND user_id IS NOT NULL
|
|
|
|
SQL
|
|
|
|
DB.query_single(sql, post_ids: unfiltered_post_ids).first.to_i
|
|
|
|
end
|
2017-12-13 14:19:42 +08:00
|
|
|
else
|
|
|
|
participants.size
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def participants
|
|
|
|
@participants ||=
|
|
|
|
begin
|
|
|
|
participants = {}
|
2022-12-27 09:05:37 +08:00
|
|
|
User
|
|
|
|
.where(id: post_counts_by_user.keys)
|
|
|
|
.includes(:primary_group, :flair_group)
|
|
|
|
.each { |u| participants[u.id] = u }
|
2013-02-06 03:16:51 +08:00
|
|
|
participants
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-09-07 12:30:40 +08:00
|
|
|
def topic_allowed_group_ids
|
|
|
|
@topic_allowed_group_ids ||=
|
|
|
|
begin
|
|
|
|
@topic.allowed_groups.map(&:id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-12 21:55:27 +08:00
|
|
|
def group_allowed_user_ids
|
|
|
|
return @group_allowed_user_ids unless @group_allowed_user_ids.nil?
|
|
|
|
|
2021-09-07 12:30:40 +08:00
|
|
|
@group_allowed_user_ids =
|
|
|
|
GroupUser.where(group_id: topic_allowed_group_ids).pluck("distinct user_id")
|
2019-04-12 21:55:27 +08:00
|
|
|
end
|
|
|
|
|
2020-07-29 05:15:04 +08:00
|
|
|
def category_group_moderator_user_ids
|
|
|
|
@category_group_moderator_user_ids ||=
|
|
|
|
begin
|
2024-09-04 09:38:46 +08:00
|
|
|
if SiteSetting.enable_category_group_moderation? && @topic.category.present?
|
2020-07-29 05:15:04 +08:00
|
|
|
posts_user_ids = Set.new(@posts.map(&:user_id))
|
|
|
|
Set.new(
|
2024-09-04 09:38:46 +08:00
|
|
|
GroupUser
|
|
|
|
.joins(
|
|
|
|
"INNER JOIN category_moderation_groups ON category_moderation_groups.group_id = group_users.group_id",
|
|
|
|
)
|
|
|
|
.where(
|
|
|
|
"category_moderation_groups.category_id": @topic.category.id,
|
|
|
|
user_id: posts_user_ids,
|
|
|
|
)
|
|
|
|
.distinct
|
|
|
|
.pluck(:user_id),
|
2020-07-29 05:15:04 +08:00
|
|
|
)
|
|
|
|
else
|
|
|
|
Set.new
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2020-07-29 05:15:04 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def all_post_actions
|
2014-06-04 23:41:11 +08:00
|
|
|
@all_post_actions ||= PostAction.counts_for(@posts, @user)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def links
|
2017-09-15 02:08:16 +08:00
|
|
|
@links ||= TopicLink.topic_map(@guardian, @topic.id)
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2019-05-04 02:26:37 +08:00
|
|
|
def reviewable_counts
|
2020-08-08 00:13:02 +08:00
|
|
|
@reviewable_counts ||=
|
|
|
|
begin
|
2019-06-07 16:12:30 +08:00
|
|
|
sql = <<~SQL
|
2020-08-08 00:13:02 +08:00
|
|
|
SELECT
|
|
|
|
target_id,
|
2019-06-07 16:12:30 +08:00
|
|
|
MAX(r.id) reviewable_id,
|
|
|
|
COUNT(*) total,
|
|
|
|
SUM(CASE WHEN s.status = :pending THEN 1 ELSE 0 END) pending
|
2020-08-08 00:13:02 +08:00
|
|
|
FROM
|
|
|
|
reviewables r
|
|
|
|
JOIN
|
|
|
|
reviewable_scores s ON reviewable_id = r.id
|
|
|
|
WHERE
|
|
|
|
r.target_id IN (:post_ids) AND
|
2021-06-22 23:12:39 +08:00
|
|
|
r.target_type = 'Post' AND
|
|
|
|
COALESCE(s.reason, '') != 'category'
|
2020-08-08 00:13:02 +08:00
|
|
|
GROUP BY
|
|
|
|
target_id
|
2019-06-07 16:12:30 +08:00
|
|
|
SQL
|
|
|
|
|
2020-08-08 00:13:02 +08:00
|
|
|
counts = {}
|
2019-05-04 02:26:37 +08:00
|
|
|
|
2023-01-09 20:10:19 +08:00
|
|
|
DB
|
2019-06-07 16:12:30 +08:00
|
|
|
.query(sql, pending: ReviewableScore.statuses[:pending], post_ids: @posts.map(&:id))
|
|
|
|
.each do |row|
|
2020-08-08 00:13:02 +08:00
|
|
|
counts[row.target_id] = {
|
2019-06-07 16:12:30 +08:00
|
|
|
total: row.total,
|
|
|
|
pending: row.pending,
|
|
|
|
reviewable_id: row.reviewable_id,
|
2023-01-09 20:10:19 +08:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-08-08 00:13:02 +08:00
|
|
|
counts
|
|
|
|
end
|
2019-05-04 02:26:37 +08:00
|
|
|
end
|
|
|
|
|
2019-04-12 21:55:27 +08:00
|
|
|
def pending_posts
|
2019-05-04 02:26:37 +08:00
|
|
|
@pending_posts ||=
|
2023-07-29 00:16:23 +08:00
|
|
|
ReviewableQueuedPost.pending.where(target_created_by: @user, topic: @topic).order(:created_at)
|
2019-04-12 21:55:27 +08:00
|
|
|
end
|
|
|
|
|
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
|
|
|
def post_action_type_view
|
|
|
|
@post_action_type_view ||= PostActionTypeView.new
|
|
|
|
end
|
|
|
|
|
2019-04-12 21:55:27 +08:00
|
|
|
def actions_summary
|
|
|
|
return @actions_summary unless @actions_summary.nil?
|
|
|
|
|
|
|
|
@actions_summary = []
|
|
|
|
return @actions_summary unless post = posts&.first
|
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
|
|
|
post_action_type_view.topic_flag_types.each do |sym, id|
|
2019-04-12 21:55:27 +08:00
|
|
|
@actions_summary << {
|
|
|
|
id: id,
|
|
|
|
count: 0,
|
|
|
|
hidden: false,
|
FIX: serialize Flags instead of PostActionType (#28362)
### Why?
Before, all flags were static. Therefore, they were stored in class variables and serialized by SiteSerializer. Recently, we added an option for admins to add their own flags or disable existing flags. Therefore, the class variable had to be dropped because it was unsafe for a multisite environment. However, it started causing performance problems.
### Solution
When a new Flag system is used, instead of using PostActionType, we can serialize Flags and use fragment cache for performance reasons.
At the same time, we are still supporting deprecated `replace_flags` API call. When it is used, we fall back to the old solution and the admin cannot add custom flags. In a couple of months, we will be able to drop that API function and clean that code properly. However, because it may still be used, redis cache was introduced to improve performance.
To test backward compatibility you can add this code to any plugin
```ruby
replace_flags do |flag_settings|
flag_settings.add(
4,
:inappropriate,
topic_type: true,
notify_type: true,
auto_action_type: true,
)
flag_settings.add(1001, :trolling, topic_type: true, notify_type: true, auto_action_type: true)
end
```
2024-08-14 10:13:46 +08:00
|
|
|
can_act:
|
|
|
|
@guardian.post_can_act?(
|
|
|
|
post,
|
|
|
|
sym,
|
|
|
|
opts: {
|
|
|
|
post_action_type_view: post_action_type_view,
|
|
|
|
},
|
|
|
|
),
|
2019-04-12 21:55:27 +08:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
@actions_summary
|
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def link_counts
|
2024-11-23 04:49:39 +08:00
|
|
|
# Normal memoizations doesn't work in nil cases, so using the ol' `defined?` trick
|
|
|
|
# to memoize more safely, as a modifier could nil this out.
|
|
|
|
return @link_counts if defined?(@link_counts)
|
|
|
|
|
|
|
|
@link_counts =
|
|
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
|
|
:topic_view_link_counts,
|
|
|
|
TopicLink.counts_for(@guardian, @topic, posts),
|
|
|
|
)
|
2013-02-26 00:42:20 +08:00
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
|
2018-11-12 10:04:30 +08:00
|
|
|
def pm_params
|
|
|
|
@pm_params ||= TopicQuery.new(@user).get_pm_params(topic)
|
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
def suggested_topics
|
2019-02-22 07:37:18 +08:00
|
|
|
if @include_suggested
|
2023-03-31 06:03:15 +08:00
|
|
|
@suggested_topics ||=
|
|
|
|
begin
|
|
|
|
kwargs =
|
|
|
|
DiscoursePluginRegistry.apply_modifier(
|
|
|
|
:topic_view_suggested_topics_options,
|
|
|
|
{ include_random: true, pm_params: pm_params },
|
|
|
|
self,
|
|
|
|
)
|
2024-08-23 16:10:50 +08:00
|
|
|
|
2023-03-31 06:03:15 +08:00
|
|
|
TopicQuery.new(@user).list_suggested_for(topic, **kwargs)
|
|
|
|
end
|
2019-02-22 07:37:18 +08:00
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
2018-11-12 10:04:30 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def related_messages
|
2019-02-22 07:37:18 +08:00
|
|
|
if @include_related
|
|
|
|
@related_messages ||= TopicQuery.new(@user).list_related_for(topic, pm_params: pm_params)
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
|
|
|
|
2013-02-15 09:58:14 +08:00
|
|
|
# This is pending a larger refactor, that allows custom orders
|
2019-01-04 01:03:01 +08:00
|
|
|
# for now we need to look for the highest_post_number in the stream
|
|
|
|
# the cache on topics is not correct if there are deleted posts at
|
|
|
|
# the end of the stream (for mods), nor is it correct for filtered
|
|
|
|
# streams
|
2013-02-15 09:58:14 +08:00
|
|
|
def highest_post_number
|
2013-03-26 23:58:49 +08:00
|
|
|
@highest_post_number ||= @filtered_posts.maximum(:post_number)
|
2013-02-15 09:58:14 +08:00
|
|
|
end
|
|
|
|
|
2013-02-22 02:20:00 +08:00
|
|
|
def recent_posts
|
2021-10-19 10:37:46 +08:00
|
|
|
@filtered_posts.unscope(:order).by_newest.with_user.first(25)
|
2013-02-22 02:20:00 +08:00
|
|
|
end
|
|
|
|
|
2018-06-27 11:11:22 +08:00
|
|
|
# Returns an array of [id, days_ago] tuples.
|
2017-08-04 23:28:25 +08:00
|
|
|
# `days_ago` is there for the timeline calculations.
|
2016-05-18 01:03:08 +08:00
|
|
|
def filtered_post_stream
|
2018-06-20 16:24:09 +08:00
|
|
|
@filtered_post_stream ||=
|
|
|
|
begin
|
|
|
|
posts = @filtered_posts
|
2018-06-27 11:11:22 +08:00
|
|
|
columns = [:id]
|
2018-06-20 16:24:09 +08:00
|
|
|
|
2018-06-20 16:58:52 +08:00
|
|
|
if !is_mega_topic?
|
2021-07-13 00:35:24 +08:00
|
|
|
columns << "(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP - posts.created_at) / 86400)::INT AS days_ago"
|
2018-06-20 16:24:09 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
posts.pluck(*columns)
|
|
|
|
end
|
2016-05-18 01:03:08 +08:00
|
|
|
end
|
|
|
|
|
2013-08-13 22:29:25 +08:00
|
|
|
def filtered_post_ids
|
2018-06-27 11:11:22 +08:00
|
|
|
@filtered_post_ids ||=
|
|
|
|
filtered_post_stream.map do |tuple|
|
|
|
|
if is_mega_topic?
|
|
|
|
tuple
|
|
|
|
else
|
|
|
|
tuple[0]
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2018-06-27 11:11:22 +08:00
|
|
|
end
|
2013-08-13 22:29:25 +08:00
|
|
|
end
|
|
|
|
|
2017-12-13 14:19:42 +08:00
|
|
|
def unfiltered_post_ids
|
|
|
|
@unfiltered_post_ids ||=
|
|
|
|
begin
|
|
|
|
if @contains_gaps
|
2017-12-13 14:36:36 +08:00
|
|
|
unfiltered_posts.pluck(:id)
|
2017-12-13 14:19:42 +08:00
|
|
|
else
|
|
|
|
filtered_post_ids
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-06-27 12:33:57 +08:00
|
|
|
def filtered_post_id(post_number)
|
2023-02-13 12:39:45 +08:00
|
|
|
@filtered_posts.where(post_number: post_number).pick(:id)
|
2018-06-27 11:11:22 +08:00
|
|
|
end
|
|
|
|
|
2018-07-11 15:41:26 +08:00
|
|
|
def is_mega_topic?
|
|
|
|
@is_mega_topic ||= (@topic.posts_count >= MEGA_TOPIC_POSTS_COUNT)
|
|
|
|
end
|
|
|
|
|
|
|
|
def last_post_id
|
2023-02-13 12:39:45 +08:00
|
|
|
@filtered_posts.reverse_order.pick(:id)
|
2018-07-11 15:41:26 +08:00
|
|
|
end
|
|
|
|
|
2018-11-21 08:58:47 +08:00
|
|
|
def current_post_number
|
|
|
|
if highest_post_number.present?
|
|
|
|
post_number > highest_post_number ? highest_post_number : post_number
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-12 21:55:27 +08:00
|
|
|
def queued_posts_count
|
|
|
|
ReviewableQueuedPost.viewable_by(@user).where(topic_id: @topic.id).pending.count
|
|
|
|
end
|
|
|
|
|
2020-04-09 00:52:36 +08:00
|
|
|
def published_page
|
|
|
|
@topic.published_page
|
|
|
|
end
|
|
|
|
|
2023-01-20 07:58:00 +08:00
|
|
|
def mentioned_users
|
|
|
|
@mentioned_users ||=
|
|
|
|
begin
|
|
|
|
mentions = @posts.to_h { |p| [p.id, p.mentions] }.reject { |_, v| v.empty? }
|
|
|
|
usernames = mentions.values
|
|
|
|
usernames.flatten!
|
|
|
|
usernames.uniq!
|
2022-12-06 23:10:36 +08:00
|
|
|
|
2024-09-11 15:39:14 +08:00
|
|
|
users = User.where(username_lower: usernames)
|
|
|
|
users = users.includes(:user_option, :user_status) if SiteSetting.enable_user_status
|
|
|
|
users = users.index_by(&:username_lower)
|
2022-12-06 23:10:36 +08:00
|
|
|
|
2023-01-20 07:58:00 +08:00
|
|
|
mentions.reduce({}) do |hash, (post_id, post_mentioned_usernames)|
|
|
|
|
hash[post_id] = post_mentioned_usernames.map { |username| users[username] }.compact
|
|
|
|
hash
|
|
|
|
end
|
|
|
|
end
|
2022-12-06 23:10:36 +08:00
|
|
|
end
|
|
|
|
|
2024-01-11 02:30:59 +08:00
|
|
|
def categories
|
2024-03-22 01:51:41 +08:00
|
|
|
@categories ||= [category&.parent_category, category, suggested_topics&.categories].flatten
|
|
|
|
.uniq
|
|
|
|
.compact
|
2024-01-11 02:30:59 +08:00
|
|
|
end
|
|
|
|
|
2013-02-06 03:16:51 +08:00
|
|
|
protected
|
|
|
|
|
2013-02-11 02:50:26 +08:00
|
|
|
def read_posts_set
|
|
|
|
@read_posts_set ||=
|
|
|
|
begin
|
|
|
|
result = Set.new
|
2024-05-27 18:27:13 +08:00
|
|
|
return result if @user.blank?
|
|
|
|
return result if topic_user.blank?
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2013-10-04 16:06:32 +08:00
|
|
|
post_numbers =
|
|
|
|
PostTiming
|
2013-03-26 23:58:49 +08:00
|
|
|
.where(topic_id: @topic.id, user_id: @user.id)
|
2017-09-07 18:41:44 +08:00
|
|
|
.where(post_number: @posts.pluck(:post_number))
|
2013-03-26 23:58:49 +08:00
|
|
|
.pluck(:post_number)
|
2023-01-09 20:10:19 +08:00
|
|
|
|
2013-03-26 23:58:49 +08:00
|
|
|
post_numbers.each { |pn| result << pn }
|
2013-02-11 02:50:26 +08:00
|
|
|
result
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2020-10-19 14:11:49 +08:00
|
|
|
def calculate_page
|
|
|
|
posts_count =
|
|
|
|
is_mega_topic? ? @post_number : unfiltered_posts.where("post_number <= ?", @post_number).count
|
|
|
|
((posts_count - 1) / @limit) + 1
|
|
|
|
end
|
|
|
|
|
2018-07-13 14:25:12 +08:00
|
|
|
def get_sort_order(post_number)
|
|
|
|
sql = <<~SQL
|
2019-02-27 21:49:07 +08:00
|
|
|
SELECT posts.sort_order
|
|
|
|
FROM posts
|
|
|
|
WHERE posts.post_number = #{post_number.to_i}
|
|
|
|
AND posts.topic_id = #{@topic.id.to_i}
|
|
|
|
LIMIT 1
|
2018-07-13 14:25:12 +08:00
|
|
|
SQL
|
|
|
|
|
|
|
|
sort_order = DB.query_single(sql).first
|
|
|
|
|
|
|
|
if !sort_order
|
|
|
|
sql = <<~SQL
|
2019-02-27 21:49:07 +08:00
|
|
|
SELECT posts.sort_order
|
|
|
|
FROM posts
|
|
|
|
WHERE posts.topic_id = #{@topic.id.to_i}
|
|
|
|
ORDER BY @(post_number - #{post_number.to_i})
|
|
|
|
LIMIT 1
|
2018-07-13 14:25:12 +08:00
|
|
|
SQL
|
|
|
|
|
|
|
|
sort_order = DB.query_single(sql).first
|
|
|
|
end
|
|
|
|
|
|
|
|
sort_order
|
2018-07-11 15:41:26 +08:00
|
|
|
end
|
|
|
|
|
2015-09-11 04:01:23 +08:00
|
|
|
def filter_post_types(posts)
|
2021-01-22 01:47:03 +08:00
|
|
|
return posts.where(post_type: Post.types[:regular]) if @only_regular
|
2015-09-11 04:01:23 +08:00
|
|
|
|
2021-01-22 01:47:03 +08:00
|
|
|
visible_types = Topic.visible_post_types(@user)
|
2021-10-01 15:01:27 +08:00
|
|
|
|
2015-09-11 04:01:23 +08:00
|
|
|
if @user.present?
|
2016-04-21 03:29:27 +08:00
|
|
|
posts.where("posts.user_id = ? OR post_type IN (?)", @user.id, visible_types)
|
2015-09-11 04:01:23 +08:00
|
|
|
else
|
|
|
|
posts.where(post_type: visible_types)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-07-11 15:41:26 +08:00
|
|
|
def filter_posts_by_post_number(post_number, asc)
|
2018-07-13 14:25:12 +08:00
|
|
|
sort_order = get_sort_order(post_number)
|
|
|
|
|
2018-07-11 15:41:26 +08:00
|
|
|
posts =
|
|
|
|
if asc
|
2021-10-19 10:37:46 +08:00
|
|
|
@filtered_posts.where("sort_order > ?", sort_order)
|
2018-07-11 15:41:26 +08:00
|
|
|
else
|
2021-10-19 10:37:46 +08:00
|
|
|
@filtered_posts.reverse_order.where("sort_order < ?", sort_order)
|
2018-07-11 15:41:26 +08:00
|
|
|
end
|
|
|
|
|
2018-06-28 14:54:54 +08:00
|
|
|
posts = posts.limit(@limit) if !@skip_limit
|
2018-07-11 15:41:26 +08:00
|
|
|
filter_posts_by_ids(posts.pluck(:id))
|
|
|
|
|
2021-10-19 10:37:46 +08:00
|
|
|
@posts = @posts.reverse_order if !asc
|
2018-07-11 15:41:26 +08:00
|
|
|
end
|
|
|
|
|
2013-06-21 05:20:08 +08:00
|
|
|
def filter_posts_by_ids(post_ids)
|
2014-05-29 19:55:55 +08:00
|
|
|
@posts =
|
|
|
|
Post.where(id: post_ids, topic_id: @topic.id).includes(
|
2022-12-27 09:05:37 +08:00
|
|
|
{ user: %i[primary_group flair_group] },
|
2021-06-07 14:34:27 +08:00
|
|
|
:reply_to_user,
|
|
|
|
:deleted_by,
|
|
|
|
:incoming_email,
|
|
|
|
:image_upload,
|
|
|
|
)
|
2021-10-19 10:37:46 +08:00
|
|
|
|
2022-08-08 21:35:26 +08:00
|
|
|
@posts = @posts.includes({ user: :user_status }) if SiteSetting.enable_user_status
|
|
|
|
|
2021-11-24 16:40:58 +08:00
|
|
|
@posts = apply_default_scope(@posts)
|
2015-09-11 04:01:23 +08:00
|
|
|
@posts = filter_post_types(@posts)
|
2020-11-06 01:18:26 +08:00
|
|
|
@posts = @posts.with_deleted if @guardian.can_see_deleted_posts?(@topic.category)
|
2013-06-21 05:20:08 +08:00
|
|
|
@posts
|
|
|
|
end
|
|
|
|
|
2018-05-28 17:06:47 +08:00
|
|
|
def find_topic(topic_or_topic_id)
|
2022-11-24 23:28:21 +08:00
|
|
|
return topic_or_topic_id if topic_or_topic_id.is_a?(Topic)
|
|
|
|
# with_deleted covered in #check_and_raise_exceptions
|
2023-02-22 23:01:32 +08:00
|
|
|
Topic.with_deleted.includes(:category).find_by(id: topic_or_topic_id)
|
2013-02-11 02:50:26 +08:00
|
|
|
end
|
2013-07-12 16:33:45 +08:00
|
|
|
|
2024-08-02 01:51:27 +08:00
|
|
|
def find_post_replies_ids(post_id)
|
|
|
|
DB.query_single(<<~SQL, post_id: post_id)
|
|
|
|
WITH RECURSIVE breadcrumb(id, reply_to_post_number, topic_id, level) AS (
|
|
|
|
SELECT id, reply_to_post_number, topic_id, 0
|
|
|
|
FROM posts
|
|
|
|
WHERE id = :post_id
|
|
|
|
|
|
|
|
UNION
|
|
|
|
|
|
|
|
SELECT p.id, p.reply_to_post_number, p.topic_id, b.level + 1
|
|
|
|
FROM posts AS p
|
|
|
|
, breadcrumb AS b
|
|
|
|
WHERE b.reply_to_post_number = p.post_number
|
|
|
|
AND b.topic_id = p.topic_id
|
|
|
|
AND b.level < #{SiteSetting.max_reply_history}
|
|
|
|
)
|
|
|
|
SELECT id
|
|
|
|
FROM breadcrumb
|
|
|
|
WHERE id <> :post_id
|
|
|
|
ORDER BY id
|
|
|
|
SQL
|
|
|
|
end
|
|
|
|
|
2013-12-05 04:56:09 +08:00
|
|
|
def unfiltered_posts
|
2015-09-11 04:01:23 +08:00
|
|
|
result = filter_post_types(@topic.posts)
|
2020-11-06 01:18:26 +08:00
|
|
|
result = result.with_deleted if @guardian.can_see_deleted_posts?(@topic.category)
|
2016-05-04 03:19:59 +08:00
|
|
|
result = result.where("user_id IS NOT NULL") if @exclude_deleted_users
|
|
|
|
result = result.where(hidden: false) if @exclude_hidden
|
2013-12-05 04:56:09 +08:00
|
|
|
result
|
|
|
|
end
|
|
|
|
|
2021-11-24 16:40:58 +08:00
|
|
|
def apply_default_scope(scope)
|
|
|
|
scope = scope.order(sort_order: :asc)
|
|
|
|
|
|
|
|
self.class.custom_default_scopes.each { |block| scope = block.call(scope, self) }
|
|
|
|
|
|
|
|
scope
|
2021-10-19 10:37:46 +08:00
|
|
|
end
|
|
|
|
|
2013-07-12 16:33:45 +08:00
|
|
|
def setup_filtered_posts
|
2013-12-05 04:56:09 +08:00
|
|
|
# Certain filters might leave gaps between posts. If that's true, we can return a gap structure
|
|
|
|
@contains_gaps = false
|
|
|
|
@filtered_posts = unfiltered_posts
|
|
|
|
|
2021-10-01 15:01:27 +08:00
|
|
|
if @user
|
|
|
|
sql = <<~SQL
|
2019-04-10 18:54:59 +08:00
|
|
|
SELECT ignored_user_id
|
|
|
|
FROM ignored_users as ig
|
2021-10-01 15:01:27 +08:00
|
|
|
INNER JOIN users as u ON u.id = ig.ignored_user_id
|
2019-04-10 18:54:59 +08:00
|
|
|
WHERE ig.user_id = :current_user_id
|
|
|
|
AND ig.ignored_user_id <> :current_user_id
|
|
|
|
AND NOT u.admin
|
|
|
|
AND NOT u.moderator
|
2021-10-01 15:01:27 +08:00
|
|
|
SQL
|
2019-03-20 18:18:46 +08:00
|
|
|
|
2021-10-01 15:01:27 +08:00
|
|
|
ignored_user_ids = DB.query_single(sql, current_user_id: @user.id)
|
2019-03-04 22:29:05 +08:00
|
|
|
|
2021-10-01 15:01:27 +08:00
|
|
|
if ignored_user_ids.present?
|
2021-10-18 09:58:13 +08:00
|
|
|
@filtered_posts =
|
|
|
|
@filtered_posts.where.not("user_id IN (?) AND posts.post_number != 1", ignored_user_ids)
|
2021-10-01 15:01:27 +08:00
|
|
|
@contains_gaps = true
|
|
|
|
end
|
2019-02-27 21:49:07 +08:00
|
|
|
end
|
|
|
|
|
2013-12-05 04:56:09 +08:00
|
|
|
# Filters
|
2018-06-26 11:05:25 +08:00
|
|
|
if @filter == "summary"
|
2015-01-30 14:19:42 +08:00
|
|
|
@filtered_posts = @filtered_posts.summary(@topic.id)
|
2018-06-26 11:05:25 +08:00
|
|
|
@contains_gaps = true
|
2013-12-05 04:56:09 +08:00
|
|
|
end
|
|
|
|
|
2021-05-11 09:24:14 +08:00
|
|
|
if @filter.present? && @filter.to_s != "summary" && TopicView.custom_filters[@filter].present?
|
2021-05-10 06:57:58 +08:00
|
|
|
@filtered_posts = TopicView.custom_filters[@filter].call(@filtered_posts, self)
|
|
|
|
end
|
|
|
|
|
2013-12-05 04:56:09 +08:00
|
|
|
if @best.present?
|
2015-07-25 04:39:03 +08:00
|
|
|
@filtered_posts = @filtered_posts.where("posts.post_type = ?", Post.types[:regular])
|
2013-12-05 04:56:09 +08:00
|
|
|
@contains_gaps = true
|
|
|
|
end
|
|
|
|
|
2014-07-16 05:02:43 +08:00
|
|
|
# Username filters
|
2013-12-05 04:56:09 +08:00
|
|
|
if @username_filters.present?
|
|
|
|
usernames = @username_filters.map { |u| u.downcase }
|
2018-06-29 10:33:08 +08:00
|
|
|
|
|
|
|
@filtered_posts =
|
|
|
|
@filtered_posts.where(
|
2023-01-09 20:10:19 +08:00
|
|
|
"
|
2018-06-29 10:33:08 +08:00
|
|
|
posts.post_number = 1
|
|
|
|
OR posts.user_id IN (SELECT u.id FROM users u WHERE u.username_lower IN (?))
|
2023-01-09 20:10:19 +08:00
|
|
|
",
|
2018-06-29 10:33:08 +08:00
|
|
|
usernames,
|
|
|
|
)
|
|
|
|
|
2013-12-05 04:56:09 +08:00
|
|
|
@contains_gaps = true
|
|
|
|
end
|
|
|
|
|
2020-12-11 01:02:07 +08:00
|
|
|
# Filter replies
|
|
|
|
if @replies_to_post_number.present?
|
2020-12-15 04:24:36 +08:00
|
|
|
post_id = filtered_post_id(@replies_to_post_number.to_i)
|
2020-12-11 01:02:07 +08:00
|
|
|
@filtered_posts =
|
|
|
|
@filtered_posts.where(
|
2023-01-09 20:10:19 +08:00
|
|
|
"
|
2020-12-11 01:02:07 +08:00
|
|
|
posts.post_number = 1
|
|
|
|
OR posts.post_number = :post_number
|
2020-12-15 04:24:36 +08:00
|
|
|
OR posts.reply_to_post_number = :post_number
|
|
|
|
OR posts.id IN (SELECT pr.reply_post_id FROM post_replies pr WHERE pr.post_id = :post_id)",
|
|
|
|
{ post_number: @replies_to_post_number.to_i, post_id: post_id },
|
|
|
|
)
|
2020-12-11 01:02:07 +08:00
|
|
|
|
|
|
|
@contains_gaps = true
|
|
|
|
end
|
|
|
|
|
2022-03-03 04:25:36 +08:00
|
|
|
# Show Only Top Level Replies
|
|
|
|
if @filter_top_level_replies.present?
|
|
|
|
@filtered_posts =
|
|
|
|
@filtered_posts.where(
|
2023-01-09 20:10:19 +08:00
|
|
|
"
|
2022-03-03 04:25:36 +08:00
|
|
|
posts.post_number > 1
|
|
|
|
AND posts.reply_to_post_number IS NULL
|
2023-01-09 20:10:19 +08:00
|
|
|
",
|
2022-03-03 04:25:36 +08:00
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2024-08-02 01:51:27 +08:00
|
|
|
# Reply history
|
|
|
|
if @reply_history_for.present?
|
|
|
|
post_ids = find_post_replies_ids(@reply_history_for)
|
|
|
|
|
|
|
|
@filtered_posts = @filtered_posts.where("posts.id IN (:post_ids)", post_ids:)
|
|
|
|
@contains_gaps = true
|
|
|
|
end
|
|
|
|
|
2020-12-11 01:02:07 +08:00
|
|
|
# Filtering upwards
|
|
|
|
if @filter_upwards_post_id.present?
|
2024-08-02 01:51:27 +08:00
|
|
|
post_ids = find_post_replies_ids(@filter_upwards_post_id) | [@filter_upwards_post_id.to_i]
|
2020-12-11 01:02:07 +08:00
|
|
|
|
|
|
|
@filtered_posts =
|
|
|
|
@filtered_posts.where(
|
2024-08-02 01:51:27 +08:00
|
|
|
"posts.post_number = 1 OR posts.id IN (:post_ids) OR posts.id > :max_post_id",
|
|
|
|
post_ids:,
|
|
|
|
max_post_id: post_ids.max,
|
2020-12-11 01:02:07 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
@contains_gaps = true
|
|
|
|
end
|
|
|
|
|
2014-07-16 05:02:43 +08:00
|
|
|
# Deleted
|
|
|
|
# This should be last - don't want to tell the admin about deleted posts that clicking the button won't show
|
|
|
|
# copy the filter for has_deleted? method
|
|
|
|
@predelete_filtered_posts = @filtered_posts.spawn
|
2018-06-29 10:33:08 +08:00
|
|
|
|
2020-11-06 01:18:26 +08:00
|
|
|
if @guardian.can_see_deleted_posts?(@topic.category) && !@show_deleted && has_deleted?
|
2018-06-29 10:33:08 +08:00
|
|
|
@filtered_posts = @filtered_posts.where("posts.deleted_at IS NULL OR posts.post_number = 1")
|
|
|
|
|
2014-07-16 05:02:43 +08:00
|
|
|
@contains_gaps = true
|
|
|
|
end
|
2013-07-12 16:33:45 +08:00
|
|
|
end
|
|
|
|
|
2020-09-02 08:10:42 +08:00
|
|
|
def check_and_raise_exceptions(skip_staff_action)
|
2013-07-12 16:33:45 +08:00
|
|
|
raise Discourse::NotFound if @topic.blank?
|
|
|
|
# Special case: If the topic is private and the user isn't logged in, ask them
|
|
|
|
# to log in!
|
2018-02-26 00:31:51 +08:00
|
|
|
raise Discourse::NotLoggedIn.new if @topic.present? && @topic.private_message? && @user.blank?
|
|
|
|
# can user see this topic?
|
2017-09-15 02:08:16 +08:00
|
|
|
unless @guardian.can_see?(@topic)
|
|
|
|
raise Discourse::InvalidAccess.new("can't see #{@topic}", @topic)
|
2023-01-09 20:10:19 +08:00
|
|
|
end
|
2018-02-26 00:31:51 +08:00
|
|
|
# log personal message views
|
2020-09-02 08:10:42 +08:00
|
|
|
if SiteSetting.log_personal_messages_views && !skip_staff_action && @topic.present? &&
|
|
|
|
@topic.private_message? && @topic.all_allowed_users.where(id: @user.id).blank?
|
2018-03-12 19:10:17 +08:00
|
|
|
unless UserHistory
|
|
|
|
.where(
|
|
|
|
acting_user_id: @user.id,
|
|
|
|
action: UserHistory.actions[:check_personal_message],
|
|
|
|
topic_id: @topic.id,
|
2023-01-09 20:10:19 +08:00
|
|
|
)
|
2018-03-12 19:10:17 +08:00
|
|
|
.where("created_at > ?", 1.hour.ago)
|
|
|
|
.exists?
|
2018-03-11 11:51:46 +08:00
|
|
|
StaffActionLogger.new(@user).log_check_personal_message(@topic)
|
|
|
|
end
|
2018-02-26 00:31:51 +08:00
|
|
|
end
|
2013-07-12 16:33:45 +08:00
|
|
|
end
|
2023-02-22 23:01:32 +08:00
|
|
|
|
|
|
|
def visible_tags
|
|
|
|
@visible_tags ||= topic.tags.visible(guardian)
|
|
|
|
end
|
2013-02-06 03:16:51 +08:00
|
|
|
end
|