docbrown/app/models/page.rb
Joshua Wehner a20cf3e02b
Unique cross-model-slug check is likely misconfigured (#19263)
* 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>
2023-04-07 17:04:45 +02:00

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