198: Edición individual múltiple (view original Railscast)
En el episodio 165 [verlo, leerlo], creábamos una aplicación que podía editar múltiples registros simultáneamente. Cada registro en la página índice tenía a su lado una casilla de forma que podíamos escoger qué items queríamos editar y luego actualizar los campos de dichos ítems.
Si seleccionamos los tres productos mostrados arriba y hacemos clic en "edit checked" podremos actualizar su categoría, nombre o precio, por ejemplo asignándoles a todos la categoría "groceries".
La restricción obvia que tenemos aquí es que cualquier cambio será aplicado sobr todos los productos marcados, así que en este episodio vamos a escribir una aplicación similar que mostrará un cada producto escogido con sus propios campos de forma que podamos actualizar múltiples productos a la vez en un único formulario.
Añadir los checkboxes
Vamos a empezar con un scaffolding básico para listar productos. Este código nos dará la posibilidad de editar productos individualmente, aunque por supuesto esto no es lo que queremos hacer. Al igual que hicimos la vez anterior añadiremos una casilla de verificación junto a cada ítem para que podamos escoger cuáles vamos a editar. Nos tocará realizar, entonces, el primer cambio en la vista index de los productos.
/app/views/products/index.html.erb
<h1>Products</h1>
<% form_tag edit_individual_products_path do %>
<table>
<thead>
<tr>
<th></th>
<th> </th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<% for product in @products %>
<tr>
<td><%= check_box_tag "product_ids[]", product.id %></td>
<td><%= product.name %></td>
<td><%= product.category.name %></td>
<td><%= number_to_currency product.price, :unit => "£" %></td>
<td><%= link_to "Edit", edit_product_path(product) %></td>
<td><%= link_to "Destroy", product_path(product), :confirm => "Are you sure?", :method => :delete %></td>
</tr>
<% end %>
</tbody>
</table>
<p><%= submit_tag "Edit Checked" %></p>
<% end %>
<p><%= link_to "New Product", new_product_path %></p>
Hemos hecho algunos cambios en el código generado automáticamente para añadir las casillas de verificación. En primer lugar hemos envuelto la tabla con un formulario utilizando form_tag.
<% form_tag edit_individual_products_path do %> <!-- table --> <% end %>
Los datos del formulario se enviarán a una nueva acción del controlador de productos llamada edit_individual (que en breve vamos a escribir). En la misma tabla añadiremos un nuevo elemento th y posteriormente en el cuerpo de la tabla añadiremos una nueva celda para alojar la casilla que declararemos con
<%= check_box_tag "product_ids[]", product.id %>
El nombre que le pasamos a check_box_tag es product_ids[], los corchetes vacíos quieren decir que vamos a pasar múltiples ids de producto en un array para todas las casillas marcadas, además tendremos que declarar los id de cada producto como el valor asignado a cada casilla.
Por último añadiremos una etiqueta submit para poder enviar nuestro formulario.
El formulario envía la información a una nueva acción llamada edit_individual, por lo que lo siguiente que haremos será escribir dicha acción en el controlador de productos así como otra llamada update_individual que es a la que nos enviará la acción edit_individual cuando actualicemos los productos escogidos.
/app/controllers/products_controller.rb
Dado que estamos añadiendo acciones a un recurso REST tendremos que hacer algunos cambios en el fichero de rutas.
/config/routes.rb
En routes.rb hemos añadido el agrumento :collection al recursoproducts con las dos nuevas acciones. :edit_individual será una petición POST dado que la enviaremos a través de un formulario. En realidad sólo estamos recuperando información (y por tanto una petición GET sería ideal) pero dado que estamos enviando varios identificadores hace falta usar un POST. Vamos a actualizar los registros en :update_individual, por lo que queda claro que esta petición será un PUT.
Si recargamos el índice de productos veremos casillas de verificación junto a cada producto y un botón que nos envía a la edición de dichos productos.
Aún no hemos escrito la plantilla edit_invidiual por lo que por ahora veremos un error si enviamos el formulario. Antes de crear dicha plantilla modificaremos la acción edit_individual para que recupere todos los productos seleccionados por las casillas marcadas.
/app/controllers/products_controller.rb
Pasemos al código de la vista edit_individual. Le damos un título a la página y creamos un formulario. Ahora bien, ¿qué deberíamos usar aquí, form_for o form-tag?. form_for está pensado para ser utilizado con un único módelo por lo que como nosotros estamos editando varios a la vez tendremos que utilizar form_tag. Le pasaremos la URL en la acción update y declararemos que el método es PUT.
Dentro del formulario iteraremos sobre la lista de productos y utilizaremos el helper fields_for para generar los campos de cada producto, y para eso le pasaremos la cadena products[] y el producto a fields_for. Esto insertará el id del producto en los corchetes vacíos de forma que dicho producto será pasado como un parámetro separado. Más adelante tendremos que poner los campos de formulario propiamente dichos, pero por ahora simplemente mostraremos el nombre del producto. Finalmente añadiremos un submit_tag.
/app/views/products/edit_individual.html.erb
Así, si marcamos las casillas de nuestros tres productos y hacemos clic en “Edit checked” iremos a la nueva página edit_individual y veremos que se listan nuestros tres productos.
Además del nombre también queremos que aparezcan los atributos de cada producto dentro de su campo de formulario correspondiente. Dado que estos campos también se muestran en las accions new y edit el propio formulario está por lo general separado en un parcial generado automáticamente. Este parcial contiene una etiqueta form_for que rodea los elementos de formulario para el modelo por lo que querremos reutilizar dichos campos pero sin la etiqueta form_for que los envuelve. Extraeremos los campos en otro parcial que usaremos tanto en los formularios de creación y edición individual como en nuestra nueva página de edición múltiple.
Pondremos los campos del formulario en un nuevo parcial llamado _fields.html.erb.
/app/views/products/_fields.html.erb
Ya podemos invocar a este parcial desde el parcial _form, pasándole la variable f.
/app/views/products/_form.html.erb
De vuelta en el código de la vista edit_individual, también podemos invocar este parcial para que se muestren los campos de cada producto.
/app/views/products/edit_individual.html.erb
Cuando recarguemos la página veremos los campos de formulario de cada uno de los productos seleccionados con sus valores correspondientes ya rellenados.
Podemos comprobar, mirando en la sección apropiada del código fuente de la página, que los campos de formulario tienen unos nombres interesantes. Cada nombre empieza por products y luego lleva el id del producto y el campo del nombre en corchetes. Esto quiere decir que los valores de los productos serán enviados como un hash.
<p> <label for="products_3_name">Name</label> <input id="products_3_name" name="products[3][name]" size="30" type="text" value="Stereolab T-Shirt" /> </p> <p> <label for="products_3_price">Price</label> <input id="products_3_price" name="products[3][price]" size="30" type="text" value="12.49" /> </p>
Podemos utilizar ese parámetro products en la acción update_individual para actualizar todos los productos cuando se envíe el formulario. Por fortuna ActiveRecord tiene un método para hacer precisamente esto llamado update. Este método recibe dos argumentos: el primero es o bien un único id o una lista de ids y el segundo es un hash de valores. Para actualizar nuestros productos podemos pasar las listas keys y values de nuestro parámetro products. Tras la actualización de los productos crearemos un mensaje flash y redirigiremos a la página de índice.
/app/controllers/products_controller.rb
def update_individual Product.update(params[:products].keys, params[:products].values) flash[:notice] = "Products updated" redirect_to products_url end
Todos los productos se encuentran actualmente en la categoría "Groceries" lo que está claramente mal. Vamos a cambiar la categoría de la camiseta y el reproductor de DVD y a reducir un poco su precio. Cuando enviemos el formulario seremos dirigidos a la página de listado y veremos que los productos han sido actualizados.
Esto quiere decir que nuestro formulario funciona tal y como queremos: ya podemos cambiar varios productos a la vez.
Validaciones
Queremos que si alguien intenta introducir valores no válidos en nuestro formulario los errores se muestren adecuadamente. Como el modelo Product no tiene todavía ninguna validación le añadiremos una que se asegure de que el precio es un valor numérico.
/app/models/product.rb
class Product < ActiveRecord::Base belongs_to :category validates_numericality_of :price end
Tenemos que controlar la validación en la acción update_individual del controlador de productos. El método update de ActiveRecord ignorará los errores de validación y pasará al siguiente registro si se encuentra con uno que no es válido. Pero aún no está todo perdido, porque update nos devolverá una lista de los productos que ha intentado actualizar y podemos utilizarla para determinar qué productos no fueron válidos.
Una forma de obtener los productos no válidos sería utilizar el método reject en la lista y en un bloque invocar la función valid? de cada producto para filtrar los que son válidos.
Product.update(params[:products].keys, params[:products].values).reject { |p| p.valid? }
El problema de este enfoque es que ejecutará la validación para cada producto otra vez. Hay una forma más eficiente que consiste en rechazar los productos que tengan un array errors vacío. Una vez que se haya evaluado esta lista de productos no válidos veremos si está vacía: si es así, haremos igual que antes y redirigiremos al índice, pero si no volveremos a la acción edit_individual visualizando los errores de cada uno de los productos que han dado error.
/app/controllers/products_controller.rb
def update_individual
@products = Product.update(params[:products].keys, params[:products].values).reject { |p| p.errors.empty? }
if @products.empty?
flash[:notice] = "Products updated"
redirect_to products_url
else
render :action => 'edit_individual'
end
end
Si intentamos otra vez editar la camiseta y el reproductor de DVD pero establecemos un valor incorrecto para el precio del reproductor volveremos a la página de edición y veremos el formulario de nuevo pero esta vez sólo aparecerá el reproductor de DVDs, mostrando además su correspondiente error de validación.
El título del panel del mensaje de error muestra "products[]" como nombre, pero esto es fácil de corregir. Los mensajes de error se generan en el parcial de los campos utilizando el método error_messages, que recibe un parámetro :object_name que podemos emplear para establecer el nombre a mostrar.
/app/views/products/_fields.html.erb
<%= f.error_messages :object_name => "product" %>
Una vez hecho esto, el mensaje de error dirá "product" en lugar de "products[]”.
Una Cosa Más
Ya hemos terminado prácticamente toda la funcionalidad que queríamos, pero como colofón de este episodio vamos a añadir una cosa más para que la aplicación sea aún más útil. Si queremos cambiar un único atributo de varios productos (por ejemplo el precio) el formulario de edición nos resultará un poco incómodo porque aparecen demasiados campos. Lo que haremos será que el usuario pueda escoger un único atributo de una lista para que puedan actualizar dicho atributo en múltiples registros sin tener que navegar por unformulario kilométrico.
Para esto añadiremos una caja de selección junto al botón "edit checked" en la página de índice de productos que nos permita escoger qué campos queremos editar. Podemos hacerlo añadiendo la siguiente línea inmediatamente antes de la etiqueta submit_tag en la vista index de productos.
/app/views/products/index.html.erb
<p><%= select_tag :field, options_for_select([["All Fields", ""], ["Name", "name"], ["Price", "price"], ["Category", "category_id"], ["Discontinued", "discontinued"]])%></p>
Y con esto ya podremos elegir si queremos editar todos los campos o sólo uno en concreto para los productos escogidos. Para restringir los campos dibujados en el formulario tendremos que cambiar el parcial del formulario para que sólo se muestre el campo seleccionado.
/app/views/products/_fields.html.erb
<%= f.error_messages, :object_name => "product" %>
<% if params[:field].blank? || params[:field] == "name" %>
<p>
<%= f.label :name %>
<%= f.text_field :name %>
</p>
<% end %>
<% if params[:field].blank? || params[:field] == "price" %>
<p>
<%= f.label :price %>
<%= f.text_field :price %>
</p>
<% end %>
<% if params[:field].blank? || params[:field] == "category_id" %>
<p>
<%= f.label :category_id %>
<%= f.collection_select :category_id, Category.all, :id, :name %>
</p>
<% end %>
<% if params[:field].blank? || params[:field] == "discontinued" %>
<p>
<%= f.check_box :discontinued %>
<%= f.label :discontinued %>
</p>
<% end %>
Lo que hemos hecho arriba es modificar el parcial para que lea el atributo :field de la caja de selección del listado y sólo muestre un campo si coincide con el valor de :field o :field está vacío (esto es, si el usuario ha escogido que quiere editar todos los campos). No es el código más elegante posible y con Formstatic podría organizarse mejor, pero por ahora nos servirá.
Si marcamos dos de los productos del índice y escogemos "price" en la caja de selección el posterior formulario de edicion múltiple tan sólo mostrará el campo de precio para estos dos productos.
Y eso es todo por este episodio. Es relativamente sencillo editar múltiples registros en un único formulario si utilizamos fields_for adecuadamente, y esta útil técnica nos podrá servir en diversas situaciones.


