# 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) #