* Page unique_cross_model_slug seems misconfigured * Update validator test * More verbose invalid attribute message * Try dynamic model/attribute registration * Rubocop * Use the cross-model-slug existence check * Register with the dynamic cross-model-slug checker * Cleanup * Add missing keys to fr * Update format regex * Adjust validation: always presence, unique if changed * Tests can create reserved-word pages when they need to * Slugs can be mixed case? * Case-sensitive is better, actually * Refactor to use CrossModel check * Refactor, rename for clarity * Refactor, avoid mocking oneself * Refactor, injectable everything * Add reservedword check to extracted exists? checker * Move to concerns * Without dynamic registration * extend when needed * Cleanup comments, remove registration references --------- Co-authored-by: Goran <gorang.pub@gmail.com>
100 lines
2.7 KiB
Ruby
100 lines
2.7 KiB
Ruby
class Page < ApplicationRecord
|
|
extend UniqueAcrossModels
|
|
TEMPLATE_OPTIONS = %w[contained full_within_layout nav_bar_included json].freeze
|
|
|
|
TERMS_SLUG = "terms".freeze
|
|
CODE_OF_CONDUCT_SLUG = "code-of-conduct".freeze
|
|
PRIVACY_SLUG = "privacy".freeze
|
|
|
|
validates :title, presence: true
|
|
validates :description, presence: true
|
|
validates :template, inclusion: { in: TEMPLATE_OPTIONS }
|
|
validate :body_present
|
|
|
|
unique_across_models :slug
|
|
|
|
before_validation :set_default_template
|
|
before_save :evaluate_markdown
|
|
|
|
after_commit :ensure_uniqueness_of_landinge_page
|
|
after_commit :bust_cache
|
|
|
|
mount_uploader :social_image, ProfileImageUploader
|
|
resourcify
|
|
|
|
# @param slug [String]
|
|
#
|
|
# @return An HTML safe String.
|
|
#
|
|
# @yield Yield to the calling context if there's no Page match for slug.
|
|
#
|
|
# @raise LocalJumpError when no matching slug nor block given.
|
|
#
|
|
# @note Yes, treating this value as HTML safe is risky. But we already opened that vector by
|
|
# letting the administrator of pages write HTML.
|
|
#
|
|
# @todo Do we want to only allow certain slugs?
|
|
#
|
|
# rubocop:disable Rails/OutputSafety
|
|
def self.render_safe_html_for(slug:)
|
|
page = find_by(slug: slug)
|
|
if page
|
|
page.processed_html.html_safe
|
|
else
|
|
yield
|
|
end
|
|
end
|
|
# rubocop:enable Rails/OutputSafety
|
|
|
|
def self.landing_page
|
|
find_by(landing_page: true)
|
|
end
|
|
|
|
def path
|
|
is_top_level_path ? "/#{slug}" : "/page/#{slug}"
|
|
end
|
|
|
|
def feature_flag_name
|
|
"page_#{slug}"
|
|
end
|
|
|
|
def as_json(...)
|
|
super(...).slice(*%w[id title slug description is_top_level_path landing_page
|
|
body_html body_json body_markdown processed_html
|
|
social_image template ])
|
|
end
|
|
|
|
private
|
|
|
|
def evaluate_markdown
|
|
if body_markdown.present?
|
|
parsed_markdown = MarkdownProcessor::Parser.new(body_markdown)
|
|
self.processed_html = parsed_markdown.finalize
|
|
else
|
|
self.processed_html = body_html
|
|
end
|
|
end
|
|
|
|
def set_default_template
|
|
self.template = "contained" if template.blank?
|
|
end
|
|
|
|
def body_present
|
|
return unless body_markdown.blank? && body_html.blank? && body_json.blank?
|
|
|
|
errors.add(:body_markdown, I18n.t("models.page.body_must_exist"))
|
|
end
|
|
|
|
# As there can only be one global landing page, we want to ensure that
|
|
# data integrity is preserved by setting `landing_page` to `false` for all
|
|
# other pages if the current one was transformed into a landing page
|
|
def ensure_uniqueness_of_landinge_page
|
|
return unless previous_changes["landing_page"] == [false, true]
|
|
|
|
Page.where.not(id: id).update_all(landing_page: false)
|
|
end
|
|
|
|
def bust_cache
|
|
Pages::BustCacheWorker.perform_async(slug)
|
|
end
|
|
end
|