require_dependency 'landable/theme' require_dependency 'landable/page_revision' require_dependency 'landable/category' require_dependency 'landable/has_assets' require_dependency 'landable/author' module Landable class Page < ActiveRecord::Base include ActionView::Helpers::TagHelper include Landable::HasAssets include Landable::HasTemplates include Landable::Engine.routes.url_helpers include Landable::TableName include Landable::Librarian validates_presence_of :path, :status_code validates_presence_of :redirect_url, if: -> page { page.redirect? } validates_inclusion_of :status_code, in: [200, 301, 302, 410] validates_with PathValidator, fields: [:path] validates_uniqueness_of :path validates :path, presence: true validate :page_name_byte_size validate :forbid_changing_path, on: :update validate :body_strip_search validates :redirect_url, url: true, allow_blank: true validate :hero_asset_existence belongs_to :theme, class_name: 'Landable::Theme', inverse_of: :pages, counter_cache: true belongs_to :published_revision, class_name: 'Landable::PageRevision' belongs_to :category, class_name: 'Landable::Category' belongs_to :updated_by_author, class_name: 'Landable::Author' belongs_to :hero_asset, class_name: 'Landable::Asset' has_many :revisions, class_name: 'Landable::PageRevision' has_many :screenshots, class_name: 'Landable::Screenshot', as: :screenshotable has_many :audits, class_name: 'Landable::Audit', as: :auditable delegate :republish!, to: :published_revision scope :imported, -> { where('imported_at IS NOT NULL') } scope :sitemappable, lambda { where("COALESCE(meta_tags -> 'robots' NOT LIKE '%noindex%', TRUE)") .where('published_revision_id is NOT NULL') .where(status_code: 200) } scope :published, -> { where('published_revision_id is NOT NULL') } before_validation :downcase_path! before_save lambda { |page| page.lock_version ||= 0 page.is_publishable = true unless page.published_revision_id_changed? } class << self def missing new(status_code: 410) end def by_path(path) where(path: path).first || missing end def by_path!(path) where(path: path).first! end def with_fuzzy_path(path) select("*, similarity(path, #{Page.sanitize path}) _sml") .where('path LIKE ?', "%#{path}%") .order('_sml DESC, path ASC') end def example(attrs) defaults = { title: 'Example page', body: '
Example page contents would live here
' } new defaults.merge(attrs) end def generate_sitemap(options = {}) pages = Landable::Page.sitemappable xml = Builder::XmlMarkup.new(indent: 2) xml.instruct! :xml, encoding: 'UTF-8' xml.urlset(xmlns: 'http://www.sitemaps.org/schemas/sitemap/0.9') do |markup| pages.each do |page| next if options[:exclude_categories].to_a.include? page.category.try(:name) markup.url do |p| p.loc "#{options[:protocol]}://#{options[:host]}#{page.path}" p.lastmod page.updated_at.to_time.iso8601 p.changefreq 'weekly' p.priority '1' end end if options[:sitemap_additional_paths].present? options[:sitemap_additional_paths].each do |page| markup.url do |p| p.loc "#{options[:protocol]}://#{options[:host]}#{page}" p.changefreq 'weekly' p.priority '1' end end end end end end def downcase_path! path.try :downcase! end def path_extension path.match(/\.(\w{2,})$/).try(:[], 1) if path end def content_type case path_extension when nil, 'htm', 'html' 'text/html' when 'json' 'application/json' when 'xml' 'application/xml' else 'text/plain' end end def deactivate update_attribute(:status_code, 410) publish!(author_id: updated_by_author.id, notes: 'This page has been trashed') super end def html? content_type == 'text/html' end def directory_after(prefix) remainder = path.gsub(%r{^#{prefix}\/?}, '') segments = remainder.split('/', 2) if segments.length == 1 nil else segments.first end end def redirect? status_code == 301 || status_code == 302 end def path=(name) # if not present, add a leading slash for a non-empty path name = name.gsub(%r{^\/?(.*)}, '/\1') if name && !name.empty? self[:path] = name end def hero_asset_name hero_asset.try(:name) end def hero_asset_name=(name) @hero_asset_name = name asset = Asset.find_by_name(name) self.hero_asset_id = asset.try(:asset_id) end def hero_asset_url hero_asset.try(:public_url) end def publish!(options) transaction do published_revision.unpublish! if published_revision revision = revisions.create! options update_attributes!(published_revision: revision, is_publishable: false) end end def published? published_revision.present? end def revert_to!(revision) self.title = revision.title self.path = revision.path self.body = revision.body self.head_content = revision.head_content self.category_id = revision.category_id self.theme_id = revision.theme_id self.status_code = revision.status_code self.meta_tags = revision.meta_tags self.redirect_url = revision.redirect_url save! end def preview_path public_preview_page_path(self) end def preview_url public_preview_page_url(self) end def forbid_changing_path errors[:path] = 'can not be changed!' if self.path_changed? end def body_strip_search RenderService.call(self) rescue ::Liquid::Error errors[:body] = 'contains a Liquid syntax error' rescue StandardError => error errors[:body] = 'had a problem: ' + error.message end def page_name_byte_size return unless page_name.present? && page_name.bytesize > 100 errors[:page_name] = 'Invalid PageName, bytesize is too big!' end def hero_asset_existence return true if @hero_asset_name.blank? return if Asset.find_by_name(@hero_asset_name) errors[:hero_asset_name] = "System can't find an asset with this name" end def to_liquid { 'title' => title, 'url' => path, 'hero_asset' => hero_asset ? true : false, 'hero_asset_url' => hero_asset_url, 'abstract' => abstract } end module Errors extend ActiveSupport::Concern class GoneError < Error STATUS_CODE = 410 end def error? (400..599).cover? status_code end def error return nil unless error? case status_code when 410 GoneError.new else Landable::Error.new "Missing a Page error class for #{status_code}" end end end include Errors end end