locomotivecms/engine

View on GitHub
app/models/locomotive/content_type.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Locomotive
  class ContentType

    include Locomotive::Mongoid::Document

    ## extensions ##
    include ::CustomFields::Source
    include Concerns::Shared::SiteScope
    include Concerns::ContentType::Label
    include Concerns::ContentType::DefaultValues
    include Concerns::ContentType::EntryTemplate
    include Concerns::ContentType::Sync
    include Concerns::ContentType::GroupBy
    include Concerns::ContentType::OrderBy
    include Concerns::ContentType::ClassHelpers
    include Concerns::ContentType::PublicSubmissionTitleTemplate
    include Concerns::ContentType::FilterFields
    include Concerns::ContentType::Import

    ## fields ##
    field :name
    field :description
    field :slug
    field :label_field_id,                      type: BSON::ObjectId
    field :label_field_name
    field :tree_parent_field_name
    field :group_by_field_id,                   type: BSON::ObjectId
    field :order_by # either a BSON::ObjectId (field id) or a String (:_position, ...etc)
    field :order_direction,                     default: 'asc'
    field :public_submission_enabled,           type: Boolean,  default: false
    field :public_submission_email_attachments, type: Boolean,  default: false
    field :public_submission_accounts,          type: Array,    default: []
    field :recaptcha_required,                  type: Boolean,  default: false
    field :number_of_entries
    field :display_settings,                    type: Hash

    ## associations ##
    has_many :entries,   class_name: 'Locomotive::ContentEntry', dependent: :destroy

    ## named scopes ##
    scope :ordered, -> { order_by(updated_at: :desc) }
    scope :by_id_or_slug, ->(id_or_slug) {
      any_of({ _id: id_or_slug }, { slug: id_or_slug })
    }
    scope :localized, -> { elem_match(entries_custom_fields: { localized: true }) }

    ## indexes ##
    # index site_id: 1, slug: 1

    ## callbacks ##
    before_validation   :normalize_slug
    before_validation   :sanitize_public_submission_accounts
    after_validation    :bubble_fields_errors_up

    ## validations ##
    validates_presence_of   :name, :slug
    validates_uniqueness_of :slug, scope: :site_id
    validates_size_of       :entries_custom_fields, minimum: 1, message: :too_few_custom_fields

    ## behaviours ##
    custom_fields_for :entries

    ## methods ##

    # Order the list of entries, paginate it if requested
    # and filter it.
    #
    # @param [ Hash ] options Options to filter (where key), order (order_by key) and paginate (page, per_page keys)
    #
    # @return [ Criteria ] A Mongoid criteria if not paginated (array otherwise).
    #
    def ordered_entries(options = nil)
      options ||= {}

      # pagination
      page, per_page = options.delete(:page), options.delete(:per_page)

      # order list
      _order_by_definition = (options || {}).delete(:order_by).try(:split) || self.order_by_definition

      # get list
      _entries = self.entries.order_by([_order_by_definition]).where(options[:where] || {})

      # pagination or full list
      page ? _entries.page(page).per(per_page) : _entries
    end

    # Find a custom field describing an entry based on its id
    # in first or its name if not found.
    #
    # @param [ String ] id_or_name The id of name of the field
    #
    # @return [ Object ] The custom field or nit if not found
    #
    def find_entries_custom_field(id_or_name)
      return nil if id_or_name.nil? # bypass the memoization

      _field = self.entries_custom_fields.find(id_or_name) rescue nil
      _field || self.entries_custom_fields.where(name: id_or_name).first
    end

    # Tell if a field has to be displayed in the UI
    #
    def is_field_with_ui_enabled?(id_or_name)
      self.find_entries_custom_field(id_or_name)&.ui_enabled?
    end

    # A localized content type owns at least one localized field.
    def localized?
      self.entries_custom_fields.where(localized: true).count > 0
    end

    def hidden?
      (self.display_settings || {})['hidden']
    end

    def touch_site_attribute
      :content_version
    end

    protected

    def normalize_slug
      self.slug = self.name.clone if self.slug.blank? && self.name.present?
      self.slug.permalink!(true) if self.slug.present?
    end

    # We do not want to have a blank value in the list of accounts.
    def sanitize_public_submission_accounts
      if self.public_submission_accounts
        self.public_submission_accounts.reject! { |id| id.blank? }
      end
    end

    def bubble_fields_errors_up
      return if self.errors[:entries_custom_fields].empty?

      self.entries_custom_fields.each do |field|
        next if field.errors.blank?
        key = field.persisted? ? field._id.to_s : field.position.to_i
        self.errors.add(:entries_custom_fields, :invalid, message: "##{key}: #{field.errors.full_messages.first}")
      end
    end

    # Makes sure the class_name filled in a belongs_to or has_many field
    # does not belong to another site. Adds an error if it presents a
    # security problem.
    #
    # @param [ CustomFields::Field ] field The field to check
    #
    def ensure_class_name_security(field)
      if field.class_name =~ /^Locomotive::ContentEntry([a-z0-9]+)$/
        # if the content type does not exist (anymore), bypass the security checking
        content_type = Locomotive::ContentType.find($1) rescue nil

        return if content_type.nil?

        if content_type.site_id != self.site_id
          field.errors.add :class_name, :security
        end
      else
        # for now, does not allow external classes
        field.errors.add :class_name, :security
      end
    end

  end
end