diff --git a/app/models/base_timer.rb b/app/models/base_timer.rb new file mode 100644 index 00000000000..cb4cf40a9f2 --- /dev/null +++ b/app/models/base_timer.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +class BaseTimer < ActiveRecord::Base + self.table_name = "topic_timers" + + MAX_DURATION_MINUTES = 20.years.to_i / 60 + + include Trashable + + validates :status_type, presence: true + validates :category_id, presence: true, if: :publishing_to_category? + + validate :duration_in_range? + + def status_type_name + self.class.types[status_type] + end + + def enqueue_typed_job(time: nil) + self.send("schedule_auto_#{status_type_name}_job") + 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 self.destructive_types + @_destructive_types ||= types.only(:delete, :delete_replies) + end + + def public_type? + !!self.class.public_types[self.status_type] + end + + def private_type? + !!self.class.private_types[self.status_type] + end + + def publishing_to_category? + self.status_type.to_i == TopicTimer.types[:publish_to_category] + 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 +end + +# == Schema Information +# +# Table name: topic_timers +# +# id :integer not null, primary key +# based_on_last_post :boolean default(FALSE), not null +# deleted_at :datetime +# duration_minutes :integer +# execute_at :datetime not null +# public_type :boolean default(TRUE) +# status_type :integer not null +# type :string default("TopicTimer"), not null +# created_at :datetime not null +# updated_at :datetime not null +# category_id :integer +# deleted_by_id :integer +# topic_id :integer not null +# user_id :integer not null +# +# Indexes +# +# idx_topic_id_public_type_deleted_at (topic_id) UNIQUE WHERE ((public_type = true) AND (deleted_at IS NULL) AND ((type)::text = 'TopicTimer'::text)) +# index_topic_timers_on_topic_id (topic_id) WHERE (deleted_at IS NULL) +# index_topic_timers_on_user_id (user_id) +# diff --git a/app/models/topic_timer.rb b/app/models/topic_timer.rb index 483064ad6c6..5a09aaa5364 100644 --- a/app/models/topic_timer.rb +++ b/app/models/topic_timer.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true -class TopicTimer < ActiveRecord::Base - MAX_DURATION_MINUTES = 20.years.to_i / 60 - - include Trashable - +class TopicTimer < BaseTimer belongs_to :user belongs_to :topic belongs_to :category @@ -12,13 +8,10 @@ class TopicTimer < ActiveRecord::Base 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: %i[topic_id deleted_at] }, if: :public_type? validates :status_type, uniqueness: { scope: %i[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) } @@ -51,95 +44,14 @@ class TopicTimer < ActiveRecord::Base end end - def status_type_name - self.class.types[status_type] - end - - def enqueue_typed_job(time: nil) - self.send("schedule_auto_#{status_type_name}_job") - 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 self.destructive_types - @_destructive_types ||= types.only(:delete, :delete_replies) - 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 - def publishing_to_category? - self.status_type.to_i == TopicTimer.types[:publish_to_category] - 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) @@ -187,22 +99,23 @@ end # 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 +# duration_minutes :integer +# execute_at :datetime not null +# public_type :boolean default(TRUE) +# status_type :integer not null +# type :string default("TopicTimer"), not null # created_at :datetime not null # updated_at :datetime not null # category_id :integer -# public_type :boolean default(TRUE) -# duration_minutes :integer +# deleted_by_id :integer +# topic_id :integer not null +# user_id :integer not null # # Indexes # -# idx_topic_id_public_type_deleted_at (topic_id) UNIQUE WHERE ((public_type = true) AND (deleted_at IS NULL)) +# idx_topic_id_public_type_deleted_at (topic_id) UNIQUE WHERE ((public_type = true) AND (deleted_at IS NULL) AND ((type)::text = 'TopicTimer'::text)) # index_topic_timers_on_topic_id (topic_id) WHERE (deleted_at IS NULL) # index_topic_timers_on_user_id (user_id) # diff --git a/db/migrate/20250826012802_add_type_to_topic_timer.rb b/db/migrate/20250826012802_add_type_to_topic_timer.rb new file mode 100644 index 00000000000..31e0d241092 --- /dev/null +++ b/db/migrate/20250826012802_add_type_to_topic_timer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +class AddTypeToTopicTimer < ActiveRecord::Migration[8.0] + def change + add_column :topic_timers, :type, :string, null: false, default: "TopicTimer" + remove_index :topic_timers, name: :idx_topic_id_public_type_deleted_at + add_index :topic_timers, + :topic_id, + unique: true, + name: :idx_topic_id_public_type_deleted_at, + where: "public_type = true AND deleted_at IS NULL AND type = 'TopicTimer'" + end +end