Piccolo tutorial per gestire le risorse annidate in Rails 2.x
E' comune, in un'applicazione web, avere a che fare con risorse annidate, pensiamo per esempio alle attività di un progetto, oppure ai libri di una libreria e così via. A livello di progetto e di database possiamo mappare le relazioni tramite direttive di ActiveRecord in modo da poterle poi richiamare nei nostri oggetti, ma possiamo fare la medesima cosa anche con le nostre risorse. Vediamo come.
Supponiamo di voler aggiungere l'oggetto "iterations" ai nostri "projects" creati durante il tutorial scorso.
Tramite terminale:
ruby script/generate scaffold iteration name:string start:date end:date project_id:integer rake db:migrate
Andiamo ad aggiungere le relazioni ai nostri modelli
class Project < ActiveRecord::Base has_many :iterations end class Iteration < ActiveRecord::Base belongs_to :project end
Nested URL
Come nel precedente esempio, lo scaffold ha modificato il file routes.rb per mappare la risorsa, ma a noi la mappatura standard non va bene.
Per rendere le risorse gerarchiche anche nell'url cambiamo entrambe le direttive alle risorse generate nel file di routing come segue
map.resources :projects, :has_many :iterations
Ovvero stiamo dicendo a Rails che per ogni progetto deve mappare molte iterazioni.
Se la relazione fosse stata 1 ad uno avremmo potuto usare :has_one.
Gli url risultanti saranno simili a:
/project/:project_id/iterations /project/:project_id/iterations/:id
E se volessimo dare un'occhiata a tutte le routes che la nostra applicazione accetterà? Nulla di più semplice, Rake routes ci da la risposta in tempo reale. Questa direttiva di rake è utile soprattutto per controllare che abbiam fatto tutto bene e non abbiam combinato guai.
Controller
Avendo modificato il comportamento standard dello scaffold ci tocca anche modificare controller e view. La modifica è abbastanza semplice, dobbiamo infatti semplicemente dire al nostro controller che l'iterazione la deve prendere dal progetto indicato.
Modifichiamo come segue:
def index
#@iterations = Iteration.find(:all)
@project = Project.find(params[:project_id])
@iteration = @project.iterations.find(params[:id])
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @iterations.to_xml }
end
end
Nelle azioni show, edit e new invece dobbiamo solo aggiungere il riferimento a @project.
@project = Project.find(params[:project_id])
(create ed update e destroy le vedremo nel dettaglio)
View
Anche le view vanno modificate per riflettere i nuovi cambiamenti. In particolare nel progetto dovremo dare accesso alla funzione che mostri le iterazioni collegate. Diamo uno sguardo nuovamente al prodotto di rake routes e troviamo:
project_iterations GET /projects/:project_id/iterations {:action=>"index", :controller=>"iterations"}
Il primo elemento è proprio il nome dell'helper, il secondo il metodo http, il terzo la route e il quarto l'azione chiamata. Ora che abbiam trovato il nostro helper aggiungiamo il link alla view della lista progetti.
<h1>Listing projects</h1>
<table>
<tr>
<th>Name</th>
<th>Desc</th>
</tr>
<% for project in @projects %>
<tr>
<td><%=h project.name %></td>
<td><%=h project.desc %></td>
<!-- Aggiunta per iterazioni in progetto -->
<td><%= link_to 'Iterations', project_iterations(project) %></td>
<!-- Fine aggiunta -->
<td><%= link_to 'Show', project %></td>
<td><%= link_to 'Edit', edit_project_path(project) %></td>
<td><%= link_to 'Destroy', project, :confirm => 'Are you sure?', :method => :delete %></td>
</tr>
<% end %>
</table>
<br />
<%= link_to 'New project', new_project_path %>
Abbiamo così detto a Rails come lanciare la visualizzazione dei progetti, passiamo ora alle view delle iterazioni.
Iniziamo con la lista delle iterazioni:
...
<% for iteration in @iterations %>
<tr>
<td><%=h iteration.name %></td>
<td><%=h iteration.start %></td>
<td><%=h iteration.end %></td>
<td><%=h iteration.project_id %></td>
<!-- <td><%= link_to 'Show', iteration %></td> -->
<!-- <td><%= link_to 'Edit', edit_iteration_path(iteration) %></td> -->
<!-- <td><%= link_to 'Destroy', iteration, :confirm => 'Are you sure?', :method => :delete %></td> -->
<td><%= link_to 'Show', project_iteration_path(@project,iteration) %></td>
<td><%= link_to 'Edit', edit_project_iteration_path(@project, iteration) %></td>
<td><%= link_to 'Destroy', project_iteration_path(@project, iteration), :confirm => 'Are you sure?', :method => :delete %></td>
<% end %>
...
<!-- <%= link_to 'New iteration', new_iteration_path %> -->
<% link_to "New Iteration", new_project_iteration_path %>
Nuova iterazione
Per inserire una nuova iterazione dobbiamo il controller.
Nell vista dobbiamo modificare due elementi. Il form_for e il link per tornare alle iterazioni, come segue:
<h1>New iteration</h1> <% form_for([@project, @iteration]) do |f| %> <!-- Passiamo il riferimento dell'iterazione all'interno del progetto --> <%= f.error_messages %> #... FORM ...# <% end %> <%= link_to 'Back', project_iterations_path(@project) %> <!-- Ritorna alle iterazioni all'interno del progetto -->
Ora modifichiamo il controller per indicare a rails che la nuova iterazione fa parte di del progetto preso in esame.
def create
@iteration = @project.iterations.new(params[:iteration]) #Creiamo un'iterazione all'interno di un progetto
respond_to do |format|
if @iteration.save
flash[:notice] = 'Iteration was successfully created.'
#Redirect verso l'url corretto per il formato html e xml
format.html { redirect_to(project_iterations_path(@project)) }
format.xml { render :xml => @iteration, :status => :created, :location => (project_iterations_path(@project)) }
else
format.html { render :action => "new" }
format.xml { render :xml => @iteration.errors, :status => :unprocessable_entity }
end
end
end
Modifica iterazione
Analizziamo ora l'update
Iniziamo al solito con la vista, anche qui dobbiamo modificare il form_for e gli url per tornare indietro e mostrare l'iterazione
<h1>Editing iteration</h1> <% form_for([@project,@iteration]) do |f| %> <%= f.error_messages %> #...FORM...# <% end %> <%= link_to 'Show', project_iteration_path(@project,@iteration) %> | <%= link_to 'Back', project_iterations_path(@project) %>
Mentre nel controller dobbiamo modificare il metodo di update e i redirect_to come segue
def update
@iteration = @project.iterations.find(params[:id])
respond_to do |format|
if @iteration.update_attributes(params[:iteration])
flash[:notice] = 'Iteration was successfully updated.'
format.html { iteration_url(@iteration.project, @iteration) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @iteration.errors, :status => :unprocessable_entity }
end
end
end
Non dimentichiamo che prima di ogni azione recuperiamo il riferimento a @project dalla funzione privata find_project.
Cancellazione iterazione
L'ultimo metodo CRUD da analizzare è il destroy.
Qui abbiamo solo il controller e la modifica è simile a quella fatta per gli altri metodi, ovvero modifica :
def destroy
@iteration = @project.iterations.find(params[:id])
@iteration.destroy
respond_to do |format|
format.html { redirect_to(project_iterations_path(@project)) }
format.xml { head :ok }
end
end
Conclusioni
Con rails 2.x la nostra applicazione RESTful raggiunge lo stato dell'arte, alcune soluzioni, come l'has_many nelle routes sono puro zucchero sintattico come direbbe qualche mio collega.
Potete scaricare l'intero progetto da qui


