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'

}

Protected Instance Methods

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 275
def attributes_hash_is_empty?(hash, klass)
  # old style date form management... ignore them too
  part_ignore_column_types = [:datetime, :date, :time]

  hash.all? do |key, value|
    # convert any possible multi-parameter attributes like 'created_at(5i)' to simply 'created_at'
    parts = key.to_s.split('(')
    column_name = parts.first
    column = klass.columns_hash[column_name]

    # booleans and 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 column && parts.length > 1 && part_ignore_column_types.include?(column.type)

    # defaults are pre-filled on the form. we can't use them to determine if the user intends a new row.
    default_value = column_default_value(column_name, klass, column)
    casted_value = column ? ActiveScaffold::Core.column_type_cast(value, column) : value
    next true if casted_value == default_value

    if value.is_a?(Hash)
      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
build_record_from_params(params, column, record) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 231
def build_record_from_params(params, column, record)
  current = record.send(column.name)
  klass = column.association.klass
  (column.plural_association? && !column.show_blank_record?(current)) || !attributes_hash_is_empty?(params, klass)
end
column_default_value(column_name, klass, column) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 304
def column_default_value(column_name, klass, column)
  return unless column
  if Rails.version < '4.2'
    column.default
  else
    column.type_cast_from_database(column.default)
  end
end
column_plural_assocation_value_from_value(column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 198
def column_plural_assocation_value_from_value(column, value)
  # it's an array of ids
  if value && !value.empty?
    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 166
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_from_param_hash_value(parent_record, column, value, avoid_changes = false) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 208
def column_value_from_param_hash_value(parent_record, column, value, avoid_changes = false)
  if column.singular_association?
    manage_nested_record_from_params(parent_record, column, value, avoid_changes)
  elsif column.plural_association?
    # HACK: to be able to delete all associated records, hash will include "0" => ""
    value.collect { |_, val| manage_nested_record_from_params(parent_record, column, val, avoid_changes) unless val.blank? }.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 174
def column_value_from_param_simple_value(parent_record, column, value)
  if column.singular_association?
    if value.present?
      if column.polymorphic_association?
        class_name = parent_record.send(column.association.foreign_type)
        class_name.constantize.find(value) if class_name.present?
      else
        # it's a single id
        column.association.klass.find(value)
      end
    end
  elsif column.plural_association?
    column_plural_assocation_value_from_value(column, Array(value))
  elsif column.number? && column.options[:format] && column.form_ui != :number
    column.number_to_native(value)
  else
    # convert empty strings into nil. this works better with 'null => true' columns (and validations),
    # and 'null => false' columns should just convert back to an empty string.
    # ... but we can at least check the ConnectionAdapter::Column object to see if nulls are allowed
    value = nil if value.is_a?(String) && value.empty? && !column.column.nil? && column.column.null
    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 146
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.try(:type)
  if form_ui && self.respond_to?("column_value_for_#{form_ui}_type", true)
    send("column_value_for_#{form_ui}_type", parent_record, column, value)
  elsif value.is_a?(Hash)
    column_value_from_param_hash_value(parent_record, column, value, avoid_changes)
  else
    column_value_from_param_simple_value(parent_record, column, value)
  end
end
counter_cache_hack?(column, value) click to toggle source

workaround for updating counters twice bug on rails4 (github.com/rails/rails/pull/14849) TODO: remove when pull request is merged and no version with bug is supported

# File lib/active_scaffold/attribute_params.rb, line 78
def counter_cache_hack?(column, value)
  return unless Rails.version >= '4.0' && !value.is_a?(Hash)
  column.association.try(:belongs_to?) && column.association.options[:counter_cache] && !column.association.options[:polymorphic]
end
datetime_conversion_for_value(column) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 158
def datetime_conversion_for_value(column)
  if column.column
    column.column.type == :date ? :to_date : :to_time
  else
    :to_time
  end
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 240
def find_or_create_for_params(params, parent_column, parent_record)
  current = parent_record.send(parent_column.name)
  klass = parent_column.association.klass
  if params.key? klass.primary_key
    record_from_current_or_find(klass, params[klass.primary_key], current)
  elsif klass.authorized_for?(:crud_type => :create)
    parent_column.association.klass.new
  end
end
hack_for_has_many_counter_cache(parent_record, column, value) click to toggle source

workaround to update counters when belongs_to changes on persisted record on Rails 3 workaround to update counters when polymorphic has_many changes on persisted record TODO: remove when rails3 support is removed and counter cache for polymorphic has_many association works on rails4

# File lib/active_scaffold/attribute_params.rb, line 39
def hack_for_has_many_counter_cache(parent_record, column, value)
  association = parent_record.association(column.name)
  counter_attr = association.send(:cached_counter_attribute_name)
  difference = value.select(&:persisted?).size - parent_record.send(counter_attr)

  if parent_record.new_record?
    if Rails.version == '4.2.0'
      parent_record.send "#{column.name}=", value
      parent_record.send "#{counter_attr}_will_change!"
    else # < 4.2 or > 4.2.0
      parent_record.send "#{counter_attr}=", difference
      parent_record.send "#{column.name}=", value
    end
  else
    # don't decrement counter for deleted records, on destroy they will update counter
    difference += (parent_record.send(column.name) - value).size
    association.send :update_counter, difference unless difference == 0
  end

  # update counters on old parents if belongs_to is changed
  value.select(&:persisted?).each do |record|
    key = record.send(column.association.foreign_key)
    parent_record.class.decrement_counter counter_attr, key if key && key != parent_record.id
  end
  parent_record.send "#{column.name}=", value if parent_record.persisted?
end
hack_for_has_many_counter_cache?(parent_record, column) click to toggle source

TODO: remove when #hack_for_has_many_counter_cache is not needed

# File lib/active_scaffold/attribute_params.rb, line 67
def hack_for_has_many_counter_cache?(parent_record, column)
  return unless column.association.try(:macro) == :has_many && parent_record.association(column.name).send(:has_cached_counter?)
  if Rails.version < '4.0' # rails 3 needs this hack always
    true
  else # rails 4 needs this hack for polymorphic has_many
    column.association.options[:as]
  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 219
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(column.association.klass).subform.columns
    record_columns.constraint_columns = [column.association.reverse]
    update_record_from_params(record, record_columns, attributes, avoid_changes)
    record.unsaved = true
  end
  record
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 252
def record_from_current_or_find(klass, id, current)
  if current && current.is_a?(ActiveRecord::Base) && current.id.to_s == id
    # modifying the current object of a singular association
    current
  elsif current && 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
save_record_to_association(record, association_name, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 264
def save_record_to_association(record, association_name, value)
  association = record.class.reflect_on_association(association_name) if association_name
  if association.try(:collection?)
    record.send(association_name) << value
  elsif association
    record.send("#{association_name}=", value)
  end
end
update_column_from_params(parent_record, column, attribute, avoid_changes = false) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 121
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 avoid_changes && column.plural_association?
    parent_record.association(column.name).target = value
  elsif counter_cache_hack?(column, attribute)
    parent_record.send "#{column.association.foreign_key}=", value.try(:id)
    parent_record.association(column.name).target = value
  else
    begin
      if hack_for_has_many_counter_cache?(parent_record, column)
        hack_for_has_many_counter_cache(parent_record, column, 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 if column.association
    end
  end
  if column.association && [:has_one, :has_many].include?(column.association.macro) && column.association.reverse
    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 88
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_attributes = {}
  attributes.each do |k, v|
    next unless k.include? '('
    column_name = k.split('(').first
    multi_parameter_attributes[column_name] ||= []
    multi_parameter_attributes[column_name] << [k, v]
  end

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

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

  parent_record
end