module ActiveScaffold::AttributeParams

Provides support for param hashes assumed to be model attributes. Support is primarily needed for creating/editing associated records using a nested hash structure.

Paradigm Params Hash (should write unit tests on this):

params[:record] = {
  # a simple record attribute
  'name' => 'John',
  # a plural association hash
  'roles' => {
    # hack to be able to clear roles
    '0' => ''
    # associate with an existing role
    '5' => {'id' => 5}
    # associate with an existing role and edit it
    '6' => {'id' => 6, 'name' => 'designer'}
    # create and associate a new role
    '124521' => {'name' => 'marketer'}
  }
  # a singular association hash
  'location' => {'id' => 12, 'city' => 'New York'}
}

Simpler association structures are also supported, like:

params[:record] = {
  # a simple record attribute
  'name' => 'John',
  # a plural association ... all ids refer to existing records
  'roles' => ['5', '6'],
  # a singular association ... all ids refer to existing records
  'location' => '12'

}

Constants

MULTIPART_IGNORE_TYPES

old style date form management… ignore them

Protected Instance Methods

association_value_from_param_simple_value(parent_record, column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 150
def association_value_from_param_simple_value(parent_record, column, value)
  if column.association.singular?
    # value may be Array if using update_columns in field_search with multi-select
    klass = column.association.klass(parent_record)
    # find_by needed when using update_columns in type foreign type key of polymorphic association,
    # and foreign key had value, it will try to find record with id of previous type
    klass&.find_by(klass&.primary_key => value) if value.present? && !value.is_a?(Array)
  else # column.association.collection?
    column_plural_assocation_value_from_value(column, Array(value))
  end
end
attributes_hash_is_empty?(hash, klass) click to toggle source

Determines whether the given attributes hash is “empty”. This isn’t a literal emptiness - it’s an attempt to discern whether the user intended it to be empty or not.

# File lib/active_scaffold/attribute_params.rb, line 253
def attributes_hash_is_empty?(hash, klass)
  hash.all? do |key, value|
    # convert any possible multi-parameter attributes like 'created_at(5i)' to simply 'created_at'
    column_name = key.to_s.split('(', 2)[0]

    # datetimes will always have a value. so we ignore them when checking whether the hash is empty.
    # this could be a bad idea. but the current situation (excess record entry) seems worse.
    next true if mulitpart_ignored?(key, klass)

    # defaults are pre-filled on the form. we can't use them to determine if the user intends a new row.
    # booleans always have value, so they are ignored if not changed from default
    next true if default_value?(column_name, klass, value)

    if params_hash? value
      attributes_hash_is_empty?(value, klass)
    elsif value.is_a?(Array)
      value.all?(&:blank?)
    else
      value.respond_to?(:empty?) ? value.empty? : false
    end
  end
end
belongs_to_counter_cache_hack?(association, value) click to toggle source

workaround for updating counters twice bug on rails4 (github.com/rails/rails/pull/14849) rails 5 needs this hack for belongs_to, when selecting record, not creating new one (value is Hash) TODO: remove when rails5 support is removed

# File lib/active_scaffold/attribute_params.rb, line 39
def belongs_to_counter_cache_hack?(association, value)
  !params_hash?(value) && association.belongs_to? && association.counter_cache_hack?
end
build_record_from_params?(params, column, record) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 212
def build_record_from_params?(params, column, record)
  current = record.send(column.name)
  return true if column.association.collection? && !column.show_blank_record?(current)
  klass = column.association.klass(record)
  klass && !attributes_hash_is_empty?(params, klass)
end
column_plural_assocation_value_from_value(column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 176
def column_plural_assocation_value_from_value(column, value)
  # it's an array of ids
  if value.present?
    ids = value.select(&:present?)
    ids.empty? ? [] : column.association.klass.find(ids)
  else
    []
  end
end
column_value_for_datetime_type(parent_record, column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 138
def column_value_for_datetime_type(parent_record, column, value)
  new_value = self.class.condition_value_for_datetime(column, value, datetime_conversion_for_value(column))
  if new_value.nil? && value.present?
    parent_record.errors.add column.name, :invalid
  end
  new_value
end
column_value_for_month_type(parent_record, column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 146
def column_value_for_month_type(parent_record, column, value)
  Date.parse("#{value}-01")
end
column_value_from_param_hash_value(parent_record, column, value, avoid_changes = false) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 186
def column_value_from_param_hash_value(parent_record, column, value, avoid_changes = false)
  if column.association&.singular?
    manage_nested_record_from_params(parent_record, column, value, avoid_changes)
  elsif column.association&.collection?
    # HACK: to be able to delete all associated records, hash will include "0" => ""
    values = value.values.reject(&:blank?)
    values.collect { |val| manage_nested_record_from_params(parent_record, column, val, avoid_changes) }.compact
  else
    value
  end
end
column_value_from_param_simple_value(parent_record, column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 162
def column_value_from_param_simple_value(parent_record, column, value)
  if column.association
    association_value_from_param_simple_value(parent_record, column, value)
  elsif column.convert_to_native?
    column.number_to_native(value)
  elsif value.is_a?(String) && value.empty? && !column.virtual?
    # convert empty strings into nil. this works better with 'null => true' columns (and validations),
    # for 'null => false' columns is just converted to default value from column
    column.default_for_empty_value
  else
    value
  end
end
column_value_from_param_value(parent_record, column, value, avoid_changes = false) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 118
def column_value_from_param_value(parent_record, column, value, avoid_changes = false)
  # convert the value, possibly by instantiating associated objects
  form_ui = column.form_ui || column.column&.type
  if form_ui && respond_to?("column_value_for_#{form_ui}_type", true)
    send("column_value_for_#{form_ui}_type", parent_record, column, value)
  elsif params_hash? value
    column_value_from_param_hash_value(parent_record, column, params_hash(value), avoid_changes)
  else
    column_value_from_param_simple_value(parent_record, column, value)
  end
end
datetime_conversion_for_value(column) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 130
def datetime_conversion_for_value(column)
  if column.column
    column.column_type == :date ? :to_date : :to_time
  else
    :to_time
  end
end
default_value?(column_name, klass, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 286
def default_value?(column_name, klass, value)
  casted_value = ActiveScaffold::OrmChecks.cast(klass, column_name, value)
  default_value = ActiveScaffold::OrmChecks.default_value(klass, column_name)
  casted_value == default_value
end
find_or_create_for_params(params, parent_column, parent_record) click to toggle source

Attempts to create or find an instance of the klass of the association in parent_column from the request parameters given. If params exists it will attempt to find an existing object otherwise it will build a new one.

# File lib/active_scaffold/attribute_params.rb, line 222
def find_or_create_for_params(params, parent_column, parent_record)
  current = parent_record.send(parent_column.name)
  klass = parent_column.association.klass(parent_record)
  if params.key? klass.primary_key
    record_from_current_or_find(klass, params[klass.primary_key], current)
  elsif klass.authorized_for?(:crud_type => :create)
    association = parent_column.association
    record = klass.new
    if association.reverse_association&.belongs_to? && (association.collection? || current.nil?)
      record.send("#{parent_column.association.reverse}=", parent_record)
    end
    record
  end
end
manage_nested_record_from_params(parent_record, column, attributes, avoid_changes = false) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 198
def manage_nested_record_from_params(parent_record, column, attributes, avoid_changes = false)
  return nil unless build_record_from_params?(attributes, column, parent_record)
  record = find_or_create_for_params(attributes, column, parent_record)
  if record
    record_columns = active_scaffold_config_for(record.class).subform.columns
    prev_constraints = record_columns.constraint_columns
    record_columns.constraint_columns = [column.association.reverse].compact
    update_record_from_params(record, record_columns, attributes, avoid_changes)
    record_columns.constraint_columns = prev_constraints
    record.unsaved = true
  end
  record
end
mulitpart_ignored?(param_name, klass) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 279
def mulitpart_ignored?(param_name, klass)
  column_name, multipart = param_name.to_s.split('(', 2)
  return false unless multipart
  column_type = ActiveScaffold::OrmChecks.column_type(klass, column_name)
  MULTIPART_IGNORE_TYPES.include?(column_type) if column_type
end
multi_parameter_attributes(attributes) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 43
def multi_parameter_attributes(attributes)
  params_hash(attributes).each_with_object({}) do |(k, v), result|
    next unless k.include? '('
    column_name = k.split('(').first
    result[column_name] ||= []
    result[column_name] << [k, v]
  end
end
record_from_current_or_find(klass, id, current) click to toggle source

Attempts to find an instance of klass (which must be an ActiveRecord object) with id primary key Returns record from current if it’s included or find from DB

# File lib/active_scaffold/attribute_params.rb, line 239
def record_from_current_or_find(klass, id, current)
  if current.is_a?(ActiveRecord::Base) && current.id.to_s == id
    # modifying the current object of a singular association
    current
  elsif current.respond_to?(:any?) && current.any? { |o| o.id.to_s == id }
    # modifying one of the current objects in a plural association
    current.detect { |o| o.id.to_s == id }
  else # attaching an existing but not-current object
    klass.find(id)
  end
end
update_column_association(parent_record, column, attribute, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 101
def update_column_association(parent_record, column, attribute, value)
  if belongs_to_counter_cache_hack?(column.association, attribute)
    parent_record.send "#{column.association.foreign_key}=", value&.id
    parent_record.association(column.name).target = value
  elsif column.association.collection? && column.association.through_singular?
    through = column.association.through_reflection.name
    through_record = parent_record.send(through)
    through_record ||= parent_record.send "build_#{through}"
    through_record.send "#{column.association.source_reflection.name}=", value
  else
    parent_record.send "#{column.name}=", value
  end
rescue ActiveRecord::RecordNotSaved
  parent_record.errors.add column.name, :invalid
  parent_record.association(column.name).target = value
end
update_column_from_params(parent_record, column, attribute, avoid_changes = false) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 82
def update_column_from_params(parent_record, column, attribute, avoid_changes = false)
  value = column_value_from_param_value(parent_record, column, attribute, avoid_changes)
  if column.association
    if avoid_changes
      parent_record.association(column.name).target = value
      parent_record.send("#{column.association.foreign_key}=", value&.id) if column.association.belongs_to?
    else
      update_column_association(parent_record, column, attribute, value)
    end
  else
    parent_record.send "#{column.name}=", value
  end
  # needed? probably done on find_or_create_for_params, need more testing
  if column.association&.reverse_association&.belongs_to?
    Array(value).each { |v| v.send("#{column.association.reverse}=", parent_record) if v.new_record? }
  end
  value
end
update_record_from_params(parent_record, columns, attributes, avoid_changes = false) click to toggle source

Takes attributes (as from params) and applies them to the parent_record. Also looks for association attributes and attempts to instantiate them as associated objects.

This is a secure way to apply params to a record, because it’s based on a loop over the columns set. The columns set will not yield unauthorized columns, and it will not yield unregistered columns.

# File lib/active_scaffold/attribute_params.rb, line 57
def update_record_from_params(parent_record, columns, attributes, avoid_changes = false)
  crud_type = parent_record.new_record? ? :create : :update
  return parent_record unless parent_record.authorized_for?(:crud_type => crud_type)

  multi_parameter_attrs = multi_parameter_attributes(attributes)

  columns.each_column(for: parent_record, crud_type: crud_type, flatten: true) do |column|
    # Set any passthrough parameters that may be associated with this column (ie, file column "keep" and "temp" attributes)
    column.params.select { |p| attributes.key? p }.each { |p| parent_record.send("#{p}=", attributes[p]) }

    if multi_parameter_attrs.key? column.name.to_s
      parent_record.send(:assign_multiparameter_attributes, multi_parameter_attrs[column.name.to_s])
    elsif attributes.key? column.name
      update_column_from_params(parent_record, column, attributes[column.name], avoid_changes)
    end
  rescue StandardError => e
    message = "on the ActiveScaffold column = :#{column.name} for #{parent_record.inspect} "\
              "(value from params #{attributes[column.name].inspect})"
    Rails.logger.error "#{e.class.name}: #{e.message} -- #{message}"
    raise
  end

  parent_record
end