[ACCEPTED]-Make blank params[] nil-collections
Consider what you're doing here by using 9 filters in the controller to affect how 8 a model behaves when saved or updated. I 7 think a much cleaner method would be a before_save
call 6 back in the model or an observer. This way, you're 5 getting the same behavior no matter where 4 the change originates from, whether its 3 via a controller, the console or even when 2 running batch processes.
Example:
class Customer < ActiveRecord::Base
NULL_ATTRS = %w( middle_name )
before_save :nil_if_blank
protected
def nil_if_blank
NULL_ATTRS.each { |attr| self[attr] = nil if self[attr].blank? }
end
end
This yields 1 the expected behavior:
>> c = Customer.new
=> #<Customer id: nil, first_name: nil, middle_name: nil, last_name: nil>
>> c.first_name = "Matt"
=> "Matt"
>> c.middle_name = "" # blank string here
=> ""
>> c.last_name = "Haley"
=> "Haley"
>> c.save
=> true
>> c.middle_name.nil?
=> true
>>
If you just want to kill the blanks, you 1 can just do params.delete_if {|k,v| v.blank?}
.
A good gem for handling this in the model: https://github.com/rmm5t/strip_attributes
It 2 defines a before_validation
hook that trims whitespaces and 1 sets empty strings to nil.
before_save seems like the wrong location 5 to me, what if you want to use the value 4 before saving. So I overrode the setters 3 instead:
# include through module or define under active_record
def self.nil_if_blank(*args)
args.each do |att|
define_method att.to_s + '=' do |val|
val = nil if val.respond_to?(:empty?) && val.empty?
super(val)
end
end
end
#inside model
nil_if_blank :attr1, :attr2
Just to be complete I put the following 2 in lib/my_model_extensions.rb
module MyModelExtensions
def self.included(base)
base.class_eval do
def self.nil_if_blank(*args)
args.each do |att|
define_method att.to_s + '=' do |val|
val = nil if val.respond_to?(:empty?) && val.empty?
super(val)
end
end
end
end
end
end
and use it 1 like this:
class MyModel
include MyModelExtensions
nil_if_blank :attr1, :attr2
end
In the ApplicationController:
class ApplicationController < ActionController::Base
def nilify(p)
p.transform_values!{|v| v.present? ? v : nil }
end
end
In your controller, modify 2 the strong parameters filter method to call 1 nilify:
class UserController < ApplicationController
def user_params
nilify params.require(:user).permit(:email, :name)
end
end
You could do this using inject, which is 6 obvious as to what is happening.
params = params.inject({}){|new_params, kv|
new_params[kv[0]] = kv[1].blank? ? nil : kv[1]
new_params
}
There is 5 also a hack you can do with merge by merging 4 with itself, and passing a block to handle 3 the new value (although this isn't really 2 the intended use for it, but it is more 1 concise)
params.merge(params){|k, v| v.blank? ? nil : v}
Ordinarily I would encourage functionality 20 to be moved into the model, as stated in 19 other answers this means that you will get 18 the same behavior no matter where the change 17 originates from.
However, I don't think in 16 this case it is correct. The affect being 15 noticed is purely down to not being able 14 to encode the difference between a blank 13 string and nil value in the HTTP request. For 12 this reason it should be remedied at the 11 controller level. It also means that in 10 other places it is still possible to store 9 an empty string in the model (which there 8 could be for a legitimate reason for, and 7 if not it is simple to cover with standard 6 validations).
The code I'm using to overcome 5 this problem is:
# application_controller.rb
...
def clean_params
@clean_params ||= HashWithIndifferentAccess.new.merge blank_to_nil( params )
end
def blank_to_nil(hash)
hash.inject({}){|h,(k,v)|
h.merge(
k => case v
when Hash : blank_to_nil v
when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
else v == "" ? nil : v
end
)
}
end
...
I've tried to keep the code 4 as concise as possible, although readability 3 has suffered somewhat, so here is a test 2 case to demonstrate its functionality:
require "test/unit"
class BlankToNilTest < Test::Unit::TestCase
def blank_to_nil(hash)
hash.inject({}){|h,(k,v)|
h.merge(
k => case v
when Hash : blank_to_nil v
when Array : v.map{|e| e.is_a?( Hash ) ? blank_to_nil(e) : e}
else v == "" ? nil : v
end
)
}
end
def test_should_convert_blanks_to_nil
hash = {:a => nil, :b => "b", :c => ""}
assert_equal( {:a => nil, :b => "b", :c => nil}, blank_to_nil(hash) )
end
def test_should_leave_empty_hashes_intact
hash = {:a => nil, :b => "b", :c => {}}
assert_equal( {:a => nil, :b => "b", :c => {}}, blank_to_nil(hash) )
end
def test_should_leave_empty_arrays_intact
hash = {:a => nil, :b => "b", :c => []}
assert_equal( {:a => nil, :b => "b", :c => []}, blank_to_nil(hash) )
end
def test_should_convert_nested_hashes
hash = {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => "", :g => "", :h => 5}, :i => "bar"}}
assert_equal( {:a => nil, :b => "b", :c => {:d => 2, :e => {:f => nil, :g => nil, :h => 5}, :i => "bar"}}, blank_to_nil(hash) )
end
def test_should_convert_nested_hashes_in_arrays
hash = {:book_attributes => [{:name => "b", :isbn => "" },{:name => "c", :isbn => "" }], :shelf_id => 2}
assert_equal( {:book_attributes => [{:name => "b", :isbn => nil},{:name => "c", :isbn => nil}], :shelf_id => 2}, blank_to_nil(hash))
end
def test_should_leave_arrays_not_containing_hashes_intact
hash = {:as => ["", nil, "foobar"]}
assert_equal( {:as => ["", nil, "foobar"]}, blank_to_nil(hash))
end
def test_should_work_with_mad_combination_of_arrays_and_hashes
hash = {:as => ["", nil, "foobar", {:b => "b", :c => "", :d => nil, :e => [1,2,3,{:a => "" }]}]}
assert_equal( {:as => ["", nil, "foobar", {:b => "b", :c => nil, :d => nil, :e => [1,2,3,{:a => nil}]}]}, blank_to_nil(hash))
end
end
This 1 can then be used in a controller like so:
...
@book.update_attributes(clean_params[:book])
...
You can use attribute_normalizer gem and use the blank normalizer 2 that will transform empty strings in nil 1 values.
Use the "in place" collect method (also 1 known as map!)
params[:user].collect! {|c| c == "" ? nil : c}
Chris,
Here is a recursive parsing of params 1 that have blanc values.
before_filter :process_params
......
private
def process_params
....
set_blanc_values_to_nil(params)
end
# Maybe move method to ApplicationController
# recursively sets all blanc values to nil
def set_blanc_values_to_nil!(my_hash)
my_hash.keys.each do |key|
val = my_hash[key]
next if val.nil?
my_hash[key] = nil if val.is_a?(String) && val.empty?
set_blanc_values_to_nil!(val) if val.is_a? Hash
end
end
I generalized an answer and made a hook/extension 3 that can be used as an initializer. This 2 allows it to be used across multiple models. I've 1 added it as part of my ActiveRecordHelpers repo on GitHub
Here is how I did it.
def remove_empty_params(param, key)
param[key] = param[key].reject { |c| c.empty? }
end
and call it with
remove_empty_params(params[:shipments], :included_clients)
No 3 need to get super tricky in the model. And 2 this way you can control which params get 1 cleaned up.
params = {
"shipments"=>{
"included_clients" => ["", "4"]
}
}
will turn into
>> params["shipments"]
=> {"included_clients" => ["4"] }
If you know which attributes you want to 4 encode blanks as nils for you can use the 3 following attribute setter override:
def colour=(colour)
super(colour.blank? ? nil : colour)
end
A bit 2 bulky if you have a lot of attributes to 1 cover though.
More Related questions
We use cookies to improve the performance of the site. By staying on our site, you agree to the terms of use of cookies.