otwarchive-symphonyarchive/app/models/concerns/creatable.rb
2026-03-11 22:22:11 +00:00

228 lines
7.4 KiB
Ruby

# frozen_string_literal: true
# A module used for classes that can appear as the "creation" in a Creatorship
# (i.e. Work, Series, and Chapter).
module Creatable
extend ActiveSupport::Concern
included do
has_many :creatorships,
autosave: true,
as: :creation,
inverse_of: :creation
has_many :approved_creatorships,
-> { Creatorship.approved },
class_name: "Creatorship",
as: :creation,
inverse_of: :creation
has_many :pseuds,
through: :approved_creatorships,
before_add: :disallow_pseud_changes,
before_remove: :disallow_pseud_changes
has_many :users,
-> { distinct },
through: :pseuds
attr_reader :current_user_pseuds
validate :check_no_creators
validate :check_current_user_pseuds
after_save :update_current_user_pseuds
after_destroy :destroy_creatorships
end
########################################
# CALLBACKS & VALIDATIONS
########################################
# Updating pseuds directly goes through the approved_creatorships relation,
# so it will automatically approve any pseuds added in this way. So we want
# to make sure that this is a read-only relation.
def disallow_pseud_changes(*)
raise "Cannot add or remove pseuds through the pseuds association!"
end
# Make sure that there will be at least one approved creator after saving:
def check_no_creators
return if @current_user_pseuds.present? || pseuds_after_saving.any?
errors.add(:base, ts("%{type} must have at least one creator.",
type: model_name.human))
end
# Make sure that if @current_user_pseuds is not nil, then the user has
# selected at least one pseud, and that all of the pseuds they've selected
# are their own.
def check_current_user_pseuds
return unless @current_user_pseuds && User.current_user.is_a?(User)
if @current_user_pseuds.empty?
errors.add(:base, ts("You haven't selected any pseuds for this %{type}.",
type: model_name.human.downcase))
end
if @current_user_pseuds.any? { |p| p.user_id != User.current_user.id }
errors.add(:base, ts("You're not allowed to use that pseud."))
end
end
# The variable @current_user_pseuds stores which pseuds the current editor
# wants to use on this work. The pseuds should contain only pseuds owned by
# User.current_user.
def update_current_user_pseuds
return unless @current_user_pseuds
set_current_user_pseuds(@current_user_pseuds)
@current_user_pseuds = nil
end
# Clean up all creatorships associated with this item.
def destroy_creatorships
creatorships.destroy_all
end
########################################
# VIRTUAL ATTRIBUTES
########################################
# Update all creator-related attributes.
def author_attributes=(attributes)
self.new_bylines = attributes[:byline] if attributes[:byline].present?
self.new_co_creator_ids = attributes[:coauthors] if attributes[:coauthors].present?
self.current_user_pseud_ids = attributes[:ids] if attributes[:ids].present?
end
# Invite new co-creators by passing in their byline.
def new_bylines=(bylines)
bylines.split(",").reject(&:blank?).map(&:strip).each do |byline|
self.creatorships.build(byline: byline, enable_notifications: true)
end
end
# Invite new co-creators by ID.
def new_co_creator_ids=(ids)
new_pseuds = Pseud.where(id: ids).to_a
creatorships.each do |creatorship|
if new_pseuds.include?(creatorship.pseud)
new_pseuds.delete(creatorship.pseud)
end
end
new_pseuds.each do |pseud|
self.creatorships.build(pseud: pseud, enable_notifications: true)
end
end
# Update which of User.current_user's pseuds should be listed on the byline
# after saving.
def current_user_pseud_ids=(ids)
return unless User.current_user.is_a?(User)
@current_user_pseuds = Pseud.where(id: ids).to_a
end
# This behaves very similarly to new_bylines=, but because it's designed to
# be used for bulk editing works, it doesn't handle ambiguous pseuds well. So
# we need to manually refine our guess as much as possible.
def pseuds_to_add=(pseud_names)
names = pseud_names.split(",").reject(&:blank?).map(&:strip)
names.each do |name|
possible_pseuds = Pseud.parse_byline_ambiguous(name)
pseud = if possible_pseuds.size > 1
Pseud.parse_byline(name)
else
possible_pseuds.first
end
if pseud
creatorship = creatorships.find_or_initialize_by(pseud: pseud)
creatorship.enable_notifications = true
end
end
end
########################################
# USEFUL FUNCTIONS
########################################
# Update the pseuds on this item so that User.current_user's pseuds are
# replaced by the passed-in array of pseuds new_pseuds. If it's a Series, we
# also update the user's byline on any owned works in the series. If it's a
# Work, we also update the user's byline on any owned chapters in the series.
def set_current_user_pseuds(new_pseuds)
return unless User.current_user.is_a?(User)
user_id = User.current_user.id
children = if is_a?(Work)
chapters.to_a
elsif is_a?(Series)
works.to_a
else
[]
end
transaction do
children.each do |child|
next unless child.users.include?(User.current_user)
child.set_current_user_pseuds(new_pseuds)
end
# Create before destroying, so that we don't run into issues with
# deleting the very last creator.
new_pseuds.each do |pseud|
creatorships.approve_or_create_by(pseud: pseud)
end
creatorships.each do |creatorship|
creatorship.destroy unless new_pseuds.include?(creatorship.pseud) ||
creatorship.pseud&.user_id != user_id
end
end
end
# Figure out which creatorships will exist after saving.
#
# Excludes creatorships with a missing pseud, because those orphaned
# creatorships can break various bits of code if they're considered valid.
def creatorships_after_saving
creatorships.select(&:valid?).reject(&:marked_for_destruction?).
reject { |creatorship| creatorship.pseud.nil? }
end
# Calculate what the pseuds on this work will be after saving, taking into
# account validity, approval, and @current_user_pseuds.
def pseuds_after_saving
pseuds = creatorships_after_saving.select(&:approved?).map(&:pseud)
if @current_user_pseuds
pseuds = (pseuds - User.current_user.pseuds) + @current_user_pseuds
end
pseuds.uniq
end
# Check whether the passed-in user has been invited to become a creator.
def user_has_creator_invite?(user)
return false unless user.is_a?(User)
creatorships.unapproved.for_user(user).exists?
end
# Check whether the given user has some kind of creatorship (approved or
# unapproved) associated with this item.
def user_is_owner_or_invited?(user)
return false unless user.is_a?(User)
creatorships.for_user(user).exists?
end
# Get all orphan_account pseuds that (co-)created this creatable, excluding the orphan_account's default_pseud
def orphan_pseuds
self.pseuds.where(user_id: User.orphan_account.id, is_default: false)
end
end