blog.pagesd.info

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

mercredi 15 juin 2011

Télécharger des fichiers avec Sinatra

Ceci est la traduction du tutoriel "Uploading Files In Sinatra" de Darren Jones.

Il arrive fréquemment qu'on ait besoin de télécharger un fichier dans nos applications, que ce soit une image ou un fichier PDF. Dans ce tutoriel, je vais vous présenter le code nécessaire pour télécharger des fichiers avec votre application Sinatra. C'est une solution toute simple, mais ça fait ce qu'on lui demande.

L'application Sinatra

Pour commencer, nous avons besoin de créer une application Sinatra basique avec une page "upload" :

require 'rubygems'
require 'sinatra'

get '/upload' do
  haml :upload
end

__END__

@@upload
%h2 Upload
%form{:action=>"/upload",:method=>"post",:enctype=>"multipart/form-data"}
  %input{:type=>"file",:name=>"file"}
  %input{:type=>"submit",:value=>"Télécharger"}

(Remarque : je n'ai pas défini de layout pour cet exemple)

Cette page va afficher un formulaire tout simple qui va nous permettre de sélectionner un fichier sur notre PC. Le truc le plus important, c'est l'attribut "enctype" qui doit impérativement être initialisé à "multipart/form-data".

Nous devons ensuite gérer ce qui se passe lorsque le formulaire est validé en créant un autre handler (à placer juste avant la déclaration __END__) :

post '/upload' do
  unless params[:file] && (tmpfile = params[:file][:tempfile]) && (name = params[:file][:filename])
    return haml(:upload)
  end
  while blk = tmpfile.read(65536)
    File.open(File.join(Dir.pwd,"public/uploads", name), "wb") { |f| f.write(tmpfile.read) }
  end
 'terminé'
end

Etudions un peu ce code pour voir comment ça marche. Après avoir sélectionné un fichier et cliqué sur le bouton "Télécharger", un objet file est envoyé au serveur via le hash "params". Cet objet file contient une propriété "tempfile" qui correspond au fichier temporaire créé pendant le téléchargement du fichier. Il contient aussi une propriété "filemane" qui correspond au nom du fichier tel qu'il était défini sur votre système. La première ligne de code vérifie que l'on a bien sélectionné un fichier et utilise les 2 propriétés "tempfile" et "filename" pour initialiser deux variables du même nom. Dans le cas où aucun fichier n'a été sélectionné, le formulaire de téléchargement est simplement ré-affiché (ligne 2).

La boucle while est alors utilisé pour gérer le contenu du fichier au fur et à mesure de son chargement. On fait cela par bloc de 65536 octets qui selon moi devrait servir de tampon pour limiter l'utilisation de la RAM. Ces blocs sont enregistrés dans un fichier dans le répertoire public nommé "uploads". Il est nécessaire d'avoir au préalable créé vous-même ce sous-répertoire "uploads" dans le répertoire "public" de votre application. Le paramètre "wb" indique à Ruby qu'il s'agit d'un fichier binaire (le "b") et qu'il est en écriture seule (le "w"). Vous pouvez retrouver les différentes valeurs acceptées par de paramètre qui défini le mode du fichier au début de la documentation de la classe IO de Ruby. Et pour finir, une fois que le fichier a été entièrement chargé, on se contente pour l'instant d'afficher un simple message "terminé", mais il est bien évident qu'on pourrait faire mieux.

Pour avoir plus d'informations sur l'objet file en Ruby, je vous invite à consulter le chapitre qui est consacré à la classe File sur la version en ligne du Ruby Pickaxe Book.

Note du traducteur : plutôt que de copier "manuellement" le contenu du fichier, on aurait pu utiliser la méthode cp (ou copy) de la classe FileUtils :

  fullname = File.join(Dir.pwd, "public/uploads", name)
  FileUtils.cp(tmpfile.path, fullname)

Heroku

Le code présenté ci-dessus est tout à fait correct et fonctionnel, mais Heroku ne vous permet pas de stocker beaucoup de fichiers sur leurs serveurs (seuls les fichiers relatifs au code de votre application devraient s'y trouver). Par conséquent, vous êtes censés héberger vos fichiers ailleurs. La solution qui vient à l'esprit, c'est d'utiliser Amazon’s Simple Storage Service (S3). Cela vous permet de stocker vos fichiers sur les serveurs d'Amazon pour un coût extrêmement minime. Heroku étant également hébergé sur les serveurs Amazon, en y hébergeant vos propres fichiers vous devriez réduire au maximum les temps de latence pour récupérer vos fichiers.

Inscription à Amazon S3

La première chose à faire, c'est de vous enregistrer sur S3, ce que vous pouvez faire en vous rendant sur le site et en cliquant le bouton "Inscrivez-vous à Amazon S3". Si vous êtes déjà un client d'Amazon, vous pouvez ré-utiliser les informations de votre compte. Une fois que vous avez terminé le processus d'inscription, vous pouvez commencer à ajouter des fichiers à votre compte S3. Pour cela, Amazon propose une console web ou si vous préférez, vous pouvez employer l'extension S3Fox pour Firefox. Amazon S3 utilise un concept de compartiment (bucket) pour gérer vos fichiers. Et vous pouvez créer des dossiers à l'intérieur de chaque compartiment. Personnellement, j'ai l'habitude de définir un compartiment pour chacun de mes sites web.

Le code source Sinatra

Maintenant que vous disposez d'un compte Amazon S3 en état de marche, vous allez pouvoir y enregistrer vos fichiers en utilisant son interface web et en apportant quelques modifications à votre code Sinatra. Pour commence, il faut installer la librairie aws-s3:

C:\Ruby>gem install aws-s3

Lorsque vous vous êtes inscrit à Amazon S3, vous devez avoir reçu deux informations importante : un Access Key ID de 20 chiffres et une Secret Access Key. Vous poiuvez les obtenir en vous identifiant sur la page Amazon Web Services puis en cliquant sur l'onglet "Compte". Puis vous devez cliquer sur le lien "Identification de sécurité". Les informations dont vous avez besoin apparaissent dans une boite au milieu de la page (vous devrez cliquer pour faire apparaitre la clé secrète qui fait 40 caractères).

Une fois que vous avez récupéré ces informations, vous allez pouvoir la commande set de Sinatra pour les enregistrer. Et nous allons en profiter pour ajouter une instruction require afin que la librairie aws-s3 soit inclue dans notre code source :

require 'rubygems'
require 'sinatra'
require 'aws/s3'

set :bucket, 'mybucket'
set :s3_key, THISISANEXAMPLEKEYID
set :s3_secret, Thi$isJu5taNExamp/etO0itSh0u1dBel0NgeR

Bien entendu, pour un site en production il faut définir ces variables en passant par des paramètres d'environnement de Heroku pour que vos clés restent secrètes. Vous pouvez vous reporter au billet Configuration et paramétrage avec Sinatra pour avoir plus d'explications à ce sujet.

Nous devons ensuite nous occuper de nos handlers. Le premier handler pour le GET reste tel quel alors que le second pour le POST doit être légèrement modifié de façon à utiliser la librairie aws-s3 pour établir la connexio avec Amazon S3 et enregistrer le fichier. Vous pouvez voir que nous utilisons pour cela les variables définies auparavant et qu'à part cela, le code Sinatra final reste tel qu'il était :

get '/upload' do
  haml :upload
end

post '/upload' do
  unless params[:file] && (tmpfile = params[:file][:tempfile]) && (name = params[:file][:filename])
    return haml(:upload)
  end
  while blk = tmpfile.read(65536)
    AWS::S3::Base.establish_connection!(
      :access_key_id     => settings.s3_key,
      :secret_access_key => settings.s3_secret)
    AWS::S3::S3Object.store(name,open(tmpfile),settings.bucket,:access => :public_read)     
  end
 'terminé'
end

__END__

@@upload
%h2 Upload
%form{:action=>"/upload",:method=>"post",:enctype=>"multipart/form-data"}
  %input{:type=>"file",:name=>"file"}
  %input{:type=>"submit",:value=>"Télécharger"}

Et c'est tout ! Tout cela devrait amplement vous suffire pour gérer les bases du téléchargement de fichiers avec Sinatra. Il est bien évident que cela peut être amélioré, notamment dans les domaines suivants :

  • Un formulaire un peu plus sexy
  • Un meilleur retour à la fin du téléchargement (y compris dans le cas où aucun fichier n'a été sélectionné)
  • Une barre de progression au fur et à mesure du téléchargement du fichier
  • La possibilité de télécharger dans différents dossiers
  • Un contrôle du type de fichier téléchargé
  • Un traitement des images téléchargées (créer une vignette automatiquement par exemple), bien qu'il s'agisse là d'un sujet qui mérite un billet à lui tout seul,
  • La possibilité de pouvoir télécharger plusieurs fichiers à la fois

mercredi 8 juin 2011

Utiliser Rack Flash avec Sinatra

Ceci est la traduction du tutoriel Sinatra "Rack Flash" de Darren Jones.

The Flash

Flash est un tableau hash bien pratique que l'on trouve dans Rails. Il ne s'agit pas du super héros qui se déplace à la vitesse de l'éclair mais d'une astuce pour stocker temporairement des informations entre deux requêtes HTTP. Si par exemple votre application enregistre une nouvelle fiche puis redirige l'utilisateur vers une autre page, vous pouvez utiliser cette mémoire flash pour stocker un message indiquant que la fiche a bien été enregistrée. Ce message sera ensuite affiché sur la nouvelle page (après le redirect).

On peut avoir la même fonctionnalité avec Sinatra en s'appuyant sur Rack Flash.

Tout ce qu'il y a à faire, c'est d'installer le gem :

C:\Ruby>gem install rack-flash

Puis ajouter les quelques lignes suivantes dans notre application Sinatra :

require 'rack-flash'
use Rack::Flash
enable :sessions

(Rack Flash a besoin des sessions pour stocker le tableau hash et par défaut elles sont désactivées dans Sinatra)

Pour utiliser le hash "flash", il suffit par exemple de définir le message dans le handler de départ :

post '/notes/save' do
  flash[:notice] = "Votre fiche a été enregistrée"
  redirect '/'
end

Puis il faut afficher ce message dans la vue correspondant au handler pour l'URL "/" :

<div id='flash' class='notice'>
  <%= flash[:notice] %>
</div>

Et maintenant, quand quelqu'un enregistre une note, il verra le message "Votre fiche a été enregistrée" après avoir été redirigé sur la page index "/".

Vous pouvez associer une clé au message flash pour faire la distinction entre différents types de message, comme par exemple :

flash[:notice]
flash[:warning]
flash[:error]

Si vous souhaitez afficher le flash dans la requête en cours, vous devez utiliser flash.now :

get '/' do flash.now:notice = "Pas de message" unless flash:notice end

Cela affichera un message indiquant qu'il n'y a pas de message stocké dans le hash flash par la requête précédente.

Les messages restent dans le tableau hash tant qu'ils ne sont pas affichés, comme vous pouvez le voir dans le code ci-dessous :

get '/un' do
  flash[:notice] = "Coucou !"
  flash[:error] = "erreur de type 1"
  flash[:warning] =  "Attention au chien"
  redirect '/deux' 
end

get '/deux' do
  flash[:error] = "erreur de type 2"
  redirect '/trois' 
end

get '/trois' do
  flash[:notice] = "Salut !"
  redirect '/' 
end

get '/' do
  erb :index
end

Lorsque nous atteindrons enfin l'URL "/" et que la vue "index" sera affichée, on aura flash[:error] = "erreur de type 2", flash[:notice] = "Salut !" et flash[:warning] sera toujours "Attention au chien".

Si vous ne voulez pas que les messages s'éternisent dans le hash flash jusqu'à ce qu'ils soient utilisé, vous pouvez vous en débarrasser en utilisant l'option "sweep" :

use Rack::Flash, :sweep => true

Grace à elle, le hash flash est nettoyé après chaque requête, que les messages aient été affichés ou non. Dans l'exemple précédent, le seul élément qui restera dans le hash sera flash[:notice] = "Salut !".

Et pour finir, un bout de code Erb prêt à insérer dans votre layout pour afficher un message flash lorsqu'il en existe, en définissant une classe CSS qui dépend du type de message :

<% flash.each do |key,msg| %>
  <div id='flash' class='<%= key %>'>
    <%= msg %>
  </div>
<% end %>

Ou si vous préférez le Haml :

- flash.each do |key,msg|
  #message{:class => key}= msg

Cela a pour effet de créer une balise <div> pour chaque message stocké dans le hash flash, en utilisant sa clé comme nom de classe CSS.

En espérant que ça puisse vous servir : la plupart des applications utilise cette technique, alors n'hésitez pas à l'essayer dans vos applications Sinatra.

mardi 17 mai 2011

Authentification super simple avec Sinatra

Ceci est la traduction du tutoriel "Really Simple Authentication in Sinatra" de Darren Jones.

Une demande répétée des lecteurs de I Dit It My Way est un billet sur l'authentification. Il existe des tas de gems qui permettent de faire ça, mais je préfère programmer ça par moi-même. Voici donc une méthode vraiment simple pour construire le côté administration d'un site web avec Sinatra.

Pour démarrer, il faut créer quelques paramètres :

set :username,'michel'
set :password,'topsecret'
set :token,'1truklong&complike@$e$ouven!r'

Puis les handlers pour les différentes routes. En commençant par l'URL "/admin" pour pouvoir se connecter.

get('/admin') { haml :admin }

Ca ne prend qu'une ligne parce que tout ce qu'il y a à faire c'est d'afficher la vue "admin.haml" :

%form(action="/login" method="post")
  %label(for="username")Code utilisateur :
  %input#username(type="text" name="username")
  %label(for="password")Mot de passe :
  %input#password(type="password" name="password")
  %input(type="submit" value="Connexion") or <a href="/">Annuler</a>

C'est un formulaire tout simple qui contient deux zones de texte pour saisir le code utilisateur et le mot de passe plus un bouton pour valider. Ce formulaire est renvoyé à l'aide d'une requête POST vers l'URL "/login" et nous devons donc ajouter un handler pour la traiter. C'est à ce niveau que nous allons accomplir le plus gros du travail pour authentifier l'utilisateur.

post '/login' do
  if params['username'] == settings.username && params['password'] == settings.password
      response.set_cookie(settings.username,settings.token) 
      redirect '/'
    else
      "Code utilisateur ou mot de passe incorrect"
    end
end

Ce code vérifie tout d'abord si le code utilisateur et le mot de passe saisi via le formulaire (stockés dans le hash params) correspondent à ceux définis dans le paramétrage (les valeurs settings.xxxxx). Si c'est le cas, on crée un cookie en utilisant le code utilisateur comme clé et pour la valeur le "token" qu'on avait configuré au tout début. On pourrait se contenter d'utiliser "true" pour la valeur, mais le fait d'utiliser un token renforce le niveau de sécurité (si quelqu'un sait expliquer comment ?). Lorsque l'utilisateur n'a pas donné le bon code utilisateur ou le bon mot de passe, on renvoie simplement un message d'avertissement.

Et maintenant, tout ce qu'il nous reste à faire c'est de donner la possibilité de se déconnecter. Ce que nous allons faire en gérant l'URL "/logout".

get('/logout') { response.set_cookie(settings.username, false) ; redirect '/' }

Là on modifie le cookie correspondant à l'utilisateur en lui donnant la valeur "false" puis on le renvoie à la racine du site.

On a donc géré tout ce qui concerne l'authentification d'un utilisateur. Il nous reste à traiter la partie autorisation pour réellement autoriser ou interdire l'accès à certaines pages. Pour cela, on va simplement ajouter deux fonctions helper à notre code Ruby :

helpers do
  def admin? ; request.cookies[settings.username] == settings.token ; end
  def protected! ; halt [ 401, 'Not Authorized' ] unless admin? ; end
end

Le premier helper, nommé "admin?", va nous servir pour vérifier si l'utilisateur est connecté ou non, en contrôlant que la valeur du cookie correspond à la valeur configurée pour le token. On pourra l'utiliser cet helper dans le code des handlers ou des vues pour par exemple afficher un message différent selon que l'utilisateur est connecté ou non.

Le second helper, nommé "protected!", utilise le premier helper pour vérifier si l'utilisateur est connecté et si ce n'est pas le cas il interrompt le code te renvoie une erreur HTTP 401 pour indiquer que l'action n'est pas autorisée. On l'utilisera au début du handler d'une route pour indiquer que l'utilisateur doit être connecté pour visualiser ce contenu.

Et pour finir le code source complet d'une application qui regroupe tout le code présenté ci-dessus. Libre à vous de à l'utiliser et de faire vos propres essais avec.

require 'rubygems'
require 'sinatra'

set :username,'Bond'
set :password,'007'
set :token,'osh@kerp@$@l@cuill3re'

helpers do
  def admin? ; request.cookies[settings.username] == settings.token ; end
  def protected! ; halt [ 401, 'Not Authorized' ] unless admin? ; end
end

get '/' do
  haml :index
end

get('/admin') { haml :admin }

post '/login' do
  if params['username'] == settings.username && params['password'] == settings.password
    response.set_cookie(settings.username,settings.token) 
    redirect '/'
  else
      "Code utilisateur ou mot de passe incorrect"
  end
end

get('/logout') { response.set_cookie(settings.username, false) ; redirect '/' }

get '/public' do
  'Tout le monde peut voir ça'
end

get '/private' do
  protected!
  'Rien que pour vos yeux !'
end

__END__
@@layout
!!! 5
%html
  %head
    %meta(charset="utf-8")
    %title Authentication Super Simple
  %body
    %a(href='/admin')Connexion
    %a(href='/logout')Déconnexion
    %a(href='/public')Public
    %a(href='/private')Privé
    = yield
@@admin
%form(action="/login" method="post")
  %label(for="username")Code utilisateur :
  %input#username(type="text" name="username")
  %label(for="password")Mot de passe :
  %input#password(type="password" name="password")
  %input(type="submit" value="Connexion") or <a href="/">Cancel</a>
@@index
-if admin?
  %h1 Bienvenue 007 !
-else
  %h1 Bienvenue !

C'est un exemple simple et sans prétention si vous souhaitez expérimenter un système d'authentification et d'autorisations sous Sinatra. Et si vous avez des idées pour le faire évoluer ou l'améliorer, n'hésitez pas à en faire part dans les commentaires ci-dessous.

mercredi 4 mai 2011

Erreurs 404 et autres avec Sinatra

Ceci est la traduction du tutoriel "404 and other Errors in Sinatra" de Darren Jones.

Un truc super bien quand on débute avec Sinatra c'est sa page d'erreur :

Erreur 404

Les applications web ont toutes besoin d'afficher des pages d'erreurs et c'est quelque chose de très simple à faire avec Sinatra.

Les erreurs 404

Elles se produisent lorsque l'URL appelée ne peut pas être trouvée. On peut gérer cette erreur à l'aide du code Ruby suivant :

not_found do
  "Votre page n'a pas pu être trouvée"
end

Cela affichera tout simplement le message "Votre page n'a pas pu être trouvée". On peut tout de même avoir quelque chose d'un peu plus sophistiqué et employer une vue "not_found.erb" qui aura l'avantage d'être rendue avec le "layout.erb" habituel.

not_found do
  erb :not_found
end

On peut même faire tenir ça sur une seule ligne :

not_found { erb :not_found }

Si vous préférez appeler votre fichier "404.erb" au lieu de "not_found.erb", vous devrez simplement le mettre entre apostrophe étant donné que les symboles ne peuvent pas commencer par un chiffre :

not_found do
  erb :'404'
end

Les erreurs 500

Ces erreurs surviennent en cas d'erreur interne du serveur, généralement lorsque quelque chose a planté dans l'application. Le code suivant sert pour gérer ce genre d'erreur :

error do
  @error = request.env['sinatra_error']
  haml :'500'
end

Ce code initialise une variable d'instance nommée @error qu'il sera possible d'utiliser dans la vue pour détailler l'erreur à l'aide des méthodes @error.name et @error.message.

Il est aussi possible de créer des erreurs personnalisées en utilisant le code suivant :

error BigError do
   "BOUM ! Il y a eu un gros souci ! " + request.env['sinatra.error'].message
end

On peut alors renvoyer cette erreur avec un message adéquat :

get '/' do
  raise BigError, 'Est-ce que vous aviez convenablement fermé toutes vos accolades ?'
end

L'accès à l'URL "/" provoquera alors l'affichage du message suivant :

BOUM ! Il y a eu un gros souci ! Est-ce que vous aviez convenablement fermé toutes vos accolades ?

Selon moi, il est important de gérer les erreurs dès le tout début du projet. Pour cela, je colle généralement les deux lignes suivantes au début de mon fichier :

not_found { haml :'404' }
error { @error = request.env['sinatra_error'] ; haml :'500' }

mardi 3 mai 2011

Configuration et paramétrage avec Sinatra

Ceci est la traduction du tutoriel "Sinatra Settings and Configuration" de Darren Jones.

Définir des paramètres

Pour définir des variables session d'application qui seront accessible partout dans une application, on peut utiliser la commande set en respectant la syntaxe suivante :

set :name, 'MonSite'
set :author, 'Michel'

Ces variables sont ensuite utilisables dans notre code Ruby sous la forme settings.name et settings.author :

get '/hello' do
  "Cette page a été rédigée par " + settings.author
end

Puis en se connectant à l'URL "/hello" on obtient alors :

Cette page a été rédigée par Michel.

On peut également utiliser la commande set pour modifier les paramètres de certains gems, comme par exemple :

set :haml, { :format => :html5 }

Grâce à quoi Haml génèrera du code HTML5 avec le doctype correct, pas de barre fermante pour la balise image, etc...

Il est même possible de modifier les paramètres par défaut de Sinatra. C'est ce qui permet par exemple de redéfinir le répertoire "public" :

set :public, Proc.new { root }

De cette façon, le répertoire "public" sera identique au répertoire racine du site.

Et pour modifier le répertoire où sont enregistrées les vues :

set :views, Proc.new { File.join(root, "templates") }

Après cela, Sinatra recherchera dans le dossier "templates" pour tous les fichiers de vues de notre application (en lieu et place du répertoire "views" utilisé par défaut).

Quand le paramètre correspond à un booléen, il est possible d'utiliser les commandes enable et disable plutôt que la commande set :

enable :sessions
disable :twitter

Ce qui est exactement identique à :

set :sessions, true
set :twitter, false

La documentation officielle de Sinatra fourni d'autres explications et des exemple, entre autre la liste des paramètres internes de Sinatra.

Les blocs de configuration

Sinatra permet de regrouper un ensemble de paramètres dans un bloc de configuration :

configure do
  set :name, 'michel'
  set :haml, { :format => :html5 }
end

Cela va initialiser tous les paramètres ainsi que tout ce qui est nécessaire au démarrage.

Et cerise sur le gâteau, on peut définir un bloc de configuration pour chaque environnement :

configure :development do
  set :db, File.join("sqlite3://",settings.root, "development.db"
end

configure :test do
  set :db, File.join("sqlite3://",settings.root, "test.db"
end

configure :production do
  File.join("sqlite3://",settings.root, "production.db"
  set :sass, { :style => :compressed }
end

Comme vous pouvez le voir, j'ai configuré Sass pour que sa sortie soit compressée en production et que le fichier généré soit le plus petit possible. Il existe tout un tas d'autres options pour Sass et Haml qui peuvent être initialisées de cette façon.

Gérer des paramètres d'environnement sur Heroku

Pour finir, on a la possibilité de définir des variables d'environnement directement sur Heroku, ce qui nous évite d'avoir à rendre la valeur de certains paramètres publics. Supposons que nous ayons le code ci-dessous :

set :password, 't0psecret'

Le hic c'est que si on compte partager notre code, les autres personnes auront accès à notre mot de passe top secret ! La solution dans ce cas là c'est de définir une variable d'environnement appelée "PASSWORD", chose de très simple à faire depuis une console bash. Il suffit de se placer dans le répertoire de l'application et d'entrer le code ci-dessous :

$ heroku config:add PASSWORD=t0psecret

Ce qui doit renvoyer le message suivant :

Adding config vars:
  PASSWORD    => t0psecret
Restarting app...done.

Il faut utiliser la commande config seule pour lister toutes les variables d'environnement existantes sur Heroku :

$ heroku config

Comme on peut le voir suite à cette commande, Heroku défini déjà par lui-même un certain nombre de variables d'environnement de façon automatique dans le but de nous faciliter la vie :

PASSWORD => t0psecret
DATABASE_URL => postgres://ibzju...s.com/ibzjubamts
RACK_ENV     => production
URL          => http://bloggl.heroku.com

Si on a besoin de se débarrasser de toutes les variables de configuration, on peut employer la commande suivante :

$ heroku config:clear

Ce que Heroku confirmera avant de redémarrer l'application :

Clearing all config vars and restarting app...done.

Après avoir créé nos variables d'environnement sur Heroku, il ne nous reste plus qu'à modifier notre code source pour en tirer parti :

set :password, ENV['PASSWORD'] || 'secret'

Ce code va utiliser la variable d'environnement enregistrée dans ENV['PASSWORD'] si elle est définie ou sinon se contenter du simple mot de passe "secret" défini en dur dans le code. Grâce à cette méthode vous ne risquez pas de divulguer quoique ce soit d'important ou de personnel dans votre code source Ruby.

Pour être complet, le Dev Center d'Heroku présente d'autres informations concernant l'utilisation des variables de configuration sur Heroku.

vendredi 29 avril 2011

Un formulaire de contact avec Sinatra

Ceci est la traduction du tutoriel "An Email Contact Form in Sinatra" de Darren Jones.

Dans cet épisode, je vais vous montrer les étapes nécessaires pour créer une page avec un formulaire de contact qui vous enverra un email. Cet exemple peut facilement être adapté pour envoyer tout autre type d'email à partir d'une interface web. Dans ce tutoriel, je vais utiliser mon compte GMail pour réaliser l'envoi du mail.

Nous utiliserons également la librairie Pony pour envoyer les mails et il faut donc commencer par installer le gem Pony :

C:\Ruby>gem install pony

Pour démarrer, nous allons nous occuper du formulaire de contact. Celui-ci sera disponible à l'URL "/contact" grâce au gestionnaire suivant :

get '/contact' do
  erb :contact
end

Puis nous enregistrons la vue "contact.erb" dans le répertoire "views" :

<p>Vous pouvez utiliser le formulaire ci-dessous pour nous contacter :</p>
<form action="/contact" method="post">
  <label for="name">Votre nom :</label>
  <input type="text" name="name">
  <label for="email">Votre adresse mél :</label>
  <input type="text" name="email">
  <label for="to">Votre message :</label>
  <textarea name="message" rows="16" cols="28">
  <input type="submit" value="Envoyer">
</form>

Nous devons maintenant gérer les données du formulaire lorsqu'il est validé. Pour cela, nous allons créer un gestionnaire de type POST pour la même URL "/contact" (celle que nous avons indiquée dans l'attribut action du formulaire web) :

post '/contact' do 
    require 'pony'
    Pony.mail(
      :from => params[:name] + "<" + params[:email] + ">",
      :to => 'adresseperso@gmail.com',
      :subject => "Vous avez un message de " + params[:name],
      :body => params[:message],
      :port => '587',
      :via => :smtp,
      :via_options => { 
        :address              => 'smtp.gmail.com', 
        :port                 => '587', 
        :enable_starttls_auto => true, 
        :user_name            => 'adresseperso', 
        :password             => 'p@55w0rd', 
        :authentication       => :plain, 
        :domain               => 'localhost.localdomain'
      })
    redirect '/success' 
end

Note : en local sur mon PC j'ai dû utiliser le port 25 au lieu du port 587 pour que l'envoi de mail marche avec le SMTP de GMail.

Grosso modo, nous nous contentons de remplir les informations attendues par Pony et d'envoyer le mail en utilisant notre compte GMail personnel (par conséquent pensez à employer vos propres identifiants !). Si cela fonctionne correctement, on doit recevoir un email avec le sujet "Vous avez un message de Daz". Al la fin du code, l'utilisateur est redirigé vers l'URL "/success" que nous prenons en charge de la façon suivante :

get('/success') {"Merci pour votre message. Nous vous contacterons bientôt."}

C'est clair qu'il faudrait développez un peu plus pour une vrai application, mais vous voyez l'idée.

Heroku

Heroku vous permet d'utiliser l'option Sendgrid pour envoyer vos emails. Vous pouvez vous inscrire pour bénéficier d'un compte gratuit (limité à 200 méls par jour) à l'aide de la commande suivante (à lancer via une console Git bash) :

$ heroku addons:add sendgrid:free

Pour utiliser Sendgrid, vous devez modifier le paramétrage de Pony de la façon suivante :

post '/contact' do 
    require 'pony'
     Pony.mail(
      :from => params[:name] + "<" + params[:email] + ">",
      :to => 'adresseperso@gmail.com',
      :subject => "Vous avez un message de " + params[:name],
      :body => params[:message],
      :port => '587',
      :via => :smtp,
      :via_options => { 
        :address              => 'smtp.sendgrid.net', 
        :port                 => '587', 
        :enable_starttls_auto => true, 
        :user_name            => ENV['SENDGRID_USERNAME'], 
        :password             => ENV['SENDGRID_PASSWORD'], 
        :authentication       => :plain, 
        :domain               => ENV['SENDGRID_DOMAIN']
      })
    redirect '/success' 
end

ENV['SENDGRID_USERNAME], ENV['SENDGRID_PASSWORD'] et ENV['SENDGRID_DOMAIN'] sont trois variables d'environnement de Heroku qui sont définies automatiquement lorsque vous installez l'option Sendgrid.

En pratique, il serait plus judicieux de définir ces informations séparément, en utilisant la commande "set" de Sinatra :

set :email_username, ENV['SENDGRID_USERNAME] || 'adresseperso'
set :email_password, ENV['SENDGRID_PASSWORD'] || 'p@55w0rd'
set :email_address, 'daz@gmail.com'
set :email_service, ENV['EMAIL_SERVICE'] || 'gmail.com'
set :email_domain, ENV['SENDGRID_DOMAIN'] || 'localhost.localdomain'

La variable d'environnement ENV['EMAIL_SERVICE'] n'étant pas définie automatiquement, vous devez penser à l'initialiser vous-même avec la commande suivante :

$ heroku config:add EMAIL_SERVICE=sendgrid.net

Et après cela, vous pouvez mettre à jour votre code pour envoyer un email en utilisant Pony :

post '/contact' do 
    require 'pony'
     Pony.mail(
      :from => params[:name] + "<" + params[:email] + ">",
      :to => settings.email_address,
      :subject => "Vous avez un message de " + params[:name],
      :body => params[:message],
      :port => '587',
      :via => :smtp,
      :via_options => { 
        :address              => 'smtp.' + settings.email_service, 
        :port                 => '587', 
        :enable_starttls_auto => true, 
        :user_name            => settings.email_username, 
        :password             => settings.email_password, 
        :authentication       => :plain, 
        :domain               => settings.email_domain
      })
    redirect '/success' 
end

Cette méthode vous permet d'utiliser votre formulaire de contact en local (en utilisant votre adresse GMail perso) ou depuis Heroku (en passant par Sendgrid).

mercredi 9 mars 2011

Introduction à Haml

J'aime bien les vidéos de Screencasts.org, surtout leur côté 5/10 minutes vite fait pour aborder (ou réviser) un sujet. Après l'introduction à Sinatra, j'ai donc décidé de suivre l'introduction à Haml. Haml (qui se prononce AMeul, comme dans meule) est un langage de template très concis destiné à remplacer HTML dans les applications Rails ou Sinatra.

Cette fois-ci, la vidéo est accompagnée de sa retranscription complète ce qui rend son visionnage encore plus facile.

Après 10 secondes de publicité, on enchaine sur un rappel de la dure condition des développeurs en quête de beauté et de simplicité dans un monde d'IE6, SOAP et autre Dreamweaver.

On rentre ensuite dans le vif du sujet en démarrant avec un petit morceau de code HTML (beurk beurk beurk) tel qu'on en voit souvent :

<div id="profile">
  <div class="left column">
    <div id="date"> 11 November 2010 </div>
    <div id="address"> 1 Infinite Loop </div>
  </div>
  <div class="right column">
    <div id="email"> steve@apple.com </div>
    <div id="bio"> Makes magical tablets </div>
  </div>
</div>

Puis, à partir de ce truc, on nous retrace par petites touches successives la genèse de Haml (miam miam miam) :

#profile
  .left.column
    #date 11 November 2010
    #address 1 Infinite Loop
  .right.column
    #email steve@apple.com
    #bio Makes magical tablets

Pour bien enfoncer le clou, on fait aussi le parallèle entre le template Erb correspondant (ouuh !) :

<div id="profile">
  <div class="left column">
    <div id="date"><%= print_date %></div>
    <div id="address"><%= user.address %></div>
  </div>
  <div class="right column">
    <div id="email"><%= user.email %></div>
    <div id="bio"><%= user.bio %></div>
  </div>
</div>

Et l'équivalent sous Haml (ouah !) :

#profile
  .left.column
    #date= print_date
    #address= user.address
  .right.column
    #email= user.email
    #bio= user.bio

Viennent ensuite quelques explications sur la syntaxe :

  • !!! pour déclarer du doctype
  • %element pour définir un élément (%html pour la balise <html ...>, %p pour la balise <p>...)
  • %element#id pour définir un identifiant
  • %element.class pour définir une classe
  • possibilité d'omettre %div qui est la balise par défaut
  • ajouter des attributs sous la forme (attribut="valeur")
  • importance de l'indentation pour imbriquer les balises

Le tutoriel se termine par l'installation d'Haml (gem install haml), un exemple d'utilisation en ligne de commande et un lien pour tester Haml en ligne.

Pour l'instant, je suis dubitatif et je me demande si c'est une vrai bonne idée de masquer autant que ça HTML ?

Juste pour référence, voilà ce que l'exemple de code donnerait avec Razor où on reste plus calqué sur le code HTML :

<div id="profile">
  <div class="left column">
    <div id="date">@Me.print_date()</div>
    <div id="address">@user.address</div>
  </div>
  <div class="right column">
    <div id="email">@user.email</div>
    <div id="bio">@user.bio</div>
  </div>
</div>

Malgré tout (pour s'entrainer en attendant de suivre l'introduction à Sass) le site Html2Haml permet de convertir du code HTML en Haml et un tutoriel en français sur Haml.

mardi 8 mars 2011

Introduction à Sinatra

Screencasts.org propose (gratuitement) une courte vidéo de 8 minutes pour aborder très très simplement et très clairement les bases de Sinatra. C'est en anglais, mais 8 minutes ça passe vite et ça offre un premier contact avec ce que permet Sinatra :

  • comment installer Sinatra puis créer une première application avec seulement 2 lignes de codes (1)
  • le principe des routes basées sur la correspondance d'une méthode HTTP et d'une URL
  • le fonctionnement des templates et des vues avec un exemple en Haml (2)
  • l'utilisation de variables
  • comment gérer des paramètres nommés dans les routes

Notes :

  1. Il est même possible de faire encore plus court !
  2. Screencasts.orgs propose aussi une introduction à Haml

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.

mercredi 15 septembre 2010

Installer Nesta sous Windows

Mise à jour : L'installation et le fonctionnement de Nesta ont changé depuis ce billet. Il est donc préférable de se reporter au site http://nestacms.com/ pour obtenir des informations plus actualisées (mais l'installation du toolkit DevKit pour RedCloth est toujours d'actualité).


Pour essayer d'approfondir mes connaissances en Ruby et Sinatra, j'ai regardé ce qui existait comme exemples d'applications Sinatra toutes faites en essayant plus particulièrement de trouver quelque chose de simple pour gérer des sites internet.

J'ai finalement décidé de tester Nesta qui me parait assez facile à utiliser et devrait être assez simple à prendre en main étant donné qu'au total il ne représente qu'une vingtaine de kilo-octets de code Ruby.

Nesta est un CMS (système de gestion de contenu) plutôt léger, très bien adapté pour gérer de petits blogues ou sites internet. Il est développé en Ruby et il utilise bien entendu le framework Sinatra.

Pour pouvoir installer Nesta sur Windows, j'ai dû commencer par mettre à jour mon installation du Ruby parce que je n'avais pas installé le toolkit DevKit. Je n'en avait pas eu besoin jusqu'à présent, mais il s'est avéré indispensable pour terminer l'installation du gem RedCloth qui nécessite une compilation de je ne sais trop quoi.

Une fois ce point réglé, il faut utiliser Git à partir de Git Bash pour récupérer une copie de Nesta sur mon poste de travail :

$ git clone git://github.com/gma/nesta.git

Arrivé là, j'ai continué à suivre le tutoriel Quick Start proposé sue le site de Nesta et j'ai installé le système de gestion de dépendances bundler qui va ensuite se charger du reste de l'installation :

$ gem install bundler
$ cd nesta
$ bundle install

La dernière commande bundle install a alors donné le résultat suivant :

Fetching source index for http://rubygems.org/
Using rake (0.8.7)
Installing RedCloth (4.2.2) Temporarily enhancing PATH to include DevKit...
with native extensions
Installing builder (2.1.2)
Installing haml (3.0.12)
Using json_pure (1.4.6)
Using rubyforge (2.0.4)
Using hoe (2.6.2)
Installing hpricot (0.8.2) with native extensions
Using syntax (1.0.0)
Installing maruku (0.6.0)
Using rack (1.2.1)
Installing rack-test (0.5.3)
Installing rspec (1.3.0)
Installing rspec_hpricot_matchers (1.0)
Installing shotgun (0.8)
Using sinatra (1.0)
Installing test-unit (1.2.3)
Using bundler (1.0.0)
Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem
is installed.

Puis, comme indiqué sur le site de Nesta, j'ai récupéré le fichier de configuration et quelques pages d'exemple :

$ cp config/config.yml.sample config/config.yml
$ bundle exec rake setup:sample_content

Ensuite, étant donné que ShotGun ne fonctionne pas sous Windows (lu je ne sais plus où mais vu de mes yeux vu), je n'ai pas utilisé la commande bundle exec shotgun app.rb et j'ai lancé l'application avec la commande :

ruby app.rb

Et tada !

nesta.png

(ok, entre temps j'ai aussi fait une ou deux traductions du contenu du site d'exemple)

Prochaine étape : l'installer sous Heroku ?

lundi 13 septembre 2010

Déployer des applications Sinatra sur Heroku

Ceci est la traduction du tutoriel "Deploying Sinatra Apps on Heroku" de Darren Jones.

Vous avez enfin terminé votre super application web, mais ça n'a pas beaucoup d'intérêt si personne en peut s'en servir. Ce qu'il vous faut, c'est lancer votre application à l'assaut d'internet !

Heroku

Heroku est de très loin la façon la plus simple pour déployer des applications Sinatra. Il s'agit d'un excellent service pour tous ceux qui ont besoin d'héberger des applications en Ruby (avec des frameworks tels que Rails, Merb, Rameze et bien entendu Sinatra). L'hébergement est gratuit pour les petits sites de base, y compris la base de données, et s'adapte lorsque votre application devient plus populaire. Heroku s'occupe de tous les trucs compliqués en matière d'hébergement et convient parfaitement pour mettre en production des sites à grande échelle. le tout accompagné d'un excellent support et d'une large documentation.

Avant d'aller plus loin, vous devez avoir installé Git étant donné qu'il est nécessaire pour faire un déploiement sur Heroku.

Inscription

Pour commencer, vous devez créer un compte sur Heroku en vous enregistrant gratuitement à l'adresse http://heroku.com/signup.

Installer le gem Heroku

On part du principe que vous avez déjà installé Ruby, Gems et Sinatra et par conséquent vous n'avez plus qu'à lancer la commande suivante à partir d'une invite de commande :

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

ce qui donne :

Successfully installed mime-types-1.16
Successfully installed rest-client-1.4.2
Successfully installed rake-0.8.7
Successfully installed configuration-1.1.0
Successfully installed launchy-0.3.7
Successfully installed heroku-1.10.0
6 gems installed

Il s'agit d'une petite application en ligne de commande qui va vous permettre d'interagir avec vos sites hébergés sur Heroku. Vous pouvez consulter sa documentation à l'adresse http://docs.heroku.com/heroku-command.

Envoyer votre clé SSH

Pour pouvoir communiquer avec Heroku, vous devez utiliser une clé SSH. Si vous n'en avez pas encore, vous pouvez en générez une en tapant la commande suivante dans une console Git Bash (et pas une simple invite de commande) :

$ ssh-keygen -t rsa -C "yourname@gmail.com"

Vous trouverez plus d'informations pour générer une clé SSH sur Github (vous devriez d'ailleurs avoir déjà généré votre clé SSH lors de votre inscription à Github).

Une fois que vous disposez d'une clé SSH, vous devez l'enregistrer dans votre compte Heroku, en utilisant la ligne de commande :

C:\Ruby>heroku keys:add 

Ce qui devrait vous renvoyer le message suivant :

Enter your Heroku credentials. 
Email: youremail@gmail.com 
Password: 

Répondez à la question qui vous est posée et vous obtiendrez le message ci-dessous :

Uploading ssh public key C:\Users\nom.utilisateur/.ssh/id_rsa.pub

Ce paramétrage de la clé SSH ne doit être réalisé qu'une seule fois. A partir de maintenant, vous n'aurez plus qu'à effectuer les étapes décrites dans la suite de ce tutoriel à chaque fois que vous souhaiterez mettre en ligne une de vos applications Sinatra.

Le fichier rackup

Pour déployer une application Sinatra, vous avez besoin d'un fichier rackup. Celui-ci doit être créé dans la racine de votre application et doit se nommer "config.ru" pour que le serveur web puisse le retrouver facilement. Pour notre premier essai, nous nous plaçons dans le répertoire de l'application Reverse que nous avons développée et versionnée sous GIT au cours des tutoriels précédents pour y créer un fichier "config.ru" avec le contenu suivant :

require 'main'
run Sinatra::Application

Il faut ensuite penser à ajouter ce fichier au référentiel Git :

$ git commit -a -m "Ajout du fichier rackup pour déployer sur Heroku"

Le manifeste gem

Pour définir les dépendances gems de votre application, il faut également configurer un fichier de manifeste. Il s'agit d'un simple fichier texte nommé ".gems" à placer dans la racine de votre application et dans lequel on enregistre les gems nécessaires à notre application. Dans le cas de l'application Reverse, il suffit donc d'y faire apparaitre une seule ligne, à savoir :

sinatra

Là encore, il ne faut pas oublier d'ajouter ce fichier au référentiel Git :

$ git commit -a -m "Ajout du manifeste gem pour déployer sur Heroku"

Note : dans le cas de l'application SuperDo, le fichier ".gems" devra contenir les 3 lignes suivantes pour que l'application fonctionne correctement une fois déployée :

sinatra
data_mapper
dm-postgres-adapter

Ajouter votre site sur Heroku

La dernière étape consiste à placer votre site sur Heroku. Ouvrez une invite de commande et accédez au répertoire de votre application "Reverse". Vous devez ensuite choisir un nom pour votre application (vous ne pouvez pas utiliser "reverse" parce que Daz l'a déjà pris, ni "reversefr" puisque je l'ai pris !). Pour commencer, il faut créer l'application sur Heroku en entrant la ligne de commande suivante depuis une console Git Bash :

$ heroku create appname

Ce qui donne le résultat ci-dessous :

Creating appname.... done
Created http://appname.heroku.com/ | git@heroku.com:appname.git
Git remote heroku added

Il reste alors à pousser notre application vers Heroku à l'aide de Git en lançant la commande :

$ git push heroku master

Ce qui donne alors quelque chose du style :

Counting objects: 7, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 441 bytes, done.
Total 4 (delta 3), reused 0 (delta 0)

-----> Heroku receiving push
-----> Sinatra app detected
       Compiled slug size is 588K
-----> Launching...... done
       http://appname.heroku.com deployed to Heroku

To git@heroku.com:appname.git
   3b3ed7c..350b521  master -> master

Et maintenant, il est enfin possible de naviguer vers l'URL "http://appname.heroku.com/" pour voir votre application prendre son envol sur internet !

Mise à jour du site

Il vous suffit d'une ligne de commande Git pour pouvoir déployer les modifications que vous avez apportées à votre application (à condition de les avoir commitées !). Dans le cas où vous souhaitez déployer la branche master de votre référentiel Git, vous n'avez qu'à taper la commande suivante :

$ git push heroku master

C'est là tout l'intérêt d'utiliser Git pour déployer une application sur Heroku. Grâce à son système de branches, on peut développer sur une branche et utiliser une autre branche pour la version en production.

J'espère que ce billet aura réussi à vous convaincre qu'il est très facile de déployer des applications développées avec Sinatra et que Heroku est un service parfaitement adapté pour cela. A votre tour de mettre votre application en ligne !

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...

mardi 3 août 2010

Guide pour démarrer avec Git

Ceci est la traduction du tutoriel "Git Beginners Guide" de Darren Jones.

Dans ce nouveau tutoriel, Darren présente les bases de Git et aborde les éléments suivants :

  • Qu'est-ce que Git ?
  • Installer et configurer Git
  • Ajouter et Commiter avec Git
  • Les Branches
  • Les Tags
  • Les repositories distants
  • Quelques ressources sur Git

Qu'est-ce que Git ?

Git est un système de contrôle de version qui vous de garder une trace de toutes les modifications que l'on fait quand on travaille sur un projet. Git permet de faire un "roll back" pour revenir sur une version précédente et aussi de créer des "branches" dans votre code pour tester de nouvelles fonctionalités sans toucher à la version en cours et stable de votre code. Un système de contrôle source est essentiel quand vous travaillez en équipe, mais dans le cadre de Sinatra, c'est aussi un outil pour déployer des applications sur le cloud chez Heroku (cela fera l'objet d'un autre billet).

Git utilise la ligne de commande pour son interface utilisateur, mais pour ceux qui préfèrent, il existe aussi de nombreux outils visuels.

Installer et configurer Git

Pour commencer, vous avez besoin d'installer Git.

La VO de ce tutoriel Git Beginners Guide explique comment installer Git pour Linux, Mac et Windows. Par ailleurs, GitHub propose un tutoriel très complet pour installer Git sur Windows et un autre qui explique comment générer les clés SSH pour Git et ajouter votre clé publique à votre compte GitHub.

Une fois l'installation terminée, il faut lancer un "Git Bash" (en faisant un clic-droit sur un répertoire et en choisissant la commande "Git Bash here"). Cela a pour effet d'ouvrir un genre d'invite de commandes pour Git où vous pouvez définir quelques paramètres généraux en utilisant les deux commandes suivantes :

$ git config --global user.name "Votre Nom"
$ git config --global user.email "votremail@example.com"

Note : pour quitter Git Bash, on peut taper la commande exit.

git-1.png

Maintenant, il faut se placer dans le répertoire de notre projet. Pour nos tests, nous allons utiliser le projet Reverse réalisé lors du tutoriel précédent :

$ cd /c/Ruby/projets/reverse

Puis nous initialisons ce répertoire pour que Git assure le suivi des modifications que nous apporterons au contenu de ce répertoire.

$ git init

Nous obtenons en retour le message "Initialized empty Git repository in c:/Ruby/_projets/reverse/.git/". Si on consulte le répertoire C:\Ruby\projets\reverse dans l'explorateur de Windows, on peut constater qu'il contient désormais un sous-répertoire ".git" qui sert va servir à Git pour stocker le repository.

Ajouter et Commiter avec Git

A présent, nous allons ajouter tous les fichiers et les sous-répertoire de notre projet Reverse dans le repository que nous venons de créer. On effectue cela en tapant la commande suivante :

$ git add .

Ceci va ajouter tous les fichiers à l'index, puisque le "." représente tous les fichiers. L'index est un peu comme une zone de transit dans laquelle les fichiers attendent d'être commités dans le repository. Ce que nous allons faire avec cette commande :

$ git commit -m "Version initiale du projet Reverse"

Cela a pour effet de commiter tous les fichiers de l'index dans le repository. Le texte entre guillemets après le "-m" est un message que vous devez définir à chaque commit pour expliquer ce que vous avez fait.

Et maintenant on va apporter quelques modifications à notre projet, comme par exemple de changer légèrement la fin du fichier "layout.erb" pour y ajouter un lien :

<!DOCTYPE html>
<html lang="en">
<head>
<title>Reverse!</title>
<meta charset=utf-8 />
</head>
<body>
<h1>Reverse</h1>
<h2><%= @title %></h2>

<%= yield %>

<p>The first Sinatra project for <a href="http://ididitmyway.heroku.com/">I Did It My Way</a></p>
</body>
</html>

Puis on saisi la commande suivante dans Git Bash pour mettre à jour l'index :

$ git add views/layout.erb

Comme vous pouvez le voir, on peut décider de spécifier quel fichier on veut ajouter à l'index plutôt que d'utiliser "." pour sélectionner tous les fichiers. Et sinon, n'oubliez pas de commiter votre modification :

$ git commit -m "Ajout d'un lien en pied de page"

Le fait d'avoir à ajouter puis à commiter peut sembler un peu fastidieux. Mais cela présente l'avantage de permettre d'ajouter plusieurs modifications avant de réellement faire le commit. Toutefois, on peut ramener ça à une seule étape en utilisant l'option "-a" :

$ git commit -a -m "un message d'explication"

Cela aura pour effet d'ajouter et de commiter les modifications en une seule fois. En fait, tous les fichiers modifiés ou supprimés sont commités. Les nouveaux fichiers ne sont pas concernés.

Si vous souhaitez supprimer des fichiers, vous ne pouvez pas vous contenter de les supprimer directement depuis Windows. Vous devez aussi les supprimer du repository Git en utilisant la commande "rm". Par exemple, nous n'avons plus besoin du fichier "frank.erb" qui doit encore se trouver dans le sous-répertoire "views". Nous pouvons donc le supprimer du repository avec la commande suivante :

$ git rm views/frank.erb

Le fichier "frank.erb" est alors supprimé physiquement du sous-répertoire "views". Il reste encore à commiter pour qu'il soit supprimé définitivement du repository Git :

$ git commit -a -m "Suppression fichier inutile"

Les Branches

En ce qui me concerne, c'est LE truc de Git. Quand vous créez une branche, c'est en gros comme si vous faisiez une copie de votre code en cours. Vous pouvez alors faire plein de modifications ou d'essais sans toucher au code d'origine. Si on veut conserver ces changements, on peut alors les fusionner avec le code d'origine ou si ces changements ne conviennent pas, on peut tout simplement revenir au code de départ et supprimer la branche. Il est possible de créer autant de branches que l'on souhaite et les fusionner entre elles ou les conserver en tant que branches séparées.

Au départ, vous démarrez avec la branche par défaut qui s'appelle "master". Pour savoir sur quelle branche vous vous trouvez actuellement, il suffit de taper :

$ git branch

Et vous devriez alors avoir le résultat suivant :

* master

L'astérisque devant "master" indique qu'il s'agit de la branche en cours.

Pour créer une nouvelle branche qu'on appellera "development", il faut taper :

$ git branch development

Puis pour basculer sur cette branche vous devez saisir :

$ git checkout development

Après un message Switched to branch 'development', vous pouvez vérifier que cela a fonctionné en tapant :

$ git branch

Et cette fois, vous devez obtenir :

* development
  master

L'astérisque devant "development" vous montre que vous travaillez maintenant sur la branche "development". Si vous faites une modification quelconque, cela affectera uniquement la branche "development" et absolument pas la branche "master".

Essayez par exemple de changer quelques lignes (en supprimant par exemple le lien dans "layout.erb") puis commitez ces modifications :

$ git commit -a -m "Suppression du lien en pied de page"

Revenez alors sur la branche principale :

$ git checkout master

Vous pouvez réouvrir le fichier "layout.erb" pour constater que votre modification n'est pas là ! Et que le lien que vous venez de supprimer est toujours là ! On retourne dare-dare sur la branche de développement :

$ git checkout development

On ré-ouvre derechef la fichier "layout.erb" et notre modification est bien là !

Le plus gros intérêt des branches est de permettre de séparer la branche principale du code de la branche de développement. Mais étant donné que c'est si facile et si rapide, on peut aussi utiliser les branches pour tester de nouvelles fonctionnalités. Supposons que je veuille ajouter un super fonctionnalité Twitter dans ma branche de développement. Je commence par créer une nouvelle branche dédiée à cette fonctionnalité :

$ git branch feature-twitter

Puis je passe sur cette branche

$ git checkout feature-twitter

On va ensuite ajouter notre super fonctionnalité Twitter et tester qu'elle marche correctement. Pour gagner un peu de temps, pourquoi ne pas ajouter un lien vers Twitter dans le pied de page (soit le fichier "layout.erb si vous avez bonne mémoire). Ces modifications vont seulement concerner notre nouvelle branche "feature-twitter". Disons que ces modifications vous donnent satisfaction (et que le lien fonctionne) vous allez les commiter :

$ git commit -a -m "Ajout fonctionnalité Twitter"

Et ensuite vous allez vouloir avoir ces modifications dans votre branche de développement. Et pour cela, vous allez donc devoir les fusionner. Et pour commencer, il faut repasser sur la branche développement :

$ git checkout development

Puis y fusionner les modifications apportées dans la branche pour Twitter :

$ git merge feature-twitter

A partir de maintenant, les modifications que vous aviez faites dans la branche "feature-twitter" sont bel et bien incluses dans la branche développement. Par conséquent, vous n'avez plus besoin de la branche Twitter qui ne vous a servi que pour les tests. Vous pouvez donc la supprimer définitivement en utilisant l'option "-d" :

$ git branch -d feature-twitter

Lorsque votre branche de développement aura été recettée et sera fin prête pour le grand jour de la mise en production, il ne vous restera plus qu'à la fusionner à votre branche "master". Souvenez-vous bien qu'il faut d'abord passer sur la branche "master" puis y fusionner la branche "development" :

$ git checkout master
$ git merge development

Les Tags

Vous pouvez créer un tag pour un projet à n'importe quel moment, en utilisant la commande suivante :

$ git tag v1.0 -m "Version 1.0 du projet Reverse"

Cela vous permet d'immortaliser certains moments du développement - assurez vous d'avoir ajouté et commité tous les changements apportés avant de créer un tag. Si vous voulez voir tous les tags que vous avez créé, vous pouvez taper :

$ git tag

Et si vous souhaitez revenir à une version précédente, alors vous créez une nouvelle branche pour celle-ci. Supposons qu'à l'avenir vous ayez à revenir sur cette version (pour corriger un bug dessus par exemple), vous aurez à saisir la commande ci-dessous :

$ git checkout v1.0 -b version1

Cela va automatiquement créer une nouvelle branche nommée "version1" qui sera la copie exacte de ce qu'était le projet lorsque vous aviez créé le tag "v1.0", sans que cela vous fasse perdre quoique ce soit de votre code actuel.

Les repositories distants

Github est un service extraordinaire qui vous permet de conserver votre code dans un repository distant qui est hébergé sur leurs serveurs. Pour pouvoir démarrer, vous n'avez qu'à vous inscrire pour disposer d'un compte gratuit, définir un nouveau projet puis suivre leurs instructions pour créer un nouveau repository distant.

Une fois que votre repository distant est créé, tout ce qu'il vous reste à faire c'est d'entrer la commande ci-dessous :

$ git push origin master

Cela aura pour effet d'envoyer tout le code de votre branche "master" vers votre repository chez GitHub. C'est très pratique pour disposer d'une sauvegarde externe de votre repository dans le cloud mais aussi pour partager votre travail avec les autres (l'idéal pour tous les projets open source !). Par contre, si vous voulez que votre repository reste privé alors vous aurez besoin de prendre un compte payant sur GitHub.

Vous pouvez aussi vous mettre à utiliser le code d'autres personnes sur GitHub en clonant leur repositories et en utilisant la commande pull pour récupérer toutes les modifications qu'ils ont effectuées. Quant à eux, ils ont aussi la possibilité d'utiliser la commande pull pour récupérer les changement que vous avez apporté. Tout cela fait de Git un outil particulièrement puissant dès qu'il s'agit de collaborer sur un même projet, dans la mesure où il permet aux gens de corriger des bugs dans votre code, d'améliorer votre code ou de développer un projet totalement nouveau.

Quelques ressources sur Git

Daren a collecté un certain nombre de ressources consacrées à Git que vous pouvez retrouver sur le tutoriel d'origine.

Git est une application super puissante qui va révolutionner la façon dont vous gérez vos projets. Et il est extrêmement précieux quand il s'agit de gérer et de déployer des applications Sinatra sur la plateforme Heroku, ce dont je parlerai dans un prochain billet.

mercredi 28 juillet 2010

Création d'un premier projet avec Sinatra

Ceci est la traduction du tutoriel Sinatra "Project 1: Reverse" de Darren Jones.

Après avoir brillamment suivi pas à pas mon premier tutoriel pour installer Ruby et Sinatra sur mon PC Windows 7, je continue sur ma lancée avec la réalisation du deuxième tutoriel proposé par Darren Jones.

Le but de ce tutoriel est de programmer une première application très très simple qui va se contenter d'afficher un texte à l'envers, d'où son nom : Projet 1 : Reverse. A travers cette mini-application, on peut déjà apprendre quelques trucs sur le fonctionnement de Sinatra, voire sur Ruby si on débute comme moi.

C'est parti

Pour commencer, j'ai créé un répertoire C:\Ruby\projets\reverse puis un fichier main.rb à l'intérieur :

require 'rubygems'
require 'sinatra'

get '/' do
  "I did it my way!"
end

Toutes les applications Sinatra ont besoin des deux premières lignes. Il y a ensuite une ligne blanche pour faire plus joli puis 3 lignes de code où se situe toute l'action :

  • «get» indique quelle méthode HTTP on souhaite gérer : un GET dans le cas présent,
  • «'/'» correspond à la route à gérer, soit la racine de l'application dans ce cas,
  • «do ... end» est un bloc de code pour définir ce qui se passe quand quelqu'un demande la racine du site.

La dernière ligne à l'intérieur du bloc «do ... end» contient toujours (je pense) ce qui sera affiché dans la page, soit "I did it my way!" dans ce premier exemple.

Pour tester ce code, on peut directement double-cliquer sur le fichier main.rb dans le répertoire C:\Ruby\projets\reverse ou faire ça à la main dans une invite de commande :

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

Et Sinatra entre en scène :

== Sinatra/1.0 has taken the stage on 4567 for development with backup from WEBrick
[2010-07-27 21:44:57] INFO  WEBrick 1.3.1
[2010-07-27 21:44:57] INFO  ruby 1.9.1 (2010-07-02) [i386-mingw32]
[2010-07-27 21:44:57] INFO  WEBrick::HTTPServer#start: pid=3604 port=4567

On peut alors lancer un navigateur pour appeler l'URL http://localhost:4567/ :

reverse-1.png

Tant qu'on est là, si on essaie d'aller sur une URL qui n'existe pas, comme http://localhost:4567/reverse, on obtient alors la page d'erreur 404 de Sinatra pour indiquer qu'il ne connait pas ce morceau :

reverse-2.png

Cette page d'erreur nous conseille même sur la route ajouter dans notre fichier pour que cela fonctionne, en l'occurrence :

get '/reverse' do
  "Hello World"
end

Avec Sinatra, c'est pas plus compliqué que ça pour créer des actions correspondant à différentes routes.

Ajouter une vue

Pour l'instant, on va rester sur notre route «'/'» et essayer de faire un peu mieux que de seulement renvoyer une ligne de texte. Pour cela, il faut créer une vue en modifiant le fichier main.rb de la façon suivante :

require 'rubygems'
require 'sinatra'

get '/' do
  erb :home
end

__END__

@@ home

<h1>Reverse</h1>

<p>Welcome to the home page of my very first Sinatra app.</p>

Ce coup-ci, au lieu d'utiliser la dernière ligne du bloc pour dire à Sinatra ce qu'il doit afficher, nous lui avons demandé d'utiliser la vue "home" que nous avons codé en erb (embedded ruby). Cette vue est enregistrée à la fin du fichier, après la ligne __END__ et elle est repéré par le code @@ home.

Pour voir ce que donne cette vue, il faut revenir à l'invite de commande et arrêter le serveur s'il est toujours en cours d'exécution. Ctrl-C =>Sinatra has ended his set (crowd applauds). Puis on relance le serveur avec ruby main.rb et on réaffiche la page http://localhost:4567 :

reverse-3.png

Créer une vue externe

En fait, on n'est pas obligé de stocker les vues dans le même fichier. Il est bien plus pratique de les enregistrer dans un sous-répertoire "views" de notre projet (soit C:\Ruby\projets\reverse\views dans mon cas).

Là, il suffit de créer le fichier "home.erb" avec le code ci-dessous :

<h1>Reverse</h1>

<p>Welcome to the home page of my very first Sinatra app.</p>

Il est alors possible de simplifier le fichier "main.rb" de la façon suivante :

require 'rubygems'
require 'sinatra'

get '/' do
  erb :home
end

Il ne reste plus qu'à contrôler que tout est ok : Ctrl-C, ruby main.rb, rafraichir la page et vérifier que rien n'a changé.

Créer un layout

D'un point de vue visuel, on peut faire encore mieux en définissant un "layout" qui servira de gabarit pour englober toutes les vues. Cela permet d'éviter de répéter le même code dans toutes les vues de l'application.

Pour cela, on doit juste créer un fichier "layout.erb" dans le sous-répertoire "views" et y saisir le code html ci-dessous :

<!DOCTYPE html>
<html lang="en">
<head>
<title>Reverse!</title>
<meta charset=utf-8 />
</head>
<body>
<h1>Reverse</h1>

<%= yield %>

<p>The first Sinatra project for I Did It My Way</p>
</body>
</html>

Tout ce code html sera toujours affiché à chaque fois qu'une vue sera affichée, à part la ligne <%= yield %> qui sera remplacée par le contenu spécifique de la vue.

Ainsi, si on modifie légèrement le code de la vue "home.erb" :

<h2>Home</h2>
<p>Welcome to the home page. This app is going to be amazing....</p>

Le fait de relancer le serveur et de ré-afficher l'URL http://localhost:4567 doit donner le résultat suivant :

reverse-4.png

Comme vous pouvez le constater, le sous-titre "Home" et le message "Welcome..." en provenance de la vue "home.erb" apparaissent entre le titre "Reverse" et le paragraphe "The first app...", soit exactement là où se situait la ligne <%= yield %> dans le fichier "layout.erb".

ERB

La balise <%= yield %> est un exemple d'embedded ruby (ou erb en abrégé). On peut ainsi ajouter du code ruby dans les fichiers html en l'insérant à l'intérieur de blocs <% ... %>. C'est très utile dans le cas de conditions if :

<% if something_happens %>
<h1>Something happened</h1>
<% else %>
<h1>Nothing happend</h1>
<% end %>

Si le code ruby est placé dans un bloc <%= ... %>, alors ce code est évalué et son résultat est affiché. Comme par exemple dans le code suivant :

<% title = "Reverse" %>
<h1>
<%= title %>
</h1>

Le premier bloc de code <% ... %> défini une variable appelée "title" et le second bloc de code <%= ... %> évalue cette variable et affiche sa valeur. Même si cet exemple est ultra simple, dans la vrai vie on peut faire des tas de chose très utile grâce à l'embedded ruby.

Définir une variable

Plus concrètement, nous allons utiliser embeded ruby pour définir le titre de notre page. Dans un premier temps, on met à jour le code de "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Home"
  erb :home
end

Celui-ci initialise une variable session nommée @title (c'est une variable session parce que son nom débute par un @). Les variables session sont disponibles dans les autres parties du code, y compris dans les vues. On peut donc maintenant faire référence à notre variable session @title dans notre vue ou même notre layout.

Par conséquent, nous pouvons modifier "layout.erb" pour qu'il utilise notre variable session :

<!DOCTYPE html>
<html lang="en">
<head>
<title>Reverse!</title>
<meta charset=utf-8 />
</head>
<body>
<h1>Reverse</h1>
<h2><%= @title %></h2>

<%= yield %>

<p>The first Sinatra project for I Did It My Way</p>
</body>
</html>

Puis nous pouvons alors supprimer le titre qui était en dur dans la vue "home.erb" :

<p>Welcome to the home page. This app is going to be amazing....</p>

Par acquit de conscience, on peut redémarrer le serveur et contrôler que rien n'a changé, ce qui signifie que notre variable session a bien été prise en compte et que nous pouvons maintenant définir le titre de la page au niveau de l'action.

On va vérifier cela en créant une nouvelle route dans le source "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Home"
  erb :home
end

get '/frank' do
  @title = "My Way"
  erb :home
end

Après avoir encore une fois redémarré le serveur, on peut aller à la page http://localhost:4567/frank pour constater que le sous-titre n'est plus "Home" mais "My Way" :

reverse-5.png

Mais on continue à voir presque la même chose, étant donné qu'on utilise la même vue dans les deux cas. On va donc créer une vue différente qui s'affichera pour la route "/frank". Pour cela, on saisi le code suivant dans un fichier "frank.erb" à créer dans le sous-répertoire "views" :

<p>
And now, the end is here
And so I face the final curtain
My friend, I'll say it clear
I'll state my case, of which I'm certain
I've lived a life that's full
I traveled each and ev'ry highway
And more, much more than this, I did it my way
</p>

Il faut aussi changer la ligne erb:home par erb:frank dans le cas de la route "/frank" avant de redémarrer le serveur et de ré-afficher la page http://localhost:4567/frank pour voir ce que cela donne.

Poster quelque chose

Jusqu'à présent on n'a pas fait grand chose d'autre que des pages statiques. Mais quand on crée une application web, c'est quand même pour avoir un peu d'interaction avec le visiteur. On va donc rendre les choses un peu plus intéressante en commençant par créer un formulaire dans notre page d'accueil. Pour cela, on remplace tout le code de "home.erb" par le code suivant :

<form action="/reverse" method ="post" accept-charset="utf-8">
<input type="text" id="phrase" name="phrase" value="Write something...">
<input type="submit" value="...and reverse it!">
</form>

Une fois que c'est fait, on a aussi besoin d'une nouvelle action pour prendre en compte le formulaire lorsqu'il est envoyé. Si vous observez le code html de "home.erb", vous pouvez voir que le formulaire va être posté à l'URL "/reverse" avec une méthode POST.

Normalement, c'est le moment où vous devriez vous rendre compte que la syntaxe de Sinatra est bien faite. Et vous devriez même avoir une idée ce que nous allons ajouter dans le fichier "main.erb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Home"
  erb :home
end

post '/reverse' do
  params.inspect
end

L'avant-avant dernière ligne (l'antépénultième pour les érudits) défini la nouvelle action qui va gérer la route "/reverse". Cette action est définie pour une route de type POST, ce qui signifie qu'elle ne sera activé que pour une requête POST (ce qui correspond à l'envoi d'un formulaire). Ca c'est bon ? Mais par contre, c'est quoi cette ligne params.inspect ? "params" est une collection qui contient toutes les informations qui ont été envoyées en tant que paramètre (aussi bien à travers un formulaire que via l'URL) Par conséquent, params.inspect affiche les paires clé/valeur correspondantes à tous ces paramètres.

Allez. On relance le serveur, on accède à la page http://localhost:4567/, on saisi une phrase au hasard et on clique sur le bouton [... and reverse it] :

reverse-6.png

Cela signifie que la clé "phrase" contient la valeur "I Did It My Way". Et nous avons une clé "phrase" parce que le formulaire dans "home.erb" contient une balise input dont l'attribut name est "phrase". Il est possible d'accéder à n'importe quel paramètre stocké dans la collection "params" en employant la syntaxe params[:key]. Par exemple, params[:phrase] renverra "I Did It My Way".

Supposons que l'on ait le formulaire suivant :

<form action="/reverse" method ="post" accept-charset="utf-8">
<input type="text" name="name">
<input type="text" name="email">
<input type="text" name="password">
<input type="submit" value="submit">
</form>

On va pouvoir accéder aux valeurs de ce formulaire en utilisant params[:name], params[:email] et params[:password].

Maintenant que nous savons comment accéder aux données d'un formulaire, nous allons pouvoir faire quelque chose du texte saisi dans notre vue. Pour cela, nous ajoutons le code ci-dessous au fichier "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/reverse' do
  @title = "Here's Your Reversed Text:"
  params[:phrase].reverse
end

On re-démarre le serveur, on ré-accède à la page http://localhost:4567/, on re-saisi une phrase au hasard et on re-clique sur le bouton [... and reverse it]. Cette fois-ci, on doit voir la phrase saisie affichée à l'envers, de la droite vers la gauche :

reverse-7.png

On obtient ce résultat parce que la dernière ligne de la méthode est params[:phrase].reverse. Et si vous vous souvenez bien, la dernière ligne d'une méthode est ce qui est renvoyé pour l'URL demandée. params[:phrase] correspond au texte entré dans le formulaire et le .reverse à sa suite correspond à la méthode "reverse" pour les chaines de caractères. Et cette méthode fait exactement ce que son nom laisse supposer, soit inverser l'ordre des caractères d'une phrase. Coup de bol, c'est aussi ce à quoi notre application était destinée !

Faire un postback

On peut pousser le bouchon encore plus loin et utiliser la même URL pour nos deux pages (l'affichage du formulaire et l'affichage du résultat de notre application). C'est possible parce que pour afficher le formulaire on fait une requête GET et que pour inverser le texte on envoie le formulaire avec une requête POST. Par conséquent, on peut donc gérer ces deux actions avec la même route, mais deux actions différentes.

On peut donc réécrire "main.rb" :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/' do
  @title = "Here's Your Reversed Text:"
  params[:phrase].reverse
end

Cette fois-ci, l'URL de la route est toujours la même, mais la méthode HTTP est différente (soit un GET, soit un POST). Le fait de poster vers soit-même s'appelle un "postback". En plus du GET et du POST, Sinatra gère les deux autres méthodes HTTP, à savoir PUT et DELETE. Nous n'en avons pas besoin pour cette application, mais nous aurons l'occasion d'y revenir dans un autre projet.

Pour que le postback fonctionne, il faut aussi penser à modifier l'attribut action du formulaire au niveau de la vue "home.erb" pour qu'il pointe vers la même URL :

<form action="/" method ="post" accept-charset="utf-8">
<input type="text" id="phrase" name="phrase" value="Write something...">
 <input type="submit" value="...and reverse it!">
</form>

Et comme pour l'instant le résultat de notre application se présente seulement sous la forme d'une ligne de texte, on va enjoliver ça en créant une nouvelle vue "reverse.erb" qui va nous permettre d'afficher ce résultat de façon un peu plus élégante :

<h3>Here is your reversed text......</h3>
<p><strong><%= @reversed_text %></strong></p>

Il suffit alors de référencer cette vue dans le fichier "main.rb", dans lequel nous initialisons la variable session "@reversed_text" que nous avons utilisée dans notre nouvelle vue :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/' do
  @title = "Here's Your Reversed Text:"
  @reversed_text = params[:phrase].reverse
  erb :reverse
end

Si on relance tout et que l'on fait tout bien comme il faut, on arrive sur l'écran suivant :

reverse-8.png

Les paramètres nommés

On pourrait en rester là puisque l'application fait ce qui était prévu. Mais on peut faire mieux, comme par exemple permettre à l'utilisateur d'indiquer la phrase qu'il veut inverser directement au niveau de l'URL. OK, mais comment faire pour retrouver le texte qui a été proposé ? Pas compliqué, il faut juste ajouter une route avec un paramètre nommé :

get '/:phrase' do

Cela va ajouter automatiquement un paramètre "phrase" à la collection "params". On retrouvera donc tout ce que l'utilisateur aura indiqué dans l'URL en utilisant params[:phrase]. Par exemple, pour l'URL http://localhost:4567/frank on aura params[:phrase] égal à "frank" et pour l'URL http://localhost:4567/sinatra on aura params[:phrase] égal à "sinatra" .

On va donc compléter notre fichier "main.rb" pour lui ajouter du code destiné à gérer cette route :

require 'rubygems'
require 'sinatra'

get '/' do
  @title = "Enter Your text here"
  erb :home
end

post '/' do
  @title = "Here's Your Reversed Text:"
  @reversed_text = params[:phrase].reverse
  erb :reverse
end

get '/:phrase' do
  @title = "Here's Your Reversed Text:"
  @reversed_text = params[:phrase].reverse
  erb :reverse
end

Voyons voir si ça marche. Tout ce qu'il y a à faire (après avoir relancé le serveur pour la dernière fois), c'est d'appeler l'URL http://localhost:4567/ suivi d'une phrase de votre choix et de vérifier que ce texte s'affiche bien à l'envers, comme dans la copie d'écran ci-dessous :

reverse-9.png

Voilà c'est fini

Et ainsi se termine le tutoriel consacré à la première application Sinatra proposée par Darren Jones (@daz4126) dont ce billet constitue une traduction très très libre.

jeudi 22 juillet 2010

Installer Sinatra sous Windows 7

Ceci est une adaptation en français du tutoriel "Installing Sinatra" de Darren Jones.

Je suis tombé sur une super série de tutoriels pour apprendre à utiliser Sinatra. C'est le résultat de la bonne résolution de Daz (Darren Jones) pour l'année 2010 et ça s'appelle I Dit It My Way ou "The Sinatra Songbook Project".

C'est l'occasion pour se mettre à Sinatra et je me suis donc lancé dans l'installation des outils nécessaires à son utilisation, en suivant un des premiers tutoriels : Installing Sinatra.

Installer Ruby sur Windows

il faut commencer par installer Ruby étant donné que ce n’est pas quelque chose de déjà présent sur mon PC. C’est pas compliqué du tout. Il suffit d’utiliser RubyInstaller pour Windows (la façon la plus simple d’installer Ruby sous Windows) qui va s’occuper de tout.

  • Cliquer sur le gros bouton rouge « Download » sur la page d’accueil
  • Sélectionner la version la plus récente de RubyInstallers (soit Ruby 1.9.1-p429 à ce jour)
  • Enregistrer le fichier sur le disque dur (soit dans le répertoire C:\Temp dans mon cas)
  • Double-cliquer sur C:\Temp\rubyinstaller-1.9.1-p429.exe pour lancer l’installation
  • Le seul truc un peu important, c’est de demander à faire l’installation dans C:\Ruby (et pas C:\Ruby191 comme le propose l’installeur) et de cocher les choix « Add Ruby executables to your PATH » et « Associate .rb and .rbw files with this Ruby installation ».

L’installation est très rapide et au final on se retrouve avec un répertoire « C:\Ruby » d’une cinquantaine de méga.

Installer Sinatra sur Windows

On entre ensuite dans le vif du sujet qui consiste à installer Sinatra. Pour cela, il faut ouvrir une « Invite de commandes » et aller dans le répertoire « C:\Ruby » et lancer la commande suivante :

C:\Ruby>gem install sinatra

Ce qui donne presque aussitôt :

Successfully installed rack-1.2.1
Successfully installed sinatra-1.0
2 gems installed
Installing ri documentation for rack-1.2.1...
Installing ri documentation for sinatra-1.0...
Updating class cache with 0 classes...
Installing RDoc documentation for rack-1.2.1...
Installing RDoc documentation for sinatra-1.0...

C:\Ruby>

Ca y est, mon PC devrait contenir Ruby + Sinatra !

Créer une première application Sinatra

On va alors pouvoir tester que ça marche en codant notre toute première application Sinatra. Pour cela, il suffit d'ouvrir Notepad pour créer le fichier C:\Ruby\test.rb avec le code suivant :

require 'rubygems'
require 'sinatra'
get '/hi' do
  "I Did It My Way!"
end

Puis on demande à Ruby d’exécuter ce programme :

C:\Ruby>ruby test.rb

Ce qui donne :

== Sinatra/1.0 has taken the stage on 4567 for development with backup from WEBrick
[2010-07-22 12:07:23] INFO  WEBrick 1.3.1
[2010-07-22 12:07:23] INFO  ruby 1.9.1 (2010-07-02) [i386-mingw32]
[2010-07-22 12:07:23] INFO  WEBrick::HTTPServer#start: pid=952 port=4567

Ca marche ! Il ne reste plus qu’à lancer un des navigateurs installé sur le PC pour aller voir ce que donne l’adresse « http://localhost:4567/hi ». Et là, je retrouve bien le message attendu, à savoir « I Dit It My Way ! ».

C’est magnifique ! Tout fonctionne comme prévu. Il n'y a qu'à arrêter l'exécution du programme test.rb par un simple Ctrl-C et se préparer pour la suite des tutoriels...

== Sinatra has ended his set (crowd applauds)
[2010-07-22 12:10:43] INFO  going to shutdown ...
[2010-07-22 12:10:43] INFO  WEBrick::HTTPServer#start done.