sferik/rails_admin

View on GitHub
lib/rails_admin/config/fields/base.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# frozen_string_literal: true

require 'rails_admin/config/proxyable'
require 'rails_admin/config/configurable'
require 'rails_admin/config/hideable'
require 'rails_admin/config/groupable'
require 'rails_admin/config/inspectable'

module RailsAdmin
  module Config
    module Fields
      class Base # rubocop:disable Metrics/ClassLength
        include RailsAdmin::Config::Proxyable
        include RailsAdmin::Config::Configurable
        include RailsAdmin::Config::Hideable
        include RailsAdmin::Config::Groupable
        include RailsAdmin::Config::Inspectable

        attr_reader :name, :properties, :abstract_model, :parent, :root
        attr_accessor :defined, :order, :section

        NAMED_INSTANCE_VARIABLES = %i[
          @parent @root @section @children_fields_registered
          @associated_model_config @group
        ].freeze

        def initialize(parent, name, properties)
          @parent = parent
          @root = parent.root

          @abstract_model = parent.abstract_model
          @defined = false
          @name = name.to_sym
          @order = 0
          @properties = properties
          @section = parent
        end

        register_instance_option :css_class do
          "#{name}_field"
        end

        def type_css_class
          "#{type}_type"
        end

        def virtual?
          properties.blank?
        end

        register_instance_option :column_width do
          nil
        end

        register_instance_option :sticky? do
          false
        end

        register_instance_option :sortable do
          !virtual? || children_fields.first || false
        end

        def sort_column
          if sortable == true
            "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(name)}"
          elsif (sortable.is_a?(String) || sortable.is_a?(Symbol)) && sortable.to_s.include?('.') # just provide sortable, don't do anything smart
            sortable
          elsif sortable.is_a?(Hash) # just join sortable hash, don't do anything smart
            "#{sortable.keys.first}.#{sortable.values.first}"
          elsif association # use column on target table
            "#{associated_model_config.abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}"
          else # use described column in the field conf.
            "#{abstract_model.quoted_table_name}.#{abstract_model.quote_column_name(sortable)}"
          end
        end

        register_instance_option :searchable do
          !virtual? || children_fields.first || false
        end

        register_instance_option :search_operator do
          RailsAdmin::Config.default_search_operator
        end

        register_instance_option :queryable? do
          !virtual?
        end

        register_instance_option :filterable? do
          !!searchable
        end

        register_instance_option :filter_operators do
          []
        end

        register_instance_option :default_filter_operator do
          nil
        end

        def filter_options
          {
            label: label,
            name: name,
            operator: default_filter_operator,
            operators: filter_operators,
            type: type,
          }
        end

        # serials and dates are reversed in list, which is more natural (last modified items first).
        register_instance_option :sort_reverse? do
          false
        end

        # list of columns I should search for that field [{ column: 'table_name.column', type: field.type }, {..}]
        register_instance_option :searchable_columns do
          @searchable_columns ||=
            case searchable
            when true
              [{column: "#{abstract_model.table_name}.#{name}", type: type}]
            when false
              []
            when :all # valid only for associations
              table_name = associated_model_config.abstract_model.table_name
              associated_model_config.list.fields.collect { |f| {column: "#{table_name}.#{f.name}", type: f.type} }
            else
              [searchable].flatten.collect do |f|
                if f.is_a?(String) && f.include?('.')                            #  table_name.column
                  table_name, column = f.split '.'
                  type = nil
                elsif f.is_a?(Hash)                                              #  <Model|table_name> => <attribute|column>
                  am = AbstractModel.new(f.keys.first) if f.keys.first.is_a?(Class)
                  table_name = am&.table_name || f.keys.first
                  column = f.values.first
                  property = am&.properties&.detect { |p| p.name == f.values.first.to_sym }
                  type = property&.type
                else                                                             #  <attribute|column>
                  am = (association? ? associated_model_config.abstract_model : abstract_model)
                  table_name = am.table_name
                  column = f
                  property = am.properties.detect { |p| p.name == f.to_sym }
                  type = property&.type
                end
                {column: "#{table_name}.#{column}", type: (type || :string)}
              end
            end
        end

        register_instance_option :formatted_value do
          value
        end

        # output for pretty printing (show, list)
        register_instance_option :pretty_value do
          formatted_value.presence || ' - '
        end

        # output for printing in export view (developers beware: no bindings[:view] and no data!)
        register_instance_option :export_value do
          pretty_value
        end

        # Accessor for field's help text displayed below input field.
        register_instance_option :help do
          (@help ||= {})[::I18n.locale] ||= generic_field_help
        end

        register_instance_option :html_attributes do
          {
            required: required?,
          }
        end

        register_instance_option :default_value do
          nil
        end

        # Accessor for field's label.
        #
        # @see RailsAdmin::AbstractModel.properties
        register_instance_option :label do
          (@label ||= {})[::I18n.locale] ||= abstract_model.model.human_attribute_name name
        end

        register_instance_option :hint do
          (@hint ||= '')
        end

        # Accessor for field's maximum length per database.
        #
        # @see RailsAdmin::AbstractModel.properties
        register_instance_option :length do
          @length ||= properties&.length
        end

        # Accessor for field's length restrictions per validations
        #
        register_instance_option :valid_length do
          @valid_length ||= abstract_model.model.validators_on(name).detect { |v| v.kind == :length }.try(&:options) || {}
        end

        register_instance_option :partial do
          :form_field
        end

        # Accessor for whether this is field is mandatory.
        #
        # @see RailsAdmin::AbstractModel.properties
        register_instance_option :required? do
          context =
            if bindings && bindings[:object]
              bindings[:object].persisted? ? :update : :create
            else
              :nil
            end

          (@required ||= {})[context] ||= !!([name] + children_fields).uniq.detect do |column_name|
            abstract_model.model.validators_on(column_name).detect do |v|
              !(v.options[:allow_nil] || v.options[:allow_blank]) &&
                %i[presence numericality attachment_presence].include?(v.kind) &&
                (v.options[:on] == context || v.options[:on].blank?) &&
                (v.options[:if].blank? && v.options[:unless].blank?)
            end
          end
        end

        # Accessor for whether this is a serial field (aka. primary key, identifier).
        #
        # @see RailsAdmin::AbstractModel.properties
        register_instance_option :serial? do
          properties&.serial?
        end

        register_instance_option :view_helper do
          :text_field
        end

        register_instance_option :read_only? do
          !editable?
        end

        # init status in the view
        register_instance_option :active? do
          false
        end

        register_instance_option :visible? do
          returned = true
          (RailsAdmin.config.default_hidden_fields || {}).each do |section, fields|
            next unless self.section.is_a?("RailsAdmin::Config::Sections::#{section.to_s.camelize}".constantize)

            returned = false if fields.include?(name)
          end
          returned
        end

        # columns mapped (belongs_to, paperclip, etc.). First one is used for searching/sorting by default
        register_instance_option :children_fields do
          []
        end

        register_instance_option :eager_load do
          false
        end

        register_deprecated_instance_option :eager_load?, :eager_load

        def eager_load_values
          case eager_load
          when true
            [name]
          when false, nil
            []
          else
            Array.wrap(eager_load)
          end
        end

        register_instance_option :render do
          bindings[:view].render partial: "rails_admin/main/#{partial}", locals: {field: self, form: bindings[:form]}
        end

        def editable?
          !((@properties && @properties.read_only?) || (bindings[:object] && bindings[:object].readonly?))
        end

        # Is this an association
        def association?
          is_a?(RailsAdmin::Config::Fields::Association)
        end

        # Reader for validation errors of the bound object
        def errors
          ([name] + children_fields).uniq.collect do |column_name|
            bindings[:object].errors[column_name]
          end.uniq.flatten
        end

        # Reader whether field is optional.
        #
        # @see RailsAdmin::Config::Fields::Base.register_instance_option :required?
        def optional?
          !required?
        end

        # Inverse accessor whether this field is required.
        #
        # @see RailsAdmin::Config::Fields::Base.register_instance_option :required?
        def optional(state = nil, &block)
          if !state.nil? || block
            required state.nil? ? proc { instance_eval(&block) == false } : state == false
          else
            optional?
          end
        end

        # Writer to make field optional.
        #
        # @see RailsAdmin::Config::Fields::Base.optional
        def optional=(state)
          optional(state)
        end

        # Reader for field's type
        def type
          @type ||= self.class.name.to_s.demodulize.underscore.to_sym
        end

        # Reader for field's value
        def value
          bindings[:object].safe_send(name)
        rescue NoMethodError => e
          raise e.exception <<~ERROR
            #{e.message}
            If you want to use a RailsAdmin virtual field(= a field without corresponding instance method),
            you should declare 'formatted_value' in the field definition.
              field :#{name} do
                formatted_value{ bindings[:object].call_some_method }
              end
          ERROR
        end

        # Reader for nested attributes
        register_instance_option :nested_form do
          false
        end

        # Allowed methods for the field in forms
        register_instance_option :allowed_methods do
          [method_name]
        end

        def generic_help
          "#{required? ? I18n.translate('admin.form.required') : I18n.translate('admin.form.optional')}. "
        end

        def generic_field_help
          model = abstract_model.model_name.underscore
          model_lookup = :"admin.help.#{model}.#{name}"
          translated = I18n.translate(model_lookup, help: generic_help, default: [generic_help])
          (translated.is_a?(Hash) ? translated.to_a.first[1] : translated).html_safe
        end

        def parse_value(value)
          value
        end

        def parse_input(_params)
          # overridden
        end

        def inverse_of
          nil
        end

        def method_name
          name
        end

        def form_default_value
          (default_value if bindings[:object].new_record? && value.nil?)
        end

        def form_value
          form_default_value.nil? ? formatted_value : form_default_value
        end
      end
    end
  end
end