# 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\"/, '—') # 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