Tutoriel Ruby on Rails

Apprendre Rails par l'exemple

Michael Hartl

Contenu

  1. Chapitre 1 De zéro au déploiement
    1. 1.1 Introduction
      1. 1.1.1 Commentaires pour les lecteurs différents
      2. 1.1.2 “Dimensionner” Rails
      3. 1.1.3 Conventions utilisées dans ce livre
    2. 1.2 Debout et au boulot
      1. 1.2.1 Environnements de développement
        1. IDEs
        2. Éditeurs de texte et lignes de commande
        3. Navigateurs
        4. Note à propos des outils
      2. 1.2.2 Ruby, RubyGems, Rails, et Git
        1. Installation de Rails (Windows)
        2. Installer Git
        3. Installer Ruby
        4. Installer RubyGems
        5. Installer Rails
      3. 1.2.3 La première application
      4. 1.2.4 Bundler
      5. 1.2.5 Le serveur rails (rails server)
      6. 1.2.6 Modèles-Vue-Contrôleur (MVC)
    3. 1.3 Contrôle de versions avec Git
      1. 1.3.1 Installation et réglages
        1. Initialisation des réglages système
        2. Initialisation des réglages du dépôt (repository)
      2. 1.3.2 Ajout et mandat de dépôt
      3. 1.3.3 Qu'est-ce que Git peut faire de bien pour vous ?
      4. 1.3.4 GitHub
      5. 1.3.5 Branch, edit, commit, merge
        1. Branch
        2. Edit
        3. Commit
        4. Merge
        5. Push
    4. 1.4 Déploiement
      1. 1.4.1 Réglages Heroku
      2. 1.4.2 Déploiement Heroku, première étape
      3. 1.4.3 Déploiement Heroku, seconde étape
      4. 1.4.4 Commandes Heroku
    5. 1.5 Conclusion
  2. Chapitre 2 Une application démo
    1. 2.1 Planifier l'application
      1. 2.1.1 Modéliser les utilisateurs
      2. 2.1.2 Modéliser les micro-messages
    2. 2.2 La ressource Utilisateurs (Users)
      1. 2.2.1 Un tour de l'utilisateur
      2. 2.2.2 MVC en action
      3. 2.2.3 Faiblesses de la ressource Utilisateurs (Users)
    3. 2.3 La ressource Micro-messages (Microposts)
      1. 2.3.1 Un petit tour du micro-message
      2. 2.3.2 Appliquer le micro aux micro-messages
      3. 2.3.3 Un utilisateur has_many micro-messages
      4. 2.3.4 Hiérarchie des héritages
      5. 2.3.5 Déployer l'application Démo
    4. 2.4 Conclusion
  3. Chapitre 3 Pages statiques courantes
    1. 3.1 Pages statiques
      1. 3.1.1 Pages HTML statiques
      2. 3.1.2 Les pages statiques avec Rails
    2. 3.2 Premiers tests
      1. 3.2.1 Outils de test
        1. Auto-test
      2. 3.2.2 TDD : Rouge, Vert, Refactor
        1. Spork
        2. Rouge
        3. Vert
        4. Refactor
    3. 3.3 Pages (un peu) dynamiques
      1. 3.3.1 Test d'un changement de titre
      2. 3.3.2 Réussir les tests de titre
      3. 3.3.3 Variables d'instance et Ruby embarqué
      4. 3.3.4 Supprimer les répétitions avec les layouts
    4. 3.4 Conclusion
    5. 3.5 Exercices
  4. Chapitre 4 Rails au goût Ruby
    1. 4.1 Motivation
      1. 4.1.1 Un helper pour le titre
      2. 4.1.2 Feuilles de styles (CSS — Cascading Style Sheets)
    2. 4.2 Chaines de caractères et méthodes
      1. 4.2.1 Commentaires
      2. 4.2.2 Chaines de caractères
        1. Impression
        2. Chaines de caractères « apostrophées »
      3. 4.2.3 Objets et passage de message
      4. 4.2.4 Définition de méthode
      5. 4.2.5 Retour à l'« helper » de titre
    3. 4.3 Autres structures de données
      1. 4.3.1 Tableaux et rangs
      2. 4.3.2 Blocs
      3. 4.3.3 Tables de hachage et symboles
      4. 4.3.4 CSS revisitées
    4. 4.4 Classes Ruby
      1. 4.4.1 Constructeurs
      2. 4.4.2 Héritages de classes
      3. 4.4.3 Modifier les classes d'origine
      4. 4.4.4 Classe de contrôleur
      5. 4.4.5 La classe utilisateur
    5. 4.5 Exercices
  5. Chapitre 5 Poursuivre la mise en page
    1. 5.1 Ajout de structure
      1. 5.1.1 Navigation du site
      2. 5.1.2 Personnalisation CSS
      3. 5.1.3 Partiels
    2. 5.2 Liens pour la mise en page
      1. 5.2.1 Test d'intégration
      2. 5.2.2 Routes Rails
      3. 5.2.3 Nommer les routes
    3. 5.3 Inscription de l'utilisateur : une première étape
      1. 5.3.1 Contrôleur Utilisateur
      2. 5.3.2 URL d'inscription
    4. 5.4 Conclusion
    5. 5.5 Exercices
  6. Chapitre 6 Modéliser et afficher les utilisateurs, partie I
    1. 6.1 Modèle utilisateur
      1. 6.1.1 Migrations de la base de données
      2. 6.1.2 Le fichier modèle
        1. Annotation des modèles
        2. Attributs accessibles
      3. 6.1.3 Créer des objets Utilisateur
      4. 6.1.4 Recherche dans les objets Utilisateurs
      5. 6.1.5 Actualisation des objets Utilisateurs
    2. 6.2 Validations utilisateur
      1. 6.2.1 Valider l'existence
      2. 6.2.2 Valider la longueur
      3. 6.2.3 Valider le format
      4. 6.2.4 Valider l'unicité
        1. L'avertissement d'unicité
    3. 6.3 Afficher les utilisateurs
      1. 6.3.1 Débuggage et environnements Rails
      2. 6.3.2 Modèle, Vue et Contrôleur Utilisateur
      3. 6.3.3 Ressource Utilisateurs
        1. Paramètres pour le déboggage
    4. 6.4 Conclusion
    5. 6.5 Exercices
  7. Chapitre 7 Modéliser et afficher les utilisateurs, partie II
    1. 7.1 Mots de passe non sécurisés
      1. 7.1.1 Valider le mot de passe
      2. 7.1.2 Migrer un mot de passe
      3. 7.1.3 Fonction de rappel dans l'Active Record
    2. 7.2 Sécuriser les mots de passe
      1. 7.2.1 Test de mot de passe sécurisé
      2. 7.2.2 Un peu de théorie sur la sécurisation des mots de passe
      3. 7.2.3 Implémenter la méthode has_password?
      4. 7.2.4 Méthode d'authentification
    3. 7.3 Meilleures vues d'utilisateurs
      1. 7.3.1 Tester la page de l'utilisateur (avec factories)
      2. 7.3.2 Un nom et un Gravatar
        1. Un « helper » de Gravatar
      3. 7.3.3 Une barre utilisateur latérale
    4. 7.4 Conclusion
      1. 7.4.1 Dépôt Git
      2. 7.4.2 Déploiement Heroku
    5. 7.5 Exercices
  8. Chapitre 8 Inscription
    1. 8.1 Formulaire d'inscription
      1. 8.1.1 Utiliser form_for
      2. 8.1.2 Le formulaire HTML
    2. 8.2 Échec de l'inscription
      1. 8.2.1 Test de l'échec
      2. 8.2.2 Un formulaire fonctionnel
      3. 8.2.3 Inscription : messages d'erreur
      4. 8.2.4 Filtrer les paramètres d'identification
    3. 8.3 Succès de l'inscription
      1. 8.3.1 Tester le succès de l'inscription
      2. 8.3.2 Le formulaire d'inscription finalisé
      3. 8.3.3 Le message « flash »
      4. 8.3.4 La première inscription
    4. 8.4 Test d'intégration RSpec
      1. 8.4.1 Tests d'intégration avec les styles
      2. 8.4.2 Un échec d'inscription ne devrait pas créer un nouvel utilisateur
      3. 8.4.3 Le succès d'une inscription devrait créer un nouvel utilisateur
    5. 8.5 Conclusion
    6. 8.6 Exercices
  9. Chapitre 9 Connexion, déconnexion
    1. 9.1 Les sessions
      1. 9.1.1 Le contrôleur de session
      2. 9.1.2 Formulaire d'identification
    2. 9.2 Échec de l'identification
      1. 9.2.1 Examen de la soumission du formulaire
      2. 9.2.2 Échec de l'identification (test et code)
    3. 9.3 Succès de l'identification
      1. 9.3.1 L'action create finalisée
      2. 9.3.2 Se souvenir de moi
      3. 9.3.3 Utilisateur courant
    4. 9.4 Déconnexion
      1. 9.4.1 Détruire la session
      2. 9.4.2 Connexion à l'inscription
      3. 9.4.3 Changement des liens de la mise en page
      4. 9.4.4 Test d'intégration de l'identification/déconnexion
    5. 9.5 Conclusion
    6. 9.6 Exercices
  10. Chapitre 10 Actualiser, afficher et supprimer des utilisateurs
    1. 10.1 Actualiser l'utilisateur
      1. 10.1.1 Formulaire de modification
      2. 10.1.2 Permettre les modifications
    2. 10.2 Protéger les pages
      1. 10.2.1 Utilisateurs identifiés requis
      2. 10.2.2 Nécessité du bon utilisateur
      3. 10.2.3 Redirection conviviale
    3. 10.3 Afficher les utilisateurs
      1. 10.3.1 Liste des utilisateurs
      2. 10.3.2 Exemples d'utilisateurs
      3. 10.3.3 Pagination
        1. Test de la pagination
      4. 10.3.4 Restructuration des partiels
    4. 10.4 Supprimer des utilisateurs
      1. 10.4.1 Utilisateurs administrateurs
        1. Révision de attr_accessible
      2. 10.4.2 L'action destroy (« supprimer »)
    5. 10.5 Conclusion
    6. 10.6 Exercices
  11. Chapitre 11 Micro-messages d'utilisateurs
    1. 11.1 Le modèle Micropost (« Micro-message »)
      1. 11.1.1 Le modèle initial
        1. Attributs accessibles
      2. 11.1.2 Associations Utilisateur/micro-messages
      3. 11.1.3 Affinements du micro-message
        1. Portée par défaut
        2. Dépendances de la suppression
      4. 11.1.4 Validations du micro-message
    2. 11.2 Afficher les micro-messages
      1. 11.2.1 Etoffement de la page de l'utilisateur
      2. 11.2.2 Exemples de micro-messages
    3. 11.3 Manipuler les micro-messages
      1. 11.3.1 Contrôle de l'accès
      2. 11.3.2 Créer des micro-messages
      3. 11.3.3 Une proto-alimentation
      4. 11.3.4 Supprimer des micro-messages
      5. 11.3.5 Test de la nouvelle page d'accueil
    4. 11.4 Conclusion
    5. 11.5 Exercices
  12. Chapitre 12 Suivi des utilisateurs
    1. 12.1 Le modèle Relation (Relationship model)
      1. 12.1.1 Un problème du modèle de données (et sa solution)
      2. 12.1.2 Associations Utilisateur/Relations
      3. 12.1.3 Validations
      4. 12.1.4 Auteurs suivis
      5. 12.1.5 Les Lecteurs
    2. 12.2 Une interface web pour les auteurs et les lecteurs
      1. 12.2.1 Exemple de donnée de suivi
      2. 12.2.2 Statistiques et formulaire de suivi
      3. 12.2.3 Pages d'auteurs suivis et de lecteurs
      4. 12.2.4 Un bouton de suivi standard
      5. 12.2.5 Un bouton fonctionnant avec Ajax
    3. 12.3 L'état de l'alimentation
      1. 12.3.1 Motivation et stratégie
      2. 12.3.2 Une première implémentation de peuplement
      3. 12.3.3 Champs d'application, sous-sélections et lambda
      4. 12.3.4 Nouvel état de l'alimentation
    4. 12.4 Conclusion
      1. 12.4.1 Extensions de l'application exemple
        1. Réponses
        2. Notification
        3. Notifications aux lecteurs
        4. Rappel du mot de passe
        5. Confirmation d'inscription
        6. Alimentation RSS
        7. REST API
        8. Recherche
      2. 12.4.2 Guide vers d'autres ressources
    5. 12.5 Exercices

Avant-propos

Ma précédente compagnie (CD Baby) fut une des premières à basculer intégralement vers Ruby on Rails, et à rebasculer aussi intégralement vers PHP (googlez-moi si vous voulez prendre la mesure du drame). On m'a tellement recommandé ce livre Michael Hartl que je n'ai pu faire autrement que de le lire. C'est ainsi que le Tutoriel Ruby on Rails m'a fait revenir à nouveau à Rails.

Bien qu'ayant parcouru de nombreux livres sur Rails, c'est ce tutoriel-là qui m'a véritablement « mis en possession » de Rails. Tout est fait ici « à la manière de Rails » — une manière qui ne m'avait jamais semblé naturelle avant que je ne lise ce livre. C'est aussi le seul ouvrage sur Rails qui met en place, d'un bout à l'autre, un Développement Dirigé par les Tests (Test-Driven Development), une approche que je savais hautement recommandée par les experts mais dont je n'avais jamais compris aussi bien la pertinence que dans ce livre. Enfin, en incluant Git, GitHub et Heroku dans les exemples de la démonstration, l'auteur vous donne vraiment le goût de ce qu'est le développement d'un projet dans la vie réelle. Et le exemples de code ne sont pas en reste.

La narration linéaire adoptée par ce tutoriel est vraiment un bon format. Personnellement, j'ai étudié Le Tutoriel Rails en trois longues journées, en faisant tous les exemples et les exercices proposés à la fin de chaque chapitre. C'est en lisant ce livre du début à la fin, sans sauter la moindre partie, qu'on en tire tout le bénéfice.

Régalez-vous !

Derek Sivers (sivers.org)
Précédemment : Fondateur de CD Baby
Actuellement : Fondateur de Thoughts Ltd.

Remerciements

Ce Tutoriel Ruby on Rails doit beaucoup à mon livre précédent sur Rails, RailsSpace, et donc à mon co-auteur Aurelius Prochazka. J'aimerais remercier Aure à la fois pour le travail qu'il a accompli sur ce précédent livre et pour son soutien pour le présent ouvrage. J'aimerais aussi remercier Debra Williams Cauley, mon éditeur pour les deux ouvrages ; aussi longtemps qu'elle jouera avec moi au baseball, je continuerai d'écrire des livres pour elle.

J'aimerais remercier une longue liste de Rubyistes qui m'ont parlé et inspiré au cours des années : David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, les gens bien de Pivotal Labs, le gang Heroku, les mecs de thoughtbot et l'équipe de GitHub. Enfin, tellement, tellement, tellement de lecteurs — beaucoup trop pour les citer tous — qui ont contribué par leur rapport de bogues et leurs suggestions durant l'écriture de ce livre, et je tiens à saluer leur aide sans laquelle ce livre ne serait pas ce qu'il est.

À propos de l'auteur

Michael Hartl est programmeur, éducateur et entrepreneur. Il est le co-auteur de RailsSpace, un tutoriel Rails publié en 2007, et a été co-fondateur et développeur en chef de Insoshi, une plateforme de réseau social populaire en Ruby on Rails. Précédement, il a enseigné la théorie et la physique informatique au California Institute of Technology (Caltech), où il a reçu le Lifetime Achievement Award for Excellence en enseignement. Michael est diplômé du Harvard College, a un Ph.D. en physique (un doctorat. NdT) de Caltech, et il est ancien élève du programme des entrepreneurs Y Combinator.

Copyright et license

Le Tutoriel Ruby on Rails : apprendre Rails par l'exemple. Copyright © 2010 par Michael Hartl. Tout le code source du Tutoriel Ruby on Rails est disponible sous la license MIT License et la licence Beerware License.

   Copyright (c) 2010 Michael Hartl

   Permission est accordée, à titre gratuit, à toute personne obtenant
   une copie de ce logiciel et la documentation associée, pour faire des 
   modification dans le logiciel sans restriction et sans limitation des
   droits d’utiliser, copier, modifier, fusionner, publier, distribuer,
   concéder sous licence, et / ou de vendre les copies du Logiciel, et à
   autoriser les personnes auxquelles le Logiciel est meublé de le faire, 
   sous réserve des conditions suivantes:

   L’avis de copyright ci-dessus et cette autorisation doit être inclus
   dans toutes les copies ou parties substantielles du Logiciel.

   LE LOGICIEL EST FOURNI «TEL QUEL», SANS GARANTIE D’AUCUNE SORTE, 
   EXPLICITE OU IMPLICITE, Y COMPRIS, MAIS SANS S’Y LIMITER, LES 
   GARANTIES DE QUALITÉ MARCHANDE, ADAPTATION À UN USAGE PARTICULIER ET
   D’ABSENCE DE CONTREFAÇON. EN AUCUN CAS LES AUTEURS OU TITULAIRES DU
   ETRE TENU RESPONSABLE DE TOUT DOMMAGE, RÉCLAMATION OU AUTRES
   RESPONSABILITÉ, SOIT DANS UNE ACTION DE CONTRAT, UN TORT OU AUTRE,
   PROVENANT DE, DE OU EN RELATION AVEC LE LOGICIEL OU L’UTILISATION OU
   DE TRANSACTIONS AUTRES LE LOGICIEL.
	
/*
 * ------------------------------------------------------------
 * "LA LICENCE BEERWARE" (Révision 42) :
 * Michael Hartl a écrit ce code. Aussi longtemps que vous
 * conservez cette note, vous pouvez faire ce que vous voulez
 * de ce travail. Si nous nous rencontrons un jour, et que vous
 * pensez que ce travail en vaut la peine, vous pourrez me
 * payer une bière en retour.
 * ------------------------------------------------------------
 */

chapitre 8 Inscription

Maintenant que nous avons un modèle User fonctionnel, il est temps d'ajouter quelques capacités dont ne peut pas se passer un site web : laisser ses utilisateurs s'inscrire — et ainsi tenir la promesse faite à la section 5.3, « Inscription de l'utilisateur : une première étape ». Nous allons utiliser un formulaire HTML pour soumettre les informations d'inscription de l'utilisateur à notre application à la section 8.1, qui serviront à créer un nouvel utilisateur et à sauver ses attributs dans la base de données à la section 8.3. Comme d'habitude, nous allons écrire des tests à mesure que nous développons, et à la section 8.4 nous utiliserons le support RSpec pour la syntaxe de la navigation web pour écrire des tests d'intégration succincts et expressifs.

Puisque nous allons créer un nouvel utilisateur dans ce chapitre, il est peut-être nécessaire de supprimer tous les utilisateurs créés à la console dans la base de données (par exemple ceux de la section 7.3.2), de telle sorte que vos résultats correspondent à ceux montrés dans ce tutoriel. Vous pouvez faire cela comme suit :

$ rake db:reset

Si vous suivez votre développement en utilisant le contrôle de version, faites une branche sujet comme d'habitude :

$ git checkout master
$ git checkout -b signing-up

8.1 Formulaire d'inscription

Souvenez-vous d'après la section 5.3.1 que nous avons déjà des tests pour la page des nouveaux utilisateurs (signup page), vu à l'origine dans l'extrait 5.26 et reproduits dans l'extrait 8.1 (comme promis à la section 7.3.1, nous sommes passés de get ’new’ à get :new parce que c'est ce que mes doigts veulent taper). De plus, nous avons vu dans l'illustration 5.10 (et à nouveau dans l'illustration 8.1) que cette page d'inscription est pour le moment vierge : donc tout à fait inapte à inscrire de nouveaux utilisateurs. Le but de cette section sera de commencer à changer ce triste état des choses en produisant la maquette de formulaire d'inscription de l'illustration 8.2.

Extrait 8.1. Les tests pour la page d'inscription des utilisateurs (vu d'abord dans l'extrait 5.26).
spec/controllers/users_controller_spec.rb
require 'spec_helper'

describe UsersController do
  render_views
  .
  .
  .
  describe "GET 'new'" do

    it "devrait réussir" do
      get :new
      response.should be_success
    end

    it "devrait avoir le bon titre" do
      get :new
      response.should have_selector("title", :content => "Inscription")
    end
  end
  .
  .
  .
end
blank_signup_page
Illustration 8.1: L'état actuel de la page d'inscription /signup(taille normale)
signup_mockup
Illustration 8.2: Une maquette de la page d'inscription. (taille normale)

8.1.1 Utiliser form_for

L'élément HTML nécessaire pour soumettre des informations à un site à distane est un formulaire, ce qui suggère qu'une bonne première étape vers la registration des utilisateurs est de faire un formulaire susceptible de recueillir leurs informations d'inscription. Nous pouvons accomplir cela en Rails avec la méthode helper form_for (formulaire_pour) ; le résultat apparait dans l'extrait 8.2 (les lecteurs familiers de Rails 2.x doivent noter que form_for utilise maintenant la syntaxe ERb « pourcent-égal » pour insérer le contenu ; c'est-à-dire qu'où Rails 2.x utilisait <% form_for ... %>, Rails 3 utilise maintenant <%= form_for ... %>).

Extrait 8.2. Un formulaire pour l'inscription du nouvel utilisateur.
app/views/users/new.html.erb
<h1>Inscription</h1>

<%= form_for(@user) do |f| %>
  <div class="field">
    <%= f.label :nom %><br />
    <%= f.text_field :nom %>
  </div>
  <div class="field">
    <%= f.label :email %><br />
    <%= f.text_field :email %>
  </div>
  <div class="field">
    <%= f.label :password %><br />
    <%= f.password_field :password %>
  </div>
  <div class="field">
    <%= f.label :password_confirmation, "Confirmation" %><br />
    <%= f.password_field :password_confirmation %>
  </div>
  <div class="actions">
    <%= f.submit "Inscription" %>
  </div>
<% end %>

Étudions ce code bout par bout. La présence du mot-clé do indique que form_for comprend un bloc (section 4.3.2), qui possède une variable, que nous avons appelée f pour « formulaire ». À l'intérieur de l'helper form_for, f est un objet qui représente un formulaire ; comme c'est souvent le cas avec les helpers Rails, nous n'avons pas besoin de connaitre tous les détails de l'implémentation, mais ce que nous avons besoin de connaitre est ce que l'objet f fait : quand appelé avec une méthode correspondant à un élément formulaire HTML — tel qu'un champ de texte (text field), un bouton radio ou un champ de mot de passe — il retourne le code pour cet élément spécifiquement conçu pour définir un attribut de l'objet @user. En d'autres termes :

<div class="field">
  <%= f.label :nom %><br />
  <%= f.text_field :nom %>
</div>

… renvoie le code HTML nécessaire pour construire dans la page un élément champ de texte (text_field) labélisé (label) susceptible de définir l'attribut nom d'un modèle User.

Pour voir cela en action, nous avons besoin de naviguer et de regarder le code HTML produit en fait par ce formulaire, mais ici nous avons un problème : la page pour le moment échoue, parce que nous n'avons pas défini la variable d'instance @user — comme toute variable d'instance non définie (section 4.2.3), @user est pour le moment nil. De façon appropriée, si vous jouez votre suite de tests maintenant, vous verrez que la page d'inscription échoue. Pour obtenir qu'elle s'affiche et rendre notre formulaire, nous devons définir une variable @user dans l'action du contrôleur correspondant à new.html.erb, c'est-à-dire l'action new dans le contrôleur Users. L'helper form_for attend que @user soit un objet User, et puisque nous créons un nouvel utilisateur nous utiliserons simplement User.new, comme dans l'extrait 8.3.

Extrait 8.3. Ajout d'une variable d'instance @user à l'action new.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def new
    @user = User.new
    @titre = "Inscription"
  end
end

Avec la variable @user ainsi définie, les tests devraient réussir à nouveau,1 et maintenant le formulaire (avec un peu de stylisation qui vient de l'extrait 8.4) apparait comme dans l'illustration 8.3.

Extrait 8.4. Une très mince quantité de CSS pour le formulaire d'inscription.
public/stylesheets/custom.css
.
.
.
div.field, div.actions {
  margin-bottom: 10px;
}
signup_form
Illustration 8.3: Le formulaire d'inscription /signup pour de nouveaux utilisateurs. (taille normale)

8.1.2 Le formulaire HTML

Comme indiqué par illustration 8.3, la page d'inscription est maintenant rendue proprement, indiquant que le code form_for dans l'extrait 8.2 produit du code HTML valide. Si vous regardez ce code HTML pour le formulaire généré (en utilisant soit Firebug ou le menu « Code source de la page » de votre navigateur), vous devriez voir le balisage tel que dans l'extrait 8.5. Bien que de nombreux détails ne soient pas utiles à notre propos, prenons un moment pour mettre en évidence les parties les plus importantes de sa structure.

Extrait 8.5. Le code HTML pour le formulaire de l'illustration 8.3.
<form action="/users" class="new_user" id="new_user" method="post">
<div style="margin:0;padding:0;display:inline">
<input nom="authenticity_token" type="hidden"
       value="rB82sI7Qw5J9J1UMILG/VQL411vH5putR+JwlxLScMQ=" />
</div>

  <div class="field">
    <label for="user_nom">Nom</label><br />
    <input id="user_nom" nom="user[nom]" size="30" type="text" />
  </div>

  <div class="field">
    <label for="user_email">Email</label><br />
    <input id="user_email" nom="user[email]" size="30" type="text" />
  </div>
  <div class="field">
    <label for="user_password">Password</label><br />
    <input id="user_password" nom="user[password]" size="30" type="password" />
  </div>

  <div class="field">
    <label for="user_password_confirmation">Confirmation</label><br />
    <input id="user_password_confirmation" nom="user[password_confirmation]"
           size="30" type="password" />
  </div>
  <div class="actions">
    <input id="user_submit" nom="commit" type="submit" value="Inscription" />
  </div>
</form>

Nous allons commencer avec la structure interne. En comparant l'extrait 8.2 avec l'extrait 8.5, nous voyons que le Ruby embarqué :

<div class="field">
  <%= f.label :nom %><br />
  <%= f.text_field :nom %>
</div>

… produit le code HTML :

<div class="field">
  <label for="user_nom">Nom</label><br />
  <input id="user_nom" nom="user[nom]" size="30" type="text" />
</div>

… et :

<div class="field">
  <%= f.label :password %><br />
  <%= f.password_field :password %>
</div>

… produit le code HTML :

<div class="field">
  <label for="user_password">Password</label><br />
  <input id="user_password" nom="user[password]" size="30" type="password" />
</div>

Comme vu dans l'illustration 8.4, les champs de saisie textuels (type="text") affichent simplement leur contenu, tandis que les champs mot de passe (type="password") masquent leur contenu pour des questions de sécurité, comme le montre l'illustration 8.4.

filled_in_form
Illustration 8.4: Un formulaire rempli, montrant la différence entre les champs text et les champs password (mot de passe). (taille normale)

Comme nous le verrons à la section 8.3, la clé pour créer un utilisateur est l'attribut spécial name dans chaque input:

<input id="user_nom" name="user[nom]" - - - />
.
.
.
<input id="user_password" name="user[password]" - - - />

Ces valeurs name permettent à Rails de construire une table d'initialisation (via la variable params initialement vue à la section 6.3.2) pour créer un utilisateur en utilisant les valeurs fournies, comme nous le verrons à la section 8.2.

Le second élément important est la balise form elle-même. Rails crée la balise form en utilisant l'objet @user : parce que chaque objet Ruby connait sa propre classe (section 4.4.1), Rails peut savoir que @user est de classe User ; plus encore, puisque @user est un nouvel utilisateur, Rails sait construire un formulaire avec la méthode post, qui est le verbe HTTP approprié pour créer un nouvel objet (Box 3.1):

<form action="/users" class="new_user" id="new_user" method="post">

Ici les attributs class et id ne sont pas très utiles ; ce qui est important c'est action="/users" et method="post". Ensemble, ils constituent les instructions pour prendre en compte une requête HTML POST à l'URL « /users ». Nous en verrons les effets au cours des deux sections à venir.

Enfin, notez le code plutôt obscur de la balise de nom « authenticity token » :

<div style="margin:0;padding:0;display:inline">
<input name="authenticity_token" type="hidden"
       value="rB82sI7Qw5J9J1UMILG/VQL411vH5putR+JwlxLScMQ=" />
</div>

Ici Rails utilise une valeur spéciale unique pour contrecarrer une attaque appelée contrefaçon (forgery) ; consultez the Stack Overflow entry on the Rails authenticity token si vous êtes intéressé par les détails de son fonctionnement et de son importance. Heureusement, Rails prend soin du problème pour nous, et la balise input est hidden (cachée) donc vous n'avez pas à vous en soucier plus que ça ; mais comme elle saute aux yeux en consultant le code HTML source, je voulais au moins vous en parler.

8.2 Échec de l'inscription

Bien que nous ayons brièvement examiné le code HTML du formulaire de l'illustration 8.3 (vu dans l'extrait 8.5), il sera encore plus compréhensible dans le contexte d'un échec d'inscription. Dans cette section, nous allons nous arranger pour soumettre un formulaire d'inscription invalide qui devra redonner la page d'inscription (cf. la maquette de l'illustration 8.5).

signup_failure_mockup
Illustration 8.5: Une maquette pour l'échec de la page d'inscription. (taille normale)

8.2.1 Tester l'échec

Souvenez-vous de la section 6.3.3 qu'ajouter resources :users au fichier routes.rb (extrait 6.26) permettait de s'assurer automatiquement que notre application Rails répondait aux URLs RESTful de la Table 6.2. En particulier, cela assurait qu'une requête POST pour /users était traitée par l'action create. Notre stratégie pour l'action create (créer) est d'utiliser la soumission du formulaire pour fabriquer un nouvel objet utilisateur à l'aide de User.new, en essayant (et échouant) de sauver cet utilisateur, et alors de retourner la page d'inscription pour une possible re-soumission. Notre tâche est d'écrire les tests pour cette action, et d'ajouter ensuite create au contrôleur Users pour le faire réussir.

Commençons par revisiter le code du formulaire d'inscription :

<form action="/users" class="new_user" id="new_user" method="post">

Comme indiqué à la section 8.1.2, ce code HTML émet une requête POST pour l'URL /users. En analogie à la méthode get, qui émet une requête GET à l'intérieur des tests, nous utilisons la méthode post pour émettre une requête POST pour l'action create. Comme nous allons le voir brièvement, create reçoit une table de hachage correspondant au type d'objet qui doit être créé ; puisque c'est un test pour l'échec de l'inscription, nous allons juste passer la table @attr avec une entrée vierge, comme montré dans l'extrait 8.6. Cela revient à visiter la page d'inscription et à cliquer sur le bouton de soumission « S'inscrire » sans avoir rempli aucun des champs de saisie du formulaire.

Extrait 8.6. Tests pour l'échec de l'inscription de l'utilisateur.
spec/controllers/users_controller_spec.rb
require 'spec_helper'

describe UsersController do
  render_views
  .
  .
  .

  describe "POST 'create'" do

    describe "échec" do

      before(:each) do
        @attr = { :nom => "", :email => "", :password => "",
                  :password_confirmation => "" }
      end

      it "ne devrait pas créer d'utilisateur" do
        lambda do
          post :create, :user => @attr
        end.should_not change(User, :count)
      end

      it "devrait avoir le bon titre" do
        post :create, :user => @attr
        response.should have_selector("title", :content => "Inscription")
      end

      it "devrait rendre la page 'new'" do
        post :create, :user => @attr
        response.should render_template('new')
      end
    end
  end
end

Les deux tests finaux sont relativement simples : nous nous assurons que le titre est correct, et puis nous vérifions qu'une inscription défectueuse renvoie bien à nouveau la page d'inscription d'un nouvel utilisateur (en utilisant la méthode RSpec render_template). Le premier test, en revanche, est un peu plus délicat.

Le but du test :

it "ne devrait pas créer d'utilisateur" do
  lambda do
    post :create, :user => @attr
  end.should_not change(User, :count)
end

… est censé vérifier que l'échec de l'action create ne crée pas d'utilisateur dans la base de données. Pour ce faire, il introduit deux éléments nouveaux. D'abord, nous utilisons la méthode RSpec change pour retourner le changement du nombre d'utilisateurs dans la base de données :

change(User, :count)

Cela diffère de la méthode Active Record count, qui retourne simplement le nombre d'enregistrements du type donné dans la base de données. Par exemple, si vous avez ré-initialisé la base de données de développement au début de ce chapitre, ce compte devrait actuellement être de 0 :

$ rails console
>> User.count
=> 0

La seconde nouvelle idée est d'enrouler l'étape post :create dans un « package » en utilisant la construction Ruby appelée un lambda2 (une fonction anonyme), qui nous permet de vérifier qu'elle ne change pas le compte de User :

lambda do
  post :create, :user => @attr
end.should_not change(User, :count)

Ce bloc lambda peut vous sembler étrange pour le moment, mais les autres exemples utilisés dans les tests à venir devraient vous rendre ce modèle plus clair.

8.2.2 Un formulaire fonctionnel

Nous devons maintenant faire réussir les tests de la section 8.2.1 avec le code de l'extrait 8.7. Cet extrait inclut une seconde utilisation de la méthode render, que nous avons vue pour la première fois dans le contexte des partiels (section 5.1.3) ; comme vous pouvez le voir, render fonctionne aussi bien dans les actions de contrôleur . Notez que nous avons pris la liberté d'introduire une structure if-else (si-sinon) qui nous permet de traiter les cas d'échec et de succès séparément en se basant sur la valeur de @user.save.

Extrait 8.7. Une action create qui peut traiter les échec d'inscription (mais pas encore les succès).
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(params[:user])
    if @user.save
      # Traite un succès d'enregistrement.
    else
      @titre = "Inscription"
      render 'new'
    end
  end
end

La meilleure façon de comprendre le fonctionnement du code de l'extrait 8.7 est de soumettre le formulaire avec des données d'inscription invalides ; le résultat apparait dans l'illustration 8.6.

signup_failure_rails_3
Illustration 8.6: Échec d'inscription avec une table params(taille normale)

Pour avoir une vision plus claire de la façon dont Rails traite la soumission, jetons un coup d'œil plus précis à la table params dans les informations de débuggage en bas de la page de l'illustration 8.6 :

--- !map:ActiveSupport::HashWithIndifferentAccess 
commit: Inscription
authenticity_token: rB82sI7Qw5J9J1UMILG/VQL411vH5puR+Jw1xL5cMQ=
action: create
controller: users
user: !map:ActiveSupport::HashWithIndifferentAccess 
  nom: Foo Bar
  password_confirmation: dude
  password: dude
  email: foo@invalid

Nous avons vu en abordant la section 6.3.2 que la table params contenait des informations sur chaque requête ; dans le cas d'une URL comme « /users/1 », la valeur de params[:id] est l'id de l'utilisateur correspondant (1 dans ce exemple). Dans le cas d'un POSTing du formulaire d'inscription, params contient plutôt une table de tableaux, une construction que nous avons vue pour la première fois à la section 4.3.3, qui introduisait la variable stratégiquement appelée params dans la session de console. L'information de débuggage ci-dessus montre que la soumission du formulaire produit une table user avec des attributs correspondant aux valeurs soumises, où les clés viennent des attributs name des balises input du formulaire (extrait 8.2) ; par exemple, la valeur de :

<input id="user_email" name="user[email]" size="30" type="text" />

… avec name "user[email]" est précisément l'attribut email de la table user.

Bien que les clés de la table apparaissent comme des chaines de caractère dans le message de débuggage, Rails utilise en interne des symboles, de telle sorte que params[:user] est la table des attributs de l'utilisateur — en fait, exactement, les attributs nécessaires comme argument pour User.new, comme nous l'avons initialement vu à la section 4.4.5 et revu dans l'extrait 8.7. Cela signifie que la ligne :

@user = User.new(params[:user])

… est équivalente à :

@user = User.new(:nom => "Foo Bar", :email => "foo@invalid",
                 :password => "dude", :password_confirmation => "dude")

C'est exactement le format nécessaire pour initialiser le modèle objet User avec les attributs donnés.

Bien entendu, instancier une telle variable a des implications dans le succès de l'inscription — comme nous le verrons à la section 8.3, une fois que @user est défini proprement, appeler @user.save est tout ce qu'il y a à faire pour achever l'enregistrement — mais cela a des conséquences même dans l'échec d'inscription considéré ici. Notez dans l'illustration 8.6 que les champs sont pré-remplis avec les données de la soumission défectueuse. C'est parce que form_for remplit automatiquement les champs avec les attributs de l'objet @user, donc, par exemple, si @user.nom vaut "Foo" alors :

<%= form_for(@user) do |f| %>
  <div class="field">
    <%= f.label :nom %><br />
    <%= f.text_field :nom %>
  </div>
  .
  .
  .

… produira le code HTML :

<form action="/users" class="new_user" id="new_user" method="post">

  <div class="field">
    <label for="user_nom">Nom</label><br />
    <input id="user_nom" name="user[nom]" size="30" type="text" value="Foo"/>
  </div>
  .
  .
  .

Ici l'attribut value de la balise input vaut "Foo", et c'est ce qui apparait dans le champ de texte.

8.2.3 Message d'erreur à l'inscription

Bien que ce ne soit pas strictement nécessaire, il est utile de retourner à l'utilisateur des messages d'erreur en cas d'échec pour lui indiquer les problèmes à régler, qui permettront de réussir son inscription. Rails fournit de tels messages en se basant sur les validations du modèle User. Par exemple, essayons de sauver un utilisateur avec une adresse mail invalide et un mot de passe trop court :

$ rails console
>> user = User.new(:nom => "Foo Bar", :email => "foo@invalid",
?>                 :password => "dude", :password_confirmation => "dude")
>> user.save
=> false
>> user.errors.full_messages
=> ["Email is invalid", "Password is too short (minimum is 6 characters)"]

Ici, l'objet errors.full_messages (que nous avons vu brièvement à la section 6.2.1) contient un tableau de messages d'erreurs.

Comme dans la console ci-dessus, l'échec de l'enregistrement de l'extrait 8.7 génère une liste de messages d'erreurs associée à l'objet @user. Pour afficher les messages dans le navigateur, nous allons rendre un partiel de messages d'erreur sur la page new (extrait 8.8).3

Extrait 8.8. Code pour afficher les messages d'erreur sur le formulaire d'inscription.
app/views/users/new.html.erb
<h1>Inscription</h1>

<%= form_for(@user) do |f| %>
  <%= render 'shared/error_messages' %>
  .
  .
  .
<% end %>

Notez ici que nous rendons (render) un partiel appelé « shared/error_messages » ; cela reflète une convention Rails courante qui définit que les partiels à utiliser par de multiples contrôleurs se trouvent dans un dossier dédié shared/ (partagé) (nous verrons cette convention pleinement remplie à la section 10.1.1). Cela signifie que nous devons créer ce nouveau dossier en même temps que le fichier du partiel _error_messages.html.erb. Le partiel lui-même est présenté dans l'extrait 8.9.

Extrait 8.9. Un partiel pour afficher les messages d'erreur à la soumissions du formulaire.
app/views/shared/_error_messages.html.erb
<% if @user.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(@user.errors.count, "erreur") %> 
        ont empêché d'enregistrer votre inscription</h2>
    <p>Merci de corriger ces champs :</p>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

Ce partiel introduit plusieurs nouvelles constructions Rails et Ruby, incluant deux méthodes pour les objets de classe Array (Tableau). Ouvrons une session de console pour voir comment elles fonctionnent.

La première méthode est count (compter), qui retourne simplement le nombre d'éléments dans l'objet :

$ rails console
>> a = [1, 2, 3]
=> [1, 2, 3]
>> a.count
=> 3

L'autre nouvelle méthode est any?, l'une des deux méthodes complémentaires :

>> [].empty?
=> true
>> [].any?
=> false
>> a.empty?
=> false
>> a.any?
=> true

Nous voyons ici que la méthode empty? (vide ?), que nous avons vue initialement à la section 4.2.3 dans le contexte des chaines de caractères, qui fonctionne aussi sur les tableaux, retourne true (vrai) si le tableau est vide et false (faux) dans le cas contraire. La méthode any? (quelque chose ?) est simplement l'opposée de la méthode empty?, retournant true s'il y a au moins un élément et false dans le cas contraire..

L'autre nouvelle idée est l'helper de texte pluralize (mettre_au_pluriel). Elle n'est pas accessible par la console, mais nous pouvons l'inclure explicitement par la module ActionView::Helpers::TextHelper :4

>> include ActionView::Helpers::TextHelper
=> Object 
>> pluralize(1, "error")
=> "1 error" 
>> pluralize(5, "error")
=> "5 errors"

Nous voyons ici que pluralize prend un entier comme argument et retourne alors le nombre avec une version plurielle adéquate de son second argument. Sous cette méthode se trouve un puissant inflecteur qui sait comment pluraliser un grand nombre de mots (qui incluent de nombreux pluriels irréguliers — en anglais par défaut.NdT) :

>> pluralize(2, "woman")
=> "2 women"
>> pluralize(3, "erratum")
=> "3 errata"

Comme résultat, le code :

<%= pluralize(@user.errors.count, "erreurs") %>

… retourne "1 erreur" ou "2 erreurs" (etc.) en fonction du nombre d'erreurs.

Notez que l'extrait 8.9 inclut l'id CSS error_explanation pour styliser les messages d'erreur (souvenez-vous que nous avons vu à la section 5.1.2 que CSS utilise le signe dièse # pour styliser les ids). De plus, sur les pages d'erreur, Rails encadre automatiquement les champs d'un div de classe CSS field_with_errors. Ces labels nous permettent alors de styliser les messages d'erreur avec le code CSS montré dans l'extrait 8.10. Comme résultat, en cas d'échec de la soumission les messages d'erreur apparaissent comme dans l'illustration 8.7. Comme les messages sont générés par les validations du modèle, ils changeront automatiquement si vous changez d'avis, disons, sur le format des adresses mail ou la longueur minimum pour les mots de passe.

Extrait 8.10. CSS pour styliser les messages d'erreur.
public/stylesheets/custom.css
.
.
.
.field_with_errors {
  margin-top: 10px;
  padding: 2px;
  background-color: red;
  display: table;
}

.field_with_errors label {
  color: #fff;
}

#error_explanation {
  width: 400px;
  border: 2px solid red;
  padding: 7px;
  padding-bottom: 12px;
  margin-bottom: 20px;
  background-color: #f0f0f0;
}

#error_explanation h2 {
  text-align: left;
  font-weight: bold;
  padding: 5px 5px 5px 15px;
  font-size: 12px;
  margin: -7px;
  background-color: #c00;
  color: #fff;
}

#error_explanation p {
  color: #333;
  margin-bottom: 0;
  padding: 5px;
}

#error_explanation ul li {
  font-size: 12px;
  list-style: square;
}
signup_error_messages
Illustration 8.7: Échec d'inscription avec les messages d'erreur. (taille normale)

8.2.4 Filtrer les paramètres d'identification

Avant de s'acheminer vers la réussite de l'inscription, il reste une chose à considérer. Vous avez peut-être noté que, même si nous nous sommes évertués à crypter le mot de passe au chapitre 7, ce mot de passe ainsi que sa confirmation apparaissent tous deux en « texte clair » (cleartext) dans l'information de débuggage. Ce n'est pas un problème en soi — dans l'extrait 6.23 nous avons vu que ces informations n'apparaissaient qu'en mode développement, donc les utilisateurs ne devraient pas les voir — mais ça peut conduire à un problème éventuel : les mots de passe peuvent aussi apparaitre de façon non cryptés dans le log file (le fichier journal) que Rails utilise pour enregistrer des informations sur l'activité de l'application. En effet, dans les versions précédentes de Rails, le fichier journal de développement, dans ce cas, contiendrait des lignes comme celles montrées dans l'extrait 8.11.

Extrait 8.11. Le fichier journal de développement, avant Rails 3, avec des mots de passe en clair.
log/development.log
Parameters: {"commit"=>"Inscription", "action"=>"create",
"authenticity_token"=>"K1HchFF8uYE8ZaQKz5DVG9vF2KGoXJu4JGp/VE3NMjA=",
"controller"=>"users", 
  "user"=>{"nom"=>"Foo Bar", "password_confirmation"=>"dude",
           "password"=>"dude", "email"=>"foo@invalid"}}

Ce serait un trou de sécurité dramatique d'enregistrer les mots de passe non cryptés dans les fichiers journaux — si quiconque avait accès à ces fichiers, il pourrait obtenir les mots de passe de tous les utilisateurs du système (bien sûr, ici l'inscription échoue, mais le problème serait exactement le même pour une soumission réussie). Ce problème était tellement courant dans les applications Rails que Rails 3 implémente à présent un nouveau comportement par défaut : tous les attributs password (mot de passe) sont filtrés automatiquement, comme le montre l'extrait 8.12. Nous voyons que la chaine "[FILTERED]" remplace le mot de passe et sa confirmation (en mode production, le fichier journal sera log/production.log, et le filtrage fonctionne de la même manière).

Extrait 8.12. Le journal de développement avec les mots de passe filtrés.
log/development.log
Parameters: {"commit"=>"Inscription", "action"=>"create",
"authenticity_token"=>"K1HchFF8uYE8ZaQKz5DVG9vF2KGoXJu4JGp/VE3NMjA=",
"controller"=>"users",
  "user"=>{"nom"=>"Foo Bar", "password_confirmation"=>"[FILTERED]",
           "password"=>"[FILTERED]", "email"=>"foo@invalid"}}

Le filtrage du mot de passe lui-même est accompli via un réglage dans le fichier de configuration application.rb (extrait 8.13).

Extrait 8.13. Filtrage des mots de passe par défaut.
config/application.rb
require File.expand_path('../boot', __FILE__)

require 'rails/all'

# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)

module SampleApp
  class Application < Rails::Application
    .
    .
    .
    # Configure sensitive parameters which will be filtered from the log file.
    config.filter_parameters += [:password]
  end
end

Si vous aviez à écrire une application Rails avec un paramètre à sécuriser qui porte une autre nom que password, vous aurez besoin de l'ajouter à la liste des paramètres filtrés. Par exemple, si vous incluez un code secret dans le processus d'inscription en ajoutant une ligne comme :

<div class="field">
  <%= f.label :code_secret %><br />
  <%= f.password_field :code_secret %>
</div>

… vous devrez alors ajouter :code_secret au fichier application.rb de cette façon :

config.filter_parameters += [:password, :code_secret]

8.3 Réussite de l'inscription

Ayant traité la soumission d'un formulaire invalide, il est temps à présent d'achever le formulaire d'inscription en enregistrant réellement un nouvel utilisateur (valide) dans la base de données. D'abord, nous essayons de sauver un utilisateur ; si l'enregistrement fonctionne, les informations de l'utilisateur sont écrites dans la base de données automatiquement, et nous pouvons alors rediriger (redirect) le navigateur pour afficher le profil de l'utilisateur (avec un sympathique message de bienvenue), comme le montre la maquette dans l'illustration 8.8. Si ça rate, nous retournons simplement au comportement développé à la section 8.2.

signup_success_mockup
Illustration 8.8: Une maquette d'une inscription réussie. (taille normale)

8.3.1 Tester la réussite

Les tests pour une inscription réussie suivent le même chemin que les tests de l'échec de l'inscription de l'extrait 8.6. Jetons un coup d'œil au résultat présenté dans l'extrait 8.14.

Extrait 8.14. Tests pour une inscription réussie.
spec/controllers/users_controller_spec.rb
require 'spec_helper'

describe UsersController do
  render_views
  .
  .
  .
  describe "POST 'create'" do
    .
    .
    .
    describe "succès" do

      before(:each) do
        @attr = { :nom => "New User", :email => "user@example.com",
                  :password => "foobar", :password_confirmation => "foobar" }
      end

      it "devrait créer un utilisateur" do
        lambda do
          post :create, :user => @attr
        end.should change(User, :count).by(1)
      end

      it "devrait rediriger vers la page d'affichage de l'utilisateur" do
        post :create, :user => @attr
        response.should redirect_to(user_path(assigns(:user)))
      end    
    end
  end
end

Comme avec les tests de l'échec de l'inscription (extrait 8.6), nous utilisons ici post :create pour atteindre l'action create avec la requête HTML « POST ». Comme dans les tests d'un échec de création de l'extrait 8.6, le premier test « enroule » la création de l'utilisateur dans un bloc lambda et utilise la méthode count pour vérifier que la base de données a changé de façon adéquate :

it "devrait créer un utilisateur" do
  lambda do
    post :create, :user => @attr
  end.should change(User, :count).by(1)
end

Ici, au lieu du should_not change(User, :count) utilisé dans le cas d'une création d'utilisateur défectueuse, nous utilisons should change(User, :count).by(1), qui attend du bloc lambda qu'il change le compte User de 1.

Le second test utilise la méthode assigns vue initialement dans l'extrait 7.17 pour vérifier que l'action create redirige le tout nouvel utilisateur vers sa page show :

it "devrait rediriger vers la page d'affichage de l'utilisateur" do
  post :create, :user => @attr
  response.should redirect_to(user_path(assigns(:user)))
end   

C'est le genre de redirection qui se produit dans presque toute réussite de soumission de formulaire sur le web, et avec la syntaxe assistée de RSpec vous n'avez rien besoin de savoir sur le code de réponse HTML sous-jacent.5 L'URL elle-même est générée en utilisant la route nommée user_path présentée dans la Table 7.1.

8.3.2 Le formulaire d'inscription final

Pour obtenir la réussite de ces tests et ainsi achever un formulaire d'inscription fonctionnel, remplissez la section ex-commentée de l'extrait 8.7 avec une redirection, comme montré dans l'extrait 8.15.

Extrait 8.15. L'action utilisateur create avec une sauvegarde et une redirection.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(params[:user])
    if @user.save
      redirect_to @user
    else
      @titre = "Inscription"
      render 'new'
    end
  end
end

Notez que nous pouvons omettre le user_path dans la redirection, en écrivant simplement redirect_to @user pour rediriger l'utilisateur vers sa page d'affichage (show ), une convention que nous avons vu précédemment avec link_to dans l'extrait 7.25. Cette syntaxe est joliment succincte, mais malheureusement RSpec ne la comprend pas, donc nous devons utiliser l'expression plus bavarde user_path(@user) dans ce cas.

8.3.3 Le message « flash »

Avant de soumettre une registration valide dans le navigateur, nous allons ajouter quelque chose de très courant dans les applications web : un message qui apparait temporairement et disparait au rechargement de la page (si ce n'est pas clair, soyez patient : un exemple concret va être présenté rapidement). La manière de Rails d'accomplir cela consiste à utiliser une variable spéciale appelée le flash, qui opère comme mémoire flash dans laquelle il consigne temporairement ses données. La variable flash est en fait une table de hachage ; vous pouvez même vous souvenir de l'exemple console de la section 4.3.3, où nous avions vu comment boucler sur une table en utilisant une table stratégiquement appelée flash. Pour résumer, essayez cette session de console :

$ rails console
>> flash = { :success => "Ça marche !", :error => "Raté… :-(" }
=> {:success=>"Ça marche !", :error => "Raté… :-("}
>> flash.each do |key, value|
?>   puts "#{key}"
?>   puts "#{value}"
>> end
success
Ça marche !
error
Raté… :-(

Nous pouvons nous arranger pour afficher le contenu du flash sur le site en l'incluant au layout de notre application, comme dans l'extrait 8.16.

Extrait 8.16. Ajout du contenu de la variable flash au layout du site.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
      .
      .
      .
      <%= render 'layouts/header' %>
      <section class="round">
        <% flash.each do |key, value| %>
          <div class="flash <%= key %>"><%= value %></div>
        <% end %>
        <%= yield %>
      </section>
      .
      .
      .
</html>

Ce code s'arrange pour insérer une balise div pour chaque élément du flash, avec une classe CSS indiquant le type du message. Par exemple, si flash[:success] = "Bienvenue dans l'Application Exemple !", alors le code :

<% flash.each do |key, value| %>
  <div class="flash <%= key %>"><%= value %></div>
<% end %> 

… produira le code HTML :6

<div class="flash success">Bienvenue dans l'Application Exemple !</div>

La raison pour laquelle nous bouclons sur toutes les paires clé/valeur possible permet d'inclure les autres types de messages flash ; par exemple, dans l'extrait 9.8 nous verrons flash[:error] utilisé pour indiquer un échec d'identification.7

Testons le bon message flash en nous assurant que le message adéquat apparait sous la clé :success (extrait 8.17).

Extrait 8.17. Un test du message flash en cas de succès de l'inscription de l'utilisateur.
spec/controllers/users_controller_spec.rb
require 'spec_helper'

describe UsersController do
  render_views
  .
  .
  .
  describe "POST 'create'" do
    .
    .

    describe "success" do
      .
      .
      .
      it "devrait avoir un message de bienvenue" do
        post :create, :user => @attr
        flash[:success].should =~ /Bienvenue dans l'Application Exemple/i
      end
    end
  end
end

Cela introduit l'opération « égale-tilde » (« =~ ») pour comparer les chaines de caractère par expression régulière (nous avons déjà parlé des expressions régulières avec le email_regex de l'extrait 6.17). Plutôt que de tester tout le message flash, nous testons simplement que le « Bienvenue dans l'Application Exemple » soit présent (notez que nous ne testons pas encore l'apparence du code HTML du message flash ; nous réglerons cela en testant la balise div à la section 8.4.3.)

Si vous avez déjà beaucoup programmé, vous devez être familier des expressions régulières, mais voilà une rapide session de console dans le cas où vous auriez besoin d'une introduction :

>> "foo bar" =~ /Foo/     # Par défaut, une Regex est sensible à la casse
=> nil
>> "foo bar" =~ /foo/
=> 0

Ici les valeurs de retour de la console peuvent sembler étranges : pour aucune correspondance, la comparaison régulière retourne nil ; pour une correspondance, elle retourne l'index (la position) dans la chaine où la correspondance commence.8 Habituellement, cependant, l'index exact n'est pas très important, puisque la comparaison est le plus souvent utilisée dans un contexte booléen : en vous souvenant de la section 4.2.3, nil est false (faux) dans un contexte booléen et tout autre valeur, même 0, est true (vrai). Ainsi, nous pouvons écrire le code ainsi :

>> success = "Bienvenue dans l'Application Exemple !"
=> "Bienvenue dans l'Application Exemple !"
>> "Chaine trouvée !" if success =~ /bienvenue dans l'application exemple/
=> nil

Il n'y a aucune correspondance trouvée ici parce que les expressions régulières sont sensibles à la casse (minuscule/MAJUSCULE) par défaut, mais nous pouvons être plus permissifs dans la recherche en utilisant /.../i qui force une recherche non sensible à la casse :

>> "Chaine trouvée !" if success =~ /bienvenue dans l'application exemple/i
=> "Chaine trouvée !"

Maintenant que nous comprenons comment fonctionne le test du message flash, nous pouvons obtenir qu'il réussisse en assignant flash[:success] dans l'action create comme dans l'extrait 8.18. Le message utilise une capitalisation différente de celle du test, mais le test réussit quand même grâce au i à la fin de l'expression régulière. De cette façon nous ne cassons pas le test si nous écrivons, par exemple, application exemple au lieu de Application Exemple.

Extrait 8.18. Ajout d'un message Flash lors de l'inscription de l'utilisateur.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def create
    @user = User.new(params[:user])
    if @user.save
      flash[:success] = "Bienvenue dans l'Application Exemple!"
      redirect_to @user
    else
      @titre = "Inscription"
      render 'new'
    end
  end
end

8.3.4 La première inscription

Nous pouvons voir le résultat de tout ce travail en inscrivant notre premier utilisateur (sous le nom « Rails Tutorial » et l'adresse mail « example@railstutorial.org »), qui affiche un message convivial au succès de l'inscription, comme montré dans l'illustration 8.9 (la jolie stylisation verte pour la classe success vient du framework CSS Blueprint de la section 4.1.2). Ensuite, en rechargeant la page d'affichage de l'utilisateur, le message Flash disparait comme promis (illustration 8.10).

signup_flash
Illustration 8.9: Le résultat d'une inscription réussie, avec le message Flash (version anglaise). (taille normale)
signup_flash_reloaded
Illustration 8.10: La page de profil sans le message Flash après le rechargement éventuel de la page (version anglaise). (taille normale)

Nous pouvons maintenant interroger notre base de données juste pour nous assurer par deux fois que le nouvel utilisateur a bien été créé :

$ rails console
>> user = User.first
=> #<User id: 1, nom: "Rails Tutorial", email: "example@railstutorial.org",
created_at: "2010-02-17 03:07:53", updated_at: "2010-02-17 03:07:53",
encrypted_password: "48aa8f4444b71f3f713d87d051819b0d44cd89f4a963949f201...",
salt: "f52924ba502d4f92a634d4f9647622ccce26205176cceca2adc...">

C'est un triomphe !

8.4 Les tests d'intégration RSpec

En principe, à ce stade, nous en avons fini avec l'inscription de l'utilisateur, mais vous avez peut-être noté que nous n'avons pas testé la structure du formulaire d'inscription, ni testé que les soumissions fonctionnent vraiment. Bien sûr, nous avons testé ces choses en voyant les pages dans notre navigateur, mais le but ultime des tests automatisés est de s'assurer qu'une fois les choses opérationnelles, elles continueront de le rester. Créer de tels tests est le but de cette section — et le résultat est plutôt sympathique.

Notre méthode de test devrait être de checker la structure HTML du formulaire (en utilisant render_views et la méthode have_selector), et c'est en effet une bonne manière de développer les vues en se laissant diriger par les tests (à cet effet, la section 8.6 propose un exercice concernant ce point). Mais je préfère ne pas tester la structure HTML détaillée des vues — je ne vois aucune raison pour laquelle nous devrions devoir savoir que Rails implémente la soumission de l'adresse mail de l'utilisateur en utilisant nom="user[email]", et en effet tout test de cette structure deviendrait obsolète si une version future de Rails changeait cette convention. Plus encore, ce serait bien d'avoir un test pour le processus entier d'inscription : la visite de la page d'inscription, le remplissage du formulaire, le clic du bouton de soumission et pour s'assurer (en cas de soumission valide) qu'un nouvel utilisateur a bien été créé dans la base de données (en mode test).

Bien que ce ne soit pas la seule manière de procéder (voir le Box 8.1), ma solution de prédilection à cette question est d'utiliser les tests RSpec d'intégration, que nous avons initialement utilisés à la section 5.2.1 pour tester les routes personnalisées (telles que « /about » pour la page « À Propos »). Dans la section sus-nommée, nous n'avons vu qu'un petit exemple du pouvoir des tests d'intégration ; en abordant cette section, nous allons voir à quel point ils peuvent être puissants.

8.4.1 Tests d'intégration sur les styles

Nous avons vu dans l'extrait 5.13 que les tests d'intégration RSpec supportent le « controller-test » (test contrôleur) – telles que les constructions du type :

get '/'
response.should have_selector('title', :content => "Accueil")

Ce n'est pas le seul type de syntaxe supporté, cependant ; les tests d'intégration RSpec supportent aussi une syntaxe hautement plus expressive en navigation web.9 Dans cette section, nous verrons comment utiliser cette syntaxe pour simuler le remplissage de formulaires en utilisant un code comme :

visit signin_path
fill_in "Nom", :with => "Exemple d'Utilisateur"
click_button

8.4.2 Un échec d'inscription ne devrait pas créer un nouvel utilisateur

Maintenant nous sommes prêts à faire un test d'intégration pour l'inscription des utilisateurs. Comme nous l'avons vu à la section 5.2.1, RSpec est fourni avec un générateur pour construire de tels specs d'intégration ; dans le cas présent, nos tests d'intégration contiendront des actions variées provoquées par les utilisateurs, donc nous appellerons ce test users (utilisateurs) :

$ rails generate integration_test users
      invoke  rspec
      create    spec/requests/users_spec.rb

Comme à la section 5.2.1, le générateur ajoute automatiquement un fichier de spec, appelé users_spec.rb.10

Nous commençons avec un échec d'inscription. Une façon simple d'obtenir un échec d'inscription est de visiter l'URL de l'inscription et de cliquer sur le bouton de soumission du formulaire sans remplir aucun champ, dont le résultat sera une page comme celle de l'illustration 8.11. À l'échec de la soumission, la réponse devrait rendre le template users/new. Si vous inspectez le code HTML en résultant, vous devriez voir quelque chose comme le balisage de l'extrait 8.19. Cela signifie que nous pouvons tester la présence des messages d'erreur en cherchant une balise div avec un identifiant (id) "error_explanation". Un test pour ces étapes est montré dans l'extrait 8.20.

blank_signup
Illustration 8.11: Le résultat de la visite de la page /signup et un simple clic sur le bouton « Inscription ». (taille normale)
Extrait 8.19. Le div d'explication de l'erreur de la page de l'illustration 8.11.
<div class="error_explanation" id="error_explanation">
  <h2>5 erreurs ont empêché votre inscription</h2>
  <p>Problème avec ces champs :</p>
  <ul>
    <li>Nom can't be blank</li>
    <li>Email can't be blank</li>
    <li>Email is invalid</li>
    <li>Password can't be blank</li>
    <li>Password is too short (minimum is 6 characters)</li>
  </ul>
</div>
Extrait 8.20. Tester l'échec de l'inscription.
spec/requests/users_spec.rb
require 'spec_helper'

describe "Users" do

  describe "une inscription" do

    describe "ratée" do

      it "ne devrait pas créer un nouvel utilisateur" do
        visit signup_path
        fill_in "Nom",          :with => ""
        fill_in "eMail",        :with => ""
        fill_in "Mot de passe",     :with => ""
        fill_in "Confirmation mot de passe", :with => ""
        click_button
        response.should render_template('users/new')
        response.should have_selector("div#error_explanation")
      end
    end
  end
end

Ici, "div#error_explanation" est une inscription inspirée du style CSS pour :

<div id="error_explanation">...</div>

Notez l'aspect naturel du langage dans l'extrait 8.20. Le seul souci est qu'il ne teste pas exactement ce que nous voulons : nous ne testons pas en réalité que l'échec de la soumission échoue pour créer un nouvel utilisateur. Pour ce faire, nous avons besoin d'enrouler les étapes de test dans un unique package, et alors de vérifier qu'il ne modifie pas le nombre d'utilisateurs. Comme nous le voyons dans l'extrait 8.6 et l'extrait 8.14, cela peut être accompli avec un bloc lambda. Dans ces cas-là, le bloc lambda contient une seule ligne, mais nous voyons dans l'extrait 8.21 qu'il peut contenir des lignes multiples aussi facilement.

Extrait 8.21. Tester l'échec de l'inscription avec un bloc lambda.
spec/requests/users_spec.rb
require 'spec_helper'

describe "Users" do

  describe "Une inscription" do

    describe "ratée" do

      it "ne devrait pas créer un nouvel utilisateur" do
        lambda do
          visit signup_path
          fill_in "Nom", :with => ""
          fill_in "eMail", :with => ""
          fill_in "Mot de passe", :with => ""
          fill_in "Confirmation mot de passe", :with => ""
          click_button
          response.should render_template('users/new')
          response.should have_selector("div#error_explanation")
        end.should_not change(User, :count)
      end
    end
  end
end

Comme dans l'extrait 8.6, ce code utilise :

should_not change(User, :count)

… pour vérifier que le code à l'intérieur du bloc lambda ne change pas la valeur de User.count (Table_des_utilisateurs.compter).

Le test d'intégration de l'extrait 8.21 « intègre » ensemble les différents éléments de Rails, incluant les modèles, les vues, les contrôleurs, les routes et les helpers. Cela fournit une vérification de bout en bout de notre machinerie d'inscription, au moins en ce qui concerne les échecs de soumission.

8.4.3 Le succès d'une inscription devrait créer un nouvel utilisateur

Nous en arrivons au test d'intégration pour la réussite d'une inscription. Dans ce cas, nous avons besoin de remplir les champs d'inscription avec des données d'utilisateur valides. Un fois fait, le résultat doit rendre la page d'affichage de l'utilisateur avec une balise div contenant un message flash de réussite, et ça doit changer le nombre d'utilisateurs en l'incrémentant de 1 dans la base de données. L'extrait 8.22 montre comment réaliser cela.

Extrait 8.22. Tester la réussite de l'inscription.
spec/requests/users_spec.rb
require 'spec_helper'

describe "Users" do

  describe "Une inscription" do
    .
    .
    .
    describe "réussie" do

      it "devrait créer un nouvel utilisateurr" do
        lambda do
          visit signup_path
          fill_in "Nom", :with => "Example User"
          fill_in "eMail", :with => "user@example.com"
          fill_in "Mot de passe", :with => "foobar"
          fill_in "Confirmation mot de passe", :with => "foobar"
          click_button
          response.should have_selector("div.flash.success",
                                        :content => "Bienvenue")
          response.should render_template('users/show')
        end.should change(User, :count).by(1)
      end
    end
  end
end

En passant, bien que ce ne soit pas clair dans la documentation RSpec, nous pouvons utiliser l'id CSS de la boite de texte au lieu du label, donc fill_in :user_nom fonctionne aussi11 (c'est particulièrement bienvenue dans les formulaires n'utilisant pas les labels).

J'espère que vous êtes d'accord que la syntaxe de navigation web est incroyablement naturelle et succincte (« fill_in » signifie « remplir » et « :with » signifie « :avec ». NdT). Par exemple, pour remplir un champ avec une valeur, nous utilisons simplement du code tel que :

fill_in "Nom",         :with => "Utilisateur exemple"
fill_in "eMail", :with => "user@example.com"
fill_in "Mot de passe", :with => "foobar"
fill_in "Confirmation mot de passe", :with => "foobar"

Ici, le premier argument de fill_in sont les valeurs de labels, c'est-à-dire le texte exact que l'utilisateur voit dans son navigateur ; il n'y a pas besoin de savoir quoi que ce soit sur la structure HTML sous-jacente générée par l'helper form_for de Rails.

Pour finir, nous en arrivons au coup de grâce — tester que la réussite de l'inscription crée bien un nouvel utilisateur dans la base de données :

it "devrait créer un nouvel utilisateur" do
  lambda do
    .
    .
    .
  end.should change(User, :count).by(1)

Comme dans l'extrait 8.21, nous enroulons le code de la réussite d'une inscription dans un bloc lambda. Dans ce cas, au lieu de nous assurer que le nombre d'utilisateurs ne change pas, nous vérifions qu'il s'incrémente bien de 1 suite à l'enregistrement de l'utilisateur créé dans la base de données de test. Le résultat est le suivant :

$ rspec spec/requests/users_spec.rb 
..


Finished in 2.14 seconds
2 examples, 0 failures

Avec ça, nos tests d'intégration de l'inscription sont achevés, et nous pouvons être confiant que si les utilisateurs ne s'inscrivent pas à notre site, ça n'est pas parce que le formulaire d'inscription est défectueux.

8.5 Conclusion

La possibilité pour les utilisateurs de s'inscrire est une étape capitale de notre application. Bien que l'application exemple n'accomplit encore rien d'utile, nous avons posé ici les bases essentielles pour tout le futur développement. Dans les deux chapitres suivants, nous achèverons deux étapes tout aussi capitales : d'abord, au chapitre 9 nous achèverons notre machinerie d'authentification en permettant aux utilisateurs de s'identifier et de se déconnecter de l'application ; ensuite, au chapitre chapitre 10 nous permettrons à tous les utilisateurs d'actualiser les informations de leur compte et permettrons aux administrateurs de supprimer des utilisateurs, cela en complétant la suite complète des actions REST de la ressource Users de la Table 6.2.

Comme d'habitude, si vous utilisez Git, vous devriez fusionner vos changements dans la branche maitresse :

$ git add .
$ git commit -m "Inscription utilisateur finie"
$ git checkout master
$ git merge signing-up

8.6 Exercises

  1. En s'inspirant de l'extrait 8.23, écrivez des tests pour vérifier la présence de chaque champ de saisie dans le formulaire d'inscription (n'oubliez pas la ligne render_views, qui est essentielle pour que cela fonctionne).
  2. Souvent, les formulaires d'inscription effaceront les champs de mot de passe pour les soumissions défectueuses, comme le montre l'illustration 8.12. Modifiez l'action create du contrôleur Users pour répliquer ce comportement. Astuce : Ré-initialisez @user.password.
  3. Le flash HTML de l'extrait 8.16 est une combinaison particulièrement laide de HTML et de ERb. Vérifiez en jouant la suite de tests que le code plus propre de l'extrait 8.24, qui utilise l'helper Rails content_tag, fonctionne aussi.
Extrait 8.23. Un template pour tester tous les champs du formulaire d'inscription.
spec/controllers/users_controller_spec.rb
require 'spec_helper'

describe UsersController do
  render_views
  .
  .
  .
  describe "GET 'new'" do
    .
    .
    .

    it "devrait avoir un champ nom" do
      get :new
      response.should have_selector("input[nom='user[nom]'][type='text']")
    end

    it "devrait avoir un champ email"

    it "devrait avoir un champ mot de passe"

    it "devrait avoir un champ confirmation du mot de passe"
  end
  .
  .
  .
end
cleared_password
Illustration 8.12: Un échec de la soumission du formulaire d'inscription avec le champ du mot de passe effacé. (taille normale)
Extrait 8.24. Le flash ERb dans le layout du site en utilisant content_tag.
app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
      .
      .
      .
      <section class="round">
        <% flash.each do |key, value| %>
          <%= content_tag(:div, value, :class => "flash #{key}") %>
        <% end %> 
        <%= yield %>
      </section>
      .
      .
      .
</html>
  1. Si vous rencontrez une erreur comme views/users/new.html.erb_spec.rb fails, supprimer ces maudits specs vues avec $ rm -rf spec/views
  2. Le nom vient du lambda-calcul, un système mathématique pour représenter les fonctions et leurs opérations. 
  3. Avant Rails 3, l'affichage des messages d'erreur se faisait par un appel magique à une méthode spéciale error_messages sur l'objet formulaire f, comme suit : <%= f.error_messages %>. Bien que souvent efficace, cette méthode magique était difficilement personnalisable, donc l'équipe Rails a décidé de recommander d'utiliser du Ruby embarqué pour afficher les erreurs à la main. 
  4. Je l'ai compris en consultant pluralize dans l'API Rails. 
  5. Dans le cas où vous seriez curieux, le code de réponse est le 302, en contraste avec la redirection « permanente » 301 exposée brièvement dans le Box 3.2
  6. Notez que la clé :success est un symbole, mais le code Ruby embarqué le convertit automatiquement en une chaine "success" avant de l'insérer dans le template. 
  7. En fait, nous utiliserons le proche relatif flash.now, mais nous différerons cette subtilité jusqu'à ce que nous en ayons besoin. 
  8. Cet indice est « offset-zéro » (décalage zéro), comme pour les listes (section 4.3.1), donc une valeur retournée valant 0 signifie que l'expression a été trouvée dans la chaine, commençant au premier caractère. 
  9. Au moment où j'écris ces lignes, cette syntaxe est disponible grâce à Webrat, qui est apparue comme une dépendance gem de rspec-rails, mais que Webrat a écrit avant l'adoption généralisée de Rack et sera peut-être supplantée par le projet Capybara. Heureusement,, Capybara est conçu comme une solution de remplacement de Webrat, donc la syntaxe devrait rester la même. 
  10. Notez le pluriel : ce n'est pas le spec User user_spec.rb, qui est un test de modèle, pas un test d'intégration. 
  11. Vous pouvez utiliser Firebug ou le « code source de la page » de votre navigateur si vous avez besoin de comprendre les ids. Ou vous pouvez noter que Rails utilise le nom de la ressource et le nom de l'attribut séparé par un tiret plat, rendant user_nom, user_email, etc.