ruby-on-rails - rails - strong parameters nested attributes
Hacer params en blanco[] nil (12)
Así es como lo hice.
def remove_empty_params(param, key)
param[key] = param[key].reject { |c| c.empty? }
end
y llámalo con
remove_empty_params(params[:shipments], :included_clients)
No hay necesidad de ser súper complicado en el modelo. Y de esta manera puedes controlar qué parámetros se limpian.
params = {
"shipments"=>{
"included_clients" => ["", "4"]
}
}
se convertirá en
>> params["shipments"]
=> {"included_clients" => ["4"] }
Cuando un usuario envía un formulario y deja ciertos campos en blanco, se guardan como en blanco en la base de datos. Me gustaría recorrer la colección params [: user] (por ejemplo) y si un campo está en blanco, configúrelo en nil antes de actualizar los atributos. No puedo entender cómo hacerlo, ya que la única forma que conozco para iterar crea nuevos objetos:
coll = params[:user].each do |c|
if c == ""
c = nil
end
end
Gracias.
Chris
Aquí hay un análisis recursivo de parámetros que tienen valores blancos.
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
Considere lo que está haciendo aquí usando filtros en el controlador para afectar el comportamiento de un modelo cuando se guarda o actualiza. Creo que un método mucho más limpio sería una llamada before_save
en el modelo o un observador. De esta manera, obtendrá el mismo comportamiento sin importar de dónde se origine el cambio, ya sea a través de un controlador, la consola o incluso cuando se ejecutan procesos por lotes.
Ejemplo:
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
Esto produce el comportamiento esperado:
>> 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
>>
Generalicé una respuesta e hice un gancho / extensión que se puede usar como inicializador. Esto permite que se utilice en múltiples modelos. Lo he agregado como parte de mi repo de ActiveRecordHelpers en GitHub
Podría hacer esto usando inyectar, lo que es obvio en cuanto a lo que está sucediendo.
params = params.inject({}){|new_params, kv|
new_params[kv[0]] = kv[1].blank? ? nil : kv[1]
new_params
}
También hay un truco que puede hacer con la fusión fusionándose consigo mismo y pasando un bloque para manejar el nuevo valor (aunque este no es realmente el uso que se pretende, pero es más conciso)
params.merge(params){|k, v| v.blank? ? nil : v}
Por lo general, recomendaría que la funcionalidad se mueva al modelo, como se indica en otras respuestas, esto significa que obtendrá el mismo comportamiento sin importar de dónde se origine el cambio.
Sin embargo, no creo que en este caso sea correcto. El efecto que se está notando es simplemente no poder codificar la diferencia entre una cadena en blanco y un valor nulo en la solicitud HTTP. Por esta razón, debe remediarse a nivel de controlador. También significa que en otros lugares todavía es posible almacenar una cadena vacía en el modelo (lo que podría haber por una razón legítima, y si no es así, es fácil de cubrir con validaciones estándar).
El código que estoy usando para superar este problema es:
# 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
...
Traté de mantener el código lo más conciso posible, aunque la legibilidad ha sufrido un poco, así que aquí hay un caso de prueba para demostrar su funcionalidad:
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
Esto se puede usar en un controlador así:
...
@book.update_attributes(clean_params[:book])
...
Puede usar la gema attribute_normalizer y el normalizador en blanco que transformará las cadenas vacías en valores nulos.
Si sabe para qué atributos desea codificar espacios en blanco como nils, puede usar la siguiente anulación del definidor de atributos:
def colour=(colour)
super(colour.blank? ? nil : colour)
end
Un poco voluminoso si tienes muchos atributos que cubrir.
Si solo quieres eliminar los espacios en blanco, puedes hacer params.delete_if {|k,v| v.blank?}
params.delete_if {|k,v| v.blank?}
.
Una buena joya para manejar esto en el modelo: https://github.com/rmm5t/strip_attributes
Define un gancho de before_validation
que recorta los espacios en blanco y establece cadenas vacías en cero.
Utilice el método de recopilación "en el lugar" (también conocido como mapa)
params[:user].collect! {|c| c == "" ? nil : c}
before_save me parece la ubicación incorrecta, ¿qué ocurre si desea utilizar el valor antes de guardar? Así que anulé a los setters en su lugar:
# 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
Solo para completar, pongo lo siguiente en 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
y úsalo así:
class MyModel
include MyModelExtensions
nil_if_blank :attr1, :attr2
end