311 lines
9.4 KiB
Ruby
311 lines
9.4 KiB
Ruby
|
|
class Series < ApplicationRecord
|
||
|
|
include Bookmarkable
|
||
|
|
include Searchable
|
||
|
|
include Creatable
|
||
|
|
|
||
|
|
has_many :serial_works, dependent: :destroy
|
||
|
|
has_many :works, through: :serial_works
|
||
|
|
has_many :work_tags, -> { distinct }, through: :works, source: :tags
|
||
|
|
has_many :work_pseuds, -> { distinct }, through: :works, source: :pseuds
|
||
|
|
|
||
|
|
has_many :taggings, as: :taggable, dependent: :destroy
|
||
|
|
has_many :tags, through: :taggings, source: :tagger, source_type: 'Tag'
|
||
|
|
|
||
|
|
has_many :subscriptions, as: :subscribable, dependent: :destroy
|
||
|
|
|
||
|
|
validates_presence_of :title
|
||
|
|
validates_length_of :title,
|
||
|
|
minimum: ArchiveConfig.TITLE_MIN,
|
||
|
|
too_short: ts("must be at least %{min} letters long.", min: ArchiveConfig.TITLE_MIN)
|
||
|
|
|
||
|
|
validates_length_of :title,
|
||
|
|
maximum: ArchiveConfig.TITLE_MAX,
|
||
|
|
too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.TITLE_MAX)
|
||
|
|
|
||
|
|
# return title.html_safe to overcome escaping done by sanitiser
|
||
|
|
def title
|
||
|
|
read_attribute(:title).try(:html_safe)
|
||
|
|
end
|
||
|
|
|
||
|
|
validates_length_of :summary,
|
||
|
|
allow_blank: true,
|
||
|
|
maximum: ArchiveConfig.SUMMARY_MAX,
|
||
|
|
too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.SUMMARY_MAX)
|
||
|
|
|
||
|
|
validates_length_of :series_notes,
|
||
|
|
allow_blank: true,
|
||
|
|
maximum: ArchiveConfig.NOTES_MAX,
|
||
|
|
too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.NOTES_MAX)
|
||
|
|
|
||
|
|
after_save :adjust_restricted
|
||
|
|
after_update_commit :expire_caches, :update_work_index
|
||
|
|
|
||
|
|
scope :visible_to_registered_user, -> { where(hidden_by_admin: false).order('series.updated_at DESC') }
|
||
|
|
scope :visible_to_all, -> { where(hidden_by_admin: false, restricted: false).order('series.updated_at DESC') }
|
||
|
|
|
||
|
|
scope :exclude_anonymous, -> {
|
||
|
|
joins("INNER JOIN `serial_works` ON (`series`.`id` = `serial_works`.`series_id`)
|
||
|
|
INNER JOIN `works` ON (`works`.`id` = `serial_works`.`work_id`)").
|
||
|
|
group("series.id").
|
||
|
|
having("MAX(works.in_anon_collection) = 0 AND MAX(works.in_unrevealed_collection) = 0")
|
||
|
|
}
|
||
|
|
|
||
|
|
scope :for_pseud, lambda { |pseud|
|
||
|
|
joins(:approved_creatorships).where(creatorships: { pseud: pseud })
|
||
|
|
}
|
||
|
|
|
||
|
|
scope :for_user, lambda { |user|
|
||
|
|
joins(approved_creatorships: :pseud).where(pseuds: { user: user })
|
||
|
|
}
|
||
|
|
|
||
|
|
scope :for_blurb, -> { includes(:work_tags, :pseuds) }
|
||
|
|
|
||
|
|
def posted_works
|
||
|
|
self.works.posted
|
||
|
|
end
|
||
|
|
|
||
|
|
def works_in_order
|
||
|
|
works.order("serial_works.position")
|
||
|
|
end
|
||
|
|
|
||
|
|
# Get the filters for the works in this series
|
||
|
|
def filters
|
||
|
|
Tag.joins("JOIN filter_taggings ON tags.id = filter_taggings.filter_id
|
||
|
|
JOIN works ON works.id = filter_taggings.filterable_id
|
||
|
|
JOIN serial_works ON serial_works.work_id = works.id").
|
||
|
|
where("serial_works.series_id = #{self.id} AND
|
||
|
|
works.posted = 1 AND
|
||
|
|
filter_taggings.filterable_type = 'Work'").
|
||
|
|
group("tags.id")
|
||
|
|
end
|
||
|
|
|
||
|
|
def direct_filters
|
||
|
|
filters.where("filter_taggings.inherited = 0")
|
||
|
|
end
|
||
|
|
|
||
|
|
# visibility aped from the work model
|
||
|
|
def visible?(user = User.current_user)
|
||
|
|
return true if user.is_a?(Admin)
|
||
|
|
|
||
|
|
if posted && !hidden_by_admin
|
||
|
|
user.is_a?(User) || !restricted
|
||
|
|
else
|
||
|
|
user_is_owner_or_invited?(user)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# Override the default definition to check whether the user was invited to
|
||
|
|
# any works in the series.
|
||
|
|
def user_is_owner_or_invited?(user)
|
||
|
|
return false unless user.is_a?(User)
|
||
|
|
return true if super
|
||
|
|
|
||
|
|
works.joins(:creatorships).merge(user.creatorships).exists? ||
|
||
|
|
works.joins(chapters: :creatorships).merge(user.creatorships).exists?
|
||
|
|
end
|
||
|
|
|
||
|
|
def visible_work_count
|
||
|
|
if User.current_user.nil?
|
||
|
|
self.works.posted.unrestricted.count
|
||
|
|
else
|
||
|
|
self.works.posted.count
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def visible_word_count
|
||
|
|
if User.current_user.nil?
|
||
|
|
# visible_works_wordcount = self.works.posted.unrestricted.sum(:word_count)
|
||
|
|
visible_works_wordcount = self.works.posted.unrestricted.pluck(:word_count).compact.sum
|
||
|
|
else
|
||
|
|
# visible_works_wordcount = self.works.posted.sum(:word_count)
|
||
|
|
visible_works_wordcount = self.works.posted.pluck(:word_count).compact.sum
|
||
|
|
end
|
||
|
|
visible_works_wordcount
|
||
|
|
end
|
||
|
|
|
||
|
|
def anonymous?
|
||
|
|
!self.works.select { |work| work.anonymous? }.empty?
|
||
|
|
end
|
||
|
|
|
||
|
|
def unrevealed?
|
||
|
|
!self.works.select { |work| work.unrevealed? }.empty?
|
||
|
|
end
|
||
|
|
|
||
|
|
# if the series includes an unrestricted work, restricted should be false
|
||
|
|
# if the series includes no unrestricted works, restricted should be true
|
||
|
|
def adjust_restricted
|
||
|
|
unless self.restricted? == !(self.works.where(restricted: false).count > 0)
|
||
|
|
self.restricted = !(self.works.where(restricted: false).count > 0)
|
||
|
|
self.save!(validate: false)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# Visibility has changed, which means we need to reindex
|
||
|
|
# the series' bookmarker pseuds, to update their bookmark counts.
|
||
|
|
def should_reindex_pseuds?
|
||
|
|
pertinent_attributes = %w[id restricted hidden_by_admin]
|
||
|
|
destroyed? || (saved_changes.keys & pertinent_attributes).present?
|
||
|
|
end
|
||
|
|
|
||
|
|
def expire_caches
|
||
|
|
self.works.touch_all
|
||
|
|
end
|
||
|
|
|
||
|
|
def expire_byline_cache
|
||
|
|
[true, false].each do |only_path|
|
||
|
|
Rails.cache.delete("#{cache_key}/byline-nonanon/#{only_path}")
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# Change the positions of the serial works in the series
|
||
|
|
def reorder_list(positions)
|
||
|
|
SortableList.new(self.serial_works.in_order).reorder_list(positions)
|
||
|
|
end
|
||
|
|
|
||
|
|
def position_of(work)
|
||
|
|
serial_works.where(work_id: work.id).pluck(:position).first
|
||
|
|
end
|
||
|
|
|
||
|
|
# return list of pseuds on this series
|
||
|
|
def allpseuds
|
||
|
|
works.collect(&:pseuds).flatten.compact.uniq.sort
|
||
|
|
end
|
||
|
|
|
||
|
|
# Remove a user (and all their pseuds) as an author of this series.
|
||
|
|
#
|
||
|
|
# We call Work#remove_author before destroying the series creatorships to
|
||
|
|
# make sure that we can handle tricky chapter creatorship cases.
|
||
|
|
def remove_author(author_to_remove)
|
||
|
|
pseuds_with_author_removed = pseuds.where.not(user_id: author_to_remove.id)
|
||
|
|
raise Exception.new("Sorry, we can't remove all authors of a series.") if pseuds_with_author_removed.empty?
|
||
|
|
transaction do
|
||
|
|
authored_works_in_series = self.works.merge(author_to_remove.works)
|
||
|
|
|
||
|
|
authored_works_in_series.each do |work|
|
||
|
|
work.remove_author(author_to_remove)
|
||
|
|
end
|
||
|
|
|
||
|
|
creatorships.where(pseud: author_to_remove.pseuds).destroy_all
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
# returns list of fandoms on this series
|
||
|
|
def allfandoms
|
||
|
|
works.collect(&:fandoms).flatten.compact.uniq.sort
|
||
|
|
end
|
||
|
|
|
||
|
|
def author_tags
|
||
|
|
self.work_tags.select{|t| t.type == "Relationship"}.sort + self.work_tags.select{|t| t.type == "Character"}.sort + self.work_tags.select{|t| t.type == "Freeform"}.sort
|
||
|
|
end
|
||
|
|
|
||
|
|
def tag_groups
|
||
|
|
self.work_tags.group_by { |t| t.type.to_s }
|
||
|
|
end
|
||
|
|
|
||
|
|
# Grabs the earliest published_at date of the visible works in the series
|
||
|
|
def published_at
|
||
|
|
if self.works.visible.posted.blank?
|
||
|
|
self.created_at
|
||
|
|
else
|
||
|
|
Work.in_series(self).visible.collect(&:published_at).compact.uniq.sort.first
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
def revised_at
|
||
|
|
if self.works.visible.posted.blank?
|
||
|
|
self.updated_at
|
||
|
|
else
|
||
|
|
Work.in_series(self).visible.collect(&:revised_at).compact.uniq.sort.last
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
######################
|
||
|
|
# SEARCH
|
||
|
|
######################
|
||
|
|
|
||
|
|
def bookmarkable_json
|
||
|
|
as_json(
|
||
|
|
root: false,
|
||
|
|
only: [
|
||
|
|
:title, :summary, :hidden_by_admin, :restricted, :created_at,
|
||
|
|
:complete
|
||
|
|
],
|
||
|
|
methods: [
|
||
|
|
:revised_at, :posted, :tag, :filter_ids, :rating_ids,
|
||
|
|
:archive_warning_ids, :category_ids, :fandom_ids, :character_ids,
|
||
|
|
:relationship_ids, :freeform_ids, :creators,
|
||
|
|
:word_count, :work_types]
|
||
|
|
).merge(
|
||
|
|
language_id: language&.short,
|
||
|
|
anonymous: anonymous?,
|
||
|
|
unrevealed: unrevealed?,
|
||
|
|
pseud_ids: anonymous? || unrevealed? ? nil : pseud_ids,
|
||
|
|
user_ids: anonymous? || unrevealed? ? nil : user_ids,
|
||
|
|
bookmarkable_type: 'Series',
|
||
|
|
bookmarkable_join: { name: "bookmarkable" }
|
||
|
|
)
|
||
|
|
end
|
||
|
|
|
||
|
|
def update_work_index
|
||
|
|
self.works.each(&:enqueue_to_index) if saved_change_to_title?
|
||
|
|
end
|
||
|
|
|
||
|
|
def word_count
|
||
|
|
self.works.posted.pluck(:word_count).compact.sum
|
||
|
|
end
|
||
|
|
|
||
|
|
# FIXME: should series have their own language?
|
||
|
|
def language
|
||
|
|
works.first.language if works.present?
|
||
|
|
end
|
||
|
|
|
||
|
|
def posted
|
||
|
|
!posted_works.empty?
|
||
|
|
end
|
||
|
|
alias_method :posted?, :posted
|
||
|
|
|
||
|
|
# Simple name to make it easier for people to use in full-text search
|
||
|
|
def tag
|
||
|
|
(work_tags + filters).uniq.map{ |t| t.name }
|
||
|
|
end
|
||
|
|
|
||
|
|
# Index all the filters for pulling works
|
||
|
|
def filter_ids
|
||
|
|
(work_tags.pluck(:id) + filters.pluck(:id)).uniq
|
||
|
|
end
|
||
|
|
|
||
|
|
# Index only direct filters (non meta-tags) for facets
|
||
|
|
def filters_for_facets
|
||
|
|
@filters_for_facets ||= direct_filters
|
||
|
|
end
|
||
|
|
def rating_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'Rating' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
def archive_warning_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'ArchiveWarning' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
def category_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'Category' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
def fandom_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'Fandom' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
def character_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'Character' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
def relationship_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'Relationship' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
def freeform_ids
|
||
|
|
filters_for_facets.select{ |t| t.type.to_s == 'Freeform' }.map{ |t| t.id }
|
||
|
|
end
|
||
|
|
|
||
|
|
def creators
|
||
|
|
anonymous? ? ['Anonymous'] : pseuds.map(&:byline)
|
||
|
|
end
|
||
|
|
|
||
|
|
def work_types
|
||
|
|
works.map(&:work_types).flatten.uniq
|
||
|
|
end
|
||
|
|
end
|