Summary: solutions and rants about my Web Applications course last exam
Parziale soluzione dell’elaborato di Applicazioni Web di febbraio 2007. Lo posto nel blog perché potrebbe essere di qualche interesse per un rails-ista alle prime armi.
Esercizio 0
Si scriva un template rhtml per visualizzare una tabella di oggetti contenuti nell’array @objects. Non sappiamo in anticipo quali saranno tutti gli attributi degli elementi di @objects; sappiamo per certo che ci sarà un attributo “name” e un attributo “image_url”. Il nome degli altri attributi si trova nell’array @other_columns. Se ad esempio @other_columns contiene [“foo”, “bar”], la tabella prodotta dovrà contenere le colonne, nell’ordine: name, image_url, foo, bar. La colonna image_url deve mostrare un’immagine.
La tabella del primo esercizio non è difficile da realizzare. L’unico punto pochi hanno colto, e nessuno ha risolto correttamente, è che @other_columns contiene nomi di attributi, e quindi non posso estrarre un attributo per nome usando semplicemente una cosa tipo
<% for column in @other_columns %>
<td><%= object.column %></td>
<% end %>
perché “column” è il nome della variabile su cui iteriamo, non è il nome di un metodo di object. Si può risolvere in Ruby con il metodo “send” che usa reflection
<% for column in @other_columns %>
<td><%= object.send(column) %></td>
<% end %>
(Ad esempio: 3.succ restituisce 4, come anche 3.send(“succ”)) Oppure si può usare la hash “attributes” di ActiveRecord::Base:
<% for column in @other_columns %>
<td><%= object.attributes[column] %></td>
<% end %>
Altra cosa: per produrre un elemento “img” la cosa migliore è usare lo helper image_tag:
<%= image_tag object.image_url %>
piuttosto che
<img src="<%= object.image_url %>" />
Esercizio 2
In che cosa consiste il meccanismo della redirezione nel protocollo HTTP? Come funziona? Qual’è l’uso che se ne fa principalmente in un’applicazione web?
Come funzionano le redirezioni? E soprattutto, a che cosa servono in un’applicazione web? E’ vero che, come molti hanno fatto notare, servono per ridirigere l’utente sulla pagina di login quando cerca di accedere a una pagina protetta se l’utente non è in sessione. Ma l’uso più comune è di ridirigere l’utente su una pagina ottenuta tramite GET dopo che ha modificato una risorsa con una richiesta POST. Di questa cosa abbiamo ampiamente discusso a lezione. Infatti il codice dello scaffolding di Rails, che vi consiglio di studiare perché è migliore del codice che molti programmatori Rails principianti scrivono a mano, fa cose di questo tipo:
def create
@user = User.new(params[:user])
if @user.save
flash[:notice] = 'User was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
Se l’oggetto @user è stato salvato con successo, l’utente viene rediretto sulla pagina che lista gli utenti. Altrimenti l’utente si ritroverebbe su una pagina ottenuta tramite POST, e quindi non bookmarkabile. Peggio ancora, se l’utente facesse un reload della pagina, e ignorasse l’avvertimento del browser, si ritroverebbe a inserire due volte lo stesso oggetto @user nel database.
Esercizi 3 e 4
In un’applicazione vogliamo tenere traccia di utenti (User) e ruoli (Role) da essi ricoperti. Ogni utente può ricoprire un ruolo, ma vogliamo sapere da quando a quando il ruolo è stato ricoperto. Ciascun ruolo ha un insieme di privilegi, che devono anch’essi essere rappresentati (come stringhe) nel database. Disegnare il diagramma entità-relazioni, e il codice sql necessario per implementarlo. (In alternativa a sql scrivere il codice delle migrations.)
Nell’applicazione del punto precedente: scrivere le classi ActiveRecord per tutti i modelli che ritenete necessari, facendo in modo che
- Il metodo current_role di User restituisca il ruolo correntemente ricoperto da un utente, oppure nil se non ricopre alcun ruolo.
- Il metodo roles di User restituisca tutti i ruoli ricoperti in ordine di tempo
- Non sia possibile salvare un utente nel database se non è presente almeno uno fra i seguenti campi: first_name, last_name, nickname. Deve essere possibile salvare se uno dei tre è compilato ma gli altri no.
Quasi nessuno dei miei studenti sa usare le relazioni molti-a-molti. Che vergogna! E quel che è peggio, molti degli altri non sono in grado nemmeno di mettere in piedi una relazione uno-a-molti. Doppia vergogna. Vai a leggere la sezione 14.6 Relationships between Tables
del libro di Rails e non farti rivedere fino a quando non hai capito quello che c’è scritto.
Prima di tutto, è necessario capire come funziona lo schema dei dati. Dobbiamo leggere con attenzione il tema, per capire quali siano le cardinalità giuste delle relazioni. Ora, è chiaro che gli utenti e i ruoli sono associati. Ma un utente, quanti ruoli può avere avuto nella sua vita? Zero, uno o anche più di uno, quindi “molti”! E un ruolo, da quanti utenti può essere stato ricoperto nel tempo? Ancora, molti! Stesso discorso per i privilegi, che possono essere associati a più di un ruolo. Ad esempio, il privilegio di “cancellare tutti i file degli utenti” può essere associato sia al ruolo “amministratore” che al ruolo “boss”.
Normalmente una relazione molti a molti come questa
* *
User ---------- Role
viene implementata con una tabella di supporto.
create table roles_users (
user_id int not null references users(id),
role_id int not null references roles(id),
primary key(user_id, role_id)
)
Quei due campi “user_id” e “role_id” sono chiavi esterne, o foreign keys. Se non sai di che cosa sto parlando, vuol dire che non capisci nulla di basi di dati.
Ora, il nostro caso è un po’ più complicato perché l’assegnamento di un utente a un ruolo ha una data di inizio e una possibile data di fine. La soluzione canonica è arricchire la tabella di supporto con gli attributi necessari:
create table roles_users (
user_id int not null references users(id),
role_id int not null references roles(id),
start_on datetime not null,
end_on datetime null,
primary key(user_id, role_id)
)
Ora questa tabella roles_users comincia ad assomigliare a un’entità a sè stante. Si dà il caso che ogni relazione molti-a-molti può essere sempre vista come una coppia di relazioni uno-a-molti; e in questo caso diventa vantaggioso inventarsi un nuovo concetto: la nomina (grant) di un utente in un ruolo. Un utente ha (avuto) molte nomine, un ruolo è stato assegnato in molte nomine. Una nomina è di un utente e di un ruolo.
1 * * 1
User ------ Grant ----- Role
A questo punto, invece di usare il vecchio has_and_belongs_to_many, conviene usare il nuovo has_many :through, che è più in linea con la maniera moderna, REST, di fare codice Rails.
class Grant < ActiveRecord::Base
belongs_to :user
belongs_to :role
end
class User < ActiveRecord::Base
has_many :grants
has_many :roles, :through => :grants
end
class Role < ActiveRecord::Base
has_many :grants
has_many :users, :through => :grants
end
L’unico accorgimento da prendere è di dare una sua propria chiave alla tabella, che non è più una semplice tabella di supporto ma è un’entità a tutti gli effetti.
create table grants (
id int auto_increment,
user_id int not null references users(id),
role_id int not null references roles(id),
start_on datetime not null,
end_on datetime null,
primary key(id)
)
C’è un interessante e conciso articolo di Scott Raymond sui vantaggi di rifattorizzare un’applicazione Rails per avvantaggiarsi di uno stile rest. (cache di google)
Per quanto riguarda la validazione, basta ricordare che non esistono solo le validazioni prefabbricate come validates_presence_of; si può sempre scrivere una validazione ad hoc, come ad esempio:
def validate
if first_name.blank? and last_name.blank? and nickname.blank?
errors.add_to_base "inserisci nome, cognome o nickname"
end
end