otwarchive-symphonyarchive/app/models/chapter.rb

210 lines
6.1 KiB
Ruby
Raw Permalink Normal View History

2026-03-11 22:22:11 +00:00
# encoding=utf-8
class Chapter < ApplicationRecord
include HtmlCleaner
include WorkChapterCountCaching
include CreationNotifier
include Creatable
belongs_to :work, inverse_of: :chapters
# acts_as_list scope: 'work_id = #{work_id}'
acts_as_commentable
validates_length_of :title, allow_blank: true, maximum: ArchiveConfig.TITLE_MAX,
too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX)
validates_length_of :summary, allow_blank: true, maximum: ArchiveConfig.SUMMARY_MAX,
too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.SUMMARY_MAX)
validates_length_of :notes, allow_blank: true, maximum: ArchiveConfig.NOTES_MAX,
too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.NOTES_MAX)
validates_length_of :endnotes, allow_blank: true, maximum: ArchiveConfig.NOTES_MAX,
too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.NOTES_MAX)
validates_presence_of :content
validates_length_of :content, minimum: ArchiveConfig.CONTENT_MIN,
too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.CONTENT_MIN)
validates_length_of :content, maximum: ArchiveConfig.CONTENT_MAX,
too_long: ts("cannot be more than %{max} characters long.", max: ArchiveConfig.CONTENT_MAX)
attr_accessor :wip_length_placeholder
before_validation :inherit_creatorships
def inherit_creatorships
if work && creatorships.empty? && current_user_pseuds.blank?
work.pseuds_after_saving.each do |pseud|
creatorships.build(pseud: pseud)
end
end
end
before_save :strip_title
before_save :set_word_count
before_save :validate_published_at
after_create :notify_after_creation
after_update :notify_after_update
scope :in_order, -> { order(:position) }
scope :posted, -> { where(posted: true) }
after_save :fix_positions
def fix_positions
if work&.persisted?
positions_changed = false
self.position ||= 1
chapters = work.chapters.order(:position)
if chapters && chapters.length > 1
chapters = chapters - [self]
chapters.insert(self.position-1, self)
chapters.compact.each_with_index do |chapter, i|
if chapter.position != i+1
Chapter.where(["id = ?", chapter.id]).update_all(["position = ?", i + 1])
positions_changed = true
end
end
end
# We're caching the chapter positions in the comment blurbs and the last
# chapter link in the work blurbs so we need to expire the blurbs and the
# work indexes.
if positions_changed
work.comments.each{ |c| c.touch }
work.expire_caches
end
end
end
after_save :invalidate_chapter_count,
if: Proc.new { |chapter| chapter.saved_change_to_posted? }
before_destroy :fix_positions_before_destroy, :invalidate_chapter_count
def fix_positions_before_destroy
if work&.persisted? && position
chapters = work.chapters.where(["position > ?", position])
chapters.each { |c| c.update_attribute(:position, c.position - 1) }
end
end
after_commit :update_series_index
def update_series_index
return unless work&.series.present? && should_reindex_series?
work.serial_works.each(&:update_series_index)
end
def should_reindex_series?
pertinent_attributes = %w[id posted]
destroyed? || (saved_changes.keys & pertinent_attributes).present?
end
def invalidate_chapter_count
if work
invalidate_work_chapter_count(work)
end
end
def moderated_commenting_enabled?
work && work.moderated_commenting_enabled?
end
# strip leading spaces from title
def strip_title
unless self.title.blank?
self.title = self.title.gsub(/^\s*/, '')
end
end
def chapter_header
"#{ts("Chapter")} #{position}"
end
def chapter_title
self.title.blank? ? self.chapter_header : self.title
end
# Header plus title, used in subscriptions
def full_chapter_title
str = chapter_header
if title.present?
str += ": #{title}"
end
str
end
def display_title
self.position.to_s + '. ' + self.chapter_title
end
def abbreviated_display_title
self.display_title.length > 50 ? (self.display_title[0..50] + "...") : self.display_title
end
# make em-dashes into html code
# def clean_emdashes
# self.content.gsub!(/\xE2\x80\"/, '&#8212;')
# end
# check if this chapter is the only chapter of its work
def is_only_chapter?
self.work.chapters.count == 1
end
def only_non_draft_chapter?
self.posted? && self.work.chapters.posted.count == 1
end
# Virtual attribute for work wip_length
# Chapter needed its own version for sense-checking purposes
def wip_length
if self.new_record? && self.work.expected_number_of_chapters == self.work.number_of_chapters
self.work.expected_number_of_chapters += 1
elsif self.work.expected_number_of_chapters && self.work.expected_number_of_chapters < self.work.number_of_chapters
"?"
else
self.work.wip_length
end
end
# Can't directly access work from a chapter virtual attribute
# Using a placeholder variable for edits, where the value isn't saved immediately
def wip_length=(number)
self.wip_length_placeholder = number
end
# Checks the chapter published_at date isn't in the future
def validate_published_at
if !self.published_at
self.published_at = Date.current
elsif self.published_at > Date.current
errors.add(:base, ts("Publication date can't be in the future."))
throw :abort
end
end
# Set the value of word_count to reflect the length of the text in the chapter content
def set_word_count
if self.new_record? || self.content_changed? || self.word_count.nil?
counter = WordCounter.new(self.content)
self.word_count = counter.count
else
self.word_count
end
end
# Return the name to link comments to for this object
def commentable_name
self.work.title
end
def expire_comments_count
super
work&.expire_comments_count
end
def expire_byline_cache
[true, false].each do |only_path|
Rails.cache.delete("#{cache_key}/byline-nonanon/#{only_path}")
end
end
end