artirix/browsercms

View on GitHub
lib/cms/concerns/can_be_addressable.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Cms
  module Concerns
    module CanBeAddressable

      # Adds Addressable behavior to a model. This allows models to be inserted into the sitemap, having parent
      # sections. By default, this method is available to all ActiveRecord::Base classes.
      #
      # @params [Hash] options
      # @option options [String] :path The base path where instances will be placed.
      # @option options [String] :no_dynamic_path Set as true if the Record has a :path attribute managed as a column in the db. (Default: false)
      # @option options [Symbol] :destroy_if Name of a custom method used to determine when this object should be destroyed. Rather than dependant: destroy to determine if the section node should be destroyed when this object is.
      def is_addressable(options={})
        has_one_options = {as: :node, inverse_of: :node, class_name: 'Cms::SectionNode', validate: true}
        unless options[:destroy_if]
          has_one_options[:dependent] = :destroy
        else
          before_destroy options[:destroy_if]
          after_destroy :destroy_node
        end

        has_one :section_node, has_one_options
        # For reasons that aren't clear, just using :autosave doesn't work.
        after_save do
          if section_node && section_node.changed?
            section_node.save
          end
        end

        after_validation do
          # Copy errors from association for slug
          if section_node && !section_node.valid?
            section_node.errors[:slug].each do |message|
              errors.add(:slug, message)
            end
          end
        end

        include Cms::Concerns::Addressable
        extend Cms::Concerns::Addressable::ClassMethods
        include Cms::Concerns::Addressable::NodeAccessors
        include Cms::Concerns::Addressable::MarkAsDirty
        extend Cms::Configuration::ConfigurableTemplate

        if options[:path]
          @path = options[:path]
          include GenericSitemapBehavior
        end

        @template = options[:template]

        unless options[:no_dynamic_path]
          include Addressable::DynamicPath
        end
      end

      # @return [Boolean] Until is_addressable is called, this will always be false.
      def addressable?
        false
      end

      # @return [Boolean] Some addressable content types don't require a slug.
      def requires_slug?
        false
      end
    end

    # Implements behavior for displaying content blocks that should appear in the sitemap.
    #
    module GenericSitemapBehavior
      def self.included(klass)
        klass.before_validation :assign_parent_if_needed
      end

      def partial_for
        "addressable_content_block"
      end

      def hidden?
        false
      end

      def assign_parent_if_needed
        unless parent || parent_id
          new_parent = Cms::Section.with_path(self.class.path).first

          unless new_parent
            logger.warn "Creating default section for #{self.try(:display_name)} in #{self.class.path}."
            section_attributes = {:name => self.class.name.demodulize.pluralize,
                                  :path => self.class.path,
                                  :hidden => true,
                                  allow_groups: :all}
            section_parent = Cms::Section.root.first
            if section_parent
              section_attributes[:parent] = section_parent
            end
            new_parent = Cms::Section.create!(section_attributes)
          end
          self.parent_id = new_parent.id
        end
      end
    end

    module Addressable

      module ClassMethods

        def requires_slug?
          !@path.nil?
        end

        # The base path where new records will be added.
        # @return [String]
        def path
          @path
        end

        # @return [Boolean] Once is_addressable is called, this will always be true.
        def addressable?
          true
        end

        def calculate_path(slug)
          "#{self.path}/#{slug}"
        end

        # Used in UI forms to show what the complete URL will look like with a slug added to it.
        def base_path
          "#{self.path}/"
        end

        # Find an addressable object with the given content type.
        #
        # @param [String] slug
        # @return [Addressable] The content block with that slug (if it exists). Nil otherwise
        def with_slug(slug)
          section_node = SectionNode.where(slug: slug).where(node_type: self.name).first
          section_node ? section_node.node : nil
        end

        # Returns the layout (Page Template) that should be used to render instances of this content.
        # Can be specified as is_addressable template: 'subpage'
        # @return [String] template/default unless template was set.
        def layout
          normalize_layout(self, @template)
        end
      end

      module DynamicPath

        # @return [String] The relative path to this content
        def path
          self.class.calculate_path(slug)
        end

        def slug
          if section_node
            section_node.slug
          else
            nil
          end
        end

        def slug=(slug)
          if section_node
            section_node.slug = slug
          else
            @slug = slug # Store temporarily until there is a section_node created.
          end
          dirty!
        end
      end

      # Resources are accessible to guests if their parent section is. Variables are passed in for performance reasons
      # since this gets called 'MANY' times on the sitemap.
      #
      # @param [Array<Section>] public_sections
      # @param [Section] parent
      def accessible_to_guests?(public_sections, parent)
        public_sections.include?(parent)
      end

      # Returns all classes which need a custom route to show themselves.
      def self.classes_that_require_custom_routes
        descendants.select { |klass| klass.path != nil }
      end

      # List of all classes that can be in the sitemap.
      #
      # @return [Array<Class>] Returns all classes which inherit from Cms::Concerns::Addressable
      def self.descendants
        ObjectSpace.each_object(::Class).select { |klass| klass < Cms::Concerns::Addressable }
      end

      # Allows for manual destruction of node
      def destroy_node
        node.destroy
      end

      # Whether or not this content is the 'landing' page for its section.
      #
      # @return [Boolean] false always, since a content block won't ever be the landing page for the section
      def landing_page?
        false
      end

      # Returns the value that will appear in the <title> element of the page when this content is rendered.
      # Subclasses can override this.
      #
      # @return [String]
      # @example Override the page title
      #   class MyWidget < ActiveRecord::Base
      #     def page_title
      #       "My Widget | #{name}"
      #     end
      #   end
      def page_title
        name
      end

      # Returns a list of all Addressable objects that are ancestors to this record.
      # @param [Hash] options
      # @option [Symbol] :include_self If this object should be included in the Array
      # @return [Array<Addressable>] Or [] if no ancestors and/or has no parent.
      #
      def ancestors(options={})
        return [] unless node
        ancestor_nodes = node.ancestors
        ancestors = ancestor_nodes.collect { |node| node.node }
        ancestors << self if options[:include_self]
        ancestors
      end

      def parent
        @parent if @parent
        node ? node.section : nil
      end

      def cache_parent(section)
        @parent = section
      end

      def parent_id
        parent.try(:id)
      end

      def parent_id=(id)
        self.parent = Cms::Section.find(id)
        # Handles slug being set before there is a parent
        if @slug
          self.slug = @slug
          @slug = nil
        end
      end

      def parent=(sec)
        if node
          node.move_to_end(sec)
        else
          build_section_node(:node_id => self.id, :section => sec)
        end
      end

      # Computes the name of the partial used to render this object in the sitemap.
      def partial_for
        self.class.name.demodulize.underscore
      end

      # alias :node, :section_node
      module NodeAccessors
        def node
          section_node
        end

        def node=(n)
          self.section_node = n
        end
      end

      # These exist for backwards compatibility to avoid having to change tests.
      # I want to get rid of these in favor of parent and parent_id
      module DeprecatedPageAccessors

        def self.included(model_class)
          #model_class.attr_accessible :section_id, :section
        end


        def build_node(opts)
          build_section_node(opts)
        end

        def section_id
          section ? section.id : nil
        end

        def section_id=(sec_id)
          self.section = Section.find(sec_id)
        end

        def section
          parent
        end

        def section=(sec)
          self.parent = sec

        end
      end

      # Allows records to be marked as being changed, which can be called to trigger updates when
      # associated objects may have been changed.
      module MarkAsDirty
        # Forces this record to be changed, even if nothing has changed
        # This is necessary if just the section.id has changed, for example
        # test if this is necessary now that the attributes are in the
        # model itself.
        def dirty!
          # Seems like a hack, is there a better way?
          self.updated_at = Time.now
        end
      end
    end


  end
end