discourse/app/models/topic_timer.rb

270 lines
8.3 KiB
Ruby

# frozen_string_literal: true
class TopicTimer < ActiveRecord::Base
MAX_DURATION_MINUTES = 2.years.to_i / 60
self.ignored_columns = [
"duration" # TODO(2021-06-01): remove
]
include Trashable
belongs_to :user
belongs_to :topic
belongs_to :category
validates :user_id, presence: true
validates :topic_id, presence: true
validates :execute_at, presence: true
validates :status_type, presence: true
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }, if: :public_type?
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at, :user_id] }, if: :private_type?
validates :category_id, presence: true, if: :publishing_to_category?
validate :executed_at_in_future?
validate :duration_in_range?
scope :scheduled_bump_topics, -> { where(status_type: TopicTimer.types[:bump], deleted_at: nil).pluck(:topic_id) }
scope :pending_timers, ->(before_time = Time.now.utc) do
where("execute_at <= :before_time AND deleted_at IS NULL", before_time: before_time)
end
before_save do
self.created_at ||= Time.zone.now if execute_at
self.public_type = self.public_type?
if (will_save_change_to_execute_at? &&
!attribute_in_database(:execute_at).nil?) ||
will_save_change_to_user_id?
# TODO(martin - 2021-05-01) - Remove this backwards compatability for outstanding
# jobs once they have all been run and after Jobs::TopicTimerEnqueuer is in place
self.send("cancel_auto_#{self.class.types[status_type]}_job")
end
end
# These actions are in place to make sure the topic is in the correct
# state at the point in time where the timer is saved. It does not
# guarantee that the topic will be in the correct state when the timer
# job is executed, but each timer job handles deleted topics etc. gracefully.
#
# This is also important for the Open Temporarily and Close Temporarily timers,
# which change the topic's status straight away and set a timer to do the
# opposite action in the future.
after_save do
if (saved_change_to_execute_at? || saved_change_to_user_id?)
if status_type == TopicTimer.types[:silent_close] || status_type == TopicTimer.types[:close]
topic.update_status('closed', false, user) if topic.closed?
end
if status_type == TopicTimer.types[:open]
topic.update_status('closed', true, user) if topic.open?
end
end
end
def status_type_name
self.class.types[status_type]
end
def enqueue_typed_job(time: nil)
return if typed_job_scheduled?
self.send("schedule_auto_#{status_type_name}_job")
end
# TODO(martin - 2021-05-01) - Remove this backwards compatability for outstanding
# jobs once they have all been run and after Jobs::TopicTimerEnqueuer is in place
def typed_job_scheduled?
scheduled = Jobs.scheduled_for(
TopicTimer.type_job_map[status_type_name], topic_timer_id: id
).any?
if [:close, :silent_close, :open].include?(status_type_name)
return scheduled || Jobs.scheduled_for(:toggle_topic_closed, topic_timer_id: id).any?
end
scheduled
end
def self.type_job_map
{
close: :close_topic,
open: :open_topic,
publish_to_category: :publish_topic_to_category,
delete: :delete_topic,
reminder: :topic_reminder,
bump: :bump_topic,
delete_replies: :delete_replies,
silent_close: :close_topic,
clear_slow_mode: :clear_slow_mode
}
end
def self.types
@types ||= Enum.new(
close: 1,
open: 2,
publish_to_category: 3,
delete: 4,
reminder: 5,
bump: 6,
delete_replies: 7,
silent_close: 8,
clear_slow_mode: 9
)
end
def self.public_types
@_public_types ||= types.except(:reminder, :clear_slow_mode)
end
def self.private_types
@_private_types ||= types.only(:reminder, :clear_slow_mode)
end
def public_type?
!!self.class.public_types[self.status_type]
end
def private_type?
!!self.class.private_types[self.status_type]
end
def runnable?
return false if deleted_at.present?
return false if execute_at > Time.zone.now
true
end
private
def duration_in_range?
return if duration_minutes.blank?
if duration_minutes.to_i <= 0
errors.add(:duration_minutes, I18n.t(
'activerecord.errors.models.topic_timer.attributes.duration_minutes.cannot_be_zero'
))
end
if duration_minutes.to_i > MAX_DURATION_MINUTES
errors.add(:duration_minutes, I18n.t(
'activerecord.errors.models.topic_timer.attributes.duration_minutes.exceeds_maximum'
))
end
end
def executed_at_in_future?
return if created_at.blank? || (execute_at > created_at)
errors.add(:execute_at, I18n.t(
'activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past'
))
end
def publishing_to_category?
self.status_type.to_i == TopicTimer.types[:publish_to_category]
end
# TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
def cancel_auto_close_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
Jobs.cancel_scheduled_job(:close_topic, topic_timer_id: id)
end
# TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
def cancel_auto_open_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
Jobs.cancel_scheduled_job(:open_topic, topic_timer_id: id)
end
# TODO(martin - 2021-05-01) - Remove cancels for toggle_topic_closed once topic timer revamp completed.
def cancel_auto_silent_close_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
Jobs.cancel_scheduled_job(:close_topic, topic_timer_id: id)
end
def cancel_auto_publish_to_category_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:publish_to_category], topic_timer_id: id)
end
def cancel_auto_delete_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:delete], topic_timer_id: id)
end
def cancel_auto_reminder_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:reminder], topic_timer_id: id)
end
def cancel_auto_bump_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:bump], topic_timer_id: id)
end
def cancel_auto_delete_replies_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:delete_replies], topic_timer_id: id)
end
def cancel_auto_clear_slow_mode_job
Jobs.cancel_scheduled_job(TopicTimer.type_job_map[:clear_slow_mode], topic_timer_id: id)
end
def schedule_auto_delete_replies_job
Jobs.enqueue(TopicTimer.type_job_map[:delete_replies], topic_timer_id: id)
end
def schedule_auto_bump_job
Jobs.enqueue(TopicTimer.type_job_map[:bump], topic_timer_id: id)
end
def schedule_auto_open_job
Jobs.enqueue(TopicTimer.type_job_map[:open], topic_timer_id: id)
end
def schedule_auto_close_job
Jobs.enqueue(TopicTimer.type_job_map[:close], topic_timer_id: id)
end
def schedule_auto_silent_close_job
Jobs.enqueue(TopicTimer.type_job_map[:close], topic_timer_id: id, silent: true)
end
def schedule_auto_publish_to_category_job
Jobs.enqueue(TopicTimer.type_job_map[:publish_to_category], topic_timer_id: id)
end
def schedule_auto_delete_job
Jobs.enqueue(TopicTimer.type_job_map[:delete], topic_timer_id: id)
end
def schedule_auto_reminder_job
# noop, TODO(martin 2021-03-11): Remove this after timers migrated and outstanding jobs cancelled
end
def schedule_auto_clear_slow_mode_job
Jobs.enqueue(TopicTimer.type_job_map[:clear_slow_mode], topic_timer_id: id)
end
end
# == Schema Information
#
# Table name: topic_timers
#
# id :integer not null, primary key
# execute_at :datetime not null
# status_type :integer not null
# user_id :integer not null
# topic_id :integer not null
# based_on_last_post :boolean default(FALSE), not null
# deleted_at :datetime
# deleted_by_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# category_id :integer
# public_type :boolean default(TRUE)
# duration_minutes :integer
#
# Indexes
#
# idx_topic_id_public_type_deleted_at (topic_id) UNIQUE WHERE ((public_type = true) AND (deleted_at IS NULL))
# index_topic_timers_on_user_id (user_id)
#