discourse/app/services/topic_status_updater.rb
Martin Brennan 10b9a32abb
FIX: Message for bulk closing topics silently (#27400)
We were using `autoclose` as the topic status update
when silently closing topics using the bulk
actions (introduced in 0464ddcd9b).

However, this resulted in a message like this showing in
the topic as a small moderator post:

> This topic was automatically closed after X days.

This is not accurate, the topic was bulk closed by someone.
Instead, we can use `closed` as the status, and a more accurate

> Closed on DATE

message is used. `TopicStatusUpdater` needed an additional
option to keep the same "fake read" behaviour as autoclose
so we can keep the same functionality for silently closing
topics in bulk actions.
2024-06-11 09:36:54 +10:00

202 lines
6.0 KiB
Ruby

# frozen_string_literal: true
TopicStatusUpdater =
Struct.new(:topic, :user) do
def update!(status, enabled, opts = {})
status = Status.new(status, enabled)
@topic_timer = topic.public_topic_timer
updated = nil
Topic.transaction do
updated = change(status, opts)
if updated
highest_post_number = topic.highest_post_number
create_moderator_post_for(status, opts)
update_read_state_for(
status,
highest_post_number,
silent_tracking: opts[:silent_tracking],
)
end
end
updated
end
private
def change(status, opts = {})
result = true
if status.pinned? || status.pinned_globally?
topic.update_pinned(status.enabled?, status.pinned_globally?, opts[:until])
elsif status.autoclosed?
rc = Topic.where(id: topic.id, closed: !status.enabled?).update_all(closed: status.enabled?)
topic.closed = status.enabled?
result = false if rc == 0
else
rc =
Topic.where(:id => topic.id, status.name => !status.enabled).update_all(
status.name => status.enabled?,
)
topic.public_send("#{status.name}=", status.enabled?)
result = false if rc == 0
end
DiscourseEvent.trigger(:topic_closed, topic) if status.manually_closing_topic?
if status.visible? && status.disabled?
UserProfile.remove_featured_topic_from_all_profiles(topic)
end
if status.visible? && result
topic.update_category_topic_count_by(status.enabled? ? 1 : -1)
UserStatCountUpdater.public_send(
status.enabled? ? :increment! : :decrement!,
topic.first_post,
)
end
if status.visible?
topic.update(
visibility_reason_id: opts[:visibility_reason_id] || Topic.visibility_reasons[:unknown],
)
end
if @topic_timer
if status.manually_closing_topic? || status.closing_topic?
topic.delete_topic_timer(TopicTimer.types[:close])
topic.delete_topic_timer(TopicTimer.types[:silent_close])
elsif status.manually_opening_topic? || status.opening_topic?
topic.delete_topic_timer(TopicTimer.types[:open])
topic.inherit_auto_close_from_category
end
end
# remove featured topics if we close/archive/make them invisible. Previously we used
# to run the whole featuring logic but that could be very slow and have concurrency
# errors on large sites with many autocloses and topics being created.
if (
(status.enabled? && (status.autoclosed? || status.closed? || status.archived?)) ||
(status.disabled? && status.visible?)
)
CategoryFeaturedTopic.where(topic_id: topic.id).delete_all
end
result
end
def create_moderator_post_for(status, opts)
message = opts[:message]
topic.add_moderator_post(user, message || message_for(status), options_for(status, opts))
topic.reload
end
def update_read_state_for(status, old_highest_read, silent_tracking: false)
if (status.autoclosed? && status.enabled?) || (status.closed? && silent_tracking)
# let's pretend all the people that read up to the autoclose message
# actually read the topic
PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number)
end
if status.closed? && status.enabled?
sql_query = <<-SQL
SELECT DISTINCT post_timings.user_id
FROM post_timings
JOIN user_options ON user_options.user_id = post_timings.user_id
WHERE post_timings.topic_id = :topic_id
AND user_options.topics_unread_when_closed = 'f'
SQL
user_ids = DB.query_single(sql_query, topic_id: topic.id)
if user_ids.present?
PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number, user_ids)
end
end
end
def message_for(status)
if status.autoclosed?
locale_key = status.locale_key.dup
locale_key << "_lastpost" if @topic_timer&.based_on_last_post
message_for_autoclosed(locale_key)
end
end
def message_for_autoclosed(locale_key)
num_minutes =
if @topic_timer&.based_on_last_post
(@topic_timer.duration_minutes || 0).minutes.to_i
elsif @topic_timer&.created_at
Time.zone.now - @topic_timer.created_at
else
Time.zone.now - topic.created_at
end
# all of the results above are in seconds, this brings them
# back to the actual minutes integer
num_minutes = (num_minutes / 1.minute).round
if num_minutes.minutes >= 2.days
I18n.t("#{locale_key}_days", count: (num_minutes.minutes / 1.day).round)
else
num_hours = (num_minutes.minutes / 1.hour).round
if num_hours >= 2
I18n.t("#{locale_key}_hours", count: num_hours)
else
I18n.t("#{locale_key}_minutes", count: num_minutes)
end
end
end
def options_for(status, opts = {})
{
bump: status.opening_topic?,
post_type: Post.types[:small_action],
silent: opts[:silent],
action_code: status.action_code,
}
end
Status =
Struct.new(:name, :enabled) do
%w[pinned_globally pinned autoclosed closed visible archived].each do |status|
define_method("#{status}?") { name == status }
end
def enabled?
enabled
end
def disabled?
!enabled?
end
def action_code
"#{name}.#{enabled? ? "enabled" : "disabled"}"
end
def locale_key
"topic_statuses.#{action_code.tr(".", "_")}"
end
def opening_topic?
(closed? || autoclosed?) && disabled?
end
def closing_topic?
(closed? || autoclosed?) && enabled?
end
def manually_closing_topic?
closed? && enabled?
end
def manually_opening_topic?
closed? && disabled?
end
end
end