diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb
index 0f25334160c..93bf81f2651 100644
--- a/app/controllers/bookmarks_controller.rb
+++ b/app/controllers/bookmarks_controller.rb
@@ -208,56 +208,9 @@ def create
# PUT /bookmarks/1
# PUT /bookmarks/1.xml
def update
- new_collections = []
- unapproved_collections = []
- errors = []
- bookmark_params[:collection_names]&.split(",")&.map(&:strip)&.uniq&.each do |collection_name|
- collection = Collection.find_by(name: collection_name)
- if collection.nil?
- errors << ts("%{name} does not exist.", name: collection_name)
- else
- if @bookmark.collections.include?(collection)
- next
- elsif collection.closed? && !collection.user_is_maintainer?(User.current_user)
- errors << ts("%{title} is closed to new submissions.", title: collection.title)
- elsif @bookmark.add_to_collection(collection) && @bookmark.save
- if @bookmark.approved_collections.include?(collection)
- new_collections << collection
- else
- unapproved_collections << collection
- end
- else
- errors << ts("Something went wrong trying to add collection %{title}, sorry!", title: collection.title)
- end
- end
- end
-
- # messages to the user
- unless errors.empty?
- flash[:error] = ts("We couldn't add your submission to the following collections: ") + errors.join("
")
- end
-
- unless new_collections.empty?
- flash[:notice] = ts("Added to collection(s): %{collections}.",
- collections: new_collections.collect(&:title).join(", "))
- end
- unless unapproved_collections.empty?
- flash[:notice] = flash[:notice] ? flash[:notice] + " " : ""
- flash[:notice] += if unapproved_collections.size > 1
- ts("You have submitted your bookmark to moderated collections (%{all_collections}). It will not become a part of those collections until it has been approved by a moderator.", all_collections: unapproved_collections.map(&:title).join(", "))
- else
- ts("You have submitted your bookmark to the moderated collection '%{collection}'. It will not become a part of the collection until it has been approved by a moderator.", collection: unapproved_collections.first.title)
- end
- end
-
- flash[:notice] = (flash[:notice]).html_safe unless flash[:notice].blank?
- flash[:error] = (flash[:error]).html_safe unless flash[:error].blank?
-
- if @bookmark.update(bookmark_params) && errors.empty?
- flash[:notice] = flash[:notice] ? " " + flash[:notice] : ""
- flash[:notice] = ts("Bookmark was successfully updated.").html_safe + flash[:notice]
- flash[:notice] += t("bookmarks.create.warnings.private_bookmark_added_to_collection") if new_collections.any? || unapproved_collections.any?
- flash[:notice] = flash[:notice].html_safe
+ if @bookmark.update(bookmark_params)
+ flash[:notice] = t("bookmarks.update.bookmark_updated")
+ new_moderated_collections_message
redirect_to(@bookmark)
else
@bookmarkable = @bookmark.bookmarkable
@@ -351,6 +304,27 @@ def set_own_bookmarks
end
end
+ # Flash a message about the new moderated collections that the user has
+ # submitted their bookmark to.
+ def new_moderated_collections_message
+ new_moderated_collections = @bookmark.collection_items
+ .select(&:previously_new_record?)
+ .select(&:unreviewed_by_collection?)
+ .map(&:collection)
+
+ return if new_moderated_collections.blank?
+
+ links = new_moderated_collections.map do |collection|
+ view_context.link_to(collection.title, collection_path(collection))
+ end
+
+ message = t("bookmarks.new_moderated_collections_message_html",
+ count: new_moderated_collections.length,
+ collections: view_context.safe_join(links, ", "))
+
+ flash[:notice] = view_context.safe_join([flash[:notice], message].compact, " ")
+ end
+
private
def bookmark_params
diff --git a/app/controllers/collection_items_controller.rb b/app/controllers/collection_items_controller.rb
index 86a70880761..a58b24ade02 100644
--- a/app/controllers/collection_items_controller.rb
+++ b/app/controllers/collection_items_controller.rb
@@ -107,8 +107,8 @@ def create
errors << ts("%{collection_title}, either you don't own this item or are not a moderator of the collection.", collection_title: collection.title)
elsif @item.is_a?(Work) && @item.anonymous? && !current_user.is_author_of?(@item)
errors << ts("%{collection_title}, because you don't own this item and the item is anonymous.", collection_title: collection.title)
- # add the work to a collection, and try to save it
- elsif @item.add_to_collection(collection) && @item.save(validate: false)
+ # add the work to a collection
+ elsif @item.add_to_collection(collection)
# approved_by_user? and approved_by_collection? are both true.
# This is will be true for archivists adding works to collections they maintain
# or creators adding their works to a collection with auto-approval.
diff --git a/app/controllers/works_controller.rb b/app/controllers/works_controller.rb
index 61e6ad43a02..5ba6db4830a 100755
--- a/app/controllers/works_controller.rb
+++ b/app/controllers/works_controller.rb
@@ -362,7 +362,7 @@ def update
elsif params[:preview_button]
flash[:notice] = t(".unposted_notice") unless @work.posted?
- in_moderated_collection
+ in_moderated_collection(flash.now)
@preview_mode = true
render :preview
else
@@ -566,22 +566,23 @@ def send_external_invites(works)
end
# check to see if the work is being added / has been added to a moderated collection, then let user know that
- def in_moderated_collection
- moderated_collections = []
- @work.collections.each do |collection|
- next unless !collection.nil? && collection.moderated? && !collection.user_is_posting_participant?(current_user)
- next unless @work.collection_items.present?
- @work.collection_items.each do |collection_item|
- next unless collection_item.collection == collection
- if collection_item.approved_by_user? && collection_item.unreviewed_by_collection?
- moderated_collections << collection
- end
- end
- end
- if moderated_collections.present?
- flash[:notice] ||= ''
- flash[:notice] += ts(" You have submitted your work to #{moderated_collections.size > 1 ? 'moderated collections (%{all_collections}). It will not become a part of those collections' : "the moderated collection '%{all_collections}'. It will not become a part of the collection"} until it has been approved by a moderator.", all_collections: moderated_collections.map(&:title).join(', '))
+ def in_moderated_collection(flash = self.flash)
+ moderated_collections = @work.collection_items_after_saving
+ .select(&:approved_by_user?)
+ .select(&:unreviewed_by_collection?)
+ .map(&:collection)
+
+ return if moderated_collections.blank?
+
+ links = moderated_collections.map do |collection|
+ view_context.link_to(collection.title, collection_path(collection))
end
+
+ message = t("works.moderated_collections_message_html",
+ count: moderated_collections.length,
+ collections: view_context.safe_join(links, ", "))
+
+ flash[:notice] = view_context.safe_join([flash[:notice], message].compact, " ")
end
public
@@ -786,7 +787,7 @@ def set_work_form_fields
@serial_works = @work.serial_works
if @collection.nil?
- @collection = @work.approved_collections.first
+ @collection = @work.approved_collections_after_saving.first
end
if params[:claim_id]
diff --git a/app/helpers/works_helper.rb b/app/helpers/works_helper.rb
index da6bd272e84..a684fc04133 100644
--- a/app/helpers/works_helper.rb
+++ b/app/helpers/works_helper.rb
@@ -30,6 +30,15 @@ def work_meta_list(work, chapter = nil)
content_tag(:dl, list.to_s, class: 'stats').html_safe
end
+ def collections_meta_tag(work)
+ collections_data = show_collections_data(work.approved_collections_after_saving)
+ return if collections_data.blank?
+
+ content_tag(:dt, t("works_helper.collections_meta_tag.collections"), class: "collections") +
+ "\n".html_safe +
+ content_tag(:dd, collections_data, class: "collections")
+ end
+
def work_page_title(work, title, options = {})
fandoms = work.fandoms
title_fandom = if fandoms.empty?
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index b05045407ac..4d7700005c8 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -15,4 +15,8 @@ def update_sanitizer_version
def self.random_order
order(Arel.sql("RAND()"))
end
+
+ def unmark_for_destruction
+ @marked_for_destruction = false
+ end
end
diff --git a/app/models/collection.rb b/app/models/collection.rb
index b51005911f7..c4b5b4f154e 100755
--- a/app/models/collection.rb
+++ b/app/models/collection.rb
@@ -18,10 +18,10 @@ class Collection < ApplicationRecord
has_many :children, class_name: "Collection", foreign_key: "parent_id", inverse_of: :parent
has_one :collection_profile, dependent: :destroy
- accepts_nested_attributes_for :collection_profile
+ accepts_nested_attributes_for :collection_profile, update_only: true
has_one :collection_preference, dependent: :destroy
- accepts_nested_attributes_for :collection_preference
+ accepts_nested_attributes_for :collection_preference, update_only: true
before_validation :clear_icon
before_validation :cleanup_url
@@ -297,13 +297,11 @@ def gift_notification
self.collection_profile.gift_notification || (parent ? parent.collection_profile.gift_notification : "")
end
- def moderated?() = self.collection_preference.moderated
-
- def closed?() = self.collection_preference.closed
-
- def unrevealed?() = self.collection_preference.unrevealed
-
- def anonymous?() = self.collection_preference.anonymous
+ delegate :moderated?,
+ :closed?,
+ :unrevealed?,
+ :anonymous?,
+ to: :collection_preference, allow_nil: true
def challenge?() = !self.challenge.nil?
diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb
index 1c2dd9ddb57..f2489b85d0b 100644
--- a/app/models/collection_item.rb
+++ b/app/models/collection_item.rb
@@ -5,13 +5,10 @@ class CollectionItem < ApplicationRecord
[ts("Rejected"), :rejected]
]
- belongs_to :collection, inverse_of: :collection_items
+ belongs_to :collection, inverse_of: :collection_items, autosave: false
belongs_to :item, polymorphic: :true, inverse_of: :collection_items, touch: true
- belongs_to :work, class_name: "Work", foreign_key: "item_id", inverse_of: :collection_items
- belongs_to :bookmark, class_name: "Bookmark", foreign_key: "item_id"
- validates_uniqueness_of :collection_id, scope: [:item_id, :item_type],
- message: ts("already contains this item.")
+ validates :collection_id, uniqueness: { scope: [:item_id, :item_type] }
enum :user_approval_status, {
rejected: -1,
@@ -25,11 +22,21 @@ class CollectionItem < ApplicationRecord
approved: 1
}, suffix: :by_collection
+ validate :collection_must_exist, on: :create
+ def collection_must_exist
+ if collection.nil?
+ errors.add(:collection, :blank)
+ elsif collection.new_record?
+ errors.add(:collection, :not_found, name: collection.name)
+ end
+ end
+
validate :collection_is_open, on: :create
def collection_is_open
- if self.new_record? && self.collection && self.collection.closed? && !self.collection.user_is_maintainer?(User.current_user)
- errors.add(:base, ts("The collection %{title} is not currently open.", title: self.collection.title))
- end
+ return unless collection.present? && collection.closed? &&
+ !collection.user_is_maintainer?(User.current_user)
+
+ errors.add(:collection, :closed, title: collection.title)
end
scope :include_for_works, -> { includes(item: :pseuds) }
@@ -47,46 +54,110 @@ def self.for_user(user=User.current_user)
scope :invited_by_collection, -> { approved_by_collection.unreviewed_by_user }
scope :approved_by_both, -> { approved_by_collection.approved_by_user }
- before_save :set_anonymous_and_unrevealed
+ before_validation :set_anonymous_and_unrevealed, on: :create
def set_anonymous_and_unrevealed
- if self.new_record? && collection
- self.unrevealed = true if collection.reload.unrevealed?
- self.anonymous = true if collection.reload.anonymous?
+ return unless collection
+
+ self.unrevealed = collection.unrevealed?
+ self.anonymous = collection.anonymous?
+ end
+
+ before_validation :approve_automatically, on: :create
+ def approve_automatically
+ return unless item && collection
+
+ # approve with the current user, who is the person who has just
+ # added this item -- might be either moderator or owner
+ approve(User.current_user)
+
+ # if the collection is open or the user who owns this work is a member, go ahead and approve
+ # for the collection
+ return if approved_by_collection?
+
+ approve_by_collection if !collection.moderated? ||
+ collection.user_is_posting_participant?(User.current_user)
+ end
+
+ before_save :send_work_invitation
+ def send_work_invitation
+ return if approved_by_user? || !approved_by_collection? || !self.new_record? || User.current_user.is_author_of?(item)
+
+ # a maintainer is attempting to add this work to their collection
+ # so we send an email to all the works owners
+ item.users.each do |email_author|
+ next if email_author.preference.collection_emails_off
+
+ I18n.with_locale(email_author.preference.locale_for_mails) do
+ UserMailer.invited_to_collection_notification(email_author.id, item.id, collection.id).deliver_now
+ end
end
end
+ def destroyed_by_item?
+ item && destroyed_by_association &&
+ item.association(:collection_items).reflection == destroyed_by_association
+ end
+
+ after_update :notify_of_unrevealed_or_anonymous
+ def notify_of_unrevealed_or_anonymous
+ # This CollectionItem's anonymous/unrevealed status can only affect the
+ # item's status if (a) the CollectionItem is approved by the user and (b)
+ # the item is a work. (Bookmarks can't be anonymous/unrevealed at the
+ # moment.)
+ return unless approved_by_user? && item.is_a?(Work)
+
+ # Check whether anonymous/unrevealed is becoming true, when the work
+ # currently has it set to false:
+ newly_anonymous = (saved_change_to_anonymous?(to: true) && !item.anonymous?)
+ newly_unrevealed = (saved_change_to_unrevealed?(to: true) && !item.unrevealed?)
+
+ return unless newly_unrevealed || newly_anonymous
+
+ # Don't notify if it's one of the work creators who is changing the work's
+ # status.
+ return if item.users.include?(User.current_user)
+
+ item.users.each do |user|
+ I18n.with_locale(user.preference.locale_for_mails) do
+ UserMailer.anonymous_or_unrevealed_notification(
+ user.id, item.id, collection.id,
+ anonymous: newly_anonymous, unrevealed: newly_unrevealed
+ ).deliver_after_commit
+ end
+ end
+ end
+
+ after_destroy :update_work, unless: :destroyed_by_item?
+ after_destroy :reindex_item, unless: :destroyed_by_item?
+ after_destroy :expire_caches
+ def expire_caches
+ return unless self.item.respond_to?(:expire_caches)
+
+ self.item.expire_caches
+ CacheMaster.record(item_id, "collection", collection_id)
+ end
+
after_save :update_work
- after_destroy :update_work
# Set associated works to anonymous or unrevealed as appropriate.
- #
- # Inverses are set up properly on self.item, so we use that field to check
- # whether we're currently in the process of saving a brand new work, or
- # whether the work is in the process of being destroyed. (In which case we
- # rely on the Work's callbacks to set anon/unrevealed status properly.) But
- # because we want to discard changes made in preview mode, we perform the
- # actual anon/unrevealed updates on self.work, which doesn't have proper
- # inverses and therefore is freshly loaded from the database.
def update_work
- return unless item.is_a?(Work) && item.persisted? && !item.saved_change_to_id?
+ return unless item.is_a?(Work) && item.persisted?
- if work.present?
- work.update_anon_unrevealed
+ item.set_anon_unrevealed
- # For a more helpful error message, raise an error saying that the work
- # is invalid if we fail to save it.
- raise ActiveRecord::RecordInvalid, work unless work.save(validate: false)
- end
+ item.save!(validate: false) if item.will_save_change_to_anonymous? ||
+ item.will_save_change_to_unrevealed?
end
- # Poke the item if it's just been approved or unapproved so it gets picked up by the search index
- after_update :update_item_for_status_change
- def update_item_for_status_change
- if saved_change_to_user_approval_status? || saved_change_to_collection_approval_status?
- item.save!(validate: false)
- end
+ after_save :reindex_item
+ def reindex_item
+ item&.enqueue_to_index
end
+ # reindex collection after creation, deletion, and approval_status update
+ # (we only index approved items, which is why changes there trigger the reindex-index)
+ after_commit :update_collection_index, if: :should_update_collection_index?
+
after_create_commit :notify_of_association
def notify_of_association
email_notify = self.collection.collection_preference &&
@@ -116,44 +187,18 @@ def notify_archivist_added
end
end
- before_save :approve_automatically
- def approve_automatically
- return unless self.new_record?
-
- # approve with the current user, who is the person who has just
- # added this item -- might be either moderator or owner
- # rubocop:disable Lint/BooleanSymbol
- approve(User.current_user == :false ? nil : User.current_user)
- # rubocop:enable Lint/BooleanSymbol
-
- # if the collection is open or the user who owns this work is a member, go ahead and approve
- # for the collection
- return unless !approved_by_collection? && collection
-
- approve_by_collection if !collection.moderated? || collection.user_is_maintainer?(User.current_user) || collection.user_is_posting_participant?(User.current_user)
+ def update_collection_index
+ ids = [collection_id]
+ ids.push(collection.parent_id) if collection.parent.present?
+ IndexQueue.enqueue_ids(Collection, ids, :background)
end
- before_save :send_work_invitation
- def send_work_invitation
- return if approved_by_user? || !approved_by_collection? || !self.new_record? || User.current_user.is_author_of?(item)
-
- # a maintainer is attempting to add this work to their collection
- # so we send an email to all the works owners
- item.users.each do |email_author|
- next if email_author.preference.collection_emails_off
-
- I18n.with_locale(email_author.preference.locale_for_mails) do
- UserMailer.invited_to_collection_notification(email_author.id, item.id, collection.id).deliver_now
- end
- end
- end
+ # reindex collection after creation, deletion, and certain attribute updates
+ def should_update_collection_index?
+ return true if destroyed?
- after_destroy :expire_caches
- def expire_caches
- if self.item.respond_to?(:expire_caches)
- self.item.expire_caches
- CacheMaster.record(item_id, 'collection', collection_id)
- end
+ pertinent_attributes = %w[collection_approval_status user_approval_status]
+ (self.saved_changes.keys & pertinent_attributes).present?
end
attr_writer :remove
@@ -206,8 +251,7 @@ def approve(user)
approve_by_user
approve_by_collection
else
- author_of_item = user.is_author_of?(item) ||
- (user == User.current_user && item.respond_to?(:pseuds) ? item.pseuds.empty? : item.pseud.nil?)
+ author_of_item = user.is_author_of?(item) || (user == User.current_user && item.new_record?)
archivist_maintainer = user.archivist && self.collection.user_is_maintainer?(user)
approve_by_user if author_of_item || archivist_maintainer
approve_by_collection if self.collection.user_is_maintainer?(user)
@@ -249,51 +293,4 @@ def notify_of_reveal
end
end
end
-
- after_update :notify_of_unrevealed_or_anonymous
- def notify_of_unrevealed_or_anonymous
- # This CollectionItem's anonymous/unrevealed status can only affect the
- # item's status if (a) the CollectionItem is approved by the user and (b)
- # the item is a work. (Bookmarks can't be anonymous/unrevealed at the
- # moment.)
- return unless approved_by_user? && item.is_a?(Work)
-
- # Check whether anonymous/unrevealed is becoming true, when the work
- # currently has it set to false:
- newly_anonymous = (saved_change_to_anonymous?(to: true) && !item.anonymous?)
- newly_unrevealed = (saved_change_to_unrevealed?(to: true) && !item.unrevealed?)
-
- return unless newly_unrevealed || newly_anonymous
-
- # Don't notify if it's one of the work creators who is changing the work's
- # status.
- return if item.users.include?(User.current_user)
-
- item.users.each do |user|
- I18n.with_locale(user.preference.locale_for_mails) do
- UserMailer.anonymous_or_unrevealed_notification(
- user.id, item.id, collection.id,
- anonymous: newly_anonymous, unrevealed: newly_unrevealed
- ).deliver_after_commit
- end
- end
- end
-
- # reindex collection after creation, deletion, and approval_status update
- # (we only index approved items, which is why changes there trigger the reindex-index)
- after_commit :update_collection_index, if: :should_update_collection_index?
-
- def update_collection_index
- ids = [collection_id]
- ids.push(collection.parent_id) if collection.parent.present?
- IndexQueue.enqueue_ids(Collection, ids, :background)
- end
-
- # reindex collection after creation, deletion, and certain attribute updates
- def should_update_collection_index?
- return true if destroyed?
-
- pertinent_attributes = %w[collection_approval_status user_approval_status]
- (self.saved_changes.keys & pertinent_attributes).present?
- end
end
diff --git a/app/models/concerns/association_assignment.rb b/app/models/concerns/association_assignment.rb
new file mode 100644
index 00000000000..077c0a1325b
--- /dev/null
+++ b/app/models/concerns/association_assignment.rb
@@ -0,0 +1,30 @@
+module AssociationAssignment
+ extend ActiveSupport::Concern
+
+ # Given an association of through records, the name of the "source" field,
+ # and a list of values, marks the through records as destroyed or not based
+ # on whether their "source" value is included in the list of desired values,
+ # and builds new through records when necessary.
+ #
+ # The optional parameter klass is used to filter the through records -- when
+ # it's set, this function won't mark or unmark any through records whose
+ # source doesn't have that klass.
+ def assign_through_association(through_association, source, values, klass: nil)
+ missing = Set.new(values)
+
+ through_association.each do |through_record|
+ value = through_record.send(source)
+ next if klass && !value.is_a?(klass)
+
+ if missing.delete?(value)
+ through_record.unmark_for_destruction
+ else
+ through_record.mark_for_destruction
+ end
+ end
+
+ missing.each do |value|
+ through_association.build(source => value)
+ end
+ end
+end
diff --git a/app/models/concerns/collectible.rb b/app/models/concerns/collectible.rb
new file mode 100644
index 00000000000..67305cae27d
--- /dev/null
+++ b/app/models/concerns/collectible.rb
@@ -0,0 +1,162 @@
+module Collectible
+ extend ActiveSupport::Concern
+ include AssociationAssignment
+
+ included do
+ has_many :collection_items, as: :item, inverse_of: :item, autosave: true, dependent: :destroy
+
+ has_many :approved_collection_items, -> { approved_by_both },
+ class_name: "CollectionItem", as: :item, inverse_of: :item, dependent: :destroy
+ has_many :user_approved_collection_items, -> { approved_by_user },
+ class_name: "CollectionItem", as: :item, inverse_of: :item, dependent: :destroy
+
+ has_many :collections,
+ through: :collection_items,
+ after_add: :recalculate_anon_unrevealed,
+ after_remove: :recalculate_anon_unrevealed,
+ dependent: :destroy
+ has_many :approved_collections,
+ through: :approved_collection_items,
+ source: :collection,
+ dependent: :destroy
+ has_many :user_approved_collections,
+ through: :user_approved_collection_items,
+ source: :collection,
+ dependent: :destroy
+ has_many :rejected_collections,
+ -> { CollectionItem.rejected_by_user },
+ through: :collection_items,
+ source: :collection,
+ dependent: :destroy
+
+ # NOTE: this scope includes the items in the children of the specified collection
+ scope :in_collection, lambda { |collection|
+ distinct.joins(:approved_collection_items).merge(collection.all_items)
+ }
+
+ after_validation :set_anon_unrevealed, if: -> { collection_items.loaded? }
+ end
+
+ # The collection items that will remain after this item has been saved.
+ def collection_items_after_saving
+ collection_items
+ .reject(&:marked_for_destruction?)
+ .reject(&:destroyed?)
+ end
+
+ # The collections that this item will be approved in after the item has been
+ # saved:
+ def approved_collections_after_saving
+ if collection_items.target.empty?
+ approved_collections.to_a
+ else
+ collection_items_after_saving.select(&:approved?).map(&:collection)
+ end
+ end
+
+ # All collections that this item will be included in (including rejected and
+ # unreviewed collections) after the item has been saved:
+ def collections_after_saving
+ if collection_items.target.empty?
+ collections.to_a
+ else
+ collection_items_after_saving.map(&:collection)
+ end
+ end
+
+ # Set which collections this item should be in after saving:
+ def collections_after_saving=(collections)
+ assign_through_association(collection_items, :collection, collections)
+ end
+
+ # Add collections with a comma-separated list of names:
+ def collections_to_add=(names)
+ self.collections_after_saving += parse_collection_names(names)
+ end
+
+ # The collection names that are about to be added:
+ def collections_to_add
+ collection_items_after_saving
+ .select(&:new_record?)
+ .map(&:collection)
+ .map(&:name)
+ .join(",")
+ end
+
+ # Remove collections by ID:
+ def collections_to_remove=(ids)
+ collections = Collection.find(ids.reject(&:blank?).map(&:to_i))
+ self.collections_after_saving -= collections
+ end
+
+ # The collection IDs to be removed:
+ def collections_to_remove
+ collection_items.select(&:marked_for_destruction?).map(&:collection_id)
+ end
+
+ # Assign this item's collections by a comma-separated list of names:
+ def collection_names=(names)
+ self.collections_after_saving = parse_collection_names(names)
+ end
+
+ # Returns the collections that this item will have after saving as a
+ # comma-separated list of names:
+ def collection_names
+ collections_after_saving.map(&:name).join(",")
+ end
+
+ # Add the given collection and immediately save the resulting collection item:
+ def add_to_collection(collection)
+ collection_item = collection_items.find { |ci| ci.collection == collection }
+ collection_item ||= collection_items.build(collection: collection)
+ collection_item.unmark_for_destruction
+
+ # If we're a new record, we don't need to save the collection item
+ # immediately, since it'll get saved when we're saved. And we don't need to
+ # save the collection item if it's already persisted. But otherwise, we do
+ # want to save it:
+ new_record? || collection_item.persisted? || collection_item.save
+ end
+
+ #### UNREVEALED/ANONYMOUS
+
+ def recalculate_anon_unrevealed(_collection)
+ set_anon_unrevealed
+ end
+
+ # Set the anonymous/unrevealed status of the collectible based on its
+ # collection items.
+ def set_anon_unrevealed
+ return unless has_attribute?(:anonymous) && has_attribute?(:unrevealed)
+
+ if collection_items.target.empty?
+ if new_record?
+ self.anonymous = collections.target.any?(&:anonymous?)
+ self.unrevealed = collections.target.any?(&:unrevealed?)
+ else
+ relevant = user_approved_collection_items
+
+ self.anonymous = relevant.anonymous.exists?
+ self.unrevealed = relevant.unrevealed.exists?
+ end
+ else
+ relevant = collection_items_after_saving
+ .select(&:approved_by_user?)
+
+ self.anonymous = relevant.any? { |ci| ci.anonymous? || (ci.new_record? && ci.collection&.anonymous?) }
+ self.unrevealed = relevant.any? { |ci| ci.unrevealed? || (ci.new_record? && ci.collection&.unrevealed?) }
+ end
+ end
+
+ private
+
+ # Given a comma-separated list of names, return a list of collections.
+ #
+ # For any names that can't be found, returns an unsaved collection with the
+ # desired name, so that we can include that name in error messages.
+ def parse_collection_names(names)
+ names.split(",").map(&:strip).reject(&:blank?).map do |name|
+ Collection.find_or_initialize_by(name: name)
+ end
+ end
+end
diff --git a/app/models/concerns/taggable.rb b/app/models/concerns/taggable.rb
index 15386467cdb..5f0f6e05715 100644
--- a/app/models/concerns/taggable.rb
+++ b/app/models/concerns/taggable.rb
@@ -1,5 +1,6 @@
module Taggable
extend ActiveSupport::Concern
+ include AssociationAssignment
included do
has_many :taggings, as: :taggable, inverse_of: :taggable, dependent: :destroy, autosave: true
@@ -30,7 +31,7 @@ module Taggable
define_method("#{underscore}_string=") do |tag_string|
tags = parse_tags_of_type(tag_string, klass)
- assign_tags_of_type(tags, klass)
+ assign_through_association(taggings, :tagger, tags, klass: klass)
end
alias_method "#{underscore}_strings=", "#{underscore}_string="
@@ -47,7 +48,7 @@ def tag_string
def tag_string=(tag_string)
tags = parse_tags(tag_string)
- assign_tags_of_type(tags, Tag)
+ assign_through_association(taggings, :tagger, tags, klass: Tag)
end
alias tag_strings= tag_string=
@@ -132,31 +133,6 @@ def parse_tags(tag_string)
end.uniq
end
- # Mark taggings for destruction, and create new taggings, so that we will end
- # up with the specified set of tags after saving.
- #
- # Only deletes/checks tags of the given class.
- def assign_tags_of_type(tags, klass)
- missing = Set.new(tags)
-
- taggings.each do |tagging|
- tag = tagging.tagger
-
- next unless tag.is_a?(klass)
-
- if missing.include?(tag)
- missing.delete(tag)
- tagging.reload if tagging.marked_for_destruction?
- else
- tagging.mark_for_destruction
- end
- end
-
- missing.each do |tag|
- taggings.build(tagger: tag)
- end
- end
-
def destroy_tagging(tag)
taggings.find_by(tagger: tag)&.destroy
end
diff --git a/app/models/work.rb b/app/models/work.rb
index 38549242d58..89e98194426 100755
--- a/app/models/work.rb
+++ b/app/models/work.rb
@@ -238,7 +238,6 @@ def new_recipients_have_not_blocked_gift_giver
after_save :save_chapters, :save_new_gifts
- before_create :set_anon_unrevealed
after_create :notify_after_creation
after_update :adjust_series_restriction, :notify_after_update
@@ -579,17 +578,8 @@ def visible?(user = User.current_user)
end
end
- def unrevealed?(user=User.current_user)
- # eventually here is where we check if it's in a challenge that hasn't been made public yet
- #!self.collection_items.unrevealed.empty?
- in_unrevealed_collection?
- end
-
- def anonymous?(user = User.current_user)
- # here we check if the story is in a currently-anonymous challenge
- #!self.collection_items.anonymous.empty?
- in_anon_collection?
- end
+ alias_attribute :unrevealed, :in_unrevealed_collection
+ alias_attribute :anonymous, :in_anon_collection
before_update :bust_anon_caching
def bust_anon_caching
diff --git a/app/views/works/_meta.html.erb b/app/views/works/_meta.html.erb
index 778d343ab73..c77b9d03233 100755
--- a/app/views/works/_meta.html.erb
+++ b/app/views/works/_meta.html.erb
@@ -49,14 +49,7 @@
<% end %>
- <% unless @work.approved_collections.empty? %>
-