Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept multiple attributes #30 #33

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/active_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
require 'active_form/form'
require 'active_form/form_collection'
require 'active_form/form_definition'
require 'active_form/too_many_records'
require 'active_form/errors'
require 'active_form/multiparameter_attribute'
require 'active_form/view_helpers'

module ActiveForm
Expand Down
53 changes: 52 additions & 1 deletion lib/active_form/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,21 @@ def initialize(model)
end

def submit(params)
multi_parameter_attributes = []

params.each do |key, value|
if nested_params?(value)
if key.to_s.include?("(")
multi_parameter_attributes << [ key, value ]
elsif nested_params?(value)
fill_association_with_attributes(key, value)
else
send("#{key}=", value)
end
end

if multi_parameter_attributes.present?
assign_multiparameter_attributes(multi_parameter_attributes)
end
end

def get_model(assoc_name)
Expand Down Expand Up @@ -164,6 +172,49 @@ def collect_errors_from(validatable_object)
errors.add(key, error)
end
end

def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
)
end

def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
end
if errors.present?
error_descriptions = errors.map { |ex| ex.message }.join(",")
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end

def extract_callstack_for_multiparameter_attributes(pairs)
attributes = {}

pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] ||= {}

parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
end

attributes
end

def type_cast_attribute_value(multiparameter_name, value)
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
end

def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end
end

end
22 changes: 22 additions & 0 deletions lib/active_form/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module ActiveForm
class TooManyRecords < RuntimeError;end

class AttributeAssignmentError < RuntimeError
attr_reader :exception, :attribute
def initialize(message, exception, attribute)
super(message)
@exception = exception
@attribute = attribute
end
end

# Raised when there are multiple errors while doing a mass assignment through the +attributes+
# method. The exception has an +errors+ property that contains an array of AttributeAssignmentError
# objects, each corresponding to the error while assigning to an attribute.
class MultiparameterAssignmentErrors < RuntimeError
attr_reader :errors
def initialize(errors)
@errors = errors
end
end
end
52 changes: 51 additions & 1 deletion lib/active_form/form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,19 @@ def submit(params)
@model = parent.send("build_#{association_name}") unless call_reject_if(params_for_current_scope(params))
end

multi_parameter_attributes = []

params.each do |key, value|
if nested_params?(value)
if key.to_s.include?("(")
multi_parameter_attributes << [ key, value ]
elsif nested_params?(value)
fill_association_with_attributes(key, value)
else
model.send("#{key}=", value)
end
end

assign_multiparameter_attributes(multi_parameter_attributes) if multi_parameter_attributes.present?
end

def get_model(assoc_name)
Expand Down Expand Up @@ -199,9 +205,53 @@ def collect_errors_from(validatable_object)
else
attribute
end
puts "adding #{key} error: #{error} from #{validatable_object}"
errors.add(key, error)
end
end

def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
)
end

def execute_callstack_for_multiparameter_attributes(callstack)
errors = []
callstack.each do |name, values_with_empty_parameters|
begin
send("#{name}=", MultiparameterAttribute.new(self, name, values_with_empty_parameters).read_value)
rescue => ex
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name} (#{ex.message})", ex, name)
end
end
if errors.present?
error_descriptions = errors.map { |ex| ex.message }.join(",")
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end

def extract_callstack_for_multiparameter_attributes(pairs)
attributes = {}

pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
attributes[attribute_name] ||= {}

parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
end

attributes
end

def type_cast_attribute_value(multiparameter_name, value)
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
end

def find_parameter_position(multiparameter_name)
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
end
end

end
93 changes: 93 additions & 0 deletions lib/active_form/multiparameter_attribute.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
module ActiveForm
class MultiparameterAttribute #:nodoc:
attr_reader :object, :name, :values, :column

def initialize(object, name, values)
@object = object
@name = name
@values = values
end

def read_value
return if values.values.compact.empty?

@column = object.model.class.reflect_on_aggregation(name.to_sym) || object.model.column_for_attribute(name)
klass = column.klass

if klass == Time
read_time
elsif klass == Date
read_date
else
read_other(klass)
end
end

private

def instantiate_time_object(set_values)
Time.zone.local(*set_values)
end

def read_time
# If column is a :time (and not :date or :timestamp) there is no need to validate if
# there are year/month/day fields
if column.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
end
else
# else column is a timestamp, so if Date bits were not provided, error
validate_required_parameters!([1,2,3])

# If Date bits were provided but blank, then return nil
return if blank_date_parameter?
end

max_position = extract_max_param(6)
set_values = values.values_at(*(1..max_position))
# If Time bits are not there, then default to 0
(3..5).each { |i| set_values[i] = set_values[i].presence || 0 }
instantiate_time_object(set_values)
end

def read_date
return if blank_date_parameter?
set_values = values.values_at(1,2,3)
begin
Date.new(*set_values)
rescue ArgumentError # if Date.new raises an exception on an invalid date
instantiate_time_object(set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
end
end

def read_other(klass)
max_position = extract_max_param
positions = (1..max_position)
validate_required_parameters!(positions)

set_values = values.values_at(*positions)
klass.new(*set_values)
end

# Checks whether some blank date parameter exists. Note that this is different
# than the validate_required_parameters! method, since it just checks for blank
# positions instead of missing ones, and does not raise in case one blank position
# exists. The caller is responsible to handle the case of this returning true.
def blank_date_parameter?
(1..3).any? { |position| values[position].blank? }
end

# If some position is not provided, it errors out a missing parameter exception.
def validate_required_parameters!(positions)
if missing_parameter = positions.detect { |position| !values.key?(position) }
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
end
end

def extract_max_param(upper_cap = 100)
[values.keys.max, upper_cap].min
end
end
end
4 changes: 0 additions & 4 deletions lib/active_form/too_many_records.rb

This file was deleted.

4 changes: 2 additions & 2 deletions test/dummy/app/forms/project_form.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
class ProjectForm < ActiveForm::Base
attributes :name, :description, :owner_id
attributes :name, :description, :owner_id, :published_at

association :tasks do
attributes :name, :description, :done
attributes :name, :description, :done, :finished_at

association :sub_tasks do
attributes :name, :description, :done
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddPublishedAtToProjects < ActiveRecord::Migration
def change
add_column :projects, :published_at, :datetime
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddFinishedAtToTasks < ActiveRecord::Migration
def change
add_column :tasks, :finished_at, :datetime
end
end
4 changes: 3 additions & 1 deletion test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20140821074917) do
ActiveRecord::Schema.define(version: 20141227165333) do

create_table "answers", force: true do |t|
t.text "content"
Expand Down Expand Up @@ -110,6 +110,7 @@
t.datetime "created_at"
t.datetime "updated_at"
t.integer "owner_id"
t.datetime "published_at"
end

create_table "questions", force: true do |t|
Expand Down Expand Up @@ -169,6 +170,7 @@
t.datetime "created_at"
t.datetime "updated_at"
t.integer "assignment_id"
t.datetime "finished_at"
end

add_index "tasks", ["assignment_id"], name: "index_tasks_on_assignment_id"
Expand Down
Loading