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 11 Micro-messages d'utilisateur

Le chapitre 10 a vu l'implémentation complète des actions REST de la ressource Utilisateurs (Users), donc il est temps d'ajouter une seconde ressource : les micro-messages d'utilisateurs.1 Ce sont de courts messages associés à un utilisateur particulier, vu pour la première fois dans leur forme larvaire au chapitre 2. Dans le présent chapitre, nous allons construire une version intégrale de l'esquisse de la section 2.3 en construire le modèle de données `Micropost` (Micro-messages), en l'associant au modèle `User` à l'aide des méthodes has_many et belongs_to, et en faisant les formulaires et les partiels nécessaires pour les manipuler et les afficher. Dans le Chapite 12, nous achèverons notre mini clone de Twitter en ajoutant la notion de suivi (following) des utilisateurs pour recevoir la nourriture de leurs micro-messages.

Si vous utilisez le contrôle de version git, je vous suggère de créer une nouvelle branche sujet comme d'habitude :

$ git checkout -b user-microposts

11.1 Un modèle `Micropost`

Nous commençons la ressource Microposts en créant un modèle `Micropost`, qui comprend l'essentiel des caractéristiques des micro-messages. Le code qui suit se construit sur le travail de la section 2.3 ; comme avec le modèle de cette section, notre nouveau modèle Micropost incluera des validations de données et une association avec le modèle `User`. Contrairement à ce modèle, le modèle Micropost sera complètement testé, aura aussi un classement par défaut ainsi qu'un mécanisme de destruction automatique si l'utilisateur auteur des messages est supprimé.

11.1.1 Le modèle de base

Le modèle Micropost n'a besoin que de deux attributs : un attribut content (contenu) pour conserver le contenu du micro-message,2 et un attribut user_id (identifiant d'utilisateur) pour associer le micro-message à l'utilisateur qui l'a écrit. Comme pour le modèle `User`, (extrait 6.1), nous générons ce nouveau modèle en utilisant generate model :

$ rails generate model Micropost content:string user_id:integer

Ce code produit une nouvelle migration qui crée une table microposts dans la base de données (extrait 11.1) ; vous pouvez la comparer à la migration analogue qui a permis de créer la table users dans l'extrait 6.2.

Extrait 11.1. La migration Micropost (notez l'indexation sur l'attribut user_id.)
db/migrate/<timestamp>_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration
  def self.up
    create_table :microposts do |t|
      t.string :content
      t.integer :user_id

      t.timestamps
    end
    add_index :microposts, :user_id
  end

  def self.down
    drop_table :microposts
  end
end

Notez que, puisque nous envisageons de récupérer tous les micro-messages associés à un identifiant d'utilisateur donné, l'extrait 11.1 ajoute un index (Box 6.2) sur la colonne user_id :

add_index :microposts, :user_id

Notez aussi la ligne t.timestamps qui, comme mentionné dans la section 6.1.1) ajoute les colonnes magiques created_at (créé_le…) et updated_at (modifié_le…). Nous travaillerons avec la colonne created_at dans les sections 11.1.3 et 11.2.1.

On peut jouer la migration microposts comme d'habitude (en prenant soin de préparer la base de données de test puisque le modèle de données a changé) :

$ rake db:migrate
$ rake db:test:prepare

Le résultat est un modèle Micropost dont la structure est présentée dans l'illustration 11.1.

micropost_model
Illustration 11.1: Le modèle de données Micropost.

Attributs accessibles

Avant d'étoffer le modèle Micropost, il est tout d'abord important d'utiliser attr_accessible pour indiquer les attributs modifiable publiquement. Comme abordé dans la section 6.1.2.2 et la section 10.4.1.1, oublier de définir les attributs accessibles signifie que n'importe qui pourrait changer les attributs de n'importe quel micro-message simplement en utilisant un client en ligne de commande pour produire des requêtes malveillantes. Par exemple, un utilisateur mal intentionné pourrait modifier l'attribut user_id pour associer un micro-message à un mauvais utilisateur.

Dans le cas du modèle Micropost, il y a seulement un attribut qui a besoin d'être édité par le web, l'attribut content (extrait 11.2).

Extrait 11.2. Rendre l'attribut content (et seulement l'attribut content) accessible.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content
end

Puisque user_id n'est pas listé dans le paramètre attr_accessible, il ne peut pas être modifié par le web, parce qu'un paramètre user_id dans une assignation publique telle que :

Micropost.new(:content => "foo bar", :user_id => 17)

… sera tout simplement ignorée.

La déclaration de attr_accessible dans l'extrait 11.2 est nécessaire pour la sécurité du site, mais introduit un problème dans le modèle spec (modèle de test) par défaut (extrait 11.3).

Extrait 11.3. Le module de test spec Micropost initial.
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  before(:each) do
    @attr = {
      :content => "value for content",
      :user_id => 1
    }
  end

  it "devrait créer une nouvelle instance avec les attributs valides" do
    Micropost.create!(@attr)
  end
end

Le test réussi, mais il recèle un aspect suspect (essayez de vous l'imaginer avant de passer à la suite)

Le problème est que le bloc before(:each) dans l'extrait 11.3 assigne l'identifiant de l'utilisateur par assignation publique, ce qui est très exactement ce que le paramètre attr_accessible est censé empêcher ; en particulier, comme noté ci-dessous, la partie :user_id => 1 de l'initialisation de la table est simplement ignorée. La solution est d'éviter, pour créer un micro-message, d'utiliser Micropost.new directement ; nous allons plutôt créer le nouveau micro-message à travers son association avec le modèle User, qui définit automatiquement l'identifiant de l'utilisateur (user id). Accomplir cela sera l'objet de la section suivante.

11.1.2 Association Utilisateur/Micro-message (User/Micropost)

Le but de cette section est d'établir une association entre le modèle Micropost et le modèle User —une relation entraperçue dans la section 2.3.3 et vue schématiquement dans l'illustration 11.2 et l'illustration 11.3. Chemin faisant, nous écrirons des tests pour le modèle Micropost qui, contrairement à l'extrait 11.3, sont compatibles avec l'utilisation du paramètre attr_accessible de l'extrait 11.2.

micropost_belongs_to_user
Illustration 11.2: La relation belongs_to (appartient_à) entre le micro-message et son auteur. (taille normale)
user_has_many_microposts
Illustration 11.3: La relation has_many (possède_plusieurs) entre l'utilisateur et ses micro-messages. (taille normale)

Commençons les tests pour l'association du modèle Micropost. D'abord, nous voulons répliquer le test Micropost.create! vu dans l'extrait 11.3 sans l'assignement public invalide. Ensuite, nous tirons de l''illustration 11.2 qu'un objet micropost devrait posséder une méthode user. Enfin, micropost.user devrait être l'utilisateur correspondant à l'attribut user_id du micro-message. Nous pouvons exprimer ces exigences en RSpec avec le code de l'extrait 11.4.

Extrait 11.4. Test de l'association entre micro-message et utilisateur.
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  before(:each) do
    @user = Factory(:user)
    @attr = { :content => "Contenu du message" }
  end

  it "devrait créer instance de micro-message avec bons attributs" do
    @user.microposts.create!(@attr)
  end

  describe "associations avec l'utilisateur" do

    before(:each) do
      @micropost = @user.microposts.create(@attr)
    end

    it "devrait avoir un attribut user" do
      @micropost.should respond_to(:user)
    end

    it "devrait avoir le bon utilisateur associé" do
      @micropost.user_id.should == @user.id
      @micropost.user.should == @user
    end
  end
end

Notez que, plutôt que d'utiliser Micropost.create ou Micropost.create! pour créer un micro-message, l'extrait 11.4 utilise…

@user.microposts.create(@attr)

…et …

@user.microposts.create!(@attr)

Cette tournure est la façon canonique de créer un micro-message par association avec l'utilisateur (nous utilisons un utilisateur fictif parce que ces tests s'adressent au modèle Micropost, pas au modèle User). Créé de cette manière, l'objet micropost a automatiquement son attribut user_id défini avec la bonne valeur, ce qui règle le problème relevé dans la section 11.1.1.1. En particulier, le code :

  before(:each) do
    @attr = {
      :content => "value for content",
      :user_id => 1
    }
  end

  it "devrait créer une nouvelle instance avec les attributs valides" do
    Micropost.create!(@attr)
  end

… de l'extrait 11.3 est défectueux car :user_id => 1 ne fait rien quand user_id ne fait pas partie des attributs accessibles du modèle Micropost. En passant par l'association avec l'utilisateur, en revanche, le code :..

it "devrait créer une nouvelle instance avec les attributs valides" do
  @user.microposts.create!(@attr)
end

… de l'extrait 11.4 a le bon user_id par construction.

Ces méthodes create spéciales ne fonctionnent pas encore ; elles requièrent l'association has_many adéquate dans le modèle `User`. Nous différons les tests plus détaillés de cette association à la section 11.1.3 ; pour le moment, nous allons simplement tester la présence de l'attribut microposts (extrait 11.5).

Extrait 11.5. Test de l'existence de l'attribut microposts pour l'utilisateur.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "les associations au micro-message" do

    before(:each) do
      @user = User.create(@attr)
    end

    it "devrait avoir un attribut 'microposts'" do
      @user.should respond_to(:microposts)
    end
  end
end

Nous pouvons obtenir la réussite des tests des extraits extrait 11.4 et extrait 11.5 en utilisant l'association belongs_to/has_many (définies dans l'illustration 11.2 et l''illustration 11.3), par le codes de l'extrait 11.6 et l'extrait 11.7.

Extrait 11.6. Un micro-message belongs_to (appartient_à…) un utilisateur.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content

  belongs_to :user
end
Extrait 11.7. Un utilisateur has_many (possède_plusieurs…) micro-messages.
app/models/user.rb
class User < ActiveRecord::Base
  attr_accessor :password
  attr_accessible :name, :email, :password, :password_confirmation

  has_many :microposts
  .
  .
  .
end

En utilisant cette association belongs_to/has_many, Rails construit les méthodes montrées dans la table 11.1. Vous devriez comparer les entrées dans la table 11.1 avec le code de l'extrait 11.4 et l'extrait 11.5 pour être sûr de bien comprendre la nature essentielle de ces associations (il y a une méthode dans la table 11.1 que nous n'avons pas utilisé pour le moment, la méthode build (construire) ; il en sera fait bon usage dans la section 11.1.4 et spécialement la section 11.3.2.)

MéthodeObjectif
micropost.userRetourne l'objet `User` associé au micro-messages.
user.micropostsRetourne une liste (array) des micro-messages de l'utilisateur.
user.microposts.create(arg)Crée un micro-message (user_id = user.id).
user.microposts.create!(arg)Crée un micro-message (en généraant une exception — une erreur — en cas d'échec).
user.microposts.build(arg)Retourne un nouvel objet Micropost (user_id = user.id).
Table 11.1: Résumé des méthodes de l'association utilisateur/micro-message (user/micropost).

11.1.3 Affinements du micro-message

Le test de l'association has_many (possède_plusieurs…) de l'extrait 11.5 ne teste pas grand chose — il vérifie simplement l'existence de l'attribut microposts (micro-messages). Dans cette section, nous allons ajouter un ordering (classement) et une dependency (dépendance) aux micro-messages, tout en testant aussi que la valeur retournée par la méthode user.microposts est bien une liste (array) de micro-messages.

Nous allons avoir besoin de construire quelques micro-messages dans le modèle de test `User`, ce qui signifie que nous devrons faire une « usine à micro-messages » (micropost factory). Pour ce faire, nous avons besoin d'un moyen de construire une association dans Factory Girl. Heureusement, c'est très facile — nous avons juste à utiliser la méthode micropost.association de Factory Girl, comme le montre l'extrait 11.8.3

Extrait 11.8. Le fichier Factory complet, incluant une nouvelle production pour les micro-messages.
spec/factories.rb
# En utilisant le symbole ':user', nous obtenons que Factory Girl simule
# le modèle `User`.
Factory.define :user do |user|
  user.name                  "Michael Hartl"
  user.email                 "mhartl@example.com"
  user.password              "foobar"
  user.password_confirmation "foobar"
end

Factory.sequence :email do |n|
  "person-#{n}@example.com"
end

Factory.define :micropost do |micropost|
  micropost.content "Foo bar"
  micropost.association :user
end

Portée par défaut

Nous pouvons nous arranger pour que les micro-messages produits fonctionnent pour un test sur le classement des micro-messages. Par défaut, l'utilisation de user.microposts pour relever de la base de données les micro-messages de l'utilisateur ne garantit en aucune façon l'ordre de ces messages, mais (en suivant les conventions des blogs et de Twitter), nous voulons que les micro-messages sortent dans l'ordre inverse de leur création, c'est-à-dire les plus récents en premier. Pour tester ce classement, nous commençons par créer deux micro-messages comme suit :

  @mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
  @mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)

Ici nous indiquons que le second message a été créé plus récemment, une heure plus tôt (1.hour.ago = « il y a 1 heure »), et le premier message a été créé il y a un jour (1.day.ago). Notez comme l'utilisation de Girl est pratique ici : non seulement nous pouvons désigner un utilisateur en utilisant une « assignation publique » (puisque les productions de Factory by-passent — contournent — le paramètre attr_accessible), mais nous pouvons aussi renseigner l'attribut created_at manuellement, ce que Active Record ne nous laisserait jamais faire.4

La plupart des adaptateurs de bases de données (SQLite compris) retournent les micro-messages en les classant par leur identifiant (colonne id), donc nous pouvons arranger un test initial qui échouera à coup sûr, en utilisant le code de l'extrait 11.9.

Extrait 11.9. Test de l'ordre des micro-messages de l'utilisateur.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do

    before(:each) do
      @user = User.create(@attr)
      @mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
      @mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
    end

    it "devrait avoir un attribut `microposts`" do
      @user.should respond_to(:microposts)
    end

    it "devrait avoir les bons micro-messags dans le bon ordre" do
      @user.microposts.should == [@mp2, @mp1]
    end
  end
end

La clé du problème ici est :

@user.microposts.should == [@mp2, @mp1]

… qui indique que les messages doivent être bien classés, les plus récents d'abord. Cette condition devrait échouer parce que par défaut les messages seront classés par leur identifiant (id), donc : [@mp1, @mp2]. Ce test vérifie aussi la validité de l'association has_many elle-même, en s'assurant (comme indiqué dans la table 11.1) que user.microposts est bien une liste (array) de micro-messages.

Pour faire réussir le test de classement, nous utilisons une facilité de Rails appelée default_scope (portée par défaut) avec un paramètre :order (ordre), comme le montre l'extrait 11.10 (c'est notre premier exemple de la notion de portée (scope). Nous en apprendrons davantage à propos de la « portée » dans un contexte plus général au chapitre 12.)

Extrait 11.10. Classement des micro-messages avec default_scope.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  default_scope :order => 'microposts.created_at DESC'
end

L'ordre ici est "microposts.created_at DESC", où DESC signifie descendant pour SQL, c'est-à-dire « en ordre descandant du plus récent au plus vieux ».

Dépendances de la suppression

En dehors du classement adéquat, nous aimerions ajouter un second affinement aux micro-messages. Comme nous l'avons vu à la section 10.4, les administrateurs du site ont le pouvoir de suppression (destroy) des utilisateurs. Il semble raisonnable d'établir que si un utilisateur est supprimé, ses micro-messages doivent être détruits. Nous pouvons tester cela d'abord en détruisant un auteur de micro-messages, puis ensuite en vérifiant que les micro-messages associés à cet utilisateur ne se trouvent plus dans la base de données (extrait 11.11).

Extrait 11.11. Test de la destruction des micro-messages à la destruction de leur auteur.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "micropost associations" do

    before(:each) do
      @user = User.create(@attr)
      @mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
      @mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
    end
    .
    .
    .
    it "devrait détruire les micro-messages associés" do
      @user.destroy
      [@mp1, @mp2].each do |micropost|
        Micropost.find_by_id(micropost.id).should be_nil
      end
    end
  end
  .
  .
  .
end

Ici nous avons utilisé Micropost.find_by_id, qui retourne la valeur nulle (nil) si l'enregistrement n'est pas trouvé dans la base de données, tandis que Micropost.find provoque une exception (une erreur) en cas d'échec, ce qui est plus difficile à tester (si vous êtes curieux :

lambda do 
  Micropost.find(micropost.id)
end.should raise_error(ActiveRecord::RecordNotFound)

… accomplit ce test).

Le code de l'application pour que l'extrait 11.11 réussisse le test fait moins d'une ligne ; en fait, c'est juste une option à ajouter à la méthode associative has_many, comme le montre l'extrait 11.12.

Extrait 11.12. S'assurer que les micro-messages de l'utilisateur seront détruits avec lui.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :microposts, :dependent => :destroy
  .
  .
  .
end

Avec ça, le formulaire final de l'association utilisateur/micro-message est en place.

11.1.4 Validation du micro-message

Avant d'abandonner le modèle `Micropost` (Micro-message), nous allons nous occuper d'un ou deux problèmes potentiels en ajoutant des validations (en suivant l'exemple donné dans la section 2.3.2). Les deux attributs user_id et content sont requis, et content est de plus contraint de faire moins de 140 signes, ce que nous testons avec le code de l'extrait 11.13.

Extrait 11.13. Tests pour les validations du modèle Micropost.
spec/models/micropost_spec.rb
require 'spec_helper'

describe Micropost do

  before(:each) do
    @user = Factory(:user)
    @attr = { :content => "value for content" }
  end
  .
  .
  .
  describe "validations" do

    it "requiert un identifiant d'utilisateur" do
      Micropost.new(@attr).should_not be_valid
    end

    it "requiert un contenu non vide" do
      @user.microposts.build(:content => "  ").should_not be_valid
    end

    it "derait rejeter un contenu trop long" do
      @user.microposts.build(:content => "a" * 141).should_not be_valid
    end
  end
end

Cela suit de façon générale les exemples des tests de validation du modèle `User` de la section 6.2 (les tests, dans cette section 6.2 ont été répartis sur plusieurs lignes, mais vous devriez être suffisamment à l'aise maintenant avec le code RSpec pour digérer la formulation plus compacte utilisée ci-dessus).

Comme dans la section 6.2, le code de l'extrait 11.13 utilise la multiplication de chaines (string multiplication) pour tester la validation de la longueur du micro-message :

$ rails console
>> "a" * 10
=> "aaaaaaaaaa"
>> "a" * 141
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

En contraste, au lieu d'utiliser le constructeur par défaut new comme dans :

User.new(...)

… le code dans l'extrait 11.13 utilise la méthode build :

@user.microposts.build

Rappelez-vous d'après la table 11.1 que c'est par essence équivalent à Micropost.new, à la différence près que ça règle de façon automatique l'attribut user_id du micro-message à la valeur @user.id.

Les validations elles-mêmes sont franchement analogues à celles du modèle `User`, comme le montre l'extrait 11.14.

Extrait 11.14. Les validations du modèle Micropost.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  attr_accessible :content

  belongs_to :user

  validates :content, :presence => true, :length => { :maximum => 140 }
  validates :user_id, :presence => true

  default_scope :order => 'microposts.created_at DESC'
end

Nous en avons fini avec le modelage des données pour les micro-messages d'utilisateurs. Il est temps à présent de construire l'interface web.

11.2 Affichage des micro-messages

Bien que nous n'ayons pas encore de façon de créer des micro-messages par le web — cela viendra avec la section 11.3.2 — çe ne doit pas nous empêcher de les afficher (et de tester cet affichage). À l'instar de Twitter, nous planifions d'afficher ces micro-messages d'utilisateur non pas dans une page index séparée, mais plutôt dans la page de l'utilisateur elle-même (la page show), comme le présente la maquette de l'illustration 11.4. Nous commencerons avec un template ERb assez simple qui affichera un micro-message dans le profil de l'utlisateur, puis nous ajouterons les micro-messages dans le peuplement de la base exemple de la section 10.3.2 pour que nous ayons quelque chose à afficher.

user_microposts_mockup
Illustration 11.4: Maquette de la page de profil avec des micro-messages (version anglaise). (taille normale)

Comme pour la discussion sur la machinerie d'identification dans la section 9.3.2, la section 11.2.1 poussera souvent plusieurs éléments à la fois dans le tampon (stack), avant de les « éclater » un par un. Si cette dernière explication vous plonge dans la confusion, soyez patient ; il y a un paiement plutôt sympa à la section 11.2.2.

11.2.1 Etoffement de la page de l'utilisateur

Nous commençons avec un test pour l'affichage des micro-messages de l'utilisateur. Nous travaillons dans le spec du contrôleur `Users` puisque c'est le contrôleur `Users` qui contient l'action show de l'utilisateur. Notre stratégie est de créer une paire de micro-messages d'usine associés à un utilisateur, et alors de vérifier que la page possède une balise span de classe CSS « content » contenant chaque contenu de message. L'exemple RSpec en résultant apparait dans l'extrait 11.15.

Extrait 11.15. Test d'affichage des micro-messages dans la page d'affichage de l'utilisateur (show page).
spec/controllers/users_controller_spec.rb
require 'spec_helper'

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

    before(:each) do
      @user = Factory(:user)
    end
    .
    .
    .
    it "devrait afficher les micro-messages de l'utilisateur" do
      mp1 = Factory(:micropost, :user => @user, :content => "Foo bar")
      mp2 = Factory(:micropost, :user => @user, :content => "Baz quux")
      get :show, :id => @user
      response.should have_selector("span.content", :content => mp1.content)
      response.should have_selector("span.content", :content => mp2.content)
    end
  end
  .
  .
  .
end

Bien que ces tests échoueront jusqu'à ce que soit implémenté l'extrait 11.17, nous allons commencer par insérer une table de micro-messages à l'intérieur de la page de profil de l'utilisateur, comme montré dans l'extrait 11.16.5

Extrait 11.16. Ajout des micro-messages à la page d'affichage de l'utilisateur.
app/views/users/show.html.erb
<table class="profile">
  <tr>
    <td class="main">
      .
      .
      .
      <% unless @user.microposts.empty? %>
        <table class="microposts" summary="Micro-messages de l'utilisateur">
          <%= render @microposts %>
        </table>
        <%= will_paginate @microposts %>
      <% end %>
    </td>
    <td class="sidebar round">
      <strong>Nom</strong> <%= @user.nom %><br />
      <strong>URL</strong> <%= link_to user_path(@user), @user %><br />
      <strong>Messages</strong> <%= @user.microposts.count %>
    </td>
  </tr>
</table>

Nous allons nous occuper de la table microposts dans une minute, mais certaines choses sont à noter ici d'abord. Une nouvelle idée introduite est l'utilisation de empty? dans la ligne :

@user.microposts.empty?

Ce code applique la méthode empty?, déjà abordée dans le contexte des chaines de caractères (p.e. à la section 4.2.3), cette fois à une table :

$ rails console
>> [1, 2].empty?
=> false
>> [].empty?
=> true

En utilisant la clause conditionnelle unless :

<% unless @user.microposts.empty? %>

… nous nous assurons qu'une table HTML vide ne sera pas affichée quand l'utilisateur ne possède aucun micro-message.

Nous noterons aussi, de l'extrait 11.16, que nous avons déjà anticipé la pagination des micro-messages par :

<%= will_paginate @microposts %>

Si vous comparez ce code avec la ligne analogue de la page qui présente la liste des utilisateurs, de l''extrait 10.27, vuos verez que précédemment nous n'avions que :

<%= will_paginate %>

Cela fonctionnait parce que, dans le contexte du contrôleur `Users` (Utilisateurs), will_paginate présumait l'existence d'une variable d'instance qui s'appellerait @users (ce qui, comme nous l'avons vu dans la section 10.3.3, devait être de classe WillPaginate::Collection). Dans le cas présent, puisque nous sommes toujours dans le contrôleur `Users` mais que nous voulons paginer plutôt les micro-messages, nous devons passer explicitement une variable @microposts à will_paginate. Bien entendu, cela signifie que nous allons devoir définir cette varable dans l'action show de l'utilisateur (extrait 11.18 ci-dessous).

Enfin, notez que nous avons pris la liberté d'ajouter à la barre latérale du profil le compte du nombre courant de micro-messages de l'utilisateur :

<td class="sidebar round">
  <strong>Nom</strong> <%= @user.nom %><br />
  <strong>URL</strong> <%= link_to user_path(@user), @user %><br />
  <strong>Messages</strong> <%= @user.microposts.count %>
</td>

Ici, @user.microposts.count est analogue à la méthode User.count, mis à part le fait que ne sont comptés que les micro-messages appartenant à l'utilisateur donné par le biais de l'association utilisateur/micro-messages.6

Maintenant, pour la table micro-messages elle-même :

<table class="microposts" summary="Micro-messages de l'utilisateur">
  <%= render @microposts %>
</table>

Ce code est responsable de la génération de la table des micro-messages, mais vous pouvez voir qu'il ne fait que différer la création d'un partiel « micropost ». Nous avons vu à la section 10.3.4 que le code :

<%= render @users %>

… renvoie automatiquement chacun des utilisateurs de la variable @users en utilisant le partiel _user.html.erb. De la même manière, le code :

<%= render @microposts %>

… fait exactement la même chose pour les micro-messages. Cela signifie que nous devons définir le partiel _micropost.html.erb (dans le dossier micropost des vues — views), comme dans l'extrait 11.17.

Extrait 11.17. Un partiel pour afficher un unique micro-message.
app/views/microposts/_micropost.html.erb
<tr>
  <td class="micropost">
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
      Posté il y a <%= time_ago_in_words(micropost.created_at) %>.
    </span>
  </td>
</tr>

Ce code utilise la fantastique méthode time_ago_in_words de helper, dont nous verrons l'effet dans la section 11.2.2.

À ce point, malgré la définition de tous les templates Erb nécessaires, le test de l'extrait 11.15 devrait échouer par défaut de la variable @microposts. Nous pouvons le faire réussir avec l'extrait 11.18.

Extrait 11.18. Ajout d'un variable d'instance @microposts à l'action show de l'utilisateur.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def show
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(:page => params[:page])
    @titre = @user.nom
  end
end

Notez ici l'intelligence de paginate — il fonctionne même avec l'association des micro-messages, en convertissant à la volée la liste (array) en objet de classe WillPaginate::Collection.

En ajoutant en plus le code CSS de l'extrait 11.19 à notre feuille de styles custom.css,7 nous pouvons jeter un œil à notre nouvelle page de profil utilisateur de l'illustration 11.5. C'est plutôt… décevant. Bien sûr, c'est parce qu'il n'y a pour le moment aucun micro-message. Il est temps de changer ça.

Extrait 11.19. Code CSS pour les micro-messages (inclut tous les styles CSS de ce chapitre).
public/stylesheets/custom.css
.
.
.
h1.micropost {
  margin-bottom: 0.3em;
}

table.microposts {
  margin-top: 1em;
}

table.microposts tr {
  height: 70px;
}

table.microposts tr td.gravatar {
  border-top: 1px solid #ccc;
  vertical-align: top;
  width: 50px;
}

table.microposts tr td.micropost {
  border-top: 1px solid #ccc;
  vertical-align: top;
  padding-top: 10px;
}

table.microposts tr td.micropost span.timestamp {
  display: block;
  font-size: 85%;
  color: #666;
}

div.user_info img {
  padding-right: 0.1em;
}

div.user_info a {
  text-decoration: none;
}

div.user_info span.user_name {
  position: absolute;
}

div.user_info span.microposts {
  font-size: 80%;
}

form.new_micropost {
  margin-bottom: 2em;
}

form.new_micropost textarea {
  height: 4em;
  margin-bottom: 0;
}
user_profile_no_microposts
Illustration 11.5: La page de profil utilisateur avec le code pour les micro-messages — mais aucun micro-messages (version anglaise). (taille normale)

11.2.2 Exemples de micro-messages

Avec le travail des templates pour les micro-messages d'utilisateur de la section 11.2.1, le dénouement est plutôt décevant. Nous pouvons remédier à cette triste situation en ajoutant des micro-messages fictifs par le biais du « populateur » de la section 10.3.2. Ajouter des micro-messages fictifs pour tous les utilisateurs prendrait un certain temps, donc nous ne choisirons dans un premier temps que les six premiers utilisateurs8 en faisant appel à l'option :limit de la méthode User.all :9

User.all(:limit => 6)

Nous créons alors 50 micro-messages pour chacun de ces utilisateurs (suffisamment pour dépasser la limite de pagination de 30), en générant un exemple de contenu pour chaque micro-message en utilisant la méthode Lorem.sentence du très pratique gem Faker (Faker::Lorem.sentence retourne le texte lorem ipsum ; comme cela avait été noté dans le chapitre 6, lorem ipsum possède une fascinante histoire). Le résultat est le nouvel exemple de populateur de données de l'extrait 11.20.

Extrait 11.20. Ajout de micro-messages fictifs.
lib/tasks/sample_data.rake
require 'faker'

namespace :db do
  desc "Remplissage de la base de données avec des messages fictifs"
  task :populate => :environment do
    .
    .
    .
    User.all(:limit => 6).each do |user|
      50.times do
        user.microposts.create!(:content => Faker::Lorem.sentence(5))
      end
    end
  end
end

Bien entendu, pour générer les nouveaux exemples de données, nous avons à jouer la tâche Rake db:populate :

$ rake db:populate

Avec ce code, nous sommes en position de tirer les fruits des durs labeurs de notre section 11.2.1 en affichant l'information de chaque micro-message.10 L'illustration 11.6 montre la page de profil de l'utilisateur pour un premier utilisateur (identifié), tandis que l'illustration 11.7 montre le profil d'un second utilisateur. Pour finir, l'illustration 11.8 montre la seconde page de micro-messages d'un premier utilisateur, accompagné des liens de pagination en bas de l'affichage. Dans les trois cas, observez que chaque micro-message indique le temps depuis sa création (p.e., « Posté il y a 1 minute. ») ; c'est le travail de la méthode time_ago_in_words de l'extrait 11.17. Si vous patientez une ou deux minutes puis rechargez les pages, vous verrez ce texte s'actualiser automatiquement en se fondant sur le nouveau temps.

user_profile_with_microposts
Illustration 11.6: Le profil d'utilisateur (/users/1) avec des micro-messages. (taille normale)
other_profile_with_microposts
Illustration 11.7: Profil d'un autre utilisateur, lui aussi avec des micro-messages (/users/3). (taille normale)
user_profile_microposts_page_2_rails_3
Illustration 11.8: Une seconde page de micro-messages, avec les liens de pagination (/users/1?page=2). (taille normale)

11.3 Manipuler les micro-messages

En ayant fini avec la modélistaion des données et l'affichage des micro-messages par templates, nous allons nous intéresser maintenant à l'interface qui va permettre de créer ces messages par le biais du navigateur. Le résultat sera notre troisième exemple d'utilisation d'un formulataire HTML pour créer une ressource — dans ce cas, une ressource Microposts.11 Dans cette section, nous verrons aussi la première astuce pour créer un statut d'alimentation (ou état d'alimentation) — une notion que nous porterons à sa pleine réalisation au chapitre 12. Et pour finir, comme avec les utilisateurs, nous rendrons possible la destruction des messages au travers du web.

Une dérogation d'avec les conventions est à noter : l'interface de la ressource Micropost tournera principalement au travers des contrôleurs Users et Pages plutôt que par un contrôleur qui lui serait dédié. Cela signifie que la route à la ressource Microposts est exceptionnellement simple, comme le montre l'extrait 11.21. Le code de l'extrait 11.21 conduit à son tour aux routes pleinement conformes aux conventions REST montrées dans la table 11.2, qui est un petit sous-ensemble de l'ensemble complet des routes de la table 2.3. Bien sûr, cette simplicité est le signe que les techniques mises en branle ici sont plus avancée, pas moins avancées — nous sommes très loin de notre tout premier recours à un échaffaudage du chapitre 2, et nous n'avons plus besoin d'autant de complexité.

Extrait 11.21. Routes pour la ressource Microposts.
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users
  resources :sessions,   :only => [:new, :create, :destroy]
  resources :microposts, :only => [:create, :destroy]
  .
  .
  .
end
HTTP requestURLActionPurpose
POST/micropostscreateCrée un nouveau message
DELETE/microposts/1destroySupprime le message d'id 1
Table 11.2: Routes REST produites par la ressources Microposts de l'extrait 11.21.

11.3.1 Contrôle d'accès

Nous commençons notre développement de la ressource Microposts avec quelques contrôles d'accès dans le contrôleur Micropost. L'idée est simple : les deux actions create et destroy requièrent que l'utilisateur soit identifié. Le code RSpec pour tester cela apparait dans l'extrait 11.22, ce qui requiert la création du fichier spec du contrôleur Microposts (nous testerons et ajouterons une troisième protection à la section 11.3.4 — pour que seul l'auteur d'un message donné soit en mesure de le détruire).

Extrait 11.22. Tests d'accès de contrôle pour le contrôleur Microposts.
spec/controllers/microposts_controller_spec.rb
require 'spec_helper'

describe MicropostsController do
  render_views

  describe "contrôle d'accès" do

    it "devrait refuser l'accès pour  'create'" do
      post :create
      response.should redirect_to(signin_path)
    end

    it "devrait refuser l'accès pour  'destroy'" do
      delete :destroy, :id => 1
      response.should redirect_to(signin_path)
    end
  end
end

L'écriture du code de l'application nécessaire à la réussite des test de l'extrait 11.22 requiert d'abord un peu de restructuration. Rappelez-vous comment, dans la section 10.2.1, nous avons forcé l'identification requise en utilisant un filtre « passe-avant » qui appelait la méthode authenticate (extrait 10.11). À ce moment-là, nous n'avions besoin que de la méthode authenticate dans le contrôleur `Users`, mais maintenant il se trouve que nous en avons besoin aussi dans le contrôleur Microposts, donc nous allons déplacer la méthode authenticate dans l'helper Sessions, comme le montre l'extrait 11.23.12

Extrait 11.23. Déplacement de la méthode authenticate dans l'helper de session.
app/helpers/sessions_helper.rb
module SessionsHelper
  .
  .
  .
  def authenticate
    deny_access unless signed_in?
  end

  def deny_access
    store_location
    redirect_to signin_path, :notice => "Merci de vous identifier pour rejoindre cette page."
  end
  .
  .
  .
end

(Pour éviter la répétition de code, nous devons également supprimer la méthode authenticate du contrôler `Users`.)

Avec le code de l'extrait 11.23, la méthode authenticate est maintenant accessible depuis le contrôleur `Microposts`, ce qui signifie que nous pouvons restreindre l'accès aux actions create et destroy avec le filtre « passe-avant » de l'extrait 11.24 (puisque nous n'avons pas généré ce contrôleur en ligne de commande — $ rails generate controler etc. — vous devrez créer son fichier à la main).

Extrait 11.24. Ajout de l'authentification aux actions du contrôleur `Microposts`.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_filter :authenticate

  def create
  end

  def destroy
  end
end

Notez que n'avons pas restreint l'application du fichier « passe-avant » aux actions, puisque pour le moment il s'applique aux deux seules actions. Si nous étions amenés à ajouter, disons, une action index accessible même aux utilisateurs non identifiés, nous devrions spécifier les actions protégées de façon explicite :

class MicropostsController < ApplicationController
  before_filter :authenticate, :only => [:create, :destroy]

  def create
  end

  def destroy
  end
end

11.3.2 Création des micro-messages

Au chapitre 8, nous avons implémenté l'inscription des utilisateurs en créant un formulaire HTML qui générait une requête HTTP POST pour l'action create dans le contrôleur `Users`. L'implémentation de la création d'un micro-message est similaire ; la différence principale réside dans le fait que plutôt que d'utiliser une page séparée pour /microposts/new, à l'instar des conventions Twitter, nous construirons ce formulaire dans la page d'accueil elle-même (c'est-à-dire le chemin d'accès à la racine — root path/), comme le montre la maquette de l'illutration 11.9.

home_page_with_micropost_form_mockup
Illustration 11.9: Maquette de la page d'accueil avec un formulaire pour créer des micro-messages (version anglaise). (taille normale)

Dans la page d'accueil telle que nous l'avons laissée, elle apparaissait comme dans l'illustration 5.7 — c'est-à-dire qu'elle avait un gros bouton « S'inscrire ! » en son centre. Puisque la création d'un micro-message n'a de sens que dans le contexte d'un utilisateur identifié particulier, un des buts de cete section sera de produire différentes versions de la page d'accueil en fonction du statut d'identifictaion de l'utilisateur. Nous implémenterons cela dans l'extrait 11.27 ci-dessous, mais pour le moment, le seul impératif est que les tests pour l'action create du contrôleur `Microposts` devraient identifier un utilisateur (d'usine) avant d'essayer de pouvoir créer un message.

En gardant cet avertissement à l'esprit, les tests de création des micro-messages ressemblent à ceux de la création de l'utilisateur de l'extrait 8.6 et de l'extrait 8.14 ; le résultat apparait dans l'extrait 11.25.

Extrait 11.25. Tests de l'action create du contrôleur `Microposts`.
spec/controllers/microposts_controller_spec.rb
require 'spec_helper'

describe MicropostsController do
  .
  .
  . 
  describe "POST 'create'" do

    before(:each) do
      @user = test_sign_in(Factory(:user))
    end

    describe "échec" do

      before(:each) do
        @attr = { :content => "" }
      end

      it "ne devrait pas créer de micro-message" do
        lambda do
          post :create, :micropost => @attr
        end.should_not change(Micropost, :count)
      end

      it "devrait retourner la page d'accueil" do
        post :create, :micropost => @attr
        response.should render_template('pages/home')
      end
    end

    describe "succès" do

      before(:each) do
        @attr = { :content => "Lorem ipsum" }
      end

      it "devrait créer un micro-message" do
        lambda do
          post :create, :micropost => @attr
        end.should change(Micropost, :count).by(1)
      end

      it "devrait rediriger vers la page d'accueil" do
        post :create, :micropost => @attr
        response.should redirect_to(root_path)
      end

      it "devrait avoir un message flash" do
        post :create, :micropost => @attr
        flash[:success].should =~ /enregistré/i
      end
    end
  end
end

L'action create des micro-messages est similaire à celles des utilisateurs (extrait 8.15) ; la principale différence réside dans l'utilisation de l'association utilisateur/micro-message pour construire (build) le nouveau micro-message, comme le montre l'extrait 11.26.

Extrait 11.26. L'action create du contrôleur `Microposts`.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  .
  .
  .
  def create
    @micropost  = current_user.microposts.build(params[:micropost])
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_path
    else
      render 'pages/home'
    end
  end
  .
  .
  .
end

À ce stade, les tests de l'extrait 11.25 devraient tous réussir, mais bien entendu nous n'avons pas encore de formulaire pour créer le micro-message. Nous pouvons arranger cela avec l'extrait 11.27, qui produit un code HTML différent en fonction de l'identification du visiteur au site.

Extrait 11.27. Ajout de la création des micro-messages à la page d'accueil (/).
app/views/pages/home.html.erb
<% if signed_in? %>
  <table class="front" summary="Pour visiteur identifié">
    <tr>
      <td class="main">
        <h1 class="micropost">Quoi de neuf ?</h1>
        <%= render 'shared/micropost_form' %>
      </td>
      <td class="sidebar round">
        <%= render 'shared/user_info' %>
      </td>
    </tr>
  </table>
<% else %>
  <h1>Sample App</h1>

  <p>
    C'est la page d'accueil de l'application exemple du
    <a href="http://railstutorial.org/">Tutoriel Ruby on Rails</a>.
  </p>

  <%= link_to "S'inscrire !", signup_path, :class => "signup_button round" %>
<% end %>

Avoir autant de code dans chaque branche de la condition if-else est un peu fouilli, et rendre cela plus propre en utilisant les partiels est laissé comme exercice (section 11.5). Remplir les partiels indispensables de l'extrait 11.27 n'est pas un exercice, en revanche ; nous remplissons le partiel du formulaire du micro-message dans l'extrait 11.28 et la nouvelle sidebar de la page d'accueil dans l'extrait 11.29.

Extrait 11.28. Le partiel formulaire pour créer un micro-message.
app/views/shared/_micropost_form.html.erb
<%= form_for @micropost do |f| %>
  <%= render 'shared/messages_erreurs', :object => f.object %>
  <div class="field">
    <%= f.text_area :content %>
  </div>
  <div class="actions">
    <%= f.submit "Soumettre" %>
  </div>
<% end %>
Extrait 11.29. Le partiel pour la barre latérale d'information de l'utilisateur.
app/views/shared/_user_info.html.erb
<div class="user_info">
  <a href="<%= user_path(current_user) %>">
    <%= gravatar_pour current_user, :size => 30 %>
    <span class="user_name">
      <%= current_user.nom %>
    </span>
    <span class="microposts">
      <%= pluralize(current_user.message.count, "micropost") %>
    </span>
  </a>
</div>

Notez que, comme dans la barre latérale du profil (extrait 11.16), l'information sur l'utilisateur dans l'extrait 11.29 affiche le nombre total de micro-messages de l'utilisateur. Il y a une légère différence ici dans l'affichage, cependant ; dans la barre latérale du profil, Messages est un label, et afficher Messages : 1 fait parfaitement sens. Dans le cas présent, en revanche, dire « 1 messages » est grammaticalement incorrect, donc nous nous arrangeons pour afficher « 1 message » (et « 2 messages ») en utilisant la méthode helper très pratique : pluralize.

Le formulaire défini dans l'extrait 11.28 est une réplique exacte du formulaire d'inscription de l'exrait 8.2, ce qui signifie qu'il nécessite une variable d'instance @micropost. C'est accompli dans l'extrait 11.30 — mais seulement quand l'utilisateur est identifié.

Extrait 11.30. Ajout d'une variable d'instance `micropost` à l'action home.
app/controllers/pages_controller.rb
class PagesController < ApplicationController

  def home
    @titre = "Home"
    @micropost = Micropost.new if signed_in?
  end
  .
  .
  .
end

Maintenant le code HTML devrait être correct, affichant le formulaire comment dans l'illustration 11.10, et un formulaire avec une erreur de soumission comme dans l'illustration 11.11. Vous êtes invité à ce point à créer un nouveau message pour vous-même pour vérifier que tout fonctionne correctement — mais vous devriez peut-être attendre la section 11.3.3.

home_with_form
Illustration 11.10: La page d'accueil (/) avec un nouveau formulaire de micro-message. (taille normale)
home_form_errors
Illustration 11.11: La page d'accueil avec des erreurs dans le formulaire. (taille normale)

11.3.3 Une proto-alimentation

Le commentaire à la fin de la section 11.3.2 fait allusion à un problème : la page d'accueil courante n'affiche aucun micro-message. Si vous voulez, vous pouvez vérifier que ce formulaire montré dans l'illustration 11.10 fonctionne en soumettant une entrée valide et en vous rendant ensuite sur la page de profil pour voir le message, mais cela est plutôt fastidieux. Il serait bien meilleur d'avoir une alimentation des micro-messages qui inclut les propres messages de l'utilisateur, comme le présente la maquette de l'illustration 11.12 (au chapitre 12, nous généraliserons cette alimentation pour inclure les micro-messages des utilisateurs qui sont suivis par l'utilisateur courant).

proto_feed_mockup
Illustration 11.12: Maquette de la page d'accueil avec une proto-alimentation (version anglaise). (taille normale)

Puisque chaque utilisateur devrait avoir une alimentation, nous sommes naturellement conduit à une méthode feed (alimentation) dans le modèle de l'utilisateur. Éventuellement, nous testerons que l'alimentation retourne bien les messages que l'utilisateur suit, mais pour le moment, nous testerons simplement que la méthode feed inclut les micro-messages de l'utilisateur courant mais exclut les messages d'un utilisateur différent. Nous pouvons exprimer ces exigences avec le code de l'extrait 11.31.

Extrait 11.31. Tests pour l'état de la proto-alimentation.
spec/models/user_spec.rb
require 'spec_helper'

describe User do
  .
  .
  .
  describe "Association micro-messages" do

    before(:each) do
      @user = User.create(@attr)
      @mp1 = Factory(:micropost, :user => @user, :created_at => 1.day.ago)
      @mp2 = Factory(:micropost, :user => @user, :created_at => 1.hour.ago)
    end
    .
    .
    .
    describe "État de l'alimentation" do

      it "devrait avoir une methode `feed`" do
        @user.should respond_to(:feed)
      end

      it "devrait inclure les micro-messages de l'utilisateur" do
        @user.feed.include?(@mp1).should be_true
        @user.feed.include?(@mp2).should be_true
      end

      it "ne devrait pas inclure les micro-messages d'un autre utilisateur" do
        mp3 = Factory(:micropost,
                      :user => Factory(:user, :email => Factory.next(:email)))
        @user.feed.include?(mp3).should be_false
      end
    end
  end
end

Ces test introduit la méthode tableau include? (inclure ?) qui teste simplement si une liste (array) inclut un élément donné :13

$ rails console
>> a = [1, "foo", :bar]
>> a.include?("foo")
=> true
>> a.include?(:bar)
=> true
>> a.include?("baz")
=> false

Nous pouvons obtenir une alimentation appropriée (feed) de micro-messages en sélectionnant tous les micro-messages dont l'attribut user_id serait égal à l'identifiant (id) de l'utilisateur courant, ce que nous accomplissons en utilisant la méthode where sur le modèle Micropost, comme le montre l'extrait 11.32.14

Extrait 11.32. Implémentation préliminaire pour de l'état d'alimentation en micro-messages.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    # C'est un préliminaire. Cf. chapitre 12 pour l'implémentation complète.
    Micropost.where("user_id = ?", id)
  end
  .
  .
  .
end

Le point d'interrogation dans :

Micropost.where("user_id = ?", id)

… s'assure que l'identifiant (id) est proprement « échappé » avant d'être inclus dans la requête SQL sous-jacente, évitant par là-même un sérieux trou de sécurité appelé SQL injection (injection SQL) (l'attribut id est ici juste un entier, donc il n'y a aucun danger dans ce cas, mais toujours échapper les variables injectées dans les déclarations SQL est une habitude à cultiver).

Les lecteurs attentifs peuvent noter à ce point que le code dans l'extrait 11.32 revient à écrire :

def feed
  microposts
end

Nous avons utilisé plutôt le code de l'extrait 11.32 parce qu'il prépare de façon plus naturelle la généralisation du statut d'alimentation complet du chapitre 12.

Pour utiliser l'alimentation dans l'application exemple, nous ajontons une variable d'instance @feed_items pour l'alimentation (paginée) de l'utilisateur courant, comme dans l'extrait 11.33, ainsi qu'un partiel alimentation (extrait 11.34) à la page d'accueil (extrait 11.36).

Extrait 11.33. Ajout d'une variable d'instance à l'action home.
app/controllers/pages_controller.rb
class PagesController < ApplicationController

  def home
    @titre = "Home"
    if signed_in?
      @micropost = Micropost.new
      @feed_items = current_user.feed.paginate(:page => params[:page])
    end
  end
  .
  .
  .
end
Extrait 11.34. Le partiel du statut d'alimentation.
app/views/shared/_feed.html.erb
<% unless @feed_items.empty? %>
  <table class="microposts" summary="User microposts">
    <%= render :partial => 'shared/feed_item', :collection => @feed_items %>
  </table>
  <%= will_paginate @feed_items %>
<% end %>

Le partiel d'état d'alimentation ci-dessus rend chaque élément de l'alimentation à l'aide d'un partiel repéré par feed_item (_feed_item.html.erb) en utilisant le code :

<%= render :partial => 'shared/feed_item', :collection => @feed_items %>

Ici nous passons un paramètre :collection avec les éléments d'alimentation, ce qui contraint render à utiliser le partiel donné (feed_item dans ce cas) pour rendre chaque élément de la collection proposée (nous avons omis le paramètre :partial dans les rendus précédents, en écrivant par exemple : render ’shared/micropost’, mais avec un paramètre :collection cette syntaxe ne fonctionne pas). Le code du partiel de l'élément d'alimentation lui-même est présenté dans l'extrait 11.35 ; notez l'addition d'un lien de suppression au partiel de l'élément d'alimentation, suivant l'exemple de l'extrait 10.38.

Extrait 11.35. Partiel pour un simple élément d'alimentation.
app/views/shared/_feed_item.html.erb
<tr>
  <td class="gravatar">
    <%= link_to gravatar_pour(feed_item.user), feed_item.user %>
  </td>
  <td class="micropost">
    <span class="user">
      <%= link_to feed_item.user.nom, feed_item.user %>
    </span>
    <span class="content"><%= feed_item.content %></span>
    <span class="timestamp">
      Posté il y a <%= time_ago_in_words(feed_item.created_at) %>.
    </span>
  </td>
  <% if current_user?(feed_item.user) %>
  <td>
    <%= link_to "supprimer", feed_item, :method => :delete,
                                     :confirm => "Etes-vous certain ?",
                                     :title => feed_item.content %>
  </td>
  <% end %>
</tr>

Nous pouvons alors ajouter l'alimentation à la page d'accueil en rendant le partiel d'alimentation comme d'habitude (extrait 11.36). Le résultat est un affichage de l'alimentation sur la page d'accueil, comme voulu (extrait 11.13).

Extrait 11.36. Ajout d'un statut d'alimentation sur la page d'accueil.
app/views/pages/home.html.erb
<% if signed_in? %>
  <table class="front" summary="For signed-in users">
    <tr>
      <td class="main">
        <h1 class="micropost">Quoi de neuf ?</h1>
        <%= render 'shared/micropost_form' %>
        <%= render 'shared/feed' %>
      </td>
      .
      .
      .
    </tr>
  </table>

<% else %>
  .
  .
  .
<% end %>
home_with_proto_feed
Illustration 11.13: Page d'accueil (/) avec une proto-alimentation. (taille normale)

Au point où nous sommes arrivés, créer un nouveau micro-message fonctionne comme voulu, comme le montre l'illustration 11.14 (nous écrirons un test d'intégration pour cet effet à la section 11.3.5). Il y a une subtilité, cependant : à l'échec de la soumission du micro-message, la page d'accueil attend une variable d'instance @feed_items, donc les échecs de la soumission provoquent pour le moment une rupture (comme vous devriez pouvoir le constater en jouant votre suite de tests). La solution la plus simple est de supprimer entièrement l'alimentation en lui assignant une liste (array) vide, comme dans l'extrait 11.37.15

micropost_created
Illustration 11.14: Page d'accueil après la création d'un nouveau micro-message. (taille normale)
Extrait 11.37. Ajout d'un variable d'instance (vide) @feed_items à l'action create.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  .
  .
  .
  def create
    @micropost = current_user.microposts.build(params[:micropost])
    if @micropost.save
      flash[:success] = "Micropost created!"
      redirect_to root_path
    else
      @feed_items = []
      render 'pages/home'
    end
  end
  .
  .
  .
end

11.3.4 Destruction des micro-messages

La dernière pièce fonctionnelle à ajouter à la ressource `Microposts` est la possibilité pour l'utilisateur de détruire ses messages. Comme avec la suppression des utilisateurs (section 10.4.2), nous accomplissons cela avec un lien « supprimer », comme le montre la maquette de l'illustration 11.15. Contrairement à ce cas des utilisateurs, qui restreignait la suppression aux seuls administrateurs, le lien suppression fonctionnera seulement pour les micro-messages créés par l'utilisateur courant.

micropost_delete_links_mockup
Illustration 11.15: Maquette de la proto-alimentation avec le lien de suppression des micro-messages (version anglaise). (taille normale)

Notre première étape consiste à ajouter un lien de suppression au partiel de micro-message comme dans l'extrait 11.35. Le résultat apparait dans l'extrait 11.38.

Extrait 11.38. Partiel d'affichage d'un micro-message.
app/views/microposts/_micropost.html.erb
<tr>
  <td class="micropost">
    <span class="content"><%= micropost.content %></span>
    <span class="timestamp">
      Posté il y a <%= time_ago_in_words(micropost.created_at) %>.
    </span>
  </td>
  <% if current_user?(micropost.user) %>
  <td>
    <%= link_to "supprimer", micropost, :method => :delete,
                                     :confirm => "Etes-vous certain ?",
                                     :title => micropost.content %>
  </td>
  <% end %>
</tr>

Notez : en ce qui concerne la dernière version de Rails 3.0, moi et quelques autres lecteurs rencontrent quelquefois un étrange bogue, où l'assocation micropost.user n'est pas faite proprement. Le résultat est qu'appeler micropost.user peut provoquer une exception (une erreur) NoMethodError. Pour contourner ce bogue, vous pouvez remplacer la ligne :

<% if current_user?(micropost.user) %>

… par la ligne :

<% user = micropost.user rescue User.find(micropost.user_id) %>
<% if current_user?(user) %>

Quand l'appel de micropost.user provoque une exception, ce code trouve l'utilisateur en se basant sur l'attribut user_id du micro-message.

Les tests pour l'action destroy sont simplement une généralisation des tests similaires de la suppression d'utilisateur (extrait 10.40), comme le montre l'extrait 11.39.

Extrait 11.39. Tests pour l'action destroy (« détruire ») du contrôleur `Microposts`.
spec/controllers/microposts_controller_spec.rb
describe MicropostsController do
  .
  .
  .
  describe "DELETE 'destroy'" do

    describe "pour un utilisateur non auteur du message" do

      before(:each) do
        @user = Factory(:user)
        wrong_user = Factory(:user, :email => Factory.next(:email))
        test_sign_in(wrong_user)
        @micropost = Factory(:micropost, :user => @user)
      end

      it "devrait refuser la suppression du message" do
        delete :destroy, :id => @micropost
        response.should redirect_to(root_path)
      end
    end

    describe "pour l'auteur du message" do

      before(:each) do
        @user = test_sign_in(Factory(:user))
        @micropost = Factory(:micropost, :user => @user)
      end

      it "devrait détruire le micro-message" do
        lambda do 
          delete :destroy, :id => @micropost
        end.should change(Micropost, :count).by(-1)
      end
    end
  end
end

Le code de l'application est aussi analogue au cas de l'utilisateur dans l'extrait 10.41 ; la différence principale est que, plutôt que d'utiliser un filtre « passe-avant » admin_user, dans le cas des micro-messages nous utilisons un filtre « passe-avant » authorized_user qui vérifie que l'utilisateur courant est l'auteur du micro-message. Le code apparait dans l'extrait 11.40, et le résultat de la suppression du deuxième plus récent message apparait dans l'illustration 11.16.

Extrait 11.40. L'action destroy du contrôleur `Microposts`.
app/controllers/microposts_controller.rb
class MicropostsController < ApplicationController
  before_filter :authenticate, :only => [:create, :destroy]
  before_filter :authorized_user, :only => :destroy
  .
  .
  .
  def destroy
    @micropost.destroy
    redirect_back_or root_path
  end

  private

    def authorized_user
      @micropost = Micropost.find(params[:id])
      redirect_to root_path unless current_user?(@micropost.user)
    end
end
home_post_delete
Illustration 11.16: Page d'accueil de l'utilisateur après la suppression du second plus récent micro-message (version anglaise). (taille normale)

11.3.5 Test de la nouvelle page d'accueil

Avant de quitter la création et la suppresion des micro-messages, nous allons écrire un spec d'intégration RSpec pour tester que nos formulaires fonctionnent proprement. Comme dans le cas des utilisateurs (section 8.4), nous commençons par générer un spec d'intégration `microposts` (« micro-messags ») :

$ rails generate integration_test microposts

Des tests pour l'échec et la réussite de la création d'un micro-message sont présentés dans l'extrait 11.41.

Extrait 11.41. Un test d'intégration pour les micro-messages sur la page d'accueil.
spec/requests/microposts_spec.rb
require 'spec_helper'

describe "Microposts" do

  before(:each) do
    user = Factory(:user)
    visit signin_path
    fill_in :email,    :with => user.email
    fill_in :password, :with => user.password
    click_button
  end

  describe "création" do

    describe "échec" do

      it "ne devrait pas créer un nouveau micro-message" do
        lambda do
          visit root_path
          fill_in :micropost_content, :with => ""
          click_button
          response.should render_template('pages/home')
          response.should have_selector("div#error_explanation")
        end.should_not change(Micropost, :count)
      end
    end

    describe "succès" do

      it "devrait créer un nouveau micro-message" do
        content = "Lorem ipsum dolor sit amet"
        lambda do
          visit root_path
          fill_in :micropost_content, :with => content
          click_button
          response.should have_selector("span.content", :content => content)
        end.should change(Micropost, :count).by(1)
      end
    end
  end
end

Ayant fini de tester les fonctionnalités attachées aux micro-messages, nous sommes en mesure à présent d'élaborer la fonctionnalité finale de notre Application Exemple : le suivi d'utilisateur.

11.4 Conclusion

Avec l'addition de la ressource `Microposts`, nous en avons presque fini avec notre exemple d'application. Tout ce qui reste à faire est d'ajouter une « couche sociale » en permettant aux utilisateurs de se suivre les uns les autres. Nous allons apprendre comment modeler une telle relation entre utilisateurs, et voir les implications pour les statuts d'alimentation, au chapitre 12.

Avant cela, assurez-vous d'enregistrer (commit et merge) vos changements si vous utilisez le contrôle de versions Git :

$ git add .
$ git commit -m "Added user microposts"
$ git checkout master
$ git merge user-microposts

Vous pouvez aussi « pousser » l'application vers Heroku. Le modèle de données ayant changé avec l'addition de la table microposts, vous aurez aussi besoin de migrer la base de données de production :

$ git push heroku
$ heroku rake db:migrate

11.5 Exercices

Nous avons abordé suffisamment de sujets maintenant pour entrevoir une combinaison d'extensions possible à notre application. En voilà quelques-unes parmi les très nombreuses possibilités :

  1. (challenge) Ajoutez un affichage, à l'aide de JavaScript, pour afficher le décompte de 140 signes sur la page d'accueil, à mesure que l'utilisateur entre du texte.
  2. Ajoutez des tests pour le compte des micro-messages dans la barre latérale (sans oublier le traitement du pluriel).
  3. (principalement pour les designers) Modifiez le listing des micro-messages en utilisant une liste numérotée plutôt qu'une table (note : c'est la façon pour Twitter d'afficher ses statuts d'actualisation). Ajouter ensuite les styles CSS appropriés pour que l'alimentation en résultant n'ait pas l'air trop affreuse.
  4. Ajoutez des tests pour la pagination des micro-messages.
  5. Restructurez la page d'accueil pour utiliser des partiels différents pour les deux branches des conditions if et else.
  6. Écrivez un test pour vous assurer que le lien de suppression des messages n'apparaissent pas sur le messages dont l'utilisateur courant n'est pas l'auteur.
  7. Ajoutez une route « imbriquée » (nested route) pour que /users/1/microposts affiche tous les micro-messages de l'utilisateur 1 (vous aurez aussi à ajouter une action index au contrôleur `Microposts` et à créer la vue correspondante).
  8. Les très longs mots, généralement, perturbent l'affichage, comme on peut le voir dans l'illustration 11.17. Réglez ce problème en utilisant l'helper wrap défini dans l'extrait 11.42 (notez l'utilisation de la méthode raw — brut — pour empêcher Rails d'échapper le code HTML, liée à la méthode sanitize nécessaire pour prévenir les attaques de cross-site scripting).
long_word_micropost
Illustration 11.17: Déficience de l'affichage à cause d'un mot trop long. (taille normale)
Extrait 11.42. Un helper pour traiter les mots longs.
app/helpers/microposts_helper.rb
module MicropostsHelper

  def wrap(content)
    sanitize(raw(content.split.map{ |s| wrap_long_string(s) }.join(' ')))
  end

  private

    def wrap_long_string(text, max_width = 30)
      zero_width_space = "&#8203;"
      regex = /.{1,#{max_width}}/
      (text.length < max_width) ? text : 
                                  text.scan(regex).join(zero_width_space)
    end
end
  1. Techniquement, nous traitons les sessions comme une ressource dans le chapitre 9, mais elles ne sont pas sauvées dans la base de données de la même façon que les utilisateurs et les micro-messages. 
  2. L'attribut content (contenu) sera une chaine de caractères (string), mais, comme cela est noté brièvement dans la section 2.1.2, pour des champs de saisie de textes plus longs, vous devez utiliser le type de donnée text
  3. Pour en savoir plus sur les associations dans Factory Girl, y compris toutes les options possibles, voyez la Documentation Factory Girl
  4. Souvenez-vous que created_at (créé le…) et updated_at (actualisé_le…) sont des colonnes « magiques », donc toute assignation explicite sera remplacée par ces valeurs magiques. 
  5. Conformément au marquage sémantique (semantic markup), il serait probablement meilleur d'utiliser une liste numérotée (ordered list), mais dans ce cas l'alignement du texte et des images est beaucoup plus difficile qu'avec les tables. Voyez l'exercice de la section 11.5 si vous mettez un point d'honneur à utiliser la version sémantique. 
  6. Dans le cas où vous vous poseriez la question, la méthode associative count (compter) est intelligente, et exécute le décompte directement dans la base de données. En particulier, elle ne tire pas tous les micro-messages de la base pour compter ensuite le nombre d'éléments dans la liste obtenue, ce qui serait terriblement consommateur à mesure que le nombre de micro-messages grossirait. Au lieu de ça, elle demande à la base de données de compter les micro-messages possédant un identifiant d'utilisateur donné (user_id). En passant, dans l'éventualité très peu probable où dénombrer les messages serait quand même un goulot d'étranglement, vous pouvez le rendre encore plus rapide avec un counter cache
  7. De façon pratique, l'extrait 11.19 contient en fait tous les styles CSS nécessaires à ce chapitre. 
  8. (c'est-à-dire cinq utilisateurs avec un gravatar personnalisé, et un avec le gravatar par défaut) 
  9. Consultez votre journal de développement (log/development.log) si vous êtes curieux de connaitre la commande SQL que cette méthode génère. 
  10. À dessein, le texte lorem ipsum du gem Faker est aléatoire, donc les contenus des micro-messages en exemple seront différents. 
  11. Les deux autres ressources sont la ressource `Users` de la section 8.1 et la ressource `Sessions` de la section 9.1
  12. Nous avons noté dans la section 9.3.2 que les méthodes helper sont disponibles seulement dans les vues (views) par défaut, mais nous avons fait en sorte que les méthodes helper de la ressource `Sessions` soient également accessibles par les contrôleurs en ajoutant le code include SessionsHelper au contrôleur de l'application (extrait 9.11). 
  13. L'apprentissage des méthodes telles que include? est une des raisons pour laquelle, comme cela est noté dans la section 1.1.1, je recommande la lecture d'un livre de pur Ruby après avoir achevé celui-ci. 
  14. Voyez le guide Rails à la section Active Record Query Interface pour en savoir plus sur la clause where et similaire. 
  15. Malheureusement, retourner une alimentation paginée ne fonctionne pas dans ce cas. Implémentez-la et cliquez sur un lien de pagination pour comprendre pourquoi (les screencasts du tutoriel Rails couvre ce problème de façon plus approfondie.)