292 lines
9.8 KiB
Ruby
292 lines
9.8 KiB
Ruby
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
|