comfy/comfortable-mexican-sofa

View on GitHub
app/models/comfy/cms/page.rb

Summary

Maintainability
A
25 mins
Test Coverage
# frozen_string_literal: true

class Comfy::Cms::Page < ActiveRecord::Base

  self.table_name = "comfy_cms_pages"

  include Comfy::Cms::WithFragments
  include Comfy::Cms::WithCategories

  cms_acts_as_tree counter_cache: :children_count, order: :position
  cms_has_revisions_for :fragments_attributes

  attr_accessor :content

  # -- Relationships -----------------------------------------------------------
  belongs_to :site
  belongs_to :target_page,
    class_name: "Comfy::Cms::Page",
    optional:   true

  has_many :translations,
    dependent: :destroy

  # -- Callbacks ---------------------------------------------------------------
  before_validation :assigns_label,
                    :assign_parent,
                    :escape_slug,
                    :assign_full_path
  before_create     :assign_position
  after_save        :sync_child_full_paths!
  after_find        :unescape_slug_and_path

  # -- Validations -------------------------------------------------------------
  validates :label,
    presence:   true
  validates :slug,
    presence:   true,
    uniqueness: { scope: :parent_id },
    unless:     ->(p) {
      p.site && (p.site.pages.count.zero? || p.site.pages.root == self)
    }
  validate :validate_target_page
  validate :validate_format_of_unescaped_slug

  # -- Scopes ------------------------------------------------------------------
  scope :published, -> { where(is_published: true) }

  # -- Class Methods -----------------------------------------------------------
  # Tree-like structure for pages
  def self.options_for_select(site:, current_page: nil, exclude_self: false)
    options = []

    options_for_page = ->(page, depth = 0) do
      return if page.nil?
      return if exclude_self && page == current_page

      options << ["#{'. . ' * depth}#{page.label}", page.id]

      page.children.order(:position).each do |child_page|
        options_for_page.call(child_page, depth + 1)
      end
    end

    options_for_page.call(site.pages.root)

    options
  end

  # -- Instance Methods --------------------------------------------------------
  # For previewing purposes sometimes we need to have full_path set. This
  # full path take care of the pages and its childs but not of the site path
  def full_path
    read_attribute(:full_path) || assign_full_path
  end

  # Somewhat unique method of identifying a page that is not a full_path
  def identifier
    parent_id.blank? ? "index" : full_path[1..-1].parameterize
  end

  # Full url for a page
  def url(relative: false)
    [site.url(relative: relative), full_path].compact.join
  end

  # This method will mutate page object by transfering attributes from translation
  # for a given locale.
  def translate!
    # If site locale same as page's or there's no translastions, we do nothing
    if site.locale == I18n.locale.to_s || translations.blank?
      return
    end

    translation = translations.published.find_by!(locale: I18n.locale)
    self.layout        = translation.layout
    self.label         = translation.label
    self.content_cache = translation.content_cache

    # We can't just assign fragments as it's a relation and will write to DB
    # This has odd side-effect of preserving page's fragments and just replacing
    # them from the translation. Not an issue if all fragments match.
    self.fragments_attributes = translation.fragments_attributes
    readonly!

    self
  end

protected

  def assigns_label
    self.label = label.blank? ? slug.try(:titleize) : label
  end

  def assign_parent
    return unless site
    self.parent ||= site.pages.root unless self == site.pages.root || site.pages.count.zero?
  end

  def assign_full_path
    self.full_path =
      if self.parent
        [CGI.escape(self.parent.full_path).gsub("%2F", "/"), slug].join("/").squeeze("/")
      else
        "/"
      end
  end

  def assign_position
    return unless self.parent
    return if position.to_i.positive?
    max = self.parent.children.maximum(:position)
    self.position = max ? max + 1 : 0
  end

  def validate_target_page
    return unless target_page
    p = self
    while p.target_page
      if (p = p.target_page) == self
        return errors.add(:target_page_id, "Invalid Redirect")
      end
    end
  end

  def validate_format_of_unescaped_slug
    return unless slug.present?
    unescaped_slug = CGI.unescape(slug)
    errors.add(:slug, :invalid) unless unescaped_slug =~ %r{^\p{Alnum}[\.\p{Alnum}\p{Mark}_-]*$}i
  end

  # Forcing re-saves for child pages so they can update full_paths
  def sync_child_full_paths!
    return unless full_path_previously_changed?
    children.each do |p|
      p.update_attribute(:full_path, p.send(:assign_full_path))
    end
  end

  # Escape slug unless it's nonexistent (root)
  def escape_slug
    self.slug = CGI.escape(slug) unless slug.nil?
  end

  # Unescape the slug and full path back into their original forms unless they're nonexistent
  def unescape_slug_and_path
    self.slug       = CGI.unescape(slug)      unless slug.nil?
    self.full_path  = CGI.unescape(full_path) unless full_path.nil?
  end

end