# 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