otwarchive-symphonyarchive/app/models/bookmark.rb

217 lines
7.6 KiB
Ruby
Raw Normal View History

2026-03-11 22:22:11 +00:00
class Bookmark < ApplicationRecord
include Collectible
include Searchable
include Responder
include Taggable
belongs_to :bookmarkable, polymorphic: true, inverse_of: :bookmarks
belongs_to :pseud, optional: false
validates_length_of :bookmarker_notes,
maximum: ArchiveConfig.NOTES_MAX, too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.NOTES_MAX)
validate :not_already_bookmarked_by_user, on: :create
def not_already_bookmarked_by_user
return unless self.pseud && self.bookmarkable
return if self.pseud.user.bookmarks.where(bookmarkable: self.bookmarkable).empty?
errors.add(:base, ts("You have already bookmarked that."))
end
validate :check_new_external_work
def check_new_external_work
return unless bookmarkable.is_a?(ExternalWork) && bookmarkable.new_record?
errors.add(:base, "Fandom tag is required") if bookmarkable.fandom_string.blank?
return if bookmarkable.valid?
bookmarkable.errors.full_messages.each do |message|
errors.add(:base, message)
end
end
# renaming scope :public -> :is_public because otherwise it overlaps with the "public" keyword
scope :is_public, -> { where(private: false, hidden_by_admin: false) }
scope :not_public, -> { where(private: true) }
scope :not_private, -> { where(private: false) }
scope :since, lambda { |*args| where("bookmarks.created_at > ?", (args.first || 1.week.ago)) }
scope :recs, -> { where(rec: true) }
scope :order_by_created_at, -> { order("created_at DESC") }
scope :join_work, -> {
joins("LEFT JOIN works ON (bookmarks.bookmarkable_id = works.id AND bookmarks.bookmarkable_type = 'Work')").
merge(Work.visible_to_all)
}
scope :join_series, -> {
joins("LEFT JOIN series ON (bookmarks.bookmarkable_id = series.id AND bookmarks.bookmarkable_type = 'Series')").
merge(Series.visible_to_all)
}
scope :join_external_works, -> {
joins("LEFT JOIN external_works ON (bookmarks.bookmarkable_id = external_works.id AND bookmarks.bookmarkable_type = 'ExternalWork')").
merge(ExternalWork.visible_to_all)
}
scope :join_bookmarkable, -> {
joins("LEFT JOIN works ON (bookmarks.bookmarkable_id = works.id AND bookmarks.bookmarkable_type = 'Work')
LEFT JOIN series ON (bookmarks.bookmarkable_id = series.id AND bookmarks.bookmarkable_type = 'Series')
LEFT JOIN external_works ON (bookmarks.bookmarkable_id = external_works.id AND bookmarks.bookmarkable_type = 'ExternalWork')")
}
scope :visible_to_all, -> {
is_public.with_bookmarkable_visible_to_all
}
scope :visible_to_registered_user, -> {
is_public.with_bookmarkable_visible_to_registered_user
}
# Scope for retrieving bookmarks with a bookmarkable visible to registered
# users (regardless of the bookmark's hidden_by_admin/private status).
scope :with_bookmarkable_visible_to_registered_user, -> {
join_bookmarkable.where(
"(works.posted = 1 AND works.hidden_by_admin = 0) OR
(series.hidden_by_admin = 0) OR
(external_works.hidden_by_admin = 0)"
)
}
# Scope for retrieving bookmarks with a bookmarkable visible to logged-out
# users (regardless of the bookmark's hidden_by_admin/private status).
scope :with_bookmarkable_visible_to_all, -> {
join_bookmarkable.where(
"(works.posted = 1 AND works.restricted = 0 AND works.hidden_by_admin = 0) OR
(series.restricted = 0 AND series.hidden_by_admin = 0) OR
(external_works.hidden_by_admin = 0)"
)
}
# Scope for retrieving bookmarks with a missing bookmarkable (regardless of
# the bookmark's hidden_by_admin/private status).
scope :with_missing_bookmarkable, -> {
join_bookmarkable.where(
"works.id IS NULL AND series.id IS NULL AND external_works.id IS NULL"
)
}
scope :visible_to_admin, -> { not_private }
scope :latest, -> { is_public.order_by_created_at.limit(ArchiveConfig.ITEMS_PER_PAGE).join_work }
scope :for_blurb, -> { includes(:bookmarkable, :tags, :collections, pseud: [:user]) }
# a complicated dynamic scope here:
# if the user is an Admin, we use the "visible_to_admin" scope
# if the user is not a logged-in User, we use the "visible_to_all" scope
# otherwise, we use a join to get userids and then get all posted works that are either unhidden OR belong to this user.
# Note: in that last case we have to use select("DISTINCT works.") because of cases where the same user appears twice
# on a work.
scope :visible_to_user, lambda {|user|
if user.is_a?(Admin)
visible_to_admin
elsif !user.is_a?(User)
visible_to_all
else
select("DISTINCT bookmarks.*").
visible_to_registered_user.
joins("JOIN pseuds as p1 ON p1.id = bookmarks.pseud_id JOIN users ON users.id = p1.user_id").
where("bookmarks.hidden_by_admin = 0 OR users.id = ?", user.id)
end
}
# Use the current user to determine what works are visible
scope :visible, -> { visible_to_user(User.current_user) }
before_destroy :invalidate_bookmark_count
after_save :invalidate_bookmark_count, :update_pseud_index
after_create :update_work_stats
after_destroy :update_work_stats, :update_pseud_index
def invalidate_bookmark_count
work = Work.where(id: self.bookmarkable_id)
if work.present? && self.bookmarkable_type == 'Work'
work.first.invalidate_public_bookmarks_count
end
end
# We index the bookmark count, so if it should change, update the pseud
def update_pseud_index
return unless destroyed? || saved_change_to_id? || saved_change_to_private? || saved_change_to_hidden_by_admin?
IndexQueue.enqueue_id(Pseud, pseud_id, :background)
end
def visible?(current_user=User.current_user)
return true if current_user == self.pseud.user
unless current_user == :false || !current_user
# Admins should not see private bookmarks
return true if current_user.is_a?(Admin) && self.private == false
end
if !(self.private? || self.hidden_by_admin?)
if self.bookmarkable.nil?
# only show bookmarks for deleted works to the user who
# created the bookmark
return true if pseud.user == current_user
else
if self.bookmarkable_type == 'Work' || self.bookmarkable_type == 'Series' || self.bookmarkable_type == 'ExternalWork'
return true if self.bookmarkable.visible?(current_user)
else
return true
end
end
end
return false
end
# Returns the number of bookmarks on an item visible to the current user
def self.count_visible_bookmarks(bookmarkable, current_user=:false)
bookmarkable.bookmarks.visible.size
end
# TODO: Is this necessary anymore?
before_destroy :save_parent_info
# Because of the way the elasticsearch parent/child index is set up, we need
# to know what the bookmarkable type and id was in order to delete the
# bookmark from the index after it's been deleted from the database
def save_parent_info
expire_time = (Time.now + 2.weeks).to_i
REDIS_GENERAL.setex(
"deleted_bookmark_parent_#{self.id}",
expire_time,
"#{bookmarkable_id}-#{bookmarkable_type.underscore}"
)
end
#################################
## SEARCH #######################
#################################
def document_json
BookmarkIndexer.new({}).document(self)
end
def bookmarker
pseud.try(:byline)
end
def with_notes
bookmarker_notes.present?
end
def collection_ids
approved_collections.pluck(:id, :parent_id).flatten.uniq.compact
end
def bookmarkable_date
if bookmarkable.respond_to?(:revised_at)
bookmarkable.revised_at
elsif bookmarkable.respond_to?(:updated_at)
bookmarkable.updated_at
end
end
end