blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

jeudi 22 mars 2012

GEM Hell

Il y a quelque jours, j'ai fait un gem update pour mettre à jour toutes les gems installées sur mon PC puis un gem cleanup pour supprimer toutes les anciennes versions.

Ce matin, je lance un des anciens projets qui me sert à faire différente essais avec DataMapper et Nokogiri et là c'est le drame...

C:\Ruby\_projets\cinemas> ruby app.rb
C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/dependency.rb:247:in `to_specs': Could not find multi_json (~> 1.0.3) amongst
[RedCloth-4.2.9-x86-mingw32, activesupport-3.2.2, addressable-2.2.7, backports-2.3.0, bcrypt-ruby-3.0.1-x86-mingw32, bui
lder-3.0.0, bundler-1.1.2, configuration-1.3.1, data_mapper-1.2.0, data_objects-0.10.8, diff-lcs-1.1.3, dm-aggregates-1.
2.0, dm-constraints-1.2.0, dm-core-1.2.0, dm-do-adapter-1.2.0, dm-migrations-1.2.0, dm-serializer-1.2.1, dm-sqlite-adapt
er-1.2.0, dm-timestamps-1.2.0, dm-transactions-1.2.0, dm-types-1.2.1, dm-validations-1.2.0, do_sqlite3-0.10.8-x86-mingw3
2, fastercsv-1.5.4, haml-3.1.4, heroku-2.21.3, i18n-0.6.0, json-1.6.5, json_pure-1.6.5, launchy-2.1.0, mail-2.4.4, maruk
u-0.6.0, mime-types-1.18, minitest-2.11.4, monkey-lib-0.5.4, multi_json-1.1.0, nesta-0.9.13, netrc-0.7.1, nokogiri-1.5.2
-x86-mingw32, polyglot-0.3.3, pony-1.4, rack-1.4.1, rack-flash-0.1.2, rack-protection-1.2.0, rack-test-0.6.1, rake-0.9.2
.2, rdiscount-1.6.8, rdoc-3.12, rest-client-1.6.7, rspec-2.9.0, rspec-core-2.9.0, rspec-expectations-2.9.0, rspec-mocks-
2.9.0, rubygems-update-1.8.20, rubyzip-0.9.6.1, sass-3.1.15, shotgun-0.9, sinatra-1.3.2, sinatra-advanced-routes-0.5.1,
sinatra-reloader-0.5.0, sinatra-sugar-0.5.1, sqlite3-1.3.5-x86-mingw32, stringex-1.3.2, syntax-1.0.0, term-ansicolor-1.0
.7, tilt-1.3.3, treetop-1.4.10, uuidtools-2.1.2] (Gem::LoadError)
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:777:in `block in activate_dependencies'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:766:in `each'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:766:in `activate_dependencies'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:750:in `activate'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:780:in `block in activate_dependencies'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:766:in `each'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:766:in `activate_dependencies'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/specification.rb:750:in `activate'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems.rb:211:in `rescue in try_activate'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems.rb:208:in `try_activate'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:59:in `rescue in require'
    from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:35:in `require'
    from app.rb:8:in `<main>'

Il semblerait donc que Ruby Could not find multi_json (~> 1.0.3) amongst toutes les gems que j'ai sur mon ordinateur. Alors que j'ai la gem multi_json-1.1.0 installée. Normal, c'est pas la même version.

Comme je commence à m'habituer à Ruby, je sais maintenant qu'il faut faire comme avec Oracle et regarder à la fin de la pile d'erreur pour savoir qu'est-ce qui a provoqué ce problème :

from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:59:in `rescue in require'
from C:/Ruby/lib/ruby/site_ruby/1.9.1/rubygems/custom_require.rb:35:in `require'
from app.rb:8:in `<main>'

Ma ligne 8 :

require "data_mapper"

En fouillant bien, il semblerait que dm-types (le plugin DataMapper qui permet d'avoir des types de données supplémentaires) a une dépendance avec multi-json 1.0.3 (cf dm-types.gemspec)

J'installe donc la vieille version de multi_json en faisant un voeu (c'est la 1° fois que j'utilise -v) :

C:\Ruby> gem install multi_json -v 1.0.3
Fetching: multi_json-1.0.3.gem (100%)
Successfully installed multi_json-1.0.3
1 gem installed
Installing ri documentation for multi_json-1.0.3...
Installing RDoc documentation for multi_json-1.0.3...

Je relance mon application :

C:\Ruby> cd .\_projets\cinemas
C:\Ruby\_projets\cinemas> ruby app.rb
[2012-03-22 08:06:59] INFO  WEBrick 1.3.1
[2012-03-22 08:06:59] INFO  ruby 1.9.2 (2011-02-18) [i386-mingw32]
== Sinatra/1.3.2 has taken the stage on 4567 for development with backup from WEBrick
[2012-03-22 08:06:59] INFO  WEBrick::HTTPServer#start: pid=5072 port=4567

Ca tiendra jusqu'à mon prochain gem cleanup. Mais sinon, c'est peut être pour éviter ce genre de problème qu'il y a Bundler ?

mercredi 29 septembre 2010

Des lapins à la sauce REST

Ceci est la traduction du tutoriel Sinatra "Restful Rabbits" de Darren Jones.

Dans ce billet, je vais explorer la façon de créer une ressource en construisant une architecture REST et en utilisant Sinatra et DataMapper.

Mais pour commencer, c'est quoi ce REST ?

REST signifie Representational State Transfer et a été présenté par Roy Fielding pour sa thèse de doctorat en 2000. De façon sommaire, c’est un style d’architecture qui permet d'accéder à des ressources (en général des objets stockés dans une base de données) à partir d'URLs spécifiques. REST permet également d'utiliser des URLs pour interagir avec ces ressources (pour les mettre à jour en particulier). Par exemple, l'URL /people/michel/edit pourrait permettre de modifier ma fiche personnelle. Et l'URL /people/michel/delete aurait pour effet de supprimer ma fiche de la base de données.

Au cours de ce tutoriel, les ressources que je vais chercher à gérer seront des lapins, mais vous pouvez l'adapter sans peine pour gérer n'importe quel autre objet que vous stockez dans votre base de données. Toutes les données seront enregistrées en base de données grâce à DataMapper et nous passerons par des URLs spécifiques pour effectuer les actions CRUD sur chaque objet : Création, Lecture, Modification et Suppression.

Avec REST, on emploie habituellement 7 gestionnaires d'URLs. Dans notre cas, nous en définirons 8 pour avoir en plus un gestionnaire qui nous permettra de demander confirmation avant d'effectuer une suppression. Tout cela correspond grosso-modo aux actions CRUD d'une base de données.

Voici les gestionnaires et leurs URLs associées :

  • List (/lapins) - une page d'index qui affiche toutes les ressources
  • Show (/lapins/1) - une page qui affiche une ressource données
  • New (/lapins/new) - un formulaire pour saisir une nouvelle ressource
  • Create (/lapins) - création d'une nouvelle ressource (il n'y a pas de page web pour cela)
  • Edit (/lapins/edit/1) - un formulaire pour mettre à jour une ressource existante
  • Update (/lapins/1) - modification d'une ressource (il n'y a pas de page web pour cela)
  • Delete Confirmation (/lapins/delete/1) - une page demandant si on veut réellement supprimer une ressource
  • Delete (/lapins/1) - supprime la ressource donnée (il n'y a pas de page web pour cela)

Vous avez sans doute remarqué qu'un certain nombre d'URLs sont identiques (celles pour Show, Update et Delete). Ceci est possible parce qu'il existe 4 verbes HTTP : GET, POST, UPDATE et DELETE. Sinatra est capable de savoir quel type de requête a été réalisée et d'employer l'action adéquate. Ainsi, l'URL /lapins/1 affichera le lapin dont l'identifiant est 1 quand une requête GET est effectuée, mais elle supprimera le lapin si c'est une requête DELETE qui a été faite.

Le code

Pour commencer, nous devons charger les librairies nécessaires. J'utilise en particulier Haml pour les vues, mais ce ne devrait pas être compliqué d'utiliser Erb ou un autre système de template :

require 'rubygems'
require 'sinatra'
require 'data_mapper'
require 'haml'

Puis nous définissons la base de données. Cette ligne de code teste s'il existe une base de données paramétrée sur Heroku et si ce n'est pas le cas utilise une base de données SQLite locale nommée lapins.db :

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/lapins.db")

Nous devons ensuite créer la classe Lapin (ou tout autre classe correspondant aux objets que vous souhaitez gérer). Le premier champ "id" est très important : grâce à lui votre ressource a un identifiant numérique auto-incrémenté qui permet de la référencer. Cela signifie que la base de données va automatiquement se charger d'affecter un identifiant unique à chaque ressource. Les autres propriétés de la classe Lapin sont "nom", "description", "age" et "couleur". Et il y a également deux autres champs "created_at" et "updated_at" que le plugin datamapper-timestamps va automatiquement mettre à jour lorsque la ressource sera créée ou modifiée. DataMapper a également un autre plugin très utile : datamapper-validations. Celui-ci contrôle automatiquement si le nom est renseigné (parce qu'il est marqué "required") et si l'âge est bien une valeur de type integer. Si cette validation échoue, la ressource ne sera pas créée ou mise à jour et les erreurs seront signalées (plus de détails dans la suite du tutoriel).

class Lapin
  include DataMapper::Resource 
  property :id,           Serial
  property :nom,          String, :required => true
  property :description,  Text
  property :age,          Integer
  property :couleur,      String
  property :created_at,   DateTime
  property :updated_at,   DateTime
end

Nous allons maintenant créer les différents gestionnaires, en commençant par celui pour la liste :

# List : affiche la liste des lapins
get '/lapins' do
  @lapins = Lapin.all
  haml :index
end

Le gestionnaire List correspond à l'URL "/lapins". Il commence par retrouver toutes les ressources enregistrées dans la base de données pour les stocker dans un tableau d'instance nommé @lapins. Ce tableau pourra ensuite être utilisé dans la vue "index".

Nous créons ensuite le gestionnaire New :

# New : affiche le formulaire de création d'un lapin
get '/lapins/new' do
  @lapin = Lapin.new
  haml :new
end

Celui-ci correspond à l'URL "/lapins/new". Il crée un nouvel objet Lapin et le stocke dans une variable d'instance @lapin dont le formulaire de saisie a besoin. Puis il affiche le formulaire de saisie contenu dans la vue "new".

Le gestionnaire qui suit est Create. Outre le fait qu'il est un tout petit plus compliqué, il n'existe pas non plus de vue associée à celui-ci :

# Create : création d'un nouveau lapin
post '/lapins' do
  @lapin = Lapin.new(params[:lapin])
  if @lapin.save
    status 201
    redirect '/lapins/' + @lapin.id.to_s
  else
    status 400
    haml :new
  end
end

La première chose à noter, c'est qu'il utilise la même URL que l'action List, à savoir "/lapins". Ce qui fait la différence entre les deux, c'est que ce gestionnaire est seulement invoqué lorsque la requête HTTP est de type POST, ce qui est fort heureusement le cas lorsqu'un formulaire HTML est envoyé vers le serveur.

La première chose que fait le gestionnaire Create, c'est de créer un nouvel objet Lapin en utilisant les informations envoyées par le formulaire, celles-ci étant conservées dans le hash params:lapin. Puis il vérifie si les valeurs saisies sont correctes en tentant d'enregistrer l'objet @lapin. Si cela réussi, il renvoie alors le statut HTTP 201 (qui signifie Created) et redirige le navigateur vers le gestionnaire Show pour qu'il affiche la ressource qui vient d'être créée. Quand il ne réussi pas à enregistrer l'objet @lapin, il renvoie le statut 400 (Bad Request) et réaffiche la vue "new" pour que l'utilisateur puisse corriger sa saisie et la re-soumettre.

Le gestionnaire suivant est Edit. L'URL associée à celui-ci est "/lapins/edit/:id" où ":id" est une valeur entière qui fait référence à l'identifiant d'un objet Lapin particulier. Cet identifiant est disponible dans le hash params:id et on l'emploie dans la première ligne de code pour retrouver le lapin correspondant dans la base de données puis stocker l'objet obtenu dans la variable d'instance @lapin qui pourra ensuite être utilisée par la vue "edit".

# Edit : affiche le formulaire de modification d'un lapin
get '/lapins/edit/:id' do
  @lapin = Lapin.get(params[:id])
  haml :edit
end

Lorsque l'utilisateur va valider sa saisie, les informations du formulaire "edit" seront envoyées vers le gestionnaire Update présenté ci-dessous :

# Update : modification d'un lapin existant
put '/lapins/:id' do
  @lapin = Lapin.get(params[:id])
  if @lapin.update(params[:lapin])
    status 201
    redirect '/lapins/' + params[:id]
  else
    status 400
    haml :edit  
  end
end

Le code pour gérer la modification ressemble d'assez près à celui du gestionnaire Create et là aussi il n'y a pas besoin d'avoir une vue associée. La première chose qui est faite, c'est de retrouver le lapin dont l'identifiant est passé avec l'URL pour le stocker dans la variable d'instance @lapin. Puis on modifie (et on sauvegarde) cet objet en utilisant les informations du formulaire. Si cela réussi, le navigateur est redirigé vers l'URL du gestionnaire Show pour permettre à l'utilisateur de constater que la mise à jour s'est correctement déroulée. Lorsque la mise à jour a échouée, la vue "edit" est réaffichée pour lui permettre de corriger sa saisie.

Le gestionnaire suivant est celui destiné à confirmer la suppression d'une ressource. Il va servir à afficher une page web dans laquelle on demande à l'utilisateur s'il veut réellement supprimer le lapin ou pas.

# Confirm : confirmation de la suppression d'un lapin
get '/lapins/delete/:id' do
  @lapin = Lapin.get(params[:id])
  haml :delete
end

Dans le cas où l'utilisateur clique sur le bouton "Delete" depuis cette page de confirmation, cela appelle le gestionnaire Delete :

# Delete : suppression d'un lapin
delete '/lapins/:id' do
  Lapin.get(params[:id]).destroy
  redirect '/lapins'  
end

Vous pouvez voir que l'URL "/lapins/:id" associée au gestionnaire Delete est la même que pour le gestionnaire Update. Mais dans ce cas , le formulaire de confirmation de la suppression aura fait en sorte d'envoyer une requête de type DELETE.

Le gestionnaire DELETE a besoin d'une seule ligne de code pour retrouver le lapin dans la base de données (la partie Lapin.get(params[:id])) et l'y supprimer (la partie .destroy). Puis il redirige le navigateur vers le gestionnaire List grâce auquel l'utilisateur pourra constater que le lapin a bien été supprimé.

Le gestionnaire Show est le dernier qui nous reste à prendre en compte et il est lui aussi associé à l'URL "/lapins/:id". Il est impératif de le faire apparaitre en dernier dans le code parce que Sinatra examine les routes dans l'ordre où elles apparaissent dans le code source. Par conséquent, si le gestionnaire Show avait été codé avant le gestionnaire Create dont l'URL est "/lapin/new", Sinatra aurait considéré que "new" correspondait au paramètre ":id" et il aurait invoqué le gestionnaire Show qui aurait essayé de rechercher dans la base de données un lapin dont l'identifiant serait "new", ce qui n'existe bien évidemment pas.

# Show : affichage d'un lapin
get '/lapins/:id' do
  @lapin = Lapin.get(params[:id])
  haml :show
end

C'est un gestionnaire plutôt simple. Il retrouve le lapin avec le bon identifiant et le stocke dans une variable d'instance @lapin qui pourra alors âtre manipulée dans la vue "show" associée.

Et pour finir, juste avant la fin du source Ruby, il y a une ligne de code destinée à DataMapper qui a pour effet de répercuter les modifications apportées à votre base de données (comme par exemple l'ajout d'une propriété) sans supprimer les données que celle-ci contient (on peut pas faire plus simple comme système de migration de données).

DataMapper.auto_upgrade!

__END__

La ligne __END__ indique qu'il s'agit de la fin du fichier, ou plus précisément de la fin du code Ruby. Elle sera suivie par les vues correspondant aux différents gestionnaires que nous avons codés auparavant.

Le template "layout" servira de squelette général pour toutes les vues et il contient le code nécessaire pour afficher une page HTML 5. Toutes les autres vues seront intégrées à l'endroit où apparait la ligne = yield :

@@layout
!!! 5
%html
  %head
    %meta(charset="utf-8")
    %title Lapins
  %body
    = yield

La première vue est la page d'index qui nous sert à afficher tous les lapins qui existent dans la base de données. Ceux-ci ont été stockés dans le tableau d'instance @lapins par le gestionnaire List. Nous parcourons donc ce tableau dans la vue pour générer une ligne pour chaque lapin, avec des liens pour afficher, modifier ou supprimer celui-ci. Et nous avons aussi prévu un lien en haut de la liste pour permettre d'ajouter un nouveau lapin :

@@index
%h3 Lapins
%a(href="/lapins/new")Créer un nouveau lapin
- unless @lapins.empty?
  %ul#lapins
  - @lapins.each do |lapin|
    %li{:id => "lapin-#{lapin.id}"}
      %a(href="/lapins/#{lapin.id}")= lapin.nom
      %a(href="/lapins/edit/#{lapin.id}") Modifier
      %a(href="/lapins/delete/#{lapin.id}") Supprimer
- else
  %p Pas de lapins !

La vue suivante sert pour afficher le détail d'un lapin donné, à partir de la variable d'instance @lapin initialisée par le gestionnaire Show. Chaque ligne de cette vue présente une des propriété de l'objet Lapin. Et pour finir, nous avons un lien qui permet de modifier le lapin, un autre pour le supprimer et un pour revenir à la liste des lapins :

@@show
%h3= @lapin.nom
%p Couleur : #{@lapin.couleur}
%p Age : #{@lapin.age}
%p Description : #{@lapin.description}
%p Crée le : #{@lapin.created_at}
%p Mis à jour le : #{@lapin.updated_at}
%a(href="/lapins/edit/#{@lapin.id}") Modifier
%a(href="/lapins/delete/#{@lapin.id}") Supprimer
%a(href='/lapins') Retour à l'index

Les deux vues suivantes sont très similaires. La première correspond à la page qui est renvoyée pour ajouter un nouveau lapin et la seconde celle qui sert pour modifier un lapin existant.

Ces deux pages commencent par afficher une vue partielle "errors" qui sert dans le cas où des erreurs se seraient produites suite à un premier envoi du formulaire avec des données incorrectes.

Le formulaire dans la vue "new" utilise une méthode HTTP POST et celui de la vue "edit" une méthode HTTP PUT afin d'utiliser le verbe HTTP adéquat pour viser le bon gestionnaire. Etant donné que les navigateurs ne savent pas envoyer une requête PUT, on triche en envoyant une requête POST (ce que savent faire les navigateurs) accompagnée d'un champ caché nommé "_method" ayant la valeur "PUT". Sinatra est alors assez conciliant pour considérer cela comme une vraie requête PUT.

Après le type de requête, ces deux vues affichent la vue partielle "form" qui va contenir le code complet du formulaire servant à saisir la fiche d'un lapin. Il est plus pratique de gérer ça dans une vue partielle à part plutôt que de répéter le même code dans les deux vues "new" et "edit", notamment dans le cas où le formulaire devrait évoluer.

@@new
= haml :errors, :layout => false
%form(action="/lapins" method="POST")
  %fieldset
    %legend Créer un nouveau lapin
    = haml :form, :layout => false
  %p
    %input(type="submit" value="Créer")
    ou <a href='/lapins'>Annuler</a>


@@edit
= haml :errors, :layout => false
%form(action="/lapins/#{@lapin.id}" method="POST")
  %input(type="hidden" name="_method" value="PUT")
  %fieldset
    %legend Modifier ce lapin
    = haml :form, :layout => false
  %p
    %input(type="submit" value="Modifier")
    ou <a href='/lapins'>Annuler</a>

Voici maintenant le code pour le formulaire "form". Il contient les champs de saisie nécessaire pour chaque propriété de l'objet Lapin. Et comme il est affiché depuis les vues "new" et "edit", toute modification que l'on y ferait serait alors visibles dans ces deux vues :

@@form
%label(for="nom")Nom :
%input#nom(type="text" name="lapin[nom]"value="#{@lapin.nom}")

%p
%label(for="couleur")Couleur :
%select#quantity(name="lapin[couleur]")
  - %w[noir blanc gris marron].each do |couleur|
    %option{:value => couleur, :selected => (true if couleur == @lapin.couleur)}= couleur
    
%p
%label(for="description") Description :
%textarea#description(name="lapin[description]")
  =@lapin.description

%p
%label(for="quantity") Age :
%input#age(type="text" name="lapin[age]" value="#{@lapin.age}")

Après cela nous avons la vue "errors" qui sera affichée seulement dans le cas où DataMapper renvoie des erreurs de validation (par exemple si l'âge saisi n'est pas un entier). Son rôle est d'afficher la liste des erreurs rencontrées afin de guider l'utilisateur pour qu'il puisse corriger sa saisie :

@@errors
-if @lapin.errors.any?
  %ul#errors
  -@lapin.errors.each do |error|
    %li= error

La dernière vue affiche la page pour confirmer la suppression d'un lapin. Elle affiche seulement le nom du lapin et demande à l'utilisateur s'il est certain de vouloir réellement supprimer ce lapin. Si c'est le cas, on utilise un formulaire pour atteindre le gestionnaire Delete. Comme dans le cas du PUT destiné au gestionnaire Update, on utilise le champ caché "_method" avec une valeur "DELETE" pour simuler une requête HTTP DELETE qu'aucun navigateur ne sait gérer de façon native.

@@delete
%h3 Souhaitez-vous réellement supprimer #{@lapin.nom} ?
%form(action="/lapins/#{@lapin.id}" method="post")
  %input(type="hidden" name="_method" value="DELETE")
  %input(type="submit" value="Supprimer")
  ou <a href='/lapins'>Annuler</a>

Conclusion

Ca y est, c'est fait. Vous avez maintenant un parfait exemple de la façon de gérer des ressources à la sauce REST et vous pouvez voir ce que donne la version originale développée par Darren (http://rabbits.heroku.com/rabbits) ou ma version francisée (http://lapins.heroku.com/lapins).

Etant donné que ce coup-ci j'ai également complètement traduit l'application développée, j'ai aussi cherché si le plugin datamapper-validations pouvait renvoyer des messages d'erreurs en français. Mais comme je n'ai pas trouvé comment faire, je suis passé par des messages d'erreurs personnalisés :

class Lapin
  include DataMapper::Resource 
  property :id,           Serial
  property :nom,          String, :required => true, :messages => { :presence => "Le nom est obligatoire" }
  property :description,  Text
  property :age,          Integer, :message => "L'age doit etre un nombre sans virgule"
  property :couleur,      String
  property :created_at,   DateTime
  property :updated_at,   DateTime
end

Je ne suis pas certains que ce soit la bonne solution mais ça marche, même si je n'ai pas réussi à mettre les circonflexes dans le second message sans provoquer d'erreur.

jeudi 2 septembre 2010

SuperDo : Une todo liste avec Sinatra et DataMapper

Ceci est la traduction du tutoriel "SuperDo - A Sinatra and DataMapper To Do List" de Darren Jones.

Dans ce nouveau tutoriel dédié à Sinatra, Darren explique comment construire une petite application de type Todo liste qui utilisera une base de données pour enregistrer les différentes tâches. Cela donnera l'occasion d'aborder les points suivants :

  • Installer SQLIte et DataMapper
  • Se connecter à la base de données
  • Gérer les actions de types CRUD
  • Utiliser des URLs de type RESTFul

Avant de commencer, il est bien entendu nécessaire d'avoir procédé à l'installation de Ruby, de Ruby Gems et de Sinatra.

L'application que nous allons développer s'appellera Superdo et nous pouvons d'ores et déjà voir ce qu'elle donnera sur la version que Darren à déployé sur Heroku.

Installer SQLite et Datamapper

Dans ce tutoriel, nous allons utiliser SQLite comme base de données et DataMapper comme ORM pour nous connecter à notre base de données.

Nous devons donc commencer par installer SQLite sous Windows 7 en téléchargeant la version la plus récente de sqlite3.dll depuis le site de SQLite, soit sqlitedll-3_6_23_1.zip à ce jour. Après avoir décompacté cette archive, il ne reste qu'à copier le fichier sqlite3.dll dans le répertoire C:\Ruby\bin.

Puis il faut installer le gem qui permet la prise en charge de SQLite par Ruby :

C:\Ruby>gem install sqlite3-ruby --no-rdoc --no-ri

Ce qui donne le résultat suivant :

=============================================================================

  You've installed the binary version of sqlite3-ruby.
  It was built using SQLite3 version 3.6.23.1.
  It's recommended to use the exact same version to avoid potential issues.

  At the time of building this gem, the necessary DLL files where available
  in the following download:

  http://www.sqlite.org/sqlitedll-3_6_23_1.zip

  You can put the sqlite3.dll available in this package in your Ruby bin
  directory, for example C:\Ruby\bin

=============================================================================

Successfully installed sqlite3-ruby-1.3.1-x86-mingw32
1 gem installed

Note : les paramètres --no-rdoc --no-ri ont permis d'éviter d'installer la documentation en local.

On installe ensuite le gem Datamapper :

C:\Ruby>gem install data_mapper --no-rdoc --no-ri

Ce qui donne :

Successfully installed extlib-0.9.15
Successfully installed addressable-2.2.0
Successfully installed dm-core-1.0.2
Successfully installed dm-aggregates-1.0.2
Successfully installed dm-migrations-1.0.2
Successfully installed dm-constraints-1.0.2
Successfully installed dm-transactions-1.0.2
Successfully installed fastercsv-1.5.3
Successfully installed json_pure-1.4.6
Successfully installed dm-serializer-1.0.2
Successfully installed dm-timestamps-1.0.2
Successfully installed dm-validations-1.0.2
Successfully installed uuidtools-2.1.1
Successfully installed stringex-1.1.0
Successfully installed dm-types-1.0.2
Successfully installed data_mapper-1.0.2
16 gems installed

Et il ne reste plus qu'à installer l'adaptateur SQLite pour que Datamapper puisse gérer les bases de données SQLite :

C:\Ruby>gem install dm-sqlite-adapter --no-rdoc --no-ri

Ce qui donne :

=============================================================================

  You've installed the binary version of do_sqlite3.
  It was built using Sqlite3 version 3_6_23_1.
  It's recommended to use the exact same version to avoid potential issues.

  At the time of building this gem, the necessary DLL files where available
  in the following download:

  http://www.sqlite.org/sqlitedll-3_6_23_1.zip

  You can put the sqlite3.dll available in this package in your Ruby bin
  directory, for example C:\Ruby\bin

=============================================================================

Successfully installed data_objects-0.10.2
Successfully installed do_sqlite3-0.10.2-x86-mingw32
Successfully installed dm-do-adapter-1.0.2
Successfully installed dm-sqlite-adapter-1.0.2
4 gems installed

Après cela, tout est en place pour pouvoir développer une application web avec du contenu dynamique.

Se connecter à la base de données

La première chose à faire est de créer un répertoire nommé "todo" pour notre application et de commencer par y enregistrer un fichier "main.rb" avec le code ci-dessous :

require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

class Task
  include DataMapper::Resource

  property :id, Serial
  property :name, String
  property :completed_at, DateTime
end

DataMapper.auto_upgrade!

Ca fait pas mal de nouveaux trucs d'un coup, aussi je vais les expliquer un par un.

Les 4 premières lignes déclarent les gems nécessaires :

require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'

On a besoin de "rubygems" et de "sinatra" pour toutes les applications Sinatra et le gem "dm-core" est nécessaire pour DataMapper. Par rapport au tutoriel de Darren, on a besoin en plus du gem "dm-migrations" car il n'est plus intégré à "dm_core" depuis le passage en version 1.00 de DataMapper.

Le morceau de code suivant permet de se connecter à la base de données :

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

Voila un bout de code très intéressant qui vaut le coup d'être conservé. Il commence par tester si l'application est déployée sur Heroku et dans ce cas se connecte à la base de données qui y est hébergée. Dans le cas contraire, il se connecte à une base de données SQLite locale nommée "development.db". Si celle-ci n'existe pas encore, SQLite la crée automatiquement.

Le bloc de code suivant créé une classe Task avec ses propriétés :

class Task
  include DataMapper::Resource

  property :id, Serial
  property :name, String
  property :completed_at, DateTime
end

Vous aurez besoin de la ligne de code include DataMapper::Resource dans toutes les classes qui utilisent DataMapper. Puis les 3 lignes suivantes définissent les propriétés de votre classe Task. La première défini un identifiant unique propre à chaque tâche. Le type Serial qui lui est associé indique que la propriété "id" doit être auto-incrémentée à chaque fois qu'une nouvelle tâche est ajoutée à la base de données. La deuxième propriété "name" va servir à un libellé pour chaque tâche et nous indiquons à DataMapper que celui-ci sera de type String. Et enfin, la dernière propriété "completed_at" est définie en tant que DateTime pour enregistrer à quel moment la tâche a été marquée comme terminée. Cette propriété nous permettra également de savoir si la tâche a été réalisée ou non : une tâche dont la propriété "completed_at" sera à nil étant considérée comme à faire.

La dernière ligne du fichier "main.rb" contient l'appel à la méthode auto_upgrade! :

DataMapper.auto_upgrade!

Cette méthode indique à DataMapper de mettre à jour la base de données pour refléter toutes modifications apportées à la classe Task. Grâce à cela, nous pouvons ajouter ou supprimer des propriétés à la classe Task et DataMapper se chargera de répercuter ces modifications dans la base de données. Cela permet de développer très rapidement et nous évite de mettre les mains dans le cambouis pour essayer de faire évoluer la structure de notre base de données. La commande auto_upgrade! a l'avantage de conserver les données existantes dans la base de données. Si vous préférez repartir d'une base de données vide, vous pouvez choisir d'utiliser la commande DataMapper.auto_migrate! qui efface définitivement toutes les données déjà présentes dans la base de données.

OK. Maintenant que la base de données est configurée, nous allons pouvoir la tester. Comme nous n'avons pas encore d'interface web, nous allons ouvrir une invite de commandes et aller dans le répertoire "todo" pour lancer la commande suivante :

C:\Ruby\projets\todo>irb -r main.rb

Cela a pour effet d'ouvrir un shell "irb", mais étant donné que nous avons ajouté l'option "-r main.rb", tout le code de notre fichier "main.rb" est chargé dans notre session. Par conséquent, nous avons accès à la base de données et pouvons créer, rechercher ou supprimer des tâches.

Pour commencer, examinons la liste de nos tâches :

irb(main):001:0> Task.all
=> []

Cette commande renvoie à juste titre un tableau vide puisque pour l'instant nous n'avons encore aucune tâche dans notre base de données. On va donc en créer une nouvelle :

irb(main):002:0> t = Task.new
=> #<Task @id=nil @name=nil @completed_at=nil>
irb(main):003:0> t.name = "Acheter du lait"
=> "Get milk"

Pour l'instant, cette tâche n'existe qu'en mémoire. Nous devons explicitement l'enregistrer dans la base de données :

irb(main):004:0> t.save
=> true

Vérifions que cela a correctement fonctionné en recherchant maintenant la première tâche :

irb(main):005:0> Task.first
=> #<Task @id=1 @name="Acheter du lait" @completed_at=nil>

Il existe une autre façon pour créer une nouvelle tâche, en utilisant la commande create :

irb(main):006:0> Task.create(:name => "Acheter des bananes")
=> #<Task @id=2 @name="Acheter des bananes" @completed_at=<not loaded>>

Nous pouvons ajouter des paramètres en les plaçant entre parenthèses après le nom de méthode et en utilisant la notation hash du langage Ruby. Avec la commande create, il n'est pas nécessaire d'enregistrer nous même la nouvelle tâche, car elle est automatiquement sauvegardée dans la base de données. Nous pouvons vérifier cela en demandant à afficher toutes les tâches de la table Tasks :

irb(main):007:0> Task.all
=> [#<Task @id=1 @name="Acheter du lait" @completed_at=nil>, #<Task @id=2 @name=
"Acheter des bananes" @completed_at=nil>]

Comme vous le constatez, nos deux tâches apparaissent désormais dans le tableau. Mais ça risque de devenir un peu compliqué pour s'y retrouver parmi toutes les tâches dès lors qu'on aura un grand nombre d'enregistrement. C'est pourquoi nous pouvons plus simplement utiliser la méthode count :

irb(main):008:0> Task.all.count
=> 2

Cela nous indique qu'il y a actuellement 2 tâches enregistrées dans la base de données.

Nous avons donc vu comment créer et rechercher des enregistrements dans notre base de données. Nous allons à présent voir comment les modifier. Supposons que nous préférions le lait demi-écrémé. Nous devons tout d'abord retrouver le bon enregistrement :

irb(main):009:0> t = Task.first(:name => "Acheter du lait")
=> #<Task @id=1 @name="Acheter du lait" @completed_at=nil>

Il s'agit là d'une des façons de retrouver des enregistrements dans une base de données. Dans notre cas, nous voulons retrouver la tâche dont la propriété "name" contient "Acheter du lait" pour la charger dans la variable t. Nous avons alors deux méthodes pour modifier cet enregistrement. La première consiste à modifier manuellement la variable t puis à la sauvegarder :

irb(main):010:0> t.name = "Acheter du lait demi-écrémé"
=> "Acheter du lait demi-écrémé"
irb(main):011:0> t.save
=> true
irb(main):012:0> t
=> #<Task @id=1 @name="Acheter du lait demi-écrémé" @completed_at=nil>

La seconde méthode est d'utiliser la méthode update. Disons que finallement nous voulons du lait entier :

irb(main):013:0> t.update(:name => "Acheter du lait entier")
=> true
irb(main):014:0> t
=> #<Task @id=1 @name="Acheter du lait entier" @completed_at=nil>

Et pour finir, voyons comment gérer la dernière des actions CRUD : la suppression. Disons que nous n'avons pas besoin de lait ce qui fait que nous pouvons supprimer cette tâche. On commence donc par retrouver cette tâche puis nous la supprimons à l'aide de la commande destroy :

irb(main):015:0> t = Task.get(1)
=> #<Task @id=1 @name="Acheter du lait entier" @completed_at=nil>
irb(main):016:0> t.destroy
=> true

Cette fois-ci, j'ai employé la méthode get pour retrouver la tâche dont l'identifiant est 1. Cette syntaxe n'est utilisable que lorsque l'on passe par la clé primaire pour effectuer la recherche. Dans notre cas, la clé primaire est la propriété id dont la valeur est 1 pour la tâche recherchée. Nous aurons souvent recours à cette méthode par la suite pour retrouver les tâches à partir d'URLs uniques. Nous pouvons désormais vérifier que la tâche a bien été supprimée en redemandant la liste de toutes les tâches :

irb(main):017:0> Task.all
=> [#<Task @id=2 @name="Acheter des bananes" @completed_at=nil>]

Nous constatons alors que seule la tâche "Acheter des bananes" reste enregistrée dans notre base de données.

C'était plutôt amusant et le fait d'utiliser la console constitue une excellente entrée en matière pour faire des essais et tester notre base de données. Mais notre objectif étant de créer une application internet, il est temps de développer une interface pour toutes ces actions.

Associer des URLs RESTful aux actions CRUD

L'interface web que nous allons créer pour interagir avec notre base de données suivra une architecture REST. Cela consiste à utiliser les verbes http POST, GET, PUT et DELETE. Ceux-ci sont très similaires aux actions CRUD (Create, Read, Update et Delete) destinées à mettre à jour la base de données. Chaque tâche aura sa propre URL de la forme "/tasks/:id" où ":id" correspond à l'identifiant unique de la tâche. Par exemple, la tâche "Acheter des bananes" que nous avons créée auparavant aurait l'URL "/tasks/2" étant donné que son identifiant est 2. Le fait qu'il faille lire, modifier ou supprimer une tâche dépendra du verbe http que le navigateur enverra. Par conséquent, même si l'URL sera toujours la même, l'action effectuée sera différente. Et chacune de ces actions sera traitée par un handler différent dans notre application Sinatra.

Commençons par créer le handler qui va servir à consulter les tâches. Pour cela, nous devons modifier le code du fichier "main.rb" de la façon suivante :

require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

class Task
  include DataMapper::Resource

  property :id, Serial
  property :name, String
  property :completed_at, DateTime
end

# Afficher une tâche
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

DataMapper.auto_upgrade!

Le handler qui sert à afficher une tâche est contenu dans le code ci-dessous :

# Afficher une tâche
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

Ce code recherche la tâche dont l'identifiant est égal au paramètre "id" mentionné dans l'URL et affecte cette tâche à la variable d'instance @task qui pourra être utilisée au niveau de la vue. Nous demandons ensuite à Sinatra d'afficher la vue "task" à l'aide d'erb. Il nous faut donc créer une vue "task.erb" dans le dossier "views" contenant les vues de notre application et y saisir le code ci-dessous :

<h2><%= @task.name %></h2>

Il n'y a là rien d'extraordinaire. Juste un titre pour afficher le libellé de la tâche. Pendant que nous y sommes, nous allons créer le layout de notre application. Pour cela, nous créons un fichier "layout.erb" dans le même répertoire "views" avec le code suivant :

<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>To Do Liste</title>
    <meta charset=windows-1250 />
  </head>
  <body>
    <h1>To Do Liste</h1>

    <%= yield %>

  </body>
</html>

Là encore, rien de bien spécial. Nous nous contentons d'un code HTML très basique avec un simple titre annonçant "To Do Liste". On sauvegarde et on va pouvoir tester tout ça en lançant le serveur :

C:\Ruby\projets\todo>ruby main.rb

Nous pouvons alors utiliser notre navigateur pour consulter l'URL "http://localhost:4567/task/2" et on obtient l'écran suivant :

todo-1.png

Créer de nouvelles tâches

Passons maintenant à la création d'une nouvelle tâche par l'intermédiaire d'un formulaire web. La façon standard de faire ça est de découper l'action de création en deux handlers :

  • le premier est nommé "new" et sert à afficher un formulaire de saisie
  • le second est nommé "create" et sert pour créer la nouvelle tâche à partir des données saisies (généralement en arrière plan)

Commençons par l'action "new" et son formulaire. Le code de l'action est très simple puisque nous souhaitons seulement afficher un formulaire de saisie lorsque l'utilisateur consulte l'URL "/task/new". Pour réaliser cela, copiez le code ci-dessous avant le code pour l'action "show" (sans quoi il ne serait pas pris en compte) :

# Saisir une nouvelle tâche
get '/task/new' do
  erb :new
end

Ce code se contente d'afficher la vue "new.erb" que nous allons immédiatement créer dans le dossier "views" en saisisant les quelques lignes suivantes :

<form action="/task/create" method="POST">
  <input type="text" name="name" id="name">
  <input type="submit" value="Ajouter la tâche"/>
</form>

C'est un formulaire simple qui permet à l'utilisateur de saisir le libellé d'une nouvelle tâche dans une zone de saisie nommée "name" (ce qui correspond à la colonne "name" de la table des tâches, ce qui n'est pas obligatoire mais beaucoup plus facile).

Le bouton submit va envoyer cette information vers l'URL "/task/create" pour laquelle nous allons créer le handler correspondant. Son code est un peu plus compliqué que celui de l'action "new", mais pas tant que ça :

# Créer une nouvelle tâche
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/task/' + task.id.to_s
  else
    status 412
    redirect '/tasks'
  end
end

Examinons d'un peu plus près ce qui se passe dans ce code. Pour commencer, il s'exécute lorsqu'il s'agit d'une requête POST étant donné que nous attendons les données postées depuis le formulaire. Puis il crée une nouvelle tâche en lui donnant comme nom la valeur stockée dans le paramètre "name" en provenance du formulaire. Ensuite nous vérifions que la tâche a bien été enregistrée. Si c'est le cas, nous définissons le statut http à 201 (la valeur standard pour signifier que quelque chose a été créé) et renvoyons l'utilisateur vers l'URL affichant la tâche en concaténant son identifiant après "/task/" (ce qui correspond à l'action d'affichage d'une tâche que nous avions développée auparavant). Dans le cas où la tâche n'a pas été sauvegardée, nous renvoyons un statut http à 412 ce qui indique au navigateur que certaines conditions (comme la validation de données) n'ont pas été remplies. L'utilisateur est alors redirigé vers la page d'index "/tasks" (que nous n'avons pas encore créé mais dont nous nous occuperons très bientôt).

Nous pouvons alors tester tout cela et créer une nouvelle tâche à l'aide du navigateur en allant à l'URL "http://localhost:4567/task/new" qui nous présente le formulaire de saisie reproduit ci-dessous :

todo-2.png

Continuez et ajoutez plusieurs nouvelles tâches. A chaque fois que vous validez le formulaire, vous devez voir apparaitre une nouvelle page qui affiche le nom de la nouvelle tâche créée.

Arrivé à ce point, le contenu de votre fichier "main.rb" doit être le suivant :

require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

class Task
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :completed_at, DateTime
end

# Saisir une nouvelle tâche
get '/task/new' do
  erb :new
end

# Créer une nouvelle tâche
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/task/' + task.id.to_s
  else
    status 412
    redirect '/tasks'
  end
end

# Afficher une tâche
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

DataMapper.auto_upgrade!

Afficher la liste des tâches

Pour l'instant, nous pouvons créer de nouvelles tâches et les afficher une par une. Nous allons maintenant afficher une liste qui contiendra toutes les tâches existantes. Pour cela, nous commençons par créer l'action suivante :

# Afficher toutes les tâches
get '/tasks' do
  @tasks = Task.all
  erb :index
end

Ce code récupère tout simplement la liste de toutes les tâches enregistrées dans la base de données via la méthode "all" et les stocke dans la variable d'instance "@tasks" qui sera utilisable dans la vue. Puis il affiche la vue "index.erb" que nous allons créer dans le sous-répertoire "views" :

<h2>Liste des tâches :</h2>
<% unless @tasks.empty? %>
<ul>
<% @tasks.each do |task| %>
  <li <%= "class=\"completed\"" if task.completed_at %>>
    <a href="/task/<%=task.id%>"><%= task.name %></a>
  </li>
<% end %>
</ul>
<% else %>
<p>Aucune tâche enregistrée !</p>
<% end %>

Cette vue commence par tester si le tableau des tâches est vide. Si ce n'est pas le cas, elle parcours ce tableau pour créer une liste à puces à partir des tâches qu'il contient. Pour chaque tâche, elle teste si celle-ci a été terminée ou non et ajoute une classe "completed" lorsque c'est le cas. Cela nous servira plus tard lorsque nous travaillerons sur la feuille de style de notre application. Dans le cas où le tableau "@tasks" est vide, nous affichons simplement un message pour indiquer qu'il n'y a pas de tâche. Si vous lancez votre navigateur pour visiter l'URL "http://localhost:4567/tasks", vous obtenez l'écran suivant :

todo-3.png

Modifier des tâches

Il ne nous reste plus que quelques traitements à gérer, à savoir la modification et la suppression. Nous allons pour l'instant permettre aux utilisateurs de modifier les tâches existantes. Comme pour la création avec les actions "new" et "create", la modification nécessite une action "edit" associée avec une action "update". L'action "edit" affiche un formulaire qui permet à l'utilisateur de saisir les informations d'une tâche et de valider. C'est l'action "update" qui effectue la mise à jour dans la base de données. Voici ce que donne ces deux actions dans le code ci-dessous :

# Modifier une tâche existante
get '/task/:id/edit' do
  @task = Task.get(params[:id])
  erb :edit
end

# Mettre à jour une tâche
put '/task/:id' do
  task = Task.get(params[:id])
  task.completed_at = params[:completed] ? Time.now : nil
  task.name = (params[:name])
  if task.save
    status 201
    redirect '/task/' + task.id.to_s
  else
    status 412
    redirect '/tasks'
  end
end

Nous devons également créer un fichier "edit.erb" dans le sous-répertoire des vues :

<form action="/task/<%= @task.id %>" method="post">
  <input name="_method" type="hidden" value="put" />
  <input type="text" name="name" id="name" value="<%= @task.name %>">
  <input id="completed" name="completed" type="checkbox" value="done" <%= @task.completed_at ? "checked" : "" %>/>
  <input id="task_submit" name="commit" type="submit" value="Modifier" />
</form>

Il y a pas mal de trucs à voir là dedans. Pour commencer, le handler "edit" se contente d'afficher un formulaire lorsque l'utilisateur accède à l'URL "task/2/edit". Le formulaire est assez semblable à celui pour créer une nouvelle tâche, à quelques différences près. Il contient un champ pour le nom qui est pré-rempli avec le libellé de la tâche, une case à cocher si on veut signaler que la tâche est terminée et un bouton pour envoyer les données saisies.

La particularité de ce formulaire est qu'il poste ses données vers l'URL "task/2", soit la même URL que celle que nous utilisons déjà pour afficher une tâche. C'est pourquoi on défini un champ caché avec la ligne <input name="_method" type="hidden" value="put" /> pour indiquer qu'il s'agit en fait d'une requête http PUT et pas d'une simple requête POST. Cet artifice est nécessaire parce qu'à l'heure actuelle, il n'existe aucun navigateur qui sache gérer les requêtes PUT. Cela a pour effet d'envoyer la requête sous forme de POST mais Sinatra voyant qu'il y a un champ caché "_method" avec la valeur "put", il agit comme s'il avait reçu une requête http PUT et la transmet au handler pour l'URL "task/2/edit" qui correspond à un PUT, soit la méthode "update" dans notre code.

Le handler "update" est assez proche du handler "create". Il commence par accéder à la base de données pour retrouver la tâche à modifier en utilisant l'id stocké dans la collection "params" (notez au passage que celui-ci provient de l'URL et pas du formulaire). Il vérifie ensuite si la case à cocher a été cochée et si c'est le cas il initialise la propriété "completed_at" avec l'heure en cours, pour indiquer que la tâche est terminée. Dans le cas contraire, il affecte simplement la valeur nil à cette propriété. Puis après avoir mis à jour la propriété "name", la tâche est enregistrée en suivant la même méthode qu'au niveau du handler "create".

Testons tout ça. Supposons qu'armé de courage je décide de vraiment faire du sport et plus particulièrement du vélo. Je vais donc cliquer sur le lien "Faire du sport" dans la liste des tâches puis ajouter "/edit" à la fin de l'URL de la page obtenue (c'est pas très ergonomique mais on s'occupera de ça plus tard). Cela a pour effet d'afficher le formulaire de mise à jour de la tâche où je vais pouvoir modifier le nom en "Faire du velo" puis cliquer sur le bouton "Modifier" pour enregistrer la modification.

todo-4.png

Supprimer des tâches.

Le dernier traitement à prendre en compte est la suppression de tâches existantes. Nous allons faire cela en deux étapes. En premier lieu, nous allons ajouter un lien pour la suppression dans la vue "edit". Pour cela, nous ouvrons le fichier "edit.erb" pour le modifier comme ci-dessous :

<form action="/task/<%= @task.id %>" method="post">
  <input name="_method" type="hidden" value="put" />
  <input type="text" name="name" id="name" value="<%= @task.name %>">
  <input id="completed" name="completed" type="checkbox" value="done" <%= @task.completed_at ? "checked" : "" %>/>
  <input id="task_submit" name="commit" type="submit" value="Modifier" />
</form>

<p><a href="/task/<%= @task.id %>/delete">Supprimer cette tâche</a></p>

La dernière ligne dans ce code ajoute un lien vers l'URL "/task/:id/delete" qui va conduire vers une page où nous demanderons à l'utilisateur s'il est certain de vouloir supprimer la tâche. Nous allons donc ajouter le traitement pour faire confirmer la suppression à notre fichier "main.rb" :

# Confirmer la suppression
get '/task/:id/delete' do
  @task = Task.get(params[:id])
  erb :delete
end

Ce code recherche la tâche dont l'identifiant est mentionné dans l'URL puis stocke cette tâche dans la variable d'instance "@task". Encore une fois, nous devons utiliser une variable d'instance (qui est préfixée par un @) car nous aurons besoin d'y faire référence dans la vue de confirmation. Et maintenant, il nous reste à coder cet écran de confirmation en créant un fichier "delete.erb" dans le sous-répertoire des vues et en y saisissant le code ci-dessous :

<h2><%= @task.name %><h2>
<h3>Est-ce que vous souhaitez réellement supprimer cette tâche ?</h3>
<form action="/task/<%= @task.id %>" method="post">
  <input type="hidden" name="_method" value="delete" />
  <input type="submit" value="Supprimer"> ou <a href="/tasks">Annuler</a>
</form>

Le fonctionnement de cette vue est très proche du formulaire pour la modification. Vous pouvez voir que là aussi nous avons besoin d'un champ caché pour simuler la méthode http DELETE étant donné que quasiment aucun navigateur ne sait la gérer. Et nous avons en plus ajouté un lien pour annuler la demande de suppression et revenir à la liste des tâches.

Il ne nous reste donc plus qu'à créer le code pour gérer l'action qui va réellement supprimer la tâche dans la base de données. Pour cela, nous devons ajouter le code suivant à notre fichier "main.rb" :

# Supprimer une tâche
delete '/task/:id' do
  Task.get(params[:id]).destroy
  redirect '/tasks'  
end

Nous pouvons alors tester ce code en supprimant la tâche "Faire du vélo" (de toute façon je n'ai pas de vélo). On clique sur cette tâche dans la liste des tâches, on ajoute "/edit" à la fin de l'URL obtenue puis là on suit le lien "Supprimer la tâche" ce qui nous amène sur l'écran de confirmation ci-dessous :

todo-5.png

Ces deux derniers traitements sont une excellente illustration de la façon dont REST fonctionne. Les actions pour afficher, modifier et supprimer une tâche correspondent toutes à la même URL (par exemple "/task/2") et concernent le même objet (la tâche dont l'identifiant est 2 dans notre exemple). Mais elles accomplissent toutes des fonctions très différentes et elles sont sélectionnées en fonction du verbe http employé (soit GET, UPDATE et DELETE respectivement).

Le code source du fichier "main.rb" complet présente désormais le contenu suivant :

require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

class Task
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :completed_at, DateTime
end

# Saisir une nouvelle tâche
get '/task/new' do
  erb :new
end

# Créer une nouvelle tâche
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/task/' + task.id.to_s
  else
    status 412
    redirect '/tasks'
  end
end

# Modifier une tâche existante
get '/task/:id/edit' do
  @task = Task.get(params[:id])
  erb :edit
end

# Mettre à jour une tâche
put '/task/:id' do
  task = Task.get(params[:id])
  task.completed_at = params[:completed] ? Time.now : nil
  task.name = (params[:name])
  if task.save
    status 201
    redirect '/task/' + task.id.to_s
  else
    status 412
    redirect '/tasks'
  end
end

# Confirmer la suppression
get '/task/:id/delete' do
  @task = Task.get(params[:id])
  erb :delete
end

# Supprimer une tâche
delete '/task/:id' do
  Task.get(params[:id]).destroy
  redirect '/tasks'  
end

# Afficher une tâche
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :task
end

# Afficher toutes les tâches
get '/tasks' do
  @tasks = Task.all
  erb :index
end

DataMapper.auto_upgrade!

Celui-ci contient à présent les 7 handlers REST traditionnels : index, show, new, create, edit, update et delete ainsi qu'une action supplémentaire pour faire confirmer la suppression.

Améliorer l'interface utilisateur

Notre application est maintenant complète, mais il reste encore quelques points où pouvons encore l'améliorer. Comme je l'ai indiqué auparavant, son code suit les conventions de Rails en ce qui concerne les URLs REST. Ce qui est bien avec Sinatra, c'est que vous pouvez faire les choses à votre façon. C'est pourquoi je vais maintenant modifier certaines de ces URLs.

Pour commencer, je préfèrerais que ce soit la page principale qui affiche la liste de toutes les tâches plutôt que d'avoir une URL "/tasks" pour cela. Cette modification est toute simple à faire :

# Afficher toutes les tâches
get '/' do
  @tasks = Task.all
  erb :index
end

J'aimerais aussi que le formulaire pour créer une nouvelle tâche apparaisse dans la page principale, à la suite de la liste des tâches. Pour cela, il suffit de copier le code du fichier "new.erb" dans le fichier "index.erb" (tous deux dans le sous-répertoire views). Le fichier "index.erb" contient alors le code suivant :

<h2>Liste des tâches :</h2>
<% unless @tasks.empty? %>
<ul>
<% @tasks.each do |task| %>
  <li <%= "class=\"completed\"" if task.completed_at %>>
    <a href="/task/<%=task.id%>"><%= task.name %></a>
  </li>
<% end %>
</ul>
<% else %>
<p>Aucune tâche enregistrée !</p>
<% end %>

<h2>Créer une tâche</h2>
<form action="/task/create" method="POST">
  <input type="text" name="name" id="name">
  <input type="submit" value="Ajouter la tâche"/>
</form>

Il est ensuite possible de supprimer le fichier new.erb qui ne sert plus à rien ainsi que le handler pour l'action "new" dans le fichier "main.rb" (par contre, il faut conserver celui pour l'action "create"). Je vais également supprimer l'action "show" et la vue "task.erb" qui lui est associée étant donné que cela ne sert qu'à afficher le nom d'une tâche, ce que l'on peut déjà voir dans la liste des tâches. L'avantage de cette suppression, c'est que l'URL "/task/:id" ne sert plus et que je vais pouvoir l'utiliser pour afficher le formulaire de mise à jour d'une tâche. Pour cela, il faut donc modifier l'action "edit" comme suit :

# Modifier une tâche existante
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :edit
end

Votre code fait tout de suite plus propre. Il reste encore quelques redirections qui pointent vers des URLs qui n'existent plus et qu'il faut donc corriger, généralement pour les faire pointer vers la racine du site. Suite à tout cela, le code du fichier "main.erb" est beaucoup plus léger et doit ressembler à ceci :

require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations'

DataMapper.setup(:default, ENV['DATABASE_URL'] || "sqlite3://#{Dir.pwd}/development.db")

class Task
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :completed_at, DateTime
end

# Créer une nouvelle tâche
post '/task/create' do
  task = Task.new(:name => params[:name])
  if task.save
    status 201
    redirect '/'
  else
    status 412
    redirect '/'
  end
end

# Modifier une tâche existante
get '/task/:id' do
  @task = Task.get(params[:id])
  erb :edit
end

# Mettre à jour une tâche
put '/task/:id' do
  task = Task.get(params[:id])
  task.completed_at = params[:completed] ? Time.now : nil
  task.name = (params[:name])
  if task.save
    status 201
    redirect '/'
  else
    status 412
    redirect '/'
  end
end

# Confirmer la suppression
get '/task/:id/delete' do
  @task = Task.get(params[:id])
  erb :delete
end

# Supprimer une tâche
delete '/task/:id' do
  Task.get(params[:id]).destroy
  redirect '/'  
end

# Afficher toutes les tâches
get '/' do
  @tasks = Task.all
  erb :index
end

DataMapper.auto_upgrade!

J'ai aussi décidé d'ajouter un lien de retour vers la page d'accueil quand on clique sur le titre de la page. Pour que ce lien apparaisse partout, il faut modifier le fichier "layout.erb" dans le sous-répertoire des vues :

<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>To Do Liste</title>
    <meta charset=windows-1250 />
  </head>
  <body>
    <h1><a href="/">To Do Liste</a></h1>

    <%= yield %>

  </body>
</html>

L'application est désormais plus simple à utiliser et fonctionne de façon beaucoup plus intuitive :

todo-6.png

Améliorer le code

Après ces modifications visibles par l'utilisateur, je vais réaliser quelques modifications destinées à simplifier le code de l'application. Pour commencer, je vais ajouter quelques méthodes pour faciliter la gestion des tâches terminées. Etant donné qu'il s'agit de méthodes liées aux tâches, il faut les placer dans la définition de la classe :

class Task
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :completed_at, DateTime

  def completed?
    true if completed_at
  end

  def self.completed
    all(:completed_at.not => nil)
  end
end

La première méthode est une méthode d'instance: elle est définie au niveau de la tâche, comme dans le cas d'une propriété. Elle considère que s'il y a une date de définie pour la propriété "completed_at" c'est que la tâche est terminée et renvoie donc "true" dans ce cas, ou "false" dans le cas contraire.

La seconde méthode est une méthode de classe et porte sur toutes les tâches. Elle peut servir pour filtrer vos recherches à l'aide de DataMapper. Par exemple, le fait d'utiliser Task.completed permettra de retrouver toutes les tâches qui sont terminées. Un truc très intéressant avec ces méthodes, c'est qu'elles peuvent être chainées les unes aux autres pour affiner les recherches. Par exemple, s'il existait une méthode de classe nommée "important" qui renvoyait toutes les tâches importantes (c'est pas trop possible pour l'instant, mais on pourrait parfaitement ajouter un tel truc à l'avenir !), alors on pourrait utiliser Task.important.completed pour retrouver toutes les tâches importantes qui sont terminées.

Je souhaiterais aussi avoir une méthode pour générer un lien vers une tâche. Vous sous souvenez peut-être du code assez minable que j'avais utilisé pour générer un tel lien dans la vue "index.erb" :

<a href="/task/<%=task.id%>"><%= task.name %></a>

Cela serait beaucoup plus propre si nous pouvions masquer cette complexité dans une méthode d'instance au niveau de la classe Task :

class Task
  include DataMapper::Resource
  property :id, Serial
  property :name, String
  property :completed_at, DateTime

  def completed?
    true if completed_at
  end

  def self.completed
    all(:completed_at.not => nil)
  end

  def link
    "<a href=\"task/#{self.id}\">#{self.name}</a>"
  end
end

La méthode "link" est vraiment toute simple. Elle renvoie une chaine contenant le code html qui fait un lien vers la page de mise à jour d'une tâche. Pour cela, j'ai utilisé l'interpolation de texte qui consiste à placer du code Ruby devant être évalué à l'intérieur de #{}. Et pour faire référence à la tâche concerné par la méthode, j'ai employé le mot-clé "self".

Après cela, nous pouvons simplifier le contenu de la vue "index.erb" pour utiliser ces différents méthodes, ce qui donne le code suivant :

<h2>Liste des tâches :</h2>
<% unless @tasks.empty? %>
<ul>
<% @tasks.each do |task| %>
  <li <%= "class=\"completed\"" if task.completed? %>>
    <%= task.link %>
  </li>
<% end %>
</ul>
<% else %>
<p>Aucune tâche enregistrée !</p>
<% end %>

<h2>Créer une tâche</h2>
<form action="/task/create" method="POST">
  <input type="text" name="name" id="name">
  <input type="submit" value="Ajouter la tâche"/>
</form>

Notre source est devenu bien plus lisible et par conséquent beaucoup plus facile à maintenir. Pour devenir parfait, il ne nous reste plus qu'à faire ressortir les tâches qui ont été accomplies. Etant donné que celles-ci sont d'ores et déjà marquées d'une classe CSS "completed" (il vous suffit d'afficher le code source de la page pour contrôler ça), on a juste besoin d'ajouter une ligne de CSS dans le fichier "layout.erb" :

<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>To Do Liste</title>
    <meta charset=windows-1250 />
    <style>
      .completed {text-decoration: line-through;}
    </style>
  </head>
  <body>
    <h1><a href="/">To Do Liste</a></h1>

    <%= yield %>

  </body>
</html>

Après cette ultime fioriture, vous pouvez compléter votre todo liste ou indiquer que certaines tâches sont terminées et avoir un retour visuel direct dans la liste des tâches de l'écran principal. Au final, votre application doit ressembler à la copie d'écran ci-dessous :

todo-7.png

Conclusion

Ce tutoriel correspond à une application de base de données toute simple, mais cela constitue un bon point de départ. Notre application n'est peut être pas tout à fait à la hauteur de son nom étant donné que pour l'instant il faut pas mal chercher pour faire ressortir tout son côté "Super". Mais en nous appuyant sur cette base, nous avons des tas de perspectives d'évolutions. Vous pourriez par exemple essayer de gérer différentes listes de tâches (pour étudier les associations sous DataMapper), vous pourriez ajouter un peu de Javascript ou de jQuery pour améliorer l'interface utilisateur ou encore vous pourriez intégrer une notion de priorité et afficher les tâches prioritaires en haut de liste...