locomotivecms/mounter

View on GitHub
lib/locomotive/mounter/models/page.rb

Summary

Maintainability
C
1 day
Test Coverage
# encoding: UTF-8
module Locomotive
  module Mounter
    module Models

      class Page < Base

        ## fields ##
        field :parent,            association: true
        field :title,             localized: true
        field :slug,              localized: true
        field :fullpath,          localized: true
        field :is_layout,         default: false
        field :allow_layout,      default: false
        field :redirect_url,      localized: true
        field :redirect_type,     default: 301
        field :template,          localized: true
        field :handle
        field :listed,            default: false
        field :searchable
        field :templatized,       default: false
        field :content_type
        field :published,         default: true
        field :cache_strategy
        field :response_type
        field :position

        field :seo_title,         localized: true
        field :meta_keywords,     localized: true
        field :meta_description,  localized: true

        field :editable_elements, type: :array, class_name: 'Locomotive::Mounter::Models::EditableElement'

        ## other accessors ##
        attr_accessor :content_type_id, :content_entry, :parent_id, :children, :templatized_from_parent

        ## path to the file of the template (if mounted from a FS) ##
        attr_accessor :filepath

        ## aliases ##
        alias :listed?      :listed
        alias :published?   :published
        alias :templatized? :templatized
        alias :searchable?  :searchable
        alias :is_layout?   :is_layout

        ## methods ##

        # Tell if the page is either the index page.
        #
        # @return [ Boolean ] True if index page.
        #
        def index?
          self.depth == 0 && 'index' == self.slug
        end

        # Tell if the page is either the index or the 404 page.
        #
        # @return [ Boolean ] True if index or 404 page.
        #
        def index_or_404?
          self.depth == 0 && %w(index 404).include?(self.slug)
        end

        # Return the fullpath dasherized and with the "*" character
        # for the slug of templatized page.
        #
        # @param [ Boolean ] wildcard If true, replace the slug of a templatized page by the "*" character (default: true)
        #
        # @return [ String ] The safe full path or nil if the page is not translated in the current locale
        #
        def safe_fullpath(wildcard = true)
          if self.index_or_404?
            self.slug
          else
            base  = self.parent.safe_fullpath(wildcard)
            _slug = if self.templatized? && !self.templatized_from_parent
              wildcard ? '*' : self.slug
            elsif !self.translated_in?(Locomotive::Mounter.locale)
              self.slug_translations[self.mounting_point.default_locale]
            else
              self.slug
            end
            (base == 'index' ? _slug : File.join(base, _slug)).dasherize
          end
        end

        # Return the fullpath in the current locale. If it does not exist,
        # return the one of the main locale.
        #
        # @return [ String ] A non-blank fullpath
        #
        def fullpath_or_default
          self.fullpath || self.fullpath_in_default_locale
        end

        # Return the fullpath in the default locale no matter the current locale is.
        #
        # @return [ String ] The fullpath
        #
        def fullpath_in_default_locale
          self.fullpath_translations[self.mounting_point.default_locale]
        end

        # Get the id of the parent page.
        #
        # @return [ String ] The _id attribute of the parent page
        #
        def parent_id
          @parent_id || self.parent.try(:_id)
        end

        # Force the translations of a page
        #
        # @param [ Array ] locales List of locales (Symbol or String)
        #
        def translated_in=(locales)
          self._locales = locales.map(&:to_sym)
        end

        # Set the content type, attribute required for templatized page.
        # @deprecated. Use content_type= instead.
        #
        # @param [ Object ] content_type The content type
        #
        def model=(content_type)
          Locomotive::Mounter.logger.warn 'The model attribute is deprecated. Use content_type instead.'
          self.content_type = content_type
        end

        # Modified setter in order to set correctly the slug
        #
        # @param [ String ] fullpath The fullpath
        #
        def fullpath_with_setting_slug=(fullpath)
          if fullpath && self.slug.nil?
            self.slug = File.basename(fullpath)
          end

          self.fullpath_without_setting_slug = fullpath
        end

        alias_method_chain :fullpath=, :setting_slug

        # Depth of the page in the site tree.
        # Both the index and 404 pages are 0-depth.
        #
        # @return [ Integer ] The depth
        #
        def depth
          return 0 if %w(index 404).include?(self.fullpath)
          self.fullpath_or_default.split('/').size
        end

        # Depth and position in the site tree
        #
        # @return [ Integer ] An unique id corresponding to the depth and position
        #
        def depth_and_position
          self.depth * 1000 + (self.position || 199)
        end

        # Tell if the page extends the template of another page.
        # Basically, we check if the template of the page includes
        # the "extends" liquid tag.
        #
        # @return [ Boolean ] True if the template can be a layout.
        #
        def extends_template?
          !self.template_fullpath.nil?
        end

        # Return the fullpath of the page whose template is extended
        # in the current template.
        #
        # @return [ String ] The fullpath of the "extended" page or nil if no extends tag
        #
        def template_fullpath
          return nil if self.source.nil? || self.source.strip.blank?

          self.source =~ /\{%\s*extends\s+\'?([[\w|\-|\_]|\/]+)\'?\s*%\}/
          $1
        end

        # Is it a redirect page ?
        #
        # @return [ Boolean ] True if the redirect_url property is set
        #
        def redirect?
          !self.redirect_url.blank?
        end

        # Add a child to the page. It also sets the parent of the child.
        # If the parent page is a templatized one, give the same properties to the child.
        #
        # @param [ Object ] page The child page
        #
        # @return [ Object ] The child page
        #
        def add_child(page)
          page.parent = self

          if self.templatized?
            page.templatized_from_parent = true

            # copy properties from the parent
            %w(templatized content_type content_type_id).each do |name|
              page.send(:"#{name}=", self.send(name.to_sym))
            end
          end

          (self.children ||= []) << page

          self.children.sort! { |a, b| (a.position || 999) <=> (b.position || 999) }

          page
        end

        # Build or update the list of editable elements from a hash whose
        # keys are the couple "[block]/[slug]" and the values the content
        # of the editable elements OR an array of attributes
        #
        # @param [ Hash / Array ] attributes The attributes of the editable elements
        #
        def set_editable_elements(attributes)
          return if attributes.blank?

          self.editable_elements ||= []

          attributes.to_a.each do |_attributes|
            if _attributes.is_a?(Array) # attributes is maybe a Hash
              _splashed   = _attributes.first.split('/')

              block, slug = _splashed[0..-2].join('/'), _splashed.last
              block       = nil if block.blank?

              _attributes = { 'block' => block, 'slug' => slug, 'content' => _attributes.last }
            end

            # does an editable element exist with the same couple block/slug ?
            if editable_element = self.find_editable_element(_attributes['block'], _attributes['slug'])
              editable_element.content = _attributes['content']
            else
              self.editable_elements << Locomotive::Mounter::Models::EditableElement.new(_attributes)
            end
          end
        end

        # Find an editable element from its block and slug (the couple is unique)
        #
        # @param [ String ] block The name of the block
        # @param [ String ] slug The slug of the element
        #
        # @return [ Object ] The editable element or nil if not found
        #
        def find_editable_element(block, slug)
          (self.editable_elements || []).detect do |el|
            el.block.to_s == block.to_s && el.slug.to_s == slug.to_s
          end
        end

        # Localize the fullpath based on the parent fullpath in the locales
        # passed in parameter.
        #
        # @param [ Array ] locales The list of locales the fullpath will be translated to. Can be nil (will use the locales returned by translated_in)
        #
        def localize_fullpath(locales = nil)
          locales ||= self.translated_in
          _parent_fullpath  = self.parent.try(:fullpath)
          _fullpath, _slug  = self.fullpath.try(:clone), self.slug.to_s.clone

          locales.each do |locale|
            Locomotive::Mounter.with_locale(locale) do
              if %w(index 404).include?(_slug) && (_fullpath.nil? || _fullpath == _slug)
                self.fullpath = _slug
                self.slug     = _slug
              elsif _parent_fullpath == 'index'
                self.fullpath = self.slug || _slug
              else
                self.fullpath = File.join(parent.fullpath || _parent_fullpath, self.slug || _slug)
              end
            end
          end
        end

        # Assign a default template for each locale which
        # has an empty template. This default template
        # is the one defined in the default locale.
        #
        # @param [ Symbol / String ] default_locale The default locale
        #
        def set_default_template_for_each_locale(default_locale)
          default_template = self.template_translations[default_locale.to_sym]

          return if self.template_blank?(default_template)

          self.translated_in.each do |locale|
            next if locale.to_s == default_locale.to_s

            # current template
            _template = self.template_translations[locale]

            # is it blank ?
            if self.template_blank?(_template)
              self.template_translations[locale] = default_template
            end
          end
        end

        # Set the source of the page without any pre-rendering. Used by the API reader.
        #
        # @param [ String ] content The HTML raw template
        #
        def raw_template=(content)
          @source ||= {}
          @source[Locomotive::Mounter.locale] = content
        end

        # Return the Liquid template based on the raw_template property
        # of the page. If the template is HAML or SLIM, then a pre-rendering to Liquid is done.
        #
        # @return [ String ] The liquid template or nil if not template has been provided
        #
        def source
          @source ||= {}

          if @source[Locomotive::Mounter.locale]
            @source[Locomotive::Mounter.locale] # memoization
          elsif self.template
            @source[Locomotive::Mounter.locale] = self.template.source
          else
            nil
          end
        end

        # Return the YAML front matters of the page
        #
        # @return [ String ] The YAML version of the page
        #
        def to_yaml
          fields = %w(title slug redirect_url redirect_type handle published listed allow_layout is_layout searchable cache_strategy response_type position seo_title meta_description meta_keywords)

          _attributes = self.attributes.delete_if do |k, v|
            !fields.include?(k.to_s) || (!v.is_a?(FalseClass) && v.blank?)
          end.deep_stringify_keys

          # useless attributes
          _attributes.delete('redirect_type') if self.redirect_url.blank?

          # templatized page
          _attributes['content_type'] = self.content_type.slug if self.templatized? && !self.templatized_from_parent

          # editable elements
          _attributes['editable_elements'] = {}
          (self.editable_elements || []).each do |editable_element|
            _attributes['editable_elements'].merge!(editable_element.to_yaml)
          end

          _attributes.delete('editable_elements') if _attributes['editable_elements'].empty?

          _attributes.delete('slug') if self.depth == 0

          "#{_attributes.to_yaml}---\n#{self.source}"
        end

        # Return the params used for the API
        #
        # @return [ Hash ] The params
        #
        def to_params
          params = self.filter_attributes %w(title parent_id slug redirect_url redirect_type handle listed is_layout
            allow_layout published searchable cache_strategy
            response_type position templatized seo_title meta_description meta_keywords)

          # slug
          params.delete(:slug) if self.depth == 0

          # redirect_url
          params[:redirect] = true unless self.redirect_url.blank?

          # parent_id
          params[:parent_id] = self.parent_id unless self.parent_id.blank?

          # content_type
          params[:target_klass_slug] = self.content_type.slug if self.templatized && self.content_type

          # editable_elements
          params[:editable_elements] = (self.editable_elements || []).map(&:to_params)

          # raw_template
          params[:raw_template] = self.source rescue nil

          params
        end

        # Return the params used for the API but without all the params.
        # This can be explained by the fact that for instance the update should preserve
        # the content.
        #
        # @return [ Hash ] The safe params
        #
        def to_safe_params
          fields = %w(title slug listed is_layout allow_layout published searchable handle cache_strategy
            redirect_url response_type templatized content_type_id position
            seo_title meta_description meta_keywords)

          params = self.attributes.delete_if do |k, v|
            !fields.include?(k.to_s) || (!v.is_a?(FalseClass) && v.blank?)
          end.deep_symbolize_keys

          # redirect_url
          params[:redirect] = true unless self.redirect_url.blank?

          # raw_template
          params[:raw_template] = self.source rescue nil

          params
        end

        def to_s
          self.fullpath_or_default
        end

        protected

        # Tell if a template is strictly blank (nil or empty).
        # If a template is invalid, it is not considered as a
        # blank one.
        #
        # @param [ PageTemplate ] template The template to test
        #
        # @return [ Boolean ] True if the template is strictly blank
        #
        def template_blank?(template)
          template.nil? || template.raw_source.strip.blank?
        end

      end

    end
  end
end