app/models/comfy/cms/page.rb
# 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