#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)
Nell’episodio 165 [guarda, leggi], abbiamo creato un’applicazione che poteva modificare più record contemporaneamente. Ogni record nella pagina index aveva una checkbox che ci permetteva di selezionare tutti gli oggetti che volevamo e modificare tutti i campi per quegli oggetti.
Selezionando i tre prodotti e facendo click su “edit checked” possiamo modificare la loro categoria, il nome o il prezzo, ad esempio inserendoli tutti nella categoria “groceries”.
L’ovvia restrizione è che ogni modifica che facciamo viene applicata a tutti i prodotti selezionati. In questo episodio realizzeremo un’applicazione simile che mostri però un insieme diverso di campi per ogni prodotto selezionato permettendoci così di modificare più prodotti nello stesso form, ognuno con il suo insieme di valori differente dagli altri.
Aggiungere le Checkbox
Inizieremo da un elenco dei prodotti generato con lo scaffold. Il codice dello scaffold ci fornisce la possibilità di modificare individualmente ogni prodotto, ma non è ovviamente questo quello che a noi interessa. Come abbiamo fatto nell’episodio 165 aggiungiamo una checkbox per ogni oggetto elencato in modo tale che possiamo scegliere quelli da modificare. La prima modifica che faremo sarà quindi nella view index
dei prodotti.
<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>
Per aggiungere le checkbox abbiamo effettuato una serie di modifiche al codice generato dallo scaffold. Prima di tutto abbiamo incapsulato la tabella in un form utilizzando il form_tag
.
<% form_tag edit_individual_products_path do %> <!-- table --> <% end %>
Il form verrà inviato ad una nuova azione del controller dei prodotti chiamata edit_individual
che andremo a definire a breve. Nella tabella stessa abbiamo poi aggiunto un elemento th
negli header e, nel body, una nuova cella che contiene la checkbox così dichiarata:
<%= check_box_tag "product_ids[]", product.id %>
Il nome che passiamo al check_box_tag
è product_ids[]
, le parentesi graffe vuote significano che possiamo passare a questo parametro più id
di prodotti come un array per tutte le checkbox. Con ogni checkbox passiamo anche come valore l’id
di ogni prodotto.
Infine aggiungiamo un tag submit per inviare il form.
<%= submit_tag "Edit Checked" %>
Il form viene inviato ad una nuova azione chiamata edit_individual
la prossima cosa che faremo sarà quindi scrivere questa azione nel controller dei prodotti, insieme ad un’altra chiamata update_individual
che è l’azione che verrà richiamata dalla edit_individual
per la modifica effettiva dei prodotti selezionati.
def edit_individual end def update_individual end
Poichè stiamo aggiungendo azioni ad una risorsa RESTful dovremo anche modificare il file routes.
ActionController::Routing::Routes.draw do |map| map.resources :products, :collection => { :edit_individual => :post, :update_individual => :put } map.resources :categories map.root :products end
Modifichiamo la risorsa products nel file routes.rb
, aggiungendo un argomento :collection
con due nuove azioni. :edit_individual
sarà una richiesta di tipo POST poichè ne effettuiamo l’invio attraverso un form. In realtà stiamo semplicemente raccogliendo dati quindi l’azione ideale sarebbe una GET ma visto che invieremo un array di id, è necessario un POST. Modificheremo anche i record nella :update_individual
che sarà dunque una richiesta di tipo PUT.
Aggiornando la pagina ora avremo una checkbox accanto ad ogni prodotto e un bottone da cliccare per modificare tali prodotti.
Non abbiamo ancora scritto il template edit_individual
se quindi inviassimo ora il form vedremmo un errore. Prima di creare il template modifichiamo l’azione edit_individual
in modo che selezioni tutti i prodotti con un id
contenuto nell’array di product_ids
che rappresentano le checkbox selezionate.
def edit_individual @products = Product.find(params[:product_ids]) end
Ora possiamo occuparci del codice della view edit_individual
. Diamo alla pagina un titolo e poi creiamo un form, ma cosa dobbiamo utilizzare qui, form_for
o form_tag
? Allora, form_for
viene utilizzato nella modifica di un singolo modello, ma qui stiamo modificando un gruppo di modelli e quindi utilizzeremo form_tag
. Gli passeremo l’URL all’azione update
e specificheremo che l’azione è di tipo PUT.
Nel form cicleremo tutta la lista dei prodotti e utilizzeremo fields_for
per generare i campi per ogni prodotto. Possiamo realizzare questo passando a fields_for
l’array products[]
e il prodotto. Questo farà si che l’i id
del prodotto venga inserito nelle parentesi quadre e quindi che ogni prodotto venga passato come un parametro sepatrato. Successivamente dovremo inserire gli stessi campi del form ma per ora faremo restituire solamente il nome del prodotto. Infine aggiungiamo un submit_tag
.
<% title "Edit Products" %> <% form_tag update_individual_products_path :method => :put do %> <% for product in @products %> <% fields_for "products[]", product do |f| %> <h2><%= h product.name %></h2> <% end %> <% end %> <p><%= submit_tag "Submit" %></p> <% end %>
Se quindi ora selezioniamo le checkbox dei nostri tre prodotti e facciamo click su “Edit Checked” verremo portati alla nuova pagina edit_individual
e vedremo elencati i tre prodotti.
Insieme al nome del prodotto vogliamo mostrare i campi del form per gli attributi di ogni prodotto. Poichè i campi sono mostrati anche nelle azioni new
ed edit
lo stesso form viene generalmente estrapolato in un partial. Questo partial contiene un tag form_for
che incapsula tutti gli elementi del form di un prodotto. A noi serve riutilizzare i campi senza il form_for
estrarremo quindi i campi in un altro partial che potremo utilizzare sia nel form che nella pagina per la modifica di più prodotti.
Metteremo i campi del form in un nuovo partial chiamato _fields.html.erb
.
<%= f.error_messages %> <p> <%= f.label :name %> <%= f.text_field :name %> </p> <p> <%= f.label :price %> <%= f.text_field :price %> </p> <p> <%= f.label :category_id %> <%= f.collection_select :category_id, Category.all, :id, :name %> </p> <p> <%= f.check_box :discontinued %> <%= f.label :discontinued %> </p>
Possiamo poi richiamare questo partial da quello _form
passandogli la variabile f
.
<% form_for @product do |f| %> <%= render :partial => 'fields', :f => f %> <p><%= form.submit "Submit" %></p> <% end %>
Ritornando poi al codice della view edit_individual
richiameremo questo partial in modo tale che vengano visualizzati i campi di ogni prodotto.
<% content_for :title do %> Edit Individual <% end %> <% form_tag update_individual_products_path, :method => :put do %> <% for product in @products %> <% fields_for "products[]", product do |f| %> <h2><%= h product.name %></h2> <%= render "fields", :f => f %> <% end %> <% end %> <p><%= submit_tag "Submit" %></p> <% end %>
Aggiornando la pagina vedremo i campi del form di ogni prodotto riempiti ognuno con i valori di ogni prodotto.
Se osserviamo la parte corretta del codice sorgente della pagina vedremo che i campi del form hanno dei nomi molto interessanti. Ogni nome inizia con products
ha quindi l’id
del prodotto e il nome del campo tra parentesi quadre. Questo significa che i valori dei prodotti verranno inviati come 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>
Possiamo utilizzare il parametro products
nell’azione update_individual
per aggiornare tutti i prodotti al submit del form. Dobbiamo aggiornare più prodotti simultaneamente e c’è un metodo ActiveRecord chiamato update che fà esattamente questo. Il metodo update richiede due argomenti: il primo è o un singolo id
o un array di id
e il secondo è un hash di valori. Per modificare i nostri prodotti possiamo passare al metodo update le keys
e i values
del nostro parametro products. Dopo aver modificato i prodotti creeremo un messaggio flash e reindirizzeremo alla pagina index dei prodotti.
def update_individual Product.update(params[:products].keys, params[:products].values) flash[:notice] = "Products updated" redirect_to products_url end
Tutti i nostri prodotti sono attualmente nella categoria “Groceries” e questo è charamente errato. Modificheremo quindi le categorie della t-shirt e del DVD player e ridurremo un po’ i loro prezzi, quindi invieremo il form. Facendo questo verremo reindirizzati alla pagina index in cui potremo ammirare i prodotti modificati.
Questo significa che il nostro form funziona esattamente come noi desideriamo e che possiamo modificare più prodotti alla volta.
Validazioni
Se qualcuno prova a immettere valori non validi nel nuovo form vogliamo essere in grado di mostrare i messaggi di errore in un modo utile. Il modello Product
attualmente non include nessuna validazione, ne aggiungiamo quindi una che ci assicuri che il prezzo sia un valore numerico.
class Product < ActiveRecord::Base belongs_to :category validates_numericality_of :price end
Nel controller products dobbiamo ora gestire la validazione dell’azione update_individual
. Il metodo update
ignora infatti completamete gli errori di validazione e nel caso in cui ve ne siano passerà direttamente al record successivo. Non tutto è perduto però, update
restituisce infatti un array dei prodotti che ha tentato di modificare e possiamo utilizzare questo per determinare quali di essi non erano validi.
Un modo per trovare i prodotti non validi è quello di utilizzare il metodo reject
sull’array e nel blocco del reject
, chiamare il metodo valid?
su ogni prodotto per filtrare quelli validi.
Product.update(params[:products].keys, params[:products].values).reject { |p| p.valid? }
Il problema con questo approccio è che le validazioni per ogni prodotto verranno nuovamente effettuate. Una via più efficiente sarebbe quella di scartare i prodotti con un array errors
vuoto. Assegneremo questo array di prodotti non validi ad una variabile e controlleremo se è vuoto. Se lo è effettueremo come prima un reindirizzamento alla pagine index altrimenti mostreremo nuovamente il form edit_individual
per visualizzare gli errori dei prodotti non validi.
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
Se proviamo a modificare nuovamente la t-shirt e il DVD player impostando un valore non valido per il prezzo del DVD player verremo reindirizzato alla pagina edit con il form che ora conterrà però solamente il DVD player con l’errore di validazione.
Il titolo del box degli errori attualmente mostra il nome “products[]”, ma questo si può modificare facilmente. I messaggi di errore vengono generati nei campi del partial utilizzando il metodo error_messages
. Questo metodo accetta un parametro :object_name
che possiamo utilizzare per impostare il nome che verrà mostrato.
<%= f.error_messages :object_name => "product" %>
Con questa modifica il box degli errori mostrerà ora “product” invece di “products[]”.
Aggiungiamo un’ultima cosa
La funzionalità che volevamo è attualmente sufficientemente completa, ma per concludere questo episodio aggiungeremo un’ultima cosa per rendere l’applicazione più utile. Se volessimo modificare un unico attributo su un certo numero di prodotti, per esempio per aggiornare i prezzi, il form di modifica diventerebbe difficile da gestire. Quello che implementeremo è la possibilità per gli utenti di selezionare un solo attributo da una lista così da rendere possibile la modifica di un solo attributo per molti prodotti senza la necessità di navigare attraverso un form molto lungo.
Per fare questo aggiungeremo un menù pop up accanto al bottone “edit checked” sulla pagina index dei prodotti che ci consentirà di scegliere il campo da modificare. Lo possiamo realizzare aggiungendo la linea seguente immediatamente prima del submit_tag
nella view index dei prodotti.
<p><%= select_tag :field, options_for_select([["All Fields", ""], ["Name", "name"], ["Price", "price"], ["Category", "category_id"], ["Discontinued", "discontinued"]])%></p>
Ora possiamo scegliere di modificare tutti i campi oppure solamente uno solo per ogni prodotto selezionato. Per restringere i campi visualizzati a quelli scelti dovremo modificare il partial dei campi perchè visualizzi solamente il campo selezionato.
<%= 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 %>
Quello che abbiamo fatto è modificare il partial così che legga l’attributo :field
dal nuovo menu di pop-up dell’index form e mostri ogni campo solamente se l’attributo è vuoto (ossia se l’utente ha selezionato l’opzione “All Fields”) o se ha lo stesso nome del campo. Questo non è di sicuro il codice migliore e può essere pulito utilizzando ad esempio Formtastic, ma ai nostri fini andrà bene.
Se selezioniamo due prodotti dalla pagina index e scegliamo “price” dal menu pop-up il form di modifica mostrerà unicamente i campi price per quei due prodotti.
E questo è quanto per questo episodio. Modificare più record in un unico form è abbastanza facile se si fà un utilizzo corretto di fields_for
e questa è una tecnica che può tornare utile in molte situazioni pratiche.