locomotivecms/engine

View on GitHub
app/models/locomotive/concerns/page/templatized.rb

Summary

Maintainability
A
25 mins
Test Coverage
module Locomotive
  module Concerns
    module Page
      module Templatized

        extend ActiveSupport::Concern

        included do

          ## fields ##
          field :templatized,             type: Boolean, default: false
          field :templatized_from_parent, type: Boolean, default: false
          field :target_klass_name

          ## validations ##
          validates_presence_of :target_klass_name, if: :templatized?
          validate              :ensure_target_klass_name_security

          ## callbacks ##
          before_validation :get_templatized_from_parent
          # before_validation :ensure_target_klass_name_security
          before_validation :set_slug_if_templatized
          after_save        :propagate_templatized

          ## scopes ##
          scope :templatized, -> { where(templatized: true) }

          ## virtual attributes ##
          attr_accessor :content_entry
        end

        # Return the id of the content type specified by the target_klass_name property.
        #
        # @return [ Object ] The id of the content type or nil if not found
        #
        def content_type_id
          if self.target_klass_name =~ /^Locomotive::ContentEntry([a-z0-9]+)$/
            $1
          else
            nil
          end
        end

        # Return the content type specified by the target_klass_name property.
        #
        # @return [ Object ] The content type or nil if not found
        #
        def content_type
          if id = self.content_type_id
            @content_type ||= self.site.content_types.find(id) rescue nil
          else
            nil
          end
        end

        # Return the main information about the related content type
        #
        # @return [ Hash ] Name and slug or nil if not found
        #
        def content_type_with_main_attributes
          if id = self.content_type_id
            content_type = self.site.content_types.where(_id: id).only(:name, :slug).first
          else
            nil
          end
        end

        # Return the class specified by the target_klass_name property
        #
        # @example
        #
        #   page.target_klass_name = 'Locomotive::ContentEntry12345'
        #   page.target_klass # <Locomotive::ContentEntry12345...>
        #
        # @return [ Class ] The target class
        #
        def target_klass
          target_klass_name.constantize
        end

        # Return the slug related to the target_klass.
        # In other words, it returns the slug of the target content type.
        #
        # @return [ String ] The slug of the target class / content type. Nil if no target klass matching a content type
        #
        def target_klass_slug
          self.content_type.try(:slug)
        end

        # Set the target klass from the slug of a content type
        #
        # @param [ String ] slug The slug of the content type
        #
        # @return [ Object ] The content type or nil if not found
        #
        def target_klass_slug=(slug)
          if @content_type = self.site.content_types.where(slug: slug).first
            self.target_klass_name = @content_type.entries_class_name
          end
          @content_type
        end

        # Give the name which can be used in a liquid template in order
        # to reference an entry. It uses the slug property if the target klass
        # is a Locomotive content type or the class name itself for the other classes.
        #
        # @example
        #
        #   page.target_klass_name = 'Locomotive::ContentEntry12345' # related to the content type Articles
        #   page.target_entry_name = 'article'
        #
        #   page.target_klass_name = 'OurProduct'
        #   page.target_entry_name = 'our_product'
        #
        # @return [ String ] The name in lowercase and underscored
        #
        def target_entry_name
          if self.content_type
            self.content_type.slug.singularize
          else
            self.target_klass_name.underscore
          end
        end

        # Find the entry both specified by the target klass and identified by the permalink
        #
        # @param [ String ] permalink The permalink of the entry
        #
        # @return [ Object ] The document
        #
        def fetch_target_entry(permalink)
          target_klass.find_by_permalink(permalink)
        end

        # Find all the ordered entries of the target klass filtered or not
        # by the conditions passed in parameter.
        #
        # @param [ Hash ] conditions The conditions used to filter the entries (optional)
        #
        # @return [ Object ] The documents
        #
        def fetch_target_entries(conditions = {})
          if self.content_type
            self.content_type.ordered_entries(where: conditions)
          else
            []
          end
        end

        protected

        def get_templatized_from_parent
          return if self.parent.nil?

          if self.parent.templatized?
            self.templatized        = self.templatized_from_parent = true
            self.target_klass_name  = self.parent.target_klass_name
          elsif !self.templatized?
            self.templatized        = self.templatized_from_parent = false
            self.target_klass_name  = nil
          end
        end

        def set_slug_if_templatized
          self.slug = 'content_type_template' if self.templatized? && !self.templatized_from_parent?
        end

        # Make sure the target_klass is owned by the site OR
        # if it belongs to the models allowed by the application
        # thanks to the models_for_templatization option.
        #
        def ensure_target_klass_name_security
          return if !self.templatized? || self.target_klass_name.blank?

          if self.target_klass_name =~ /^Locomotive::ContentEntry([a-z0-9]+)$/
            content_type = Locomotive::ContentType.find($1)

            if content_type.site_id != self.site_id
              self.errors.add :target_klass_name, :security
            end
          elsif !Locomotive.config.models_for_templatization.include?(self.target_klass_name)
            self.errors.add :target_klass_name, :security
          end
        end

        # Set the templatized, templatized_from_parent properties of
        # the children of the current page ONLY IF the templatized
        # attribute got changed.
        #
        def propagate_templatized
          return unless self.templatized_previously_changed?

          selector = { 'parent_ids' => { '$in' => [self._id] } }
          operations  = {
            '$set' => {
              'templatized'             => self.templatized,
              'templatized_from_parent' => self.templatized,
              'target_klass_name'       => self.target_klass_name
            }
          }

          self.collection.find(selector).update_many(operations)
        end

      end
    end
  end
end