# frozen_string_literal: true class Bookmark < ActiveRecord::Base DEFAULT_BOOKMARKABLES = [ RegisteredBookmarkable.new(PostBookmarkable), RegisteredBookmarkable.new(TopicBookmarkable), ] def self.registered_bookmarkables Set.new(DEFAULT_BOOKMARKABLES | DiscoursePluginRegistry.bookmarkables) end def self.registered_bookmarkable_from_type(type) begin resolved_type = Bookmark.polymorphic_class_for(type).name Bookmark.registered_bookmarkables.find { |bm| bm.model.name == resolved_type } # If the class cannot be found from the provided type using polymorphic_class_for, # then the type is not valid and thus there will not be any registered bookmarkable. rescue NameError end end def self.valid_bookmarkable_types Bookmark.registered_bookmarkables.map { |bm| bm.model.polymorphic_name } end belongs_to :user belongs_to :bookmarkable, polymorphic: true def self.auto_delete_preferences @auto_delete_preferences ||= Enum.new(never: 0, when_reminder_sent: 1, on_owner_reply: 2, clear_reminder: 3) end def self.select_type(bookmarks_relation, type) bookmarks_relation.select { |bm| bm.bookmarkable_type == type } end validate :polymorphic_columns_present, on: %i[create update] validate :valid_bookmarkable_type, on: %i[create update] validate :unique_per_bookmarkable, on: %i[create update], if: Proc.new { |b| b.will_save_change_to_bookmarkable_id? || b.will_save_change_to_bookmarkable_type? || b.will_save_change_to_user_id? } validate :ensure_sane_reminder_at_time, if: :will_save_change_to_reminder_at? validate :bookmark_limit_not_reached validates :name, length: { maximum: 100 } def registered_bookmarkable Bookmark.registered_bookmarkable_from_type(self.bookmarkable_type) end def polymorphic_columns_present return if self.bookmarkable_id.present? && self.bookmarkable_type.present? self.errors.add(:base, I18n.t("bookmarks.errors.bookmarkable_id_type_required")) end def unique_per_bookmarkable if !Bookmark.exists?( user_id: user_id, bookmarkable_id: bookmarkable_id, bookmarkable_type: bookmarkable_type, ) return end self.errors.add(:base, I18n.t("bookmarks.errors.already_bookmarked", type: bookmarkable_type)) end def ensure_sane_reminder_at_time return if reminder_at.blank? if reminder_at < Time.zone.now self.errors.add(:base, I18n.t("bookmarks.errors.cannot_set_past_reminder")) end if reminder_at > 10.years.from_now.utc self.errors.add(:base, I18n.t("bookmarks.errors.cannot_set_reminder_in_distant_future")) end end def bookmark_limit_not_reached return if user.bookmarks.count < SiteSetting.max_bookmarks_per_user return if !new_record? self.errors.add( :base, I18n.t( "bookmarks.errors.too_many", user_bookmarks_url: "#{Discourse.base_url}/my/activity/bookmarks", limit: SiteSetting.max_bookmarks_per_user, ), ) end def valid_bookmarkable_type return if Bookmark.valid_bookmarkable_types.include?(self.bookmarkable_type) self.errors.add( :base, I18n.t("bookmarks.errors.invalid_bookmarkable", type: self.bookmarkable_type), ) end def auto_delete_when_reminder_sent? self.auto_delete_preference == Bookmark.auto_delete_preferences[:when_reminder_sent] end def auto_clear_reminder_when_reminder_sent? self.auto_delete_preference == Bookmark.auto_delete_preferences[:clear_reminder] end def reminder_at_ics(offset: 0) (reminder_at + offset).strftime(I18n.t("datetime_formats.formats.calendar_ics")) end def clear_reminder! update!(reminder_last_sent_at: Time.zone.now, reminder_set_at: nil) end def reminder_at_in_zone(timezone) self.reminder_at.in_time_zone(timezone) end scope :with_reminders, -> { where("reminder_at IS NOT NULL") } scope :pending_reminders, ->(before_time = Time.now.utc) { with_reminders.where("reminder_at <= ?", before_time).where(reminder_last_sent_at: nil) } scope :pending_reminders_for_user, ->(user) { pending_reminders.where(user: user) } scope :for_user_in_topic, ->(user_id, topic_id) { joins( "LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'", ).joins( "LEFT JOIN topics ON (topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic') OR (topics.id = posts.topic_id)", ).where( "bookmarks.user_id = :user_id AND (topics.id = :topic_id OR posts.topic_id = :topic_id) AND posts.deleted_at IS NULL AND topics.deleted_at IS NULL", user_id: user_id, topic_id: topic_id, ) } def self.count_per_day(opts = nil) opts ||= {} result = where( "bookmarks.created_at >= ?", opts[:start_date] || (opts[:since_days_ago] || 30).days.ago, ) result = result.where("bookmarks.created_at <= ?", opts[:end_date]) if opts[:end_date] if opts[:category_id] result = result .joins( "LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'", ) .joins( "LEFT JOIN topics ON (topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic') OR (topics.id = posts.topic_id)", ) .where("topics.deleted_at IS NULL AND posts.deleted_at IS NULL") .merge(Topic.in_category_and_subcategories(opts[:category_id])) end result.group("date(bookmarks.created_at)").order("date(bookmarks.created_at)").count end ## # Deletes bookmarks that are attached to the bookmarkable records that were deleted # more than X days ago. We don't delete bookmarks instantly when trashable bookmarkables # are deleted so that there is a grace period to un-delete. def self.cleanup! Bookmark.registered_bookmarkables.each(&:cleanup_deleted) end end # == Schema Information # # Table name: bookmarks # # id :bigint not null, primary key # user_id :bigint not null # name :string(100) # reminder_at :datetime # created_at :datetime not null # updated_at :datetime not null # reminder_last_sent_at :datetime # reminder_set_at :datetime # auto_delete_preference :integer default(0), not null # pinned :boolean default(FALSE) # bookmarkable_id :integer # bookmarkable_type :string # # Indexes # # idx_bookmarks_user_polymorphic_unique (user_id,bookmarkable_type,bookmarkable_id) UNIQUE # index_bookmarks_on_reminder_at (reminder_at) # index_bookmarks_on_reminder_set_at (reminder_set_at) # index_bookmarks_on_user_id (user_id) #