#198 Edit Multiple Individually
- Download:
- source codeProject Files in Zip (97.3 KB)
- mp4Full Size H.264 Video (21.8 MB)
- m4vSmaller H.264 Video (14.7 MB)
- webmFull Size VP8 Video (38.7 MB)
- ogvFull Size Theora Video (30.7 MB)
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.
<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 id
s 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.
<%= submit_tag "Edit Checked" %>
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.
<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 id
s 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.
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.
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.
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.
<%= 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.
<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.
<%= 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.