otwarchive-symphonyarchive/app/models/creatorship.rb

293 lines
9.8 KiB
Ruby
Raw Permalink Normal View History

2026-03-11 22:22:11 +00:00
class Creatorship < ApplicationRecord
belongs_to :pseud
belongs_to :creation, polymorphic: true, touch: true
scope :approved, -> { where(approved: true) }
scope :unapproved, -> { where(approved: false) }
scope :for_user, ->(user) { joins(:pseud).merge(user.pseuds) }
########################################
# VALIDATIONS
########################################
before_validation :update_approved, on: :create
validates_presence_of :creation
validates_uniqueness_of :pseud, scope: [:creation_type, :creation_id], on: :create
validate :check_invalid, on: :create
validate :check_banned, on: :create
validate :check_disallowed, on: :create
validate :check_approved_becoming_false, on: :update
# Update approval status if this creatorship should be automatically approved.
def update_approved
if !approved? && should_automatically_approve?
self.approved = true
end
end
# Make sure that the pseud exists, and isn't ambiguous.
#
# Note that thanks to the definitions of missing? and ambiguous?, this is
# equivalent to having a validates_presence_of :pseud check, just with
# different error messages.
def check_invalid
if missing?
errors.add(:base, ts("Could not find a pseud %{name}.", name: byline))
elsif ambiguous?
errors.add(:base, ts("The pseud %{name} is ambiguous.", name: byline))
end
end
# Make sure that the user isn't banned or suspended.
def check_banned
return unless pseud&.user&.banned || pseud&.user&.suspended
errors.add(:base, ts("%{name} cannot be listed as a co-creator.",
name: pseud.byline))
throw :abort
end
# Make sure that if this is an invitation, we're not inviting someone who has
# disabled invitations.
def check_disallowed
return if approved? || pseud.nil?
return if pseud&.user&.preference&.allow_cocreator
errors.add(:base, ts("%{name} does not allow others to invite them to be a co-creator.",
name: pseud.byline))
end
# Make sure that we're not trying to set approved to false, since that could
# potentially violate some rules about co-creators. (e.g. Having a user
# listed as a chapter co-creator, but not a work co-creator.)
def check_approved_becoming_false
if !approved? && approved_changed?
errors.add(:base, "Once approved, a creatorship cannot become unapproved.")
end
end
########################################
# CALLBACKS
########################################
after_create :add_to_parents
after_update :add_to_parents, if: :saved_change_to_approved?
before_destroy :expire_caches
before_destroy :check_not_last
before_destroy :save_original_creator
after_destroy :remove_from_children
after_commit :update_indices
after_create_commit :notify_creator, if: :enable_notifications
# If a pseud is listed as a work co-creator (not invited, actually listed),
# they should also be listed on all of the work's series. Similarly, if a
# pseud is listed as a chapter co-creator, they should also be listed on the
# work.
def add_to_parents
return unless approved?
parents = if creation.is_a?(Work)
creation.series.to_a
elsif creation.is_a?(Chapter)
[creation.work]
else
[]
end
parents.each do |parent|
parent.creatorships.approve_or_create_by(pseud: pseud)
end
end
# In order to make sure that all chapter co-creators are listed on the work,
# and all work co-creators are listed on the work's series, we need to make
# sure that when a creatorship is deleted, the deletion cascades downwards.
def remove_from_children
# If the creation is being deleted and it's a work, then its chapters are
# also going to be deleted (which will cause their creatorships to be
# deleted as well). If the creation is being deleted and it's a series,
# then we shouldn't delete the work creatorships. So if the creation is
# being deleted, we don't want to cascade the deletion downwards.
return if creation.nil? || creation.destroyed?
children = if creation.is_a?(Work)
creation.chapters.to_a
elsif creation.is_a?(Series)
creation.works.to_a
else
[]
end
children.each do |child|
child.creatorships.where(pseud: pseud).destroy_all
end
end
# Make sure that both the creation and the pseud are enqueued to be
# reindexed.
def update_indices
if creation.is_a?(Searchable)
creation.enqueue_to_index
end
if pseud && creation.is_a?(Work)
IndexQueue.enqueue(pseud, :background)
end
end
# Only enable notifications for new creatorships when explicitly enabled.
attr_accessor :enable_notifications
# Notify the pseud of their new creatorship.
def notify_creator
return unless User.current_user.is_a?(User) &&
pseud.user != User.current_user &&
pseud.user != User.orphan_account
I18n.with_locale(pseud.user.preference.locale_for_mails) do
if approved?
if User.current_user.try(:is_archivist?)
UserMailer.creatorship_notification_archivist(id, User.current_user.id).deliver_later
else
UserMailer.creatorship_notification(id, User.current_user.id).deliver_later
end
else
UserMailer.creatorship_request(id, User.current_user.id).deliver_later
end
end
end
# When deleting a creatorship, we want to make sure we're not deleting the
# very last creatorship for that item.
def check_not_last
# We can always delete unapproved creatorships:
return unless approved?
# Check that the creation hasn't been deleted, and still has creatorships
# left:
return if creation.nil? || creation.destroyed? ||
creation.creatorships.approved.count > 1
errors.add(:base, ts("Sorry, we can't remove all creators of a %{type}.",
type: creation.model_name.human.downcase))
raise ActiveRecord::RecordInvalid, self
end
# Record the original creator if the creation is a work.
# This information is stored temporarily to make it available for
# Policy and Abuse on orphaned works.
def save_original_creator
return unless approved?
return unless creation.is_a?(Work)
return if creation.destroyed?
creation.original_creators.create_or_find_by(user: pseud.user).touch
end
def expire_caches
if creation_type == "Work" && self.pseud.present?
CacheMaster.record(creation_id, "pseud", self.pseud_id)
CacheMaster.record(creation_id, "user", self.pseud.user_id)
end
end
########################################
# OTHER METHODS
########################################
attr_reader :ambiguous_pseuds
# We define a virtual "byline" attribute to make it easier to handle
# ambiguous/missing pseuds. By storing the desired name in the @byline
# variable, we can generate nicely formatted messages.
def byline=(byline)
pseuds = Pseud.parse_byline_ambiguous(byline).to_a
if pseuds.size == 1
self.pseud = pseuds.first
@byline = nil
@ambiguous_pseuds = nil
else
self.pseud = nil
@byline = byline
@ambiguous_pseuds = pseuds
end
end
# Retrieve the @byline variable, or, failing that, the pseud's byline.
def byline
@byline || pseud&.byline
end
# A creatorship counts as "missing" if we couldn't find any pseuds matching
# the passed-in byline.
def missing?
pseud.nil? && @ambiguous_pseuds.blank?
end
# A creatorship counts as "ambiguous" if there was more than one pseud
# matching the passed-in byline.
def ambiguous?
pseud.nil? && @ambiguous_pseuds.present?
end
# Find or initialize a creatorship matching the options, and then set
# approved to true and save the results. This is a way of adding a new
# approved creatorship without potentially running into issues with a
# pre-existing unapproved creatorship.
def self.approve_or_create_by(options)
creatorship = find_or_initialize_by(options)
creatorship.approved = true
creatorship.save if creatorship.changed?
end
# Change authorship of works or series from a particular pseud to the orphan account
def self.orphan(pseuds, orphans, default=true)
for pseud in pseuds
for new_orphan in orphans
unless pseud.blank? || new_orphan.blank? || !new_orphan.pseuds.include?(pseud)
orphan_pseud = default ? User.orphan_account.default_pseud : User.orphan_account.pseuds.find_or_create_by(name: pseud.name)
pseud.change_ownership(new_orphan, orphan_pseud)
end
end
end
end
# Calculate whether this creatorship should count as approved, or whether
# it's just a creatorship invitation.
def should_automatically_approve?
# Approve if we're using an API key, or if the current user has special
# permissions:
return true if User.current_user.nil? ||
pseud&.user == User.current_user ||
pseud&.user == User.orphan_account ||
User.current_user.try(:is_archivist?)
# Approve if the creation is a chapter and the pseud is already listed on
# the work, or if the creation is a series and the pseud is already listed
# on one of the works:
(creation.is_a?(Chapter) && creation.work.pseuds.include?(pseud)) ||
(creation.is_a?(Series) && creation.work_pseuds.include?(pseud))
end
# Accept the creatorship invitation. This consists of setting approved to
# true, and, if the creation is a work, adding the pseud to all of its
# chapters as well.
def accept!
transaction do
update(approved: true)
if creation.is_a?(Work)
creation.chapters.each do |chapter|
chapter.creatorships.approve_or_create_by(pseud: pseud)
end
end
end
end
end