Pour un projet (dont vous ne voulez rien savoir) j’ai dû effectuer une comparaison entreTurbogears et Ruby on Rails (que j’appellerai maintenant RoR).
Comme je ne suis pas un Pythoneux (Pythoniste ?), la solution la plus simple pour moi était de prendre le tutoriel de Turbogears et d’essayer de le répéter avec RoR, afin de pouvoir comparer ces deux frameworks de façon plus précise.
Notez que bien que je suive le plan du tutoriel de Turbogears, j’introduirai aussi quelques concepts propres à RoR, là où il sera pertinent. Par exemple, dans le tutoriel Turbogears, la cohérence de design du wiki est assuré par la copie d’une template initiale. Dans ce cas particulier j’introduirai les layouts de RoR, car c’est exactement ce pour quoi ils existent.
Créons tout d’abord la hiérarchie de notre wiki:
rails wiki
create
create app/controllers
create app/helpers
create app/models
create app/views/layouts
....
create doc/README_FOR_APP
create log/server.log
create log/production.log
create log/development.log
create log/test.log
et plongeons à pieds joints:
cd wiki
Nous pouvons démarrer notre serveur web:
script/server
Cela démarrera soit Webrick soit Lighttpd (selon votre système
d’exploitation et le fait que lighttpd est installé ou non). Notez
que sur ma gentoo, j’ai dû ajouter /usr/sbin à mon path pour que
RoR trouve lighttpd.
Regardons maintenant l’URL http://localhost:3000:

RoR adhère au concept MVC (Model-View-Controller). Pour commencer nous allons donc devoir choisir notre modèle puis notre contrôleur.
Notre wiki sera fait de Pages (notre modèle), chacune ayant un
nom (name) et un contenu (content).
Notre modèle sera au final sérialisé dans une table de base de
données (BDD), et donc nous avons soit le choix d’écrire
directement du code SQL pour créer cette table (pages), avec 2
colonnes name et content, soit de travailler avec les schémas
Ruby On Rails et les migrations. Cette dernière solution a au
moins deux avantages:
Commençons d’abord par indiquer à RoR quelle sera notre BDD de
développement. Pour cela, on édite le fichier
config/database.yml, qui sera l’un des seuls fichier de
configuration que vous aurez à éditer.
Dans cet exemple, j’utiliserai SQLite en tant que BDD de
développement, et je nommerai le fichier de cette BDD
wiki.sqlite:
development:
adapter: sqlite
database: db/wiki.sqlite
Nous allons aussi créer notre modèle Page:
script/generate model Page
exists app/models/
exists test/unit/
exists test/fixtures/
create app/models/page.rb
create test/unit/page_test.rb
create test/fixtures/pages.yml
create db/migrate
create db/migrate/001_create_pages.rb
En ce qui concerne la gestion de la base de donnée, et plus particulièrement du schéma, le plus simple dans un premier temps va être de laisser Rails le gérer complètement pour nous. Pour cela, nous allons utiliser les migrations, qui sont, en gros, la manière qu’à Rails de garder en mémoire les différentes versions du schéma (on peut voir cela comme les incréments qui vont faire passer le schéma d’une version à une autre).
Lors de la création de notre modèle Page, vous avez peut-être
remarqué la ligne:
create db/migrations/001_create_pages.rb
Rails a créer pour nous une migration qui indique comme passer de
l’état “schéma vide”, à l’état “schéma décrivant une table
pages”.
Regardons le fichier db/migrations/001_create_pages.rb:
class CreatePages < ActiveRecord::Migration
def self.up
create_table :pages do |t|
# t.column :name, :string
end
end
def self.down
drop_table :pages
end
end
Ce fichier décrit donc le contenu d’une table nommée pages
... en Ruby. L’avantage étant que l’on peut ensuite générer le
code SQL correspondant au type de database choisie (mySQL, SQLite,
Oracle …).
La méthode self.up décrit ce qui va faire passer l schéma de la
version courante à la version suivante … et le self.down
l’inverse.
Décrivons donc notre modèle:
def self.up
create_table :pages do |t|
t.column :name, :string, :limit => 30
t.column :content, :text
end
end
Nous avons donc défini une table de notre BDD, nommée pages,
avec 2 colonnes:
name qui sera le nom de notre page (avec un maximum de 30 caractères).content qui sera le contenu de notre page (pas de limite explicite).Il ne nous reste plus quà dire à Rails d’appliquer notre migration à notre BDD:
> rake migrate
(in /home/sleeper/tmp/wiki)
== CreatePages: migrating
=====================================================
-- create_table(:pages)
-> 0.0438s
== CreatePages: migrated (0.0440s)
============================================
Notre première table est créée. Notez, que Rails a aussi créé le
fichier db/schema.rb, qui représente notre schéma dans sa
version actuelle.
Et c’est tout: notre BDD est créée, avec le bon schéma:
> sqlite db/wiki.sqlite
SQLite version 2.8.16
Enter ".help" for instructions
sqlite> .dump pages
BEGIN TRANSACTION;
CREATE TABLE pages ("id" INTEGER PRIMARY KEY NOT NULL, "name"
varchar(30), "content" text);
COMMIT;
sqlite>
Redémarrez votre serveur web, en appuyant d’abord sur Control-C
dans le terminal puis en re-lançant la commande script/server.
Pour afficher une page de wiki, nous devons utiliser un contrôleur
et une (ou plusieurs) vue(s). Pour être original, notre
contrôleur s’appellera Wiki.
Pour le moment il ne montrera que l’un de ces méthodes:
index. Cette méthode sera appelée chaque fois qu’un client
demandera la page http://localhost:3000/wiki/index.html.
Cette méthode (index) doit permettre d’afficher soit la première
page (nommée PageFront) ou n’importe quelle page dont le nom aura
été donné en paramètre.
Ainsi http://localhost:3000/wiki/index.html affichera la page
FrontPage, alors que http://localhost:3000/wiki/index.html?name=Foo
affichera la page Foo.
Créons donc notre contrôleur Wiki, ainsi que la méthode index
et la vue associée:
script/generate controller Wiki index
exists app/controllers/
exists app/helpers/
create app/views/wiki
exists test/functional/
create app/controllers/wiki_controller.rb
create test/functional/wiki_controller_test.rb
create app/helpers/wiki_helper.rb
create app/views/wiki/index.rhtml
Notez que nous pouvons nous passer du paramètre index dans la
commande précédente et ajouter nous même la méthode index au
contrôleur, dans le fichier app/controllers/wiki_controller.rb:
class WikiController < ApplicationController
def index
@page = Page.find_by_name( params[:name] || 'FrontPage' )
end
end
Dans notre méthode index nous cherchons donc dans la BDD (grâce
au modèle Page@), une page dont le nom correspond au nom donné en
paramètre ou FrontPage si aucun nom n’est donné.
Nous devons aussi créer la page index.html, en éditant la
template app/views/wiki/index.rhtml:
<div style="float:right; width: 10em">
<p>Viewing <span><%= @page.name %></span></p>
<p>You can return to the <a href="/wiki">FrontPage</a>. </p>
</div>
<div><%= textilize(@page.content) %></div>
Nous utilisons la variable @page que la méthode index vient
juste d’affecter, et affichons à la fois son nom et son contenu.
Notez que le contenu est d’abord passer par la méthode textilize,
qui va transformer tout markup au format Textile en HTML (cela
marchera si RedCloth est installé et peut-être appelé au moyen de require).
Essayons donc de charger la page http://localhost:3000/wiki et nous obtenons … une erreur: ... an error:

Cette erreur est causée par le fait que notre BDD est vide
... Utilisons donc script/console pour créer la page FrontPage:
script/console
Loading development environment.
>> Page.new( :name => 'FrontPage', :content => 'Welcome to *my* front page').save
=> true
>>
Rechargez la page dans votre navigateur et … tada !!

Ca marche !!!
Bon. Chaque wiki possède un moyen d’éditer la page courante. Nous allons donc ajouter un lien “Edit this page” à notre page courante.
Première chose: quand nous cliquerons notre lien “edit this page” nous serons redirigés vers une page d’un style semblable à celui de notre page courante (et même à toutes nos pages).
Plutôt que de copier index.rhtml et de la modifier, nous allons
utiliser un layout.
Les layouts sont un bon moyen avec RoR de suivre le principe du DRY (Don’t Repeat Yourself—Ne Vous Répétez Pas). Pour parler de façon simple, un layout est une coquille qui va recevoir nos nouvelles pages. Cela permet de créer et maintenir le design général de notre site à un seul endroit.
Donc allons-y et vérifions si cela marche tout d’abord avec notre
page index.html. La layout à utiliser pour un contrôleur donné
a le même nom que ce contrôleur. Nous voulons aussi que le
“Viewing [page name]” soit visible sur toutes les pages de notre
wiki. Le placer dans le layout est donc une bonne idée… mais
nous devons faire attention au fait que parfois nous ne
regarderons pas une page mais nous l’éditerons … Nous devons
donc paramètriser cette action …
Créons donc notre layout, nommé app/views/layouts/wiki.rhtml:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title> Wiki </title>
<%= stylesheet_link_tag "wiki" %>
</head>
<body>
<div id="sidebar" style="float:right; width: 10em">
<% if @page and @page.name %>
<p><%= @page_action || "Viewing" %> <span id="name"><%= @page.name %></span> </p>
<% end %>
<p>You can return to the <a href="/wiki">FrontPage</a> </p>
</div>
<div id="main">
<%= yield %>
</div>
</body>
</html>
Nous demandons juste l’inclusion d’une feuille de style nommée
wiki.css (et que l’on devra créer dans public/stylesheets),
et l’insertion des pages que nous créons en lieu et place de
yield. Nous paramètrisons aussi le nom de l’action
à afficher: par défaut ce sera “Viewing” mais on peut changer cela
en affectant l’action voulue à la variable @page_action.
Créons une feuille de style simple
(public/stylesheets/wiki.css):
body {
font-family: sans-serif;
}
div#sidebar {
border: 2px solid red;
}
span#name {
color: blue;
}
et modifions index.rhtml pour ressembler à:
<div><%= textilize(@page.content) %></div>
Nous pouvons maintenant recharger notre page:

On peut maintenant se pencher sur l’ajout de nouvelles pages. La
page qui va nous servir à éditer nos pages est simple: elle
contient seulement un formulaire, qui réveillera l’action save,
avec une zone texte (textarea) et un bouton pour soumettre le
formulaire.
Créons donc la template, nommée app/views/wiki/edit.rhtml:
<%= form_tag :action => "save" , :id => @page %>
<%= text_area 'page', 'content' %>
<%= submit_tag %>
<%= end_form_tag %>
et ajoutons à la fin de nos page (via la template index.rhtml),
un lien “edit this page”, pointant sur la page edit.html:
<%= link_to "Edit this page", { :action => "edit", :name => @page.name } %>
Nous avons aussi besoin d’une méthode edit dans notre contrôleur:
def edit
@page_action = "Editing"
@page = Page.find_by_name( params[:name] )
end
Pour faire court: nous définissons l’action courante à “Editing” (afin de changer temporairement le layout), et recherchons dans la BDD une page ayant le même nom que celui donnée par l’utilisateur.
Nous pouvons maintenant recharger notre page et cliquer sur le lien “Edit this page” :

Nous avons vu que notre formulaire était posté à l’action
save. Nous devons donc l’ajouter à notre contrôleur, et elle
doit sauvegarder nos changements dans la BDD, et nous avertir si
un problème survient.
Commençons par l’ajouter à notre contrôleur
(app/controllers/wiki_controller.rb):
def save
@page = Page.find( params[:id])
@page.content = params[:page][:content]
if @page.save
flash[:notice] = "Page successfully saved"
else
flash[:notice] = "Unable to save edited page"
end
redirect_to( :action => "index", :name => @page.name )
end
Pour résumer: nous récupérons dans la BDD, la version originale de
la page que nous venons d’éditer (en utilisant son id:
params[:id]), et nous modifions son contenu avec celui que le
formulaire nous a envoyé.
Nous sauvons alors cette nouvelle version dans la BDD (méthode
save du modèle), et updatons la notification pour informer
l’utilisateur du succès ou de l’échec de la sauvegarde. Nous le
redirigeons alors vers la page qu’il vient juste d’éditer.
Ajoutons à notre layout de quoi afficher la notification:
<div id="flash"><%= flash[:notice] %></div>
ainsi que ceci à la feuille de style wiki.css:
div#flash {
background: #E0F8F8;
border: 2px solid #244;
width: 80%;
}
et voilà:

Afin d’afficher la page Foo, l’utilisateur doit donc demander
l’URL /wiki/index.html?name=Foo, ce qui n’est pas très sexy. Le
mieux serait d’avoir quelque chose comme /wiki/Foo. RoR utilise
les différents constituants d’une URL pour indiquer le
contrôleur et la méthode à utiliser, cela reviendra à essayer
d’envoyer le message Foo à notre contrôleur, message qui
n’existe pas.
La solution la plus simple est de définir une méthode
method_missing pour notre contrôleur: elle sera appelée dès
qu’un message inconnu est envoyé au contrôleur … et nous
pouvons essayer de la rediriger vers notre méthode index avec le
bon paramètre:
def method_missing( *args )
redirect_to( :action => 'index', :name => args[0] )
end
Les WikiWords sont des mots assemblés ensembles dans un CamelCase typique. N’importe quel wiki se respectant créera automatiquement un lien pour chacun de ces mots, pointant vers une page du même nom.
Ça semble assez simple: nous devons juste construire l’expression rationnelle adéquate, et éditer le contenu au vol, pour ajouter les liens.
D’abord l’expression rationnelle. CamelCase peut se décrire comme “une lettre majuscule suivie de n’importe quel lettre ou chiffre, ceci répété au moins une fois”, ce qui exprimé en tant qu’expression rationnelle donne:
CamelCase = Regexp.new( '\b((?:[A-Z]\w+){2,})' )
Maintenant, nous devons juste transformer chaque mot correspondant à notre expression rationnelle en un lien, soit dans le contenu de la page, soit dans sa version post-textilisée:
def index
@page = Page.find_by_name( params[:name] || 'FrontPage' )
@page.content.gsub!( CamelCase, '<a href="/wiki/\1">\1</a>' )
end
Testons:

Maintenant nous devons aussi tester l’existence ou la non-existence
d’une page. Par exemple si vous cliquez sur le lien précédent
“WikiWords”, vous obtiendrez une erreur, notre contrôleur
cherchant une page nommée WikiWords dans la BDD, et cette page
n’existant pas.
Notre approche du problème sera la suivante: si index n’arrive
pas à trouver la page demandée dans la BDD, il redirigera la
requête vers la méthode edit. La méthode edit en retour, sera
modifiée pour créer une nouvelle page si elle ne trouve pas celle
demandée dans la BDD, puis l’éditer. Notez que nous utilisons la
méthode create de notre modèle au lieu de la méthode new:
create crée l’objet en mémoire et le sauve dans la BDD, alors que
new ne crée que l’objet en mémoire, déferrant son enregistrement
dans la BDD à l’appel de save.
def index
@page = Page.find_by_name( params[:name] || 'FrontPage' )
if @page.nil?
redirect_to( :action => 'edit', :name => params[:name] )
else
@page.content.gsub!( CamelCase, '<a href="/wiki/\1">\1</a>' )
end
end
def edit
@page_action = "Editing"
@page = Page.find_by_name( params[:name] ) || Page.create( :name => params[:name] )
end
Comme nous développons un gentil wiki, nous voulons être capable
de fournir à nos utilisateurs une liste des pages disponibles.
Nous allons donc créer une nouvelle action, list, et sa vue
associée list.rhtml.
Donc dans app/controllers/wiki_controller.rb ajoutons:
def list
@pages = Page.find_all
end
list se contente d’obtenir la liste de toutes les pages
disponibles dans la BDD et d’affecter cette liste à la variables
@pages.
Créons aussi le fichier app/views/wiki/list.rhtml avec le code suivant:
<h1>All Of Your Pages</h1>
<ul>
<% for page in @pages %>
<li><a href="/wiki/<%= page.name %>" ><%= page.name %></a></li>
<% end %>
</ul>
Pour chacun des éléments de la variables pages nous ajoutons une
entrée à la liste, cette entrée étant un lien vers la page en
question. Nous devons aussi ajouter un lien vers notre nouvelle
fonctionnalité dans notre layout wiki.rhtml, par exemple dans la
barre droite:
<p>View the <%= link_to "complete list of pages", :action => 'list' %></p>
I.e. nous demandons l’ajout d’un tag lien (a en HTML) qui nous
redirigera vers l’action list (i.e. l’url /wiki/list).
Rechargeons la page index.html dans notre browser, et cliquons
sur notre nouveau lien:

OK. le propos de cette section est de modifier notre liste de page, ou plutôt la façon dont elle s’affiche. Plutôt que de voir cette liste s’afficher dans sa propre page, nous voulons qu’elle apparaisse automagiquement sous notre lien “View the complete list of pages”. C’est très simple en utilisant AJAX et son support dans RoR.
Nous allons d’abord modifier notre layout wiki.rhtml pour inclure
la librairie javascript “prototype:http://prototype.conio.net/ (qui
est fournie avec RoR). Ajoutons donc a notre layout:
<%= javascript_include_tag "prototype" %>
juste sous la ligne <%= stylesheet_link_tag "wiki" %>.
Nous devons maintenant modifier le lien “View this complete list of pages” en quelque chose comme:
<p>View the <%= link_to_remote "complete list of pages",
:update => "pagelist",
:url => { :action => 'list'}
%></p>
<ul id="pagelist"></ul>
Nous demandons donc la création d’un tag de lien, qui effectuera
une requête AJAX au serveur. Ce lien appellera la méthode list de
notre contrôleur, et modifiera l’élément DOM nommé pagelist.
Nous devons donc maintenant modifier notre action list pour
pouvoir répondre à une requête AJAX. Bien que RoR donne un moyen de
savoir, à l’intérieur d’une méthode du contrôleur si elle a été
appelée via une requête AJAX ou une requête rationnelle (ceci afin
de pouvoir supporter les deux), nous modifierons list pour
répondre seulement à des requêtes AJAX.
Plusieurs solutions s’offrent à nous maintenant: soit créer
directement l’HTML à afficher dans l’action list (et violer le
concept MVC), ou utiliser des partials.
Les partials sont des templates qui seront insérées là ou elles
sont appelées. Elles sont souvent utilisées pour afficher une
information qui doit avoir le même look en plusieurs endroits (par
exemple une liste de pages peut apparaître sur plusieurs pages ou
sous-pages, et utiliser les partials permet de ne définir la façon
dont elles apparaitront dans une seule template).
Utilisons donc une partial. Le nom d’une partial commence toujours
avec un souligné, même si ce souligné n’apparait pas quand nous
appelons cette partial. Les partials se trouvent dans le même
répertoire que les autres templates (app/views/wiki dans notre
exemple).
la fonction render permet de rendre des partials de façon simple,
et prend aussi en compte les collections/listes (ce qui est
pratique dans notre cas):
def list
render( :partial => "page",
:collection => Page.find_all,
:layout => false
)
end
Nous demandons donc le rendu de la partial nommée _page.rhtml, en
utilisant la collection Page.find_all, tout ceci sans layout
(comme nous allons insérer ce rendu dans une page contenant déjà un
layout).
Ainsi la partial _page.rhtml sera appelée (et évaluée) pour
chaque élément de la collection (i.e. chaque page). Dans la
partial on pourra se référer à chacun de ces éléments sous le nom
page (i.e. le nom de la partial).
Créons maintenant app/views/wiki/_page.rhtml:
<li><%= link_to page.name, :action => "index", :name => page.name %></li>
Et c’est tout:

Une dernière chose … Supposons que nous voulions que notre liste
apparaisse de façon progressive … Il suffit de changer l’élément
pagelist en:
<ul id="pagelist" style="display: none;"></ul>
afin qu’il soit tout d’abord invisible, puis d’ajouter la librairie
javascript effects:
<%= javascript_include_tag "prototype", "effects" %>
et enfin de changer le link_to_remote en:
<p>View the <%= link_to_remote "complete list of pages",
:update => "pagelist",
:url => { :action => 'list'},
:complete => "new Effect.Appear('pagelist')"
%></p>
et c’est tout !!! En fait, pagelist sera updaté, mais étant
invisible vous ne verrez pas les changements … jusqu’à ce que la
requête AJAX soit terminée (completed) et que le morceau de
javascript (new Effect.Appear('pagelist')) ne soit appelé. Il
affichera alors de façon progressive l’élément pagelist.
Bon … tout ça c’est bien joli, mais … il faudrait quand même que les pages aient un auteur ….
Ce qui veut dire que notre modèle Page doit maintenant avoir un
autre champ: author ... et que donc le schéma de notre BDD doit
changer.
Pour ceci, il y a en gros deux solutions:
Comme nous avons choisi de créer notre schéma initial avec RoR, et que les migrations permettent de changer de façon simple entre l’une ou l’autre version de notre BDD, nous allons les utiliser.
Tout d’abord il faut bien noter une chose: nous ne voulons pas seulement modifier notre modèle/schéma, mais aussi garder les éléments présent dans notre BDD, tout en donnant une valeur par défaut au nouveau champ …
Commençons par créer une migration, et donnons lui le nom
add_author:
script/generate migration add_author
create db/migrate
create db/migrate/001_add_author.rb
Vous pouvez remarquer que notre migration a été numérotée (ce qui
permet d’appliquer les migrations dans l’ordre quand nous en
aurons plusieurs). Regardons le fichier créé, correspondant à
notre migration (db/migrate/001_add_author.rb):
class AddAuthor < ActiveRecord::Migration
def self.up
end
def self.down
end
end
Nous avons donc deux méthodes:
up dans laquelle nous allons indiquer les modifications que
nous souhaitons apporter à notre schéma et à nos données
pour passer de la version courante à la version suivantedown pour indiquer les modifications (schéma et données) à
apporter pour revenir de la version suivante à la version
courante.Dans notre cas, dans up nous devons ajouter une colonne
(author) (qui sera une chaine de caractères, avec une valeur par
défaut fixée à “AnonymousCoward”) et décider que toutes les pages
déjà existantes auront un auteur nommé ‘Fred’.
def self.up
add_column :pages, :author, :string, :default => "AnonymousCoward"
Page.find( :all ).each {|p| p.update_attribute :author, "Fred" }
end
En ce qui concerne down, on indiquera juste de
détruire la colonne author:
def self.down
remove_column :pages, :author
end
Nous pouvons alors exécuter notre migration:
rake migrate
et vérifier que notre page FrontPage a bien maintenant un auteur
nommé ‘Fred’:
> script/console
Loading development environment.
>> Page.find_by_name( 'FrontPage' )
=> #<Page:0xb7473eac @attributes={"name"=>"FrontPage", "author"=>"Fred", "id"=>"2", "content"=>"Welcome to *my* __super__ front page now with WikiWord"}>
>>
Et voilà :)
Si nous regardons notre schéma db/schema.rb, nous nous
apercevons qu’il n’a pas été mis à jour. Qu’à cela ne tienne. Nous
devons d’abord indiquer à RoR d’utiliser Ruby comme format par
défaut pour le schéma. Cela se fait en éditant le fichier
config/environment.rb, de façon à faire apparaître/dé-commenter
la ligne:
config.active_record.schema_format = :ruby
puis en ré-appliquant notre migration:
rake migrate
Si nous regardons maintenant notre fichier db/schema.rb:
ActiveRecord::Schema.define(:version => 1) do
create_table "pages", :force => true do |t|
t.column "name", :string, :limit => 30
t.column "content", :text
t.column "author", :string, :default => "AnonymousCoward"
end
end
On peut aussi remarquer qu’un numéro de version pour notre schéma est apparu ….
TODO.