locomotivecms/steam

View on GitHub
lib/locomotive/steam/entities/content_entry.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'chronic'
require 'bcrypt'

module Locomotive::Steam

  class ContentEntry

    include Locomotive::Steam::Models::Entity

    attr_accessor :content_type

    def initialize(attributes = {})
      super({
        _visible:     true,
        _position:    0,
        created_at:   Time.zone.now,
        updated_at:   Time.zone.now
      }.merge(attributes))
    end

    def _id; self[:_id] || self[:id]; end

    def _visible?; !!self[:_visible]; end
    alias :visible? :_visible?

    def _slug; self[:_slug]; end
    alias :_permalink :_slug

    def method_missing(name, *args, &block)
      if is_dynamic_attribute?(name)
        cast_value(name)
      elsif attributes.include?(name)
        self[name]
      else
        super
      end
    end

    def valid?
      errors.clear
      content_type.fields.required.each do |field|
        errors.add_on_blank(field.name.to_sym)
      end
      errors.empty?
    end

    def content_type
      @content_type || attributes[:content_type]
    end

    def content_type_id
      @content_type.try(&:_id) || attributes[:content_type_id]
    end

    def content_type_slug
      content_type.slug
    end

    def _class_name
      "Locomotive::ContentEntry#{content_type_id}"
    end

    def _label
      self[content_type.label_field_name]
    end

    def _label_of(name)
      content_type.field_label_of(name)
    end

    def localized_attributes
      @localized_attributes.tap do |hash|
        if hash && hash.has_key?(content_type.label_field_name.to_sym)
          hash[:_label] = true
        end
      end
    end

    def serialize
      super.merge(content_type_id: content_type_id)
    end

    def to_hash
      hash = {}

      # default attributes
      _attributes = %i(_id _slug _visible _position content_type_slug created_at updated_at)

      # stack level too deep raised if the _label field is an association (belongs_to, ...etc)
      unless content_type.fields_by_name[content_type.label_field_name].is_relationship?
        _attributes << :_label
      end

      # dynamic attributes
      _attributes += content_type.persisted_field_names

      _attributes.each do |name|
        hash[name.to_s] = send(name)
      end

      # errors?
      hash['errors'] = self.errors.to_hash.stringify_keys unless self.errors.empty?

      hash
    end

    def to_liquid
      Locomotive::Steam::Liquid::Drops::ContentEntry.new(self)
    end

    private

    def is_dynamic_attribute?(name)
      content_type.fields_by_name.has_key?(name)
    end

    def cast_value(name)
      field = content_type.fields_by_name[name]

      begin
        _cast_value(field)
      rescue Exception => e
        Locomotive::Common::Logger.info "[#{content_type.slug}][#{_label}] Unable to cast the \"#{name}\" field, reason: #{e.message}".yellow
        nil
      end
    end

    def _cast_value(field)
      if private_methods.include?(:"_cast_#{field.type}")
        send(:"_cast_#{field.type}", field)
      else
        attributes[field.name]
      end
    end

    def _cast_integer(field)
      _cast_convertor(field.name, &:to_i)
    end

    def _cast_float(field)
      _cast_convertor(field.name, &:to_f)
    end

    def _cast_json(field)
      _cast_convertor(field.name) do |value|
        if value.respond_to?(:to_h)
          value
        else
          value.blank? ? nil : JSON.parse(value)
        end
      end
    end

    def _cast_password(field)
      _cast_convertor(:"#{field.name}_hash") do |value|
        value.blank? ? nil : BCrypt::Password.new(value)
      end
    end

    def _cast_file(field)
      _cast_convertor(field.name) do |value, locale|
        if value.respond_to?(:url)
          value
        else
          size = (self[:"#{field.name}_size"] || {})[locale || 'default']
          FileField.new(value, self.base_url, size, self.updated_at)
        end
      end
    end

    def _cast_date(field)
      _cast_time(field, :to_date)
    end

    def _cast_date_time(field)
      _cast_time(field, :to_datetime)
    end

    def _cast_time(field, end_method)
      _cast_convertor(field.name) do |value|
        if value.is_a?(String)
          # context: time from a YAML file (String).
          # In that case, use the timezone defined by the site.
          Chronic.time_class = Time.zone
          Chronic.parse(value).send(end_method)
        else
          value
        end
      end
    end

    def _cast_select(field)
      options = field.select_options

      if (_value = @attributes[:"#{field.name}_id"]).respond_to?(:translations)
        # the field is localized, so get the labels in all the locales
        # (2 different locales might point to different options)
        if _value.default
          # unique value for all the locales, so grab the option
          name = options.by_id_or_name(_value.default)&.name
          name&.duplicate(field.name)
        else
          @attributes[field.name] = _value.duplicate(field.name)

          _cast_convertor(field.name, true) do |value, locale|
            name = options.by_id_or_name(value)&.name
            name.try(:[], locale)
          end
        end
      else
        # the field is not localized, we only have the id of the option (or its name if a
        # contact form submission in Wagon for instance),
        # so just copy the labels (in all the locales) of the matching select option
        if name = options.by_id_or_name(_value)&.name # this should either return an i18nField or nil
          attributes[field.name] = name.dup
        end
      end
    end

    def _cast_convertor(name, nil_locale = false, &block)
      if (value = attributes[name]).respond_to?(:translations)
        value.apply(&block)
      else
        nil_locale ? yield(value, nil) : yield(value)
      end
    end

    # Represent a file
    class FileField

      attr_accessor_initialize :filename, :base, :size, :updated_at

      def url
        return if filename.blank?
        base.blank? ? filename : "#{base}/#{filename}"
      end

      def to_hash
        { 'url' => url, 'filename' => filename, 'size' => size, 'updated_at' => updated_at }
      end

      def to_json
        url
      end

      def to_liquid
        Locomotive::Steam::Liquid::Drops::UploadedFile.new(self)
      end

    end

  end

end