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' => {
    # 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 205
def attributes_hash_is_empty?(hash, klass)
  ignore_column_types = [:boolean]
  hash.all? do |key,value|
    # convert any possible multi-parameter attributes like 'created_at(5i)' to simply 'created_at'
    parts = key.to_s.split('(')
    #old style date form management... ignore them too
    ignore_column_types = [:boolean, :datetime, :date, :time] if parts.length > 1
    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 and 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.
    next true if column and value == column.default.to_s

    if value.is_a?(Hash)
      attributes_hash_is_empty?(value, klass)
    elsif value.is_a?(Array)
      value.any? {|id| id.respond_to?(:empty?) ? !id.empty? : true}
    else
      value.respond_to?(:empty?) ? value.empty? : false
    end
  end
end
column_plural_assocation_value_from_value(column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 145
def column_plural_assocation_value_from_value(column, value)
  # it's an array of ids
  if value && !value.empty?
    ids = value.select {|id| id.respond_to?(:empty?) ? !id.empty? : true}
    ids.empty? ? [] : column.association.klass.find(ids)
  end
end
column_value_from_param_hash_value(parent_record, column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 153
def column_value_from_param_hash_value(parent_record, column, value)
  # this is just for backwards compatibility. we should clean this up in 2.0.
  if column.form_ui == :select
    ids = if column.singular_association?
      value[:id]
    else
      value.values.collect {|hash| hash[:id]}
    end
    (ids and not ids.empty?) ? column.association.klass.find(ids) : nil

  elsif column.singular_association?
    manage_nested_record_from_params(parent_record, column, value)
  elsif column.plural_association?
    value.collect {|key_value_pair| manage_nested_record_from_params(parent_record, column, key_value_pair[1])}.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 125
def column_value_from_param_simple_value(parent_record, column, value)
  if column.singular_association?
    # it's a single id
    column.association.klass.where(:id=> value).first if value and not value.empty?
  elsif column.plural_association?
    column_plural_assocation_value_from_value(column, value)
  elsif column.column && column.column.number? && [:i18n_number, :currency].include?(column.options[:format])
    self.class.i18n_number_to_native_format(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
    if value.is_a? String and value.empty? and !column.column.nil? and column.column.null
      nil
    else
      column.stripped_value(value)
    end
  end
end
column_value_from_param_value(parent_record, column, value) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 116
def column_value_from_param_value(parent_record, column, value)
  # convert the value, possibly by instantiating associated objects
  if value.is_a?(Hash)
    column_value_from_param_hash_value(parent_record, column, value)
  else
    column_value_from_param_simple_value(parent_record, column, value)
  end
end
create_nested_record(parent_column, parent_record) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 172
def create_nested_record(parent_column, parent_record)
  klass = parent_column.association.klass
  
  if klass.authorized_for?(:crud_type => :create)
    if parent_column.singular_association?
      return parent_record.send("build_#{parent_column.name}")
    else
      return parent_record.send(parent_column.name).build
    end
  end
end
find_nested_record(id, parent_column, parent_record) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 184
def find_nested_record(id, parent_column, parent_record)
  klass = parent_column.association.klass
  if id
    current = parent_record.send(parent_column.name)
    # modifying the current object of a singular association
    if current && current.is_a?(ActiveRecord::Base) && current.id.to_s == id
      return current
    # modifying one of the current objects in a plural association
    elsif current && current.respond_to?(:any?) && current.any? {|o| o.id.to_s == id}
      return current.detect {|o| o.id.to_s == id}
    # attaching an existing but not-current object
    else
      return klass.find(id)
    end
  else
    Rails.logger.info("Activescaffold find_nested_record missing id")
  end
end
manage_nested_record_from_params(parent_record, column, attributes) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 89
def manage_nested_record_from_params(parent_record, column, attributes)
  record = nested_record_for_action(parent_record, column, attributes)
  if record
    record_columns = active_scaffold_config_for(column.association.klass).subform.columns
    update_record_from_params(record, record_columns, attributes)
    record.unsaved = true
  end
  record
end
nested_record_for_action(parent_record, column, attributes) click to toggle source
# File lib/active_scaffold/attribute_params.rb, line 99
def nested_record_for_action(parent_record, column, attributes)
  associated_action = attributes.delete(:associated_action)

  case associated_action.to_sym
  when :create then create_nested_record(column, parent_record)
  when :update then find_nested_record(attributes[:id], column, parent_record)
  when :empty then nil
  when :delete then nil
  when :create_or_empty then
    if column.show_blank_record && attributes_hash_is_empty?(attributes, column.association.klass)
      nil
    else
      create_nested_record(column, parent_record)
    end
  end
end
update_record_from_params(parent_record, columns, attributes) 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 38
def update_record_from_params(parent_record, columns, attributes)
  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.to_sym
    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|
    # 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.has_key? p}
    end

    if multi_parameter_attributes.has_key? column.name
      parent_record.send(:assign_multiparameter_attributes, multi_parameter_attributes[column.name])
    elsif attributes.has_key? column.name
      value = column_value_from_param_value(parent_record, column, attributes[column.name]) 

      # we avoid assigning a value that already exists because otherwise has_one associations will break (AR bug in has_one_association.rb#replace)
      parent_record.send("#{column.name}=", value) unless parent_record.send(column.name) == value
      
    # plural associations may not actually appear in the params if all of the options have been unselected or cleared away.
    # the "form_ui" check is necessary, becuase without it we have problems
    # with subforms. the UI cuts out deep associations, which means they're not present in the
    # params even though they're in the columns list. the result is that associations were being
    # emptied out way too often.
    elsif column.form_ui and column.plural_association?
      parent_record.send("#{column.name}=", [])
    end
  end

  if parent_record.new_record?
    parent_record.class.reflect_on_all_associations.each do |a|
      next unless [:has_one, :has_many].include?(a.macro) and not a.options[:through]
      next unless association_proxy = parent_record.send(a.name)

      raise ActiveScaffold::ReverseAssociationRequired, "Association #{a.name}: In order to support :has_one and :has_many where the parent record is new and the child record(s) validate the presence of the parent, ActiveScaffold requires the reverse association (the belongs_to)." unless a.reverse

      association_proxy = [association_proxy] if a.macro == :has_one
      association_proxy.each { |record| record.send("#{a.reverse}=", parent_record) }
    end
  end

  parent_record
end