diff --git a/app/models/comfy/cms/page.rb b/app/models/comfy/cms/page.rb new file mode 100644 index 000000000..50a52c8de --- /dev/null +++ b/app/models/comfy/cms/page.rb @@ -0,0 +1,175 @@ +# 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 + + # page specific OG image feature + + has_one_attached :og_image + + + # -- 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 \ No newline at end of file diff --git a/app/views/comfy/admin/cms/partials/_page_form_inner.haml b/app/views/comfy/admin/cms/partials/_page_form_inner.haml index 0cb3bb7ba..fd8e8e8fc 100644 --- a/app/views/comfy/admin/cms/partials/_page_form_inner.haml +++ b/app/views/comfy/admin/cms/partials/_page_form_inner.haml @@ -1,4 +1,11 @@ .form-group = form.check_box :is_restricted .form-group - = form.text_area :preview_content, data: {'cms-cm-mode' => 'text/html'} \ No newline at end of file + = form.text_area :preview_content, data: {'cms-cm-mode' => 'text/html'} +.form-group + = form.file_field :og_image, class: 'form-control', data: {'cms-cm-mode' => 'text/html'}, label: false +- if form.object.og_image.present? + .form-group.text-right + = image_tag(url_for(form.object.og_image), class: 'img-thumbnail', width: '100', height: 'auto') + + diff --git a/app/views/shared/_meta_tags.html.erb b/app/views/shared/_meta_tags.html.erb index 089bfe333..8319322d3 100644 --- a/app/views/shared/_meta_tags.html.erb +++ b/app/views/shared/_meta_tags.html.erb @@ -1,4 +1,5 @@ -<% og_image = og_image_url(Subdomain.current) %> +<% og_image = @cms_page.present? && @cms_page.og_image.attached? ? rails_blob_url(@cms_page.og_image) : og_image_url(Subdomain.current) %> + <% set_meta_tags site: forum_html_title(Subdomain.current), title: html_title(Subdomain.current), description: site_description(Subdomain.current),