we4tech/flexi-model

View on GitHub
lib/flexi_model/fields.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# encoding: UTF-8
module FlexiModel
  module Fields
    TYPES = [:integer, :boolean, :multiple, :decimal, :float, :string, :text,
             :datetime, :date, :time, :email, :phone, :address, :location, :attachment]

    extend ActiveSupport::Concern

    MULTI_PARAMS_FIELDS = ['datetime', 'date', 'time']

    included do
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        attr_accessor :attributes

        @@flexi_visible = true
        cattr_accessor :flexi_visible

        @@flexi_fields = []
        cattr_accessor :flexi_fields

        @@none_flexi_fields = []
        cattr_accessor :none_flexi_fields

        @@_flexi_fields_by_name = {}
        cattr_accessor :_flexi_fields_by_name

        @@flexi_namespace = nil
        cattr_accessor :flexi_namespace

        @@flexi_partition_id = 0
        cattr_accessor :flexi_partition_id

        @@flexi_name_field = :name
        cattr_accessor :flexi_name_field

      CODE
    end

    # Define attribute initialization constructor
    def initialize(args = { })
      @attributes = { }
      assign_attributes(args) if args.present?
    end

    def assign_attributes(args)
      _all_keys          = args.keys
      _multi_params_keys = _all_keys.select { |k| k.to_s.match(/\([\w]+\)/) }
      _normal_keys       = _all_keys - _multi_params_keys

      # Execute only single parameter accessors
      _normal_keys.each do |k|
        self.send :"#{k.to_s}=", args[k]
      end

      # Execute multi params accessors
      if _multi_params_keys.present?
        _groups = { }
        _multi_params_keys.each do |k|
          _accessor = k.to_s.gsub(/\([\w]+\)/, '')
          _index    = k.to_s.match(/\(([\w]+)\)/)[1].to_i - 1

          _groups[_accessor] ||= []
          _groups[_accessor].insert(_index, args[k])
        end

        _groups.each do |k, values|
          _int_values = values.map(&:to_i)
          _field      = self.flexi_fields.select { |f| f.name.to_s == k }.first

          if _field
            _value = case _field.type
              when 'datetime'
                DateTime.new(*_int_values)
              when 'time'
                Time.new(*([Time.now.year, Time.now.month, Time.now.day] + _int_values))
              when 'date'
                Date.new(*_int_values)
            end

            self.send :"#{k}=", _value
          end
        end
      end
    end

    # Return default name from the object instance
    # This accessor could be set through set_flexi_name_field :title
    def _name
      if self.respond_to?(self.flexi_name_field)
        self.send(self.flexi_name_field)
      else
        raise "Define your name field accessor (which may return the object
              title, name or whatever it describes about the current instance)
              - set_flexi_name_field :different_accessor"
      end
    end

    def to_s
      %{#<#{self.class.name}:#{sprintf '0x%x', self.object_id} #{@attributes.map { |k, v| ":#{k}=>'#{v}'" }.join(' ')}>}
    end

    module ClassMethods

      # Set field name to determine default name field.
      def set_flexi_name_field(field)
        self.flexi_name_field = field
      end

      # Set current model as visible or invisible to admin panel
      # Setting false won't publicize this model over admin panel
      def set_flexi_visible(bool)
        self.flexi_visible = bool
      end

      # Isolate and group all models under a single administration panel
      # This is useful when we are hosting these models under a single platform.
      def set_flexi_partition_id(_id)
        self.flexi_partition_id = _id
      end

      # Set filed with name, type and default value or proc
      #
      # name    - Field name should be downcased multi words separated by underscore
      # type    - Supported type String, Integer, Time, Date
      # options - Hash of options
      #   default   - Default value proc or object
      #   singular  - Singular label
      #   plural    - Plural label
      #
      # Return +Field+ instance
      def flexi_field(name, type, options = { })
        accessible = options.delete(:accessible)
        default    = options.delete(:default)
        singular   = options.delete(:singular) || name.to_s.singularize
        plural     = options.delete(:plural) || name.to_s.pluralize
        _type      = type.is_a?(Symbol) || type.is_a?(String) ? type : type.name


        field            = FlexiField.new(name, _type.to_s.downcase, default)
        field.accessible = accessible
        field.singular   = singular
        field.plural     = plural
        field.options    = options

        opt_accessors = options.delete(:accessors)
        generate_accessors = opt_accessors.nil? ? true : opt_accessors

        define_field(field, generate_accessors)
      end

      alias_method :_ff, :flexi_field

      # Define flexible field definition
      (TYPES - [:attachment]).each do |_field|
        self.class_eval <<-CODE
          def _#{_field.to_s}(*args)
            options = args.last && args.last.is_a?(Hash) ? args.last : {}
            args.each do |_field_name|
              unless _field_name.is_a?(Hash)
                flexi_field _field_name, :#{_field.to_s}, options
              end
            end
          end
        CODE
      end

      # Remove field by field name
      # Return list of existing fields
      def remove_flexi_field(name)
        flexi_fields.reject! { |f| f.name == name }
      end

      def define_field(field, generate_accessors = true)
        # Add new field
        if generate_accessors
          # Remove existing definition
          remove_flexi_field field.name

          flexi_fields << field

          # Map name and field
          _flexi_fields_by_name[
              (field.name.is_a?(Symbol) ?
                  field.name : field.name.to_sym)] = field

          _define_accessors(field)
        else
          none_flexi_fields << field
        end
      end

      private
      def _define_accessors(field)
        # Define setter & getter
        self.class_eval <<-CODE, __FILE__, __LINE__ + 1
          def #{field.name.to_s}=(v)
            @attributes[:'#{field.name.to_s}'] = _cast(v, :#{field.type})
          end

          def #{field.name.to_s}
            @attributes[:'#{field.name.to_s}'] ||= _cast(_get_value(:'#{field.name.to_s}'), :#{field.type})
          end

        CODE

        # Define ? method if boolean field
        if field.type == 'boolean'
          self.class_eval <<-CODE
            def #{field.name.to_s}?
              self.#{field.name.to_s}
            end
          CODE
        end
      end
    end

    def _cast(_value, _type)
      return _value if _value.nil?

      case _type
        when :decimal, :float
          _value.to_f
        when :integer, :number
          _value.to_i
        when :multiple
          if _value.is_a?(String)
            YAML.load(_value)
          elsif Array
            _value.map do |v|
              if v.is_a?(Hash)
                if v.values.map(&:blank?).uniq == [true]
                  nil
                else
                  v
                end
              else
                v
              end
            end.compact
          end
        else
          _value
      end
    end

    def _get_value(field_name)
      return _load_value_from_record(field_name) if self._record.present?

      _default_value(field_name)
    end

    def _load_value_from_record(field_name)
      val = self._record.value_of(field_name.to_s)
      if val.present?
        val.value
      else
        nil
      end
    end

    def _default_value(field_name)
      _field = _flexi_fields_by_name[field_name]
      if _field.default.present?
        _field.value(self)
      else
        nil
      end
    end


    class FlexiField
      attr_accessor :name, :type, :default, :singular, :plural, :accessible, :options

      def initialize(name, type, default = nil)
        @name    = name
        @type    = type
        @default = default
        @options = { }
      end

      def value(context)
        if default.is_a?(Proc)
          default.call(context)
        else
          default
        end
      end
    end
  end
end