views:

325

answers:

6

When a user submits a form and leaves certain fields blank, they get saved as blank in the DB. I would like to iterate through the params[:user] collection (for example) and if a field is blank, set it to nil before updating attributes. I can't figure out how to do this though as the only way I know to iterate creates new objects:

coll = params[:user].each do |c|
    if c == ""
       c = nil
    end
ends

Thanks.

+1  A: 

You could do this using inject, which is 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 also a hack you can do with merge by merging with itself, and passing a block to handle the new value (although this isn't really the intended use for it, but it is more concise)

params.merge(params){|k, v| v.blank? ? nil : v}
madlep
+1  A: 

If you just want to kill the blanks, you can just do params.delete_if {|k,v| v.blank?}.

Chuck
Careful with this. If you're using update_attributes, and you have fields which are allowed to be blank, the user will never be able to clear those fields if you remove them from the params hash.
jdl
A: 

Use the "in place" collect method (also known as map!)

params[:user].collect! {|c| c == "" ? nil : c}
glenn jackman
+1  A: 

Chris,

Here is a recursive parsing of params 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
Vlad Zloteanu
Chris,Please also notice that it is not good to do something like:container.each{||... #modifying container here}if you plan to delete/add elements in container (not the case in here, but just keep that in mind)
Vlad Zloteanu
+2  A: 

Consider what you're doing here by using filters in the controller to affect how a model behaves when saved or updated. I think a much cleaner method would be a before_save call back in the model or an observer. This way, you're getting the same behavior no matter where the change originates from, whether its via a controller, the console or even when 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 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
>>
Matt Haley
"before_save :blank_if_nil" should be "before_save :nil_if_blank". I like this approach the best so far, with the code pushed into the model.
jdl
@jdl thanks for pointing out the typo. Fixed.
Matt Haley
A: 

Ordinarily I would encourage functionality to be moved into the model, as stated in other answers this means that you will get the same behavior no matter where the change originates from.

However, I don't think in this case it is correct. The affect being noticed is purely down to not being able to encode the difference between a blank string and nil value in the HTTP request. For this reason it should be remedied at the controller level. It also means that in other places it is still possible to store an empty string in the model (which there could be for a legitimate reason for, and if not it is simple to cover with standard validations).

The code I'm using to overcome 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 as concise as possible, although readability has suffered somewhat, so here is a test 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 can then be used in a controller like so:

...
@book.update_attributes(clean_params[:book])
...
Theozaurus