locomotivecms/mounter

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

Summary

Maintainability
A
1 hr
Test Coverage
module Locomotive
  module Mounter
    module Models

      class ContentType < Base

        ## fields ##
        field :name
        field :description
        field :slug
        field :label_field_name
        field :group_by
        field :order_by
        field :order_direction
        field :public_submission_enabled
        field :public_submission_accounts
        field :raw_item_template

        field :fields, type: :array, class_name: 'Locomotive::Mounter::Models::ContentField'

        field :entries, association: true

        ## callbacks ##
        set_callback :initialize, :after, :sanitize
        set_callback :initialize, :after, :assign_mounting_point_to_fields

        ## other accessors ##
        attr_accessor :klass_name, :group_by_field_id

        ## aliases ##
        alias_method :group_by_field_name, :group_by
        alias_method :group_by_field_name=, :group_by=

        ## methods ##

        # Return the label field (by the default the first field)
        #
        # @return [ Object ] The label field
        #
        def label_field
          self.find_field(self.label_field_name) || self.fields.first
        end

        # Return the group_by field
        #
        # @return [ Object ] The group_by field
        #
        def group_by_field
          self.find_field(self.group_by_field_name)
        end

        # Return the order_by field
        #
        # @return [ Object ] The order_by field
        #
        def order_by_field
          self.find_field(self.order_by)
        end

        # Build a content entry and add it to the list of entries of the content type.
        # The content type will be referenced into the newly built entry .
        #
        # @param [ Hash ] attributes Attributes of the new content entry
        #
        # @return [ Object ] The newly built content entry
        #
        def build_entry(attributes)
          ContentEntry.new(content_type: self).tap do |entry|
            # set the link to the mounting point
            entry.mounting_point = self.mounting_point

            # do not forget that we are manipulating dynamic fields
            attributes.each do |k, v|
              begin
                field = self.find_field(k)

                if field.nil? && v.is_a?(Hash) # not a dynamic field but localized (permalink ?)
                  entry.send(:"#{k}_translations=", v)
                elsif field.nil?
                  entry.send(:"#{k}=", v)
                else
                  entry.send(:"#{field.name}=", v)
                end
              rescue NoMethodError => e
                Mounter.logger.error e.backtrace
                raise FieldDoesNotExistException.new("The '#{self.slug}' content type does not have a field named '#{k}'.")
              end
            end

            # force the slug to be defined from its label and in all the locales
            entry.send :set_slug

            (self.entries ||= []) << entry
          end
        end

        # Tell if the content type owns a field which defines
        # a relationship to another content type.
        #
        # @return [ Boolean ] True if a relationship field exists
        #
        def with_relationships?
          self.fields.any? { |field| field.is_relationship? }
        end

        # Return the list of fields which do not describe
        # a relationship.
        #
        # @return [ Array ] The list of fields.
        #
        def non_relationship_fields
          self.fields.select { |field| !field.is_relationship? }
        end

        # Return the list of file fields.
        #
        # @return [ Array ] The list of file fields.
        #
        def file_fields
          self.fields.select { |field| field.type == :file }
        end

        # Find a field by its name (string or symbol) or its id (API)
        #
        # @param [ String / Symbol] name_or_id Name or Id of the field
        #
        # @return [ Object ] The field if it exists or nil
        #
        def find_field(name_or_id)
          self.fields.detect { |field| field.matches?(name_or_id) }
        end

        # Find a content entry by its ids (ie: _permalink or _label)
        #
        # @param [ String ] id A permalink or a label
        #
        # @return [ Object ] The content entry if it exists or nil
        #
        def find_entry(id)
          (self.entries || []).detect { |entry| [entry._permalink, entry._label].include?(id) }
        end

        # Find all the entries whose their _permalink or _label is among the ids
        # passed in parameter.
        #
        # @param [ Array ] ids List of permalinks or labels
        #
        # @return [ Array ] List of content entries or [] if none
        #
        def find_entries_among(ids)
          (self.entries || []).find_all { |entry| [*ids].any? { |v| [entry._permalink, entry._label].include?(v) } }
        end

        # Find all the entries by a field and its value.
        #
        # @param [ String ] name Name of the field
        # @param [ String / Array ] value The different values of the field to test
        #
        # @return [ Array ] List of content entries or [] if none
        #
        def find_entries_by(name, value)
          values = [*value]
          (self.entries || []).find_all do |entry|
            raw_value = entry.send(:localized_dynamic_attribute_value, name)
            values.include?(raw_value)
          end
        end

        # Return the params used for the API.
        # The options parameter can be used to get all the fields even
        # the ones describing a relationship with another content type.
        #
        # @param [ Hash ] options Default values: { all_fields: false }
        #
        # @return [ Hash ] The params
        #
        def to_params(options = nil)
          options = { all_fields: false }.merge(options || {})

          params = self.filter_attributes %w(name slug description label_field_name group_by_field_name order_by order_direction public_submission_enabled raw_item_template)

          # group by
          params[:group_by_field_name] = self.group_by_field.name if self.group_by_field

          # order by
          params[:order_by] = '_position' if self.order_by == 'manually'

          # fields
          _fields = options[:all_fields] ? self.fields : self.non_relationship_fields
          params[:entries_custom_fields] = _fields.map(&:to_params)

          params
        end

        # Return a hash with sanitized attributes. It will be used to generate
        # the corresponding yaml file.
        #
        # @return [ Hash ] A hash used by the to_yaml method
        #
        def to_hash
          fields = %w(name slug description label_field_name order_by order_direction public_submission_enabled public_submission_accounts raw_item_template)

          _attributes = self.attributes.delete_if { |k, v| !fields.include?(k.to_s) || v.blank? }.deep_stringify_keys

          # group by
          _attributes['group_by'] = self.group_by_field.name if self.group_by_field

          # order by
          _attributes['order_by'] = self.order_by_field.name if self.order_by_field
          _attributes['order_by'] = 'manually' if self.order_by == '_position'

          # custom fields
          _attributes['fields'] = self.fields

          _attributes
        end

        def to_s
          self.name
        end

        protected

        # Give an unique slug based on a label and within the scope of the content type.
        #
        # @param [ String ] label The label. If nil, we take the singularized version of the content type slug.
        #
        # @return [ String ] An unique slug
        #
        def label_to_slug(label = nil)
          label ||= self.slug.singularize
          base, index = label.parameterize('-'), 1
          unique_slug = base

          while self.find_entry(unique_slug)
            unique_slug = "#{base}-#{index}"
            index += 1
          end

          unique_slug
        end

        # Method used to clean up the content and its fields.
        # Besides, it also sets the values defined by other attributes.
        def sanitize
          # if no label_field_name provided, take the first field
          unless self.label_field_name
            self.label_field_name = (self.fields || []).first.try(:name)
          end

          # define group_by_field from group_by_field_id
          if self.group_by_field_name.blank? && self.group_by_field_id.present?
            self.group_by_field_name = self.find_field(self.group_by_field_id)
          end

          # public_submission_accounts means public_submission_enabled set to true
          self.public_submission_enabled = true if self.public_submission_accounts.is_a?(Array)
        end

        # Each field should have a reference to the mounting point
        #
        def assign_mounting_point_to_fields
          return if self.fields.blank?

          self.fields.each do |field|
            field.mounting_point = self.mounting_point
          end
        end

      end

    end
  end
end