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 12 Suivi des utilisateurs

Dans ce chapitre, nous allons achever le cœur de l'Application Exemple en ajoutant une « couche sociale » qui permettra aux utilisateurs de suivre (et d'arrêter de suivre) d'autres utilisateurs, conduisant à une page d'accueil personnalisée affichant un état d'alimentation (statut feed) des micro-messages des utilisateurs suivis. Nous ferons aussi des vues pour afficher d'une part les utilisateurs « suiveurs » (les lecteurs) et d'autre part les utilisateurs que chaque utilisateur suit (les auteurs suivis). Nous apprendrons comment modéliser le suivi de l'utilisateur dans la section 12.1, et comment réaliser l'interface web à la section 12.2 (qui comprendra une introduction à la technique Ajax). Pour finir, nous terminerons en développant un état d'alimentation pleinement fonctionnel à la section 12.3.

Ce chapitre final contient l'étude du matériau le plus difficile du tutoriel, incluant une modélisation de données complexe et quelques ruses Ruby/SQL pour créer l'état d'alimentation. Au travers de ces exemples, nous verrons comment Rails peut manipuler et même intriquer les modèles de données, ce qui devrait vous être très utile lorsque vous développerez vos propres applications et leurs exigences particulières. Pour faciliter la transition du tutoriel au développement personnel, la section 12.4 contiendra quelques suggestions d'extensions du cœur de l'application-exemple, accompagnées de conseils à propos de ressources plus avancées.

Comme d'habitude, les utilisateurs de Git devraient créer une nouvelle branche sujet :

$ git checkout -b following-users

Les nouveaux concepts abordés dans ce chapitre étant particulièrement délicats, avant d'écrire la moindre ligne de code, nous allons nous arrêter un moment pour faire un tour d'horizon de la notion de suivi d'utilisateur. Comme dans les chapitres précédents, à ce point de départ, nous allons représenter les pages à l'aide de maquettes.1 Le flux de la page complète fonctionne comme suit : un utilisateur (ici John Calvin) part de sa page de profil (illustration 12.1) et navigue sur les pages des autres utilisateurs (illustration 12.2) pour choisir un utilisateur à suivre. John Calvin navigue sur la page de profil d'un second utilisateur, Thomas Hobbes (illustration 12.3), clique sur le bouton « Suivre » pour suivre cet utilisateur. Cela change le bouton « Suivre » en bouton « Ne plus suivre », et incrémente de 1 le nombre de « lecteurs » de Thomas Hobbes (illustration 12.4). Retournant à sa page d'accueil, John Calvin voit maintenant le nombre de ses « auteurs suivis » et trouve les micro-messages de Thomas Hobbes dans son état d'alimentation (illustration 12.5). La suite de ce chapitre est dévolu à faire fonctionner ce flux de page.

page_flow_profile_mockup
Illustration 12.1: Maquette de la page de profil de l'utilisateur courant. (taille normale)
page_flow_user_index_mockup
Illustration 12.2: Maquette de la recherche d'un utilisateur à suivre. (taille normale)
page_flow_other_profile_follow_button_mockup
Illustration 12.3: Maquette du profil d'un autre utilisateur, avec le bouton de suivi. (taille normale)
page_flow_other_profile_unfollow_button_mockup
Illustration 12.4: Maquette du profil avec un bouton pour arrêter de suivre l'utilisateur et incrémentation du nombre de lecteurs. (taille normale)
page_flow_home_page_feed_mockup
Illustration 12.5: Maquette de la page d'accueil de l'utilisateur courant, avec l'état de l'alimentation et le compteur d'auteurs suivis. (taille normale)

12.1 Le modèle de relation

Notre première étape dans l'implémentation du suivi de l'utilisateur consiste à construire un modèle de données, ce qui n'est pas aussi simple qu'il peut le paraitre. Naïvement, on pourrait penser qu'une relation has_many pourrait faire l'affaire : has_many (possède_plusieurs) auteurs suivis et un has_many lecteurs. Comme nous allons le voir, cette approche recèle un problème sérieux, et nous apprendrons comment le contourner en utilisant plutôt has_many :through (possède_plusieurs :à_travers). C'est comme si beaucoup d'idées de cette section semblaient évidentes de prime abord, mais qu'en réalité il faille un certain temps avant de pouvoir assimiler ce modèle de données assez compliqué. Si vous sentez que les choses deviennent confuses, essayez de poursuivre quand même jusqu'à la fin ; puis, reprenez cette section pour voir si les choses deviennent plus claires à la deuxième lecture.

12.1.1 Problème avec le modèle de données (et ses solutions)

En guise de première étape vers la construction d'un modèle de données pour le suivi des utilisateurs, examinons un cas typique. Par exemple, considérons un utilisateur qui en suit un autre : nous pourrions dire que, par exemple, notre John Calvin suit notre Thomas Hobbes, et donc que notre Thomas Hobbes est suivi par John Calvin, donc que John Calvin est le lecteur (follower) et que Thomas Hobbes est le suivi (followed). En utilisant les conventions de pluriels de Rails, l'ensemble de tous ces utilisateurs suivis (followed) devrait s'appeler les suivis (followeds), mais c'est incorrect grammaticalement et maladroit (NdT : tous ces problèmes ne se posent qu'en anglais ; en français, on pourra utiliser auteurs_suivis et lecteurs sans problème) ; nous allons contourner la convention et les appeler following (auteurs_suivis), de telle sorte que user.following contiendra une liste (array) des autres utilisateurs suivis. De façon similaire, l'ensemble des utilisateurs suivant un utilisateur donné sont ses « lecteurs » (followers), et en conséquence user.followers sera une liste (array) de ses lecteurs.

Cela suggère de modeler les utilisateurs suivis (following) comme dans l'illustration 12.6, avec une table following et une association has_many. Puisque user.following devrait être une liste (array) d'utilisateurs, chaque rangée de la table following devrait être un utilisateur, identifié par l'id followed_id (id_du_suivi), avec un id follower_id (id_du_suiveur) pour établir l'association.2 En plus, puisque chaque rangée est censé être un utilisateur, nous aurions besoin d'inclure les autres attributs de l'utilisateur, nom, mot de passe, etc.

naive_user_has_many_following
Illustration 12.6: Implémentation naïve du suivi d'utilisateur.

Le problème avec le modèle de données de l'illustration 12.6 est qu'il est terriblement redondant : chaque rangée contient non seulement chaque identifiant d'utilisateur suivi, mais aussi toutes ses autres informations — toute information qui se trouve déjà dans la table users. Pire encore, pour modeler les « lecteurs » (followers), nous aurions besoin d'une autre table « lecteurs » (followers). Enfin, ce modèle de données est un cauchemar à maintenir, puisque chaque fois qu'un utilisateur a changé (disons) son nom, nous avons besoin d'actualiser non seulement l'enregistrement de l'utilisateur dans la table users mais aussi toutes les rangées contenant l'utilisateur dans nos deux tables following et followers.

Notre problème ici est que nous sommes passés à côté d'une abstraction sous-jacente. Une façon de trouver l'abstraction adéquate consiste à considérer l'implémentation possible de following dans l'application web. Rappelez-vous, section 6.3.3, que l'architecture REST comprend des ressources qui sont créées et détruites. Cela nous conduit à nous poser deux questions : quand un utilisateur suit un autre utilisateur, qu'est-ce qui est créé ? Quand un utilisateur ne suit plus un autre utilisateur, qu'est-ce qui est détruit ?

Après réflexion, nous voyons que dans ces cas l'application devrait soit créer ou détruire une relation (ou une connexion3) entre deux utilisateurs. Un utilisateur, donc, « possèdes plusieurs :relations » (has_many :relationships), et possèdent plusieurs auteurs suivis (following) et plusieurs lecteurs (followers) à travers ces relations. Effectivement, l'illustration 12.6 contient déjà le plus gros de l'implémentation : puisque chaque utilisateur suivi est uniquement identifié par son followed_id, nous pourrions convertir following (les lecteurs) en une table relationships (relations), omettre les détails de l'utilisateur, et utiliser l'identifiant followed_id (id_du_suivi) pour retrouver l'utilisateur suivi dans la table users. Plus encore, en considérant la relation inverse, nous pourrions utiliser la colonne follower_id (id_du_suiveur) pour extraire une liste des lecteurs de l'utilisateur.

Pour faire une liste des auteurs suivis par un utilisateur (following), il serait possible d'extraire une liste des attributs followed_id (id_utilisateur_suivi) puis de garder chaque utilisateur associé à chaque identifiant relevé. Comme vous pouvez vous en douter cependant, Rails possède une façon de rendre cette procédure bien plus pratique ; la technique en question est connue sous le nom has_many :through (possède_plusieurs :à_travers).4 Comme nous le verrons dans la section 12.1.4, Rails nous permet de dire qu'un utilisateur donné suit plusieurs utilisateurs à travers (through) une table de relations, en utilisant le code succint suivant :

has_many :following, :through => :relationships, :source => "followed_id"

Ce code peuple automatiquement user.following (utilisateur.auteurs_suivis) avec une liste (array) des utilisateurs suivis. Un diagramme du modèle de données est présenté dans l'illustration 12.7.

user_has_many_following
Illustration 12.7: Modèle du suivi d'utilisateur à travers un modèle de relation intermédiaire. (taille normale)

Pour commencer l'implémentation, nous générons d'abord un modèle `Relationship` (Relations) comme suit :

$ rails generate model Relationship follower_id:integer followed_id:integer

Puisque nous devrons retrouver les relations grâce aux identifiants follower_id (id_lecteur) et followed_id (id_auteur_suivi), nous devrions pour l'efficacité de la recherche ajouter une indexation sur chacune de ces colonnes, comme montré dans l'extrait 12.1.

Extrait 12.1. Ajout d'indexation à la table relationships.
db/migrate/<timestamp>_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
  def self.up
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], :unique => true
  end

  def self.down
    drop_table :relationships
  end
end

L'extrait 12.1 inclut également un index composite qui force l'unicité des pairs de (follower_id, followed_id), de telle sorte que la relation entre deux utilisateurs est forcément unique :

add_index :relationships, [:follower_id, :followed_id], :unique => true

(Comparez ce code à l'unicité de l'adresse mail de l'extrait 6.22.) Comme nous le verrons en commençant la section 12.1.4, notre interface utilisateur ne permettra pas cela de se produire, mais ajouter cette unicité permet de générer une erreur si un utilisateur tente de forcer la création d'une duplication de la relation (en utilisant par exemple un outil de commande en ligne comme cURL). Nous pourrions aussi ajouter une validation d'unicité au modèle `Relationship`, mais puisque c'est toujours une erreur de créer la duplication d'une relation, l'indexation unique est suffisante dans notre cas.

Pour créer la table relationships, nous migrons la base de données et préparons le test de cette base de données comme à notre habitude :

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

Le résultat est un modèle de données `Relationship` présenté dans l'illustration 12.8.

relationship_model
Illustration 12.8: Le modèle de données `Relationship`.

Comme pour tout nouveau modèle, avant de poursuivre, nous devons définir ses attributs accessibles. Dans le cas du modèle `Relationship`, l'attribut followed_id (id_utilisateur_suivi) devrait être accessible, puisque les utilisateurs vont créer des relations par le biais du web, mais l'attribut follower_id (id_lecteur) ne doit pas l'être ; dans le cas contraire, un utilisateur mal intentionné pourrait « forcer » des lecteurs à les suivre. Le résultat apparait dans l'extrait 12.2.

Extrait 12.2. Créer une relation avec un attribut followed_id accessible (mais pas follower_id).
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id
end

12.1.2 Association utilisateur/relations

Avant d'implémenter les utilisateurs suivis (following) et les lecteurs (followers), nous avons besoin d'établir l'association entre les utilisateurs et les relations. Un utilisateur possède_plusieurs (has_many) relations, et — puisqu'une relation implique deux utilisateurs — une Relation appartient_à (belongs_to) l'utilisateur suiveur (le lecteur) et l'utilisateur suivi (l'auteur).

Comme avec les micro-messages dans la section 11.1.2, nous créerons de nouvelles relations en utilisant l'association d'utilisateur, avec un code tel que :

user.relationships.create(:followed_id => ...)

Nous commençons avec un test, montré dans l'extrait 12.3, qui construit une variable d'instance @relationships (utilisé ci-dessous) en nous assurant qu'elle puisse être enregistrée en utilisant la méthode save! (sauver!). Comme pour la méthode create! (créer!), la méthode save! entraine une erreur (une exception) grâce au point d'exclamation si l'enregistrement échoue ; comparez cela à l'utilisation de create! dans l'extrait 11.4.

Extrait 12.3. Test de la création de la Relation avec la méthode save!.
spec/models/relationship_spec.rb
require 'spec_helper'

describe Relationship do

  before(:each) do
    @follower = Factory(:user)
    @followed = Factory(:user, :email => Factory.next(:email))

    @relationship = @follower.relationships.build(:followed_id => @followed.id)
  end

  it "devrait créer une nouvelle instance en donnant des attributs valides" do
    @relationship.save!
  end
end

Nous devons également tester le modèle `User` pour vérifier l'existence de l'attribut relationships, comme dans l'extrait 12.4.

Extrait 12.4. Test de l'attribut user.relationships.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do

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

    it "devrait avoir une méthode relashionships" do
      @user.should respond_to(:relationships)
    end
  end
end

Arrivés à ce point, on pourrait s'attendre à ce que le code soit semblable à celui de la section 11.1.2, et il l'est, mais à une différence majeure près : dans le cas du modèle `Micropost` (modèle des micro-messages) nous pouvions écrire :

class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end

… et…

class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

… parce que la table microposts (table des micro-messages) possède un attribut user_id pour identifier l'utilisateur (section 11.1.1). Un identifiant utilisé de cette manière pour connecter les tables d'une base de données est connu sous le nom de clé étrangère (foreign key), et puisque la clé étrangère pour le modèle `User` est user_id, Rails peut déduire l'association automatiquement : par défaut, Rails attend une clé étrangère de la forme <class>_id, où <class> est la version minuscule du nom de la classe (donc la classe User implique la clé étrangère user_id).5 Dans le cas présent, bien que nous traitions encore les utilisateurs, ils sont identifiés maintenant avec la clé étrangère follower_id (id_utilisateur_lecteur), donc nous devons le dire explicitement à Rails, comme le montre l'extrait 12.5.6

Extrait 12.5. Implémentation de l'association has_many de la relation utilisateur/relations.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :microposts, :dependent => :destroy
  has_many :relationships, :foreign_key => "follower_id",
                           :dependent => :destroy
  .
  .
  .
end

(Puisque détruire un utilisateur devrait aussi détruire ses relations, nous prenons de l'avance et ajoutons :dependent => :destroy à l'association ; l'écriture d'un test pour ce point est laissé en exercice (section 12.5).) À ce stade, les tests d'association de l'extrait 12.3 et l'extrait 12.4 devraient réussir.

À l'instar du modèle `Micropost`, le modèle `Relationship` possède avec les utilisateurs une relation belongs_to (appartient_à…) ; dans ce cas, un objet `relationship` appartient d'une part à un utilisateur-lecteur (follower) et d'autre part à un utilisateur-auteur (followed), ce que nous testons dans l'extrait 12.6.

Extrait 12.6. Test de l'association belongs_to.
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "Méthodes de suivi" do

    before(:each) do
      @relationship.save
    end

    it "devrait avoir un attribut follower (lecteur)" do
      @relationship.should respond_to(:follower)
    end

    it "devrait avoir le bon lecteur" do
      @relationship.follower.should == @follower
    end

    it "devrait avoir un attribut  followed (suivi)" do
      @relationship.should respond_to(:followed)
    end

    it "devrait avoir le bon utilisateur suivi (auteur)" do
      @relationship.followed.should == @followed
    end
  end
end

Pour écrire le code de l'application, nous définissons la relation belongs_to (appartient_à) normalement. Rails déduit les noms des clés étrangères à partir des symboles correspondants (c'est-à-dire l'identifiant follower_id de :follower, et l'identifiant followed_id de :followed), mais puisqu'il n'y a pas plus de modèle `Followed` que de modèle `Follower` nous avons aussi besoin de fournir le nom de classe User. Le résultat est montré dans l'extrait 12.7.

Extrait 12.7. Ajout des associations belongs_to au modèle `Relationship`.
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id

  belongs_to :follower, :class_name => "User"
  belongs_to :followed, :class_name => "User"
end

L'association followed (utilisateur_suivi) n'est en fait pas nécessaire jusqu'à la section 12.1.5, même la construction parallèle follower/followed est plus claire si nous implémentons les deux en même temps.

12.1.3 Validations

Avant de poursuivre, nous allons ajouter quelques validations au modèle `Relationship` pour le compléter. Les tests (extrait 12.8) et le code de l'application (extrait 12.9) sont évidents.

Extrait 12.8. Test des validations du modèle `Relationship`.
spec/models/relationship_spec.rb
describe Relationship do
  .
  .
  .
  describe "validations" do

    it "devrait exiger un attribut follower_id" do
      @relationship.follower_id = nil
      @relationship.should_not be_valid
    end

    it "devrait exiger un attribut followed_id" do
      @relationship.followed_id = nil
      @relationship.should_not be_valid
    end
  end
end
Extrait 12.9. Ajout des validations du modèle `Relationship`.
app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id

  belongs_to :follower, :class_name => "User"
  belongs_to :followed, :class_name => "User"

  validates :follower_id, :presence => true
  validates :followed_id, :presence => true
end

12.1.4 Suivi

Nous en arrivons maintenant au cœur des associations `Relationship` : following (auteurs suivis) et followers (lecteurs). Nous commençons avec following, comme dans l'extrait 12.10.

Extrait 12.10. Un test pour l'attribut user.following.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do

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

    it "devrait posséder une méthode `relationships`" do
      @user.should respond_to(:relationships)
    end

    it "devrait posséder une méthode `following" do
      @user.should respond_to(:following)
    end
  end
end

L'implémentation utilise has_many :through (possède_plusieurs :à_travers) pour la première fois : un utilisateur possède plusieurs lecteurs à travers la relation, comme le montre l'illustration 12.7. Par défaut, dans une association has_many :through, Rails cherche une clé étrangère correspondant à la version singulier de l'association ; en d'autres termes, un code comme :

has_many :followeds, :through => :relationships

… devrait assembler un tableau en utilisant le followed_id dans la table relationships. Mais, comme indiqué à la section 12.1.1, user.followeds est plutôt maladroit ; il est de loin plus naturel de traiter « following » comme la forme plurielle de « followed », et d'écrire plutôt user.following pour la table des utilisateurs suivis. Naturellement, Rails nous permet de sur-écrire la valeur par défaut, dans ce cas en utilisant le paramètre :source (extrait 12.11), qui dit explicitement à Rails que la source de la table following est le set des ids de followed.

Extrait 12.11. Ajout de l'association following du modèle User avec has_many :through.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :microposts, :dependent => :destroy
  has_many :relationships, :foreign_key => "follower_id",
                           :dependent => :destroy
  has_many :following, :through => :relationships, :source => :followed
  .
  .
  .
end

Pour créer la relation following, nous allons introduire une méthode utilisataire follow! de telle sorte que nous puissions écrire user.follow!(other_user).7 Nous allons aussi ajouter une méthode booléen associée following? pour tester si un utilisateur en suit un autre.8 Les tests dans l'extrait 12.12 montrent comment nous pouvons nous attendre à ce que ces méthodes soient utilisées dans la pratique.

Extrait 12.12. Tests pour quelques méthodes utilitaires de suivi.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do
    .
    .
    .
    it "devrait avoir une méthode following?" do
      @user.should respond_to(:following?)
    end

    it "devrait avoir une méthode follow!" do
      @user.should respond_to(:follow!)
    end

    it "devrait suivre un autre utilisateur" do
      @user.follow!(@followed)
      @user.should be_following(@followed)
    end

    it "devrait inclure l'utilisateur suivi dans la liste following" do
      @user.follow!(@followed)
      @user.following.should include(@followed)
    end
  end
end

Notez que nous avons remplacé la méthode include? vu dans l'extrait 11.31 par should include, en transformant effectivement :

@user.following.include?(@followed).should be_true

… par le code plus clair et succinct :

@user.following.should include(@followed)

Cet exemple montre juste la flexibilité des conventions booléenne de RSpec ; même si include est déjà un mot-clé Ruby (utilisé pour inclure un module, comme nous l'avons vu, par exemple, dans l'extrait 9.11), RSpec devine dans ce contexte que nous voulons tester l'existence d'un élément dans une liste.

Dans le code de l'application, la méthode following? prend un utilisateur, appelle followed et vérifie si un follower (un lecteur) avec cet identifiant existe dans la base de données ; la méthode follow! appelle create! à travers l'association relationships pour créer la relation de suivi. Le résultat apparait dans l'extrait 12.13.9

Extrait 12.13. Les méthode utilitaires following? et follow!.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def self.authenticate_with_salt(id, stored_salt)
    .
    .
    .
  end

  def following?(followed)
    relationships.find_by_followed_id(followed)
  end

  def follow!(followed)
    relationships.create!(:followed_id => followed.id)
  end
  .
  .
  .
end

Notez que dans l'extrait 12.13 nous avons omis l'utilisateur lui-même, en écrivant juste :

relationships.create!(...)

… au lieu du code équivalent :

self.relationships.create!(...)

L'utilisation d'un self explicite est largement une question de goût ici.

Bien entendu, les utilisateurs devraient être capables de ne plus suivre les autres utilisateurs autant que de les suivre, ce qui nous conduit à la méthode quelque peu prévisible unfollow!, présentée dans l'extrait 12.14.10

Extrait 12.14. Un test pour l'arrêt de suivi d'un utilisateur.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do
    .
    .
    .
    it "devrait avoir une méthode unfollow!" do
      @followed.should respond_to(:unfollow!)
    end

    it "devrait arrêter de suivre un utilisateur" do
      @user.follow!(@followed)
      @user.unfollow!(@followed)
      @user.should_not be_following(@followed)
    end
  end
end

Le code pour unfollow! est très simple : il trouve juste la relation par l'id followed et le détruit (extrait 12.15).11

Extrait 12.15. Arrêter le suivi d'un utilisateur en détruisant une relation utilisateur.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def following?(followed)
    relationships.find_by_followed_id(followed)
  end

  def follow!(followed)
    relationships.create!(:followed_id => followed.id)
  end

  def unfollow!(followed)
    relationships.find_by_followed_id(followed).destroy
  end
  .
  .
  .
end

12.1.5 Les Lecteurs

La dernière pièce du puzzle relationnel consiste à ajouter une méthode user.followers (utilisateur.lecteurs) marchant avec user.following (utilisateur.auteurs_suivis). Vous avez peut-être noté d'après l'illustration 12.7 que toutes les informations nécessaires pour extraire un tableau des lecteurs (followers) est déjà présent dans la table relationships. En effet, la technique est exactement la même que pour les utilisateur suivis, avec les rôles de follower_id et followed_id simplement inversés. Cela suggère que, si nous pouvions arranger une table reverse_relationships avec ces colonnes inversées (illustration 12.9), nous pourrions implémenter user.followers avec peu d'effort.

user_has_many_followers
Illustration 12.9: Un modèle pour les utilisateurs lecteurs en utilisant un modèle Relationship inversé. (taille normale)

Nous commençons avec les tests, avec la certitude que la magie de Rails viendra à nouveau à notre aide (extrait 12.16).

Extrait 12.16. Tester la relation inversée.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "relationships" do
    .
    .
    .    
    it "devrait avoir un méthode reverse_relationship" do
      @user.should respond_to(:reverse_relationships)
    end

    it "devrait avoir une méthode followers" do
      @user.should respond_to(:followers)
    end

    it "devrait inclure le lecteur dans le tableau des lecteurs" do
      @user.follow!(@followed)
      @followed.followers.should include(@user)
    end
  end
end

Comme vous le suspectez certainement, nous ne ferons pas une table complète de base de données juste pour conserver la relation inversée. Plutôt, nous exploiterons la symétrie sous-jaccente entre lecteurs et auteurs pour simuler une table reverse_relationships en passant followed_id (auteur_id) comme clé primaire. En d'autres termes, là où l'association relationship utilise la clé étrangère follower_id (lecteur_id) :

has_many :relationships, :foreign_key => "follower_id"

… l'association reverse_relationships utilise followed_id (auteur_id) :

has_many :reverse_relationships, :foreign_key => "followed_id"

L'association followers (lecteurs) se contruit alors à travers la relationship inversée, comme le montre l'extrait 12.17.

Extrait 12.17. Implémenter user.followers en utilisant la relation inversée.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  has_many :reverse_relationships, :foreign_key => "followed_id",
                                   :class_name => "Relationship",
                                   :dependent => :destroy
  has_many :followers, :through => :reverse_relationships, :source => :follower
  .
  .
  .
end

(Comme dans l'extrait 12.5, le test de dependent :destroy est laissé en exercice (section 12.5).) Notez que nous avons en fait à inclure le nom de classe pour cette association, c'est-à-dire :

has_many :reverse_relationships, :foreign_key => "followed_id",
                                 :class_name => "Relationship"

… sinon, Rails cherchera une classe ReverseRelationship, qui n'existe pas.

Il est important de noter aussi que nous pourrions en fait omettre la clé :source dans ce cas, en utilisant simplement :

has_many :followers, :through => :reverse_relationships

… puisque Rails cherchera automatiquement une clé étrangère follower_id dans ce cas. J'ai gardé la clé :source pour mettre l'accent sur le parallèle de structure avec l'association has_many :following, mais vous êtes libre de ne pas l'utiliser.

Avec le code de l'extrait 12.17, les associations lecteurs/auteur sont achevées, et tous les tests devraient réussir. Cette section a fait appel de façon intensive à vos compétences en matière de modélisation de données, et il n'y aucun problème si vous mettez du temps à assimiler ces concepts . En fait, l'une des meilleures façons de comprendre les associations est de les utiliser dans l'interface web, comme nous le verrons dans la prochaine section.

12.2 Une interface web pour les auteurs et les lecteurs

Dans l'introduction de ce chapitre, nous avons vu un aperçu du flux de page pour les utilisateurs-lecteurs. Dans cette section, nous implémenterons l'interface de base et les fonctionnalités de suivi/arrêt de suivi montrées dans ces maquettes. Nous construirons également des pages séparées pour montrer les lecteurs de l'auteur et la liste des auteurs suivis. À la section 12.3, nous achèverons notre Application Exemple en ajoutant l'état d'alimentation (status feed) de l'utilisateur.

12.2.1 Données de simple lecteur

Comme dans les chapitres précédents, il est très pratique d'utiliser une tâche Rake pour remplir la base de données d'échantillons de relations. Cela nous permettra de concevoir l'aspect et la convivialité des pages web d'abord, différant les fonctionnalités de back-end pour plus tard dans cette section.

La dernière fois que nous avons laissé le « peupleur » d'échantillons de données de l'extrait 11.20, il était devenu plutôt encombré, donc nous allons commencer par définir des méthodes séparées pour créer des utilisateurs et des micro-messages, et ajouter alors des échantillons de données de relations (relationships) en utilisant une nouvelle méthode make_relationships (creer_des_relations). Le résultat est montré dans l'extrait 12.18.

Extrait 12.18. Ajout des relations auteurs/lecteur pour les échantillons de données.
lib/tasks/sample_data.rake
require 'faker'

namespace :db do
  desc "Peupler la base de données avec des échantillons"
  task :populate => :environment do
    Rake::Task['db:reset'].invoke
    make_users
    make_microposts
    make_relationships
  end
end

def make_users
  admin = User.create!(:nom => "Example User",
                       :email => "example@railstutorial.org",
                       :password => "foobar",
                       :password_confirmation => "foobar")
  admin.toggle!(:admin)
  99.times do |n|
    nom  = Faker::Nom.nom
    email = "example-#{n+1}@railstutorial.org"
    password  = "password"
    User.create!(:nom => nom,
                 :email => email,
                 :password => password,
                 :password_confirmation => password)
  end
end

def make_microposts
  User.all(:limit => 6).each do |user|
    50.times do
      content = Faker::Lorem.sentence(5)
      user.microposts.create!(:content => content)
    end
  end
end

def make_relationships
  users = User.all
  user  = users.first
  following = users[1..50]
  followers = users[3..40]
  following.each { |followed| user.follow!(followed) }
  followers.each { |follower| follower.follow!(user) }
end

Ici, les échantillons de relations sont créés en utilisant le code :

def make_relationships
  users = User.all
  user  = users.first
  following = users[1..50]
  followers = users[3..40]
  following.each { |followed| user.follow!(followed) }
  followers.each { |follower| follower.follow!(user) }
end

Nous nous arrangeons un peu arbitrairement pour que le premier utilisateur suive les 50 utilisateurs suivants, et puis faire que les utilisateurs entre les identifiants de 4 à 41 suivent cet utilisateur en retour. Les relations en résultant sera suffisante pour développer l'interface de l'application.

Pour exécuter le code de l'extrait 12.18, peuplons la base de données comme d'habitude :

$ rake db:populate

12.2.2 Statistiques et formulaire de suivi

Maintenant que nos échantillons d'utilisateurs ont des listes (array) de lecteurs et suivent des auteurs, nous avons besoin d'actualiser les pages de profil et d'accueil pour le refléter. Nous allons commencer par faire un partiel pour afficher les statistiques sur les pages de profil et d'accueil, comme le montre la maquette de l'illustration 12.1 et de l'illustration 12.5. Le résultat affichera le nombre d'auteurs suivis et de lecteurs, avec des liens vers leurs pages respectives. Nous ajouterons ensuite le lien suivre/arrêter de suivre, et fabriquerons ensuite les pages dédiées pour afficher les utilisateurs suivis (auteurs suivis) et les utilisateurs qui suivent l'utilisateur courant (lecteurs).

stats_partial_mockup
Illustration 12.10: Maquette du partiel pour les statistiques.

Un gros plan de la zone des statistiques, extrait de la maquette de l'illustration 12.1, est montré dans l'illustration 12.10. Ces statistiques consistent en un compte des utilisateurs que l'utilisateur courant suit et du nombre d'utilisateurs qui le suivent, chacun d'eux avec un lien vers la page d'affichage correspondante. Au chapitre 5, nous avons esquissé de tels liens avec un texte souche ’#’, mais c'était avant d'avoir une expérience suffisantes des routes. Cette fois, bien que nous différerons les pages réelles à la section 12.2.3, nous allons faire les routes maintenant, conformément à l'extrait 12.19. Ce code utilise la méthode :member à l'intérieur d'un bloc resource, que nous n'avons jamais vu avant, mais voyons si vous pouvez deviner ce qu'il fait.

Extrait 12.19. Ajout des actions following et followers au contrôleur Users.
config/routes.rb
SampleApp::Application.routes.draw do
  resources :users do
    member do
      get :following, :followers
    end
  end
  .
  .
  .
end

Vous pouvez imaginer que les URLs pour les utilisateurs suivis (following) et suiveurs (followers) ressembleront à /users/1/following et /users/1/followers et c'est exactement ce que fait le code dans l'extrait 12.19. Puisque les deux pages afficheront des données, nous utilisons get pour faire en sorte que les URLs répondent à la requête GET (comme requis par la convention REST pour de telles pages), et la méthode member signifie que les routes répondent aux URLs contenant l'id de l'utilisateur (l'autre possibilité, collection, fonctionne sans id, de telle sorte que :

resources :users do
  collection do
    get :tigers
  end
end

… devra répondre à l'URL /users/tigers — vraisemblablement pour afficher tous les tigres dans notre application. Pour plus de détails sur de telles options de routage, voyez l'article du Rails Guides à propos de « Rails Routing from the Outside In »). Une table des routes générées par l'extrait 12.19 est présentée dans la table 12.1 ; notez les routes nommées pour les pages des auteurs suivis (following) et lecteurs (followers) que nous serons amenés à utiliser dans un instant.

Requête HTTPURLActionRoute nommé
GET/users/1/followingfollowingfollowing_user_path(1)
GET/users/1/followersfollowersfollowers_user_path(1)
Table 12.1: Routes RESTful fournies par les règles personnalisées dans la ressource de l'extrait 12.19.

Les routes une fois définies, nous sommes maintenant en mesure de construire les tests pour le partiel statistiques (nous aurions pu écrire les tests d'abord, mais les routes nommées auraient été difficiles à motiver sans le fichier routes actualisé). Nous pourrions écrire les tests pour la page de profil de l'utilisateur, puisque le partiel des statistiques apparaitra là, mais il apparaitra aussi sur la page d'accueil, et c'est une bonne opportunité pour restructurer les tests de la page d'accueil pour prendre en compte les utilisateur identifiés. Le résultat apparait dans l'extrait 12.20.

Extrait 12.20. Tester les statistiques auteurs/lecteurs sur la page d'accueil.
spec/controllers/pages_controller_spec.rb
describe PagesController do
  render_views

  before(:each) do
    @base_titre = "Ruby on Rails Tutorial Sample App"
  end
  .
  .
  .
  describe "GET 'home'" do

    describe "quand pas identifié" do

      before(:each) do
        get :home
      end

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

      it "devrait avoir le bon titre" do
        response.should have_selector("title",
                                      :content => "#{@base_titre} | Home")
      end
    end

    describe "quand identifié" do

      before(:each) do
        @user = test_sign_in(Factory(:user))
        other_user = Factory(:user, :email => Factory.next(:email))
        other_user.follow!(@user)
      end

      it "devrait avoir le bon compte d'auteurs et de lecteurs" do
        get :home
        response.should have_selector("a", :href => following_user_path(@user),
                                           :content => "0 auteur suivi")
        response.should have_selector("a", :href => followers_user_path(@user),
                                           :content => "1 lecteur")
      end
    end
  end
end

Le cœur de ce test est l'attente que le compte de lecteurs et d'auteurs suivis apparaisse sur la page, accompagnés des bonnes URLs :

response.should have_selector("a", :href => following_user_path(@user),
                                   :content => "0 auteur suivi")
response.should have_selector("a", :href => followers_user_path(@user),
                                   :content => "1 lecteur")

Nous avons utilisé ici la route nommée de la table 12.1 pour vérifier que les liens avaient les bons URLs.

Le code de l'application pour le partiel des statistiques est simplement une table HTML à l'intérieur d'un div comme dans l'extrait 12.21.

Extrait 12.21. Un partiel pour afficher les statistiques de suivi.
app/views/shared/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
  <table summary="User stats">
    <tr>
      <td>
        <a href="<%= following_user_path(@user) %>">
          <span id="following" class="stat">
          <%= pluralize(@user.following.count, "auteur suivi", "auteurs suivis") %>
          </span>
        </a>
      </td>
      <td>
        <a href="
<%= followers_user_path(@user) %>">
          <span id="followers" class="stat">
            <%= pluralize(@user.followers.count, "lecteur") %>
          </span>
        </a>
      </td>
    </tr>
  </table>
</div>

Ici les comptes utilisateur de lecteurs et d'auteurs suivis sont calculés à travers l'association en utilisant :

@user.following.count

… et :

@user.followers.count

Comparez ceci aux comptes de micro-messages de l'extrait 11.16, où nous avions écrit :

@user.microposts.count

… pour compter les micro-messages.

Puisque nous inclurons les statistiques sur la page d'affichage ainsi que sur la page d'accueil de l'utilisateur, la première ligne de l'extrait 12.21 choisit la bonne en utilisant :

<% @user ||= current_user %>

Comme discuté dans la Box 9.4, cela ne fait rien quand @user n'est pas nil (comme sur la page de profil), mais quand il l'est (comme sur la page d'accueil), il règle la valeur de @user à l'utilisateur courant.

Un détail final qui vaut le peine d'être noté : la présence des ids CSS sur certains éléments, comme dans :

<span id="following" class="stat">
...
</span>

Ceci anticipe l'implémentation Ajax de la section 12.2.5, qui accède aux éléments de la page en utilisant leur identifiant unique (id).

Avec le partiel en main, inclure les statistiques sur la page d'accueil est facile, comme montré dans l'extrait 12.22 (cela doit également faire réussir le test de l'extrait 12.20). Le résultat est présenté dans l'illustration 12.11.

Extrait 12.22. Ajouter les statistiques lecteur à la page d'accueil.
app/views/pages/home.html.erb
<% if signed_in? %>
        .
        .
        .
        <%= render 'shared/user_info' %>
        <%= render 'shared/stats' %>
      </td>
    </tr>
  </table>
<% else %>
  .
  .
  .
<% end %>
home_page_follow_stats
Illustration 12.11: La page d'accueil (/) avec les statistiques de suivi. (taille normale)

Nous « rendrons » le partiel des statistiques sur la page de profil dans un moment, mais construisons avant ça un partiel pour le bouton suivre/ne plus suivre dont le code apparait dans l'extrait 12.23.

Extrait 12.23. Un partiel pour le formulaire suivre/ne plus suivre.
app/views/users/_follow_form.html.erb
<% unless current_user?(@user) %>
  <div id="follow_form">
  <% if current_user.following?(@user) %>
    <%= render 'unfollow' %>
  <% else %>
    <%= render 'follow' %>
  <% end %>
  </div>
<% end %>

Cela ne fait rien d'autre que confier le vrai travail aux partiels follow (suivre) et unfollow (ne plus suivre), qui ont besoin de nouvelles routes avec des règles pour la ressource Relationships, qui suivent l'exemple de la ressource Microposts (extrait 11.21), comme présenté dans l'extrait 12.24.

Extrait 12.24. Ajout des routes pour les relations de l'utilisateur.
config/routes.rb
SampleApp::Application.routes.draw do
  .
  .
  .
  resources :sessions,      :only => [:new, :create, :destroy]
  resources :microposts,    :only => [:create, :destroy]
  resources :relationships, :only => [:create, :destroy]
  .
  .
  .
end

Le code des partiels suivre/ne plus suivre eux-mêmes est présenté dans l'extrait 12.25 et l'extrait 12.26.

Extrait 12.25. Un formulaire pour suivre un utilisateur.
app/views/users/_follow.html.erb
<%= form_for current_user.relationships.
                          build(:followed_id => @user.id) do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <div class="actions"><%= f.submit "Suivre" %></div>
<% end %>
Extrait 12.26. Formulaire pour ne plus suivre un utilisateur suivi.
app/views/users/_unfollow.html.erb
<%= form_for current_user.relationships.find_by_followed_id(@user),
             :html => { :method => :delete } do |f| %>
  <div class="actions"><%= f.submit "Ne plus suivre" %></div>
<% end %>

Ces formulaires utilisent tous deux form_for pour manipuler un objet de modèle Relationship ; la différence principale entre les deux est que l'extrait 12.25 construit une nouvelle relation, tandis que l'extrait 12.26 cherche la relation existente. Naturellement, la première envoie une requête POST au contrôleur Relationships pour créer (create) une relation, tandis que la seconde envoie une requête DELETE pour détruire (destroy) une relation (nous implémenterons ces actions à la section 12.2.4). Enfin, vous noterez que le formulaire suivre/ne plus suivre n'a aucun contenu autre que le bouton, mais il a encore besoin d'envoyer le followed_id, ce que nous accomplissons avec hidden_field (champ caché) ; cela produit un code HTML de la forme :

<input id="relationship_followed_id" nom="relationship[followed_id]"
type="hidden" value="3" />

… qui place l'information voulu sur la page sans l'afficher sur la page du navigateur.

Nous pouvons maintenant inclure le formulaire de suivi et les statistiques de suivi sur la page de profil simplement en rendant les partiels, comme montré dans l'extrait 12.27. Les profils avec les boutons « suivre » et « ne plus suivre », respectivement, apparaissent dans l'illustration 12.12 et l'illustration 12.13.

Extrait 12.27. Ajout du formulaire de suivi et des statistiques de suivi à la page profil de l'utilisateur.
app/views/users/show.html.erb
<table class="profile" summary="Profile information">
  <tr>
    <td class="main">
      <h1>
        <%= gravatar_for @user %>
        <%= @user.nom %>
      </h1>
      <%= render 'follow_form' if signed_in? %>
      .
      .
      .
    </td>
    <td class="sidebar round">
      <strong>Name</strong> <%= @user.nom %><br />
      <strong>URL</strong> <%= link_to user_path(@user), @user %><br />
      <strong>Microposts</strong> <%= @user.microposts.count %>
      <%= render 'shared/stats' %>
    </td>
  </tr>
</table>
profile_follow_button
Illustration 12.12: Un profil d'utilisateur avec un bouton « suivre » (/users/8). (taille normale)
profile_unfollow_button
Illustration 12.13: Un profil d'utilisateur avec un bouton « ne plus suivre » (/users/6). (taille normale)

Nous obtiendrons le fonctionnement de ces boutons bien assez tôt — en fait, nous le ferons de deux manières, la stardard (section 12.2.4) et celle utilisant Ajax (section 12.2.5) — mais d'abord nous allons achever le code HTML de l'interface en construisant les pages des lecteurs et des auteurs suivis par l'utilisateur.

12.2.3 Pages des auteurs suivis et des lecteurs

Les pages pour afficher les auteurs suivis et les lecteurs de l'utilisateur courant ressembleront à un hybride entre la page de profil de l'utilisateur et la page d'index (section 10.3.1), avec une barre lattéral pour les informations de l'utilisateur (incluant l'état de son suivi) et une table d'utilisateurs. En plus, nous inclurons un quadrillage de liens sur l'image des profils. Les maquettes présentant ces exigences sont présentées dans l'illustration 12.14 (auteurs suivis) et illustration 12.15 (lecteurs).

following_mockup
Illustration 12.14: Une maquette de la page des auteurs suivis par l'utilisateur courant. (taille normale)
followers_mockup
Illustration 12.15: Une maquette de la page des lecteurs de l'utilisateur courant. (taille normale)

Notre première étape consiste à faire fonctionner les liens vers les auteurs suivis (following) et les lecteurs (followers). Nous suivrons le modèle de Twitter et devrons exiger une identification pour les deux pages. Pour les utilisateurs identifiés, les pages devraient posséder des liens respectifs pour les auteurs suivis et les lecteurs. L'extrait 12.28 exprime ces attentes en code.12

Extrait 12.28. Test pour les actions following (auteurs suivis) et followers (lecteurs).
spec/controllers/users_controller_spec.rb
describe UsersController do
  .
  .
  .
  describe "Les pages de suivi" do

    describe "quand pas identifié" do

      it "devrait protéger les auteurs suivis" do
        get :following, :id => 1
        response.should redirect_to(signin_path)
      end

      it "devrait protéger les lecteurs" do
        get :followers, :id => 1
        response.should redirect_to(signin_path)
      end
    end

    describe "quand identifié" do

      before(:each) do
        @user = test_sign_in(Factory(:user))
        @other_user = Factory(:user, :email => Factory.next(:email))
        @user.follow!(@other_user)
      end

      it "devrait afficher les auteurs suivis par l'utilisateur" do
        get :following, :id => @user
        response.should have_selector("a", :href => user_path(@other_user),
                                           :content => @other_user.nom)
      end

      it "devrait afficher les lecteurs de l'utilisateur" do
        get :followers, :id => @other_user
        response.should have_selector("a", :href => user_path(@user),
                                           :content => @user.nom)
      end
    end
  end
end

La seule partie délicate de l'implémentation est de concevoir que nous avons besoin d'ajouter deux nouvelles actions au contrôleur Users ; en s'appuyant sur les routes définies dans l'extrait 12.19, nous avons besoin de les appeler following (auteurs suivis) et followers (lecteurs). Chaque action a besoin de définir un titre, trouver un utilisateur, récupérer soit @user.following ou @user.followers (dans une forme paginée), et enfin de rendre la page attendue. Le résultat est présenté dans l'extrait 12.29.

Extrait 12.29. Les actions following et followers.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_filter :authenticate, :except => [:show, :new, :create]
  .
  .
  .
  def following
    @titre = "Following"
    @user = User.find(params[:id])
    @users = @user.following.paginate(:page => params[:page])
    render 'show_follow'
  end

  def followers
    @titre = "Followers"
    @user = User.find(params[:id])
    @users = @user.followers.paginate(:page => params[:page])
    render 'show_follow'
  end
  .
  .
  .
end

Notez ici que les deux actions font un appel explicite à render, dans ce cas pour rendre une vue appelée show_follow (montrer_le_suivi) que nous devons créer. La raison de cette vue commune est que le ERb est très proche dans les deux cas, et l'extrait 12.30 les couvre les deux.

Extrait 12.30. La vue show_follow utilisée pour rendre les auteurs suivis et les lecteurs.
app/views/users/show_follow.html.erb
<table summary="Information à propos des auteurs suivis ou des lecteurs">
  <tr>
    <td class="main">
      <h1><%= @titre %></h1>

      <% unless @users.empty? %>
        <ul class="users">
          <%= render @users %>
        </ul>
        <%= will_paginate @users %>
      <% 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 %>
      <%= render 'shared/stats' %>
      <% unless @users.empty? %>
        <% @users.each do |user| %>
          <%= link_to gravatar_pour(user, :size => 30), user %>
        <% end %>
      <% end %>
    </td>
  </tr>
</table>

Il y a un second détail dans l'extrait 12.29 qui vaut la peine d'être noté : dans le but de protéger les pages des auteurs suivis et des lecteurs d'un accès non autorisé, nous avons changé les authentifications d'avant fitrage pour utiliser :except plutôt que :only. Jusqu'ici dans ce manuel, nous avons utilisé :only pour indiquer sur quelles actions le filtre devait s'appliquer ; avec l'addition des nouvelles actions protégées, la balance a penché, et il est plus simple à présent d'indiquer quelles actions ne doivent pas être filtrées. Nous faisons cela avc l'option :except pour le authenticate d'avant filtrage :

before_filter :authenticate, :except => [:show, :new, :create]

Avec ça, les tests devraient maintenant réussir, et les pages devraient être rendues comme dans l'illustration 12.16 (auteurs suivis) et l'illustration 12.17 (lecteurs).

user_following
Illustration 12.16: Afficher les utilisateurs qui sont suivis par l'utilisateur courant. (taille normale)
user_followers
Illustration 12.17: Afficher les lecteurs de l'utilisateur courant. (taille normale)

Vous avez peut-être noter que même avec le partiel commun show_followers, les actions following et followers ont toujours beaucoup de code dupliqué. Plus encore, le partiel show_followers lui-même partage des fonctionnalités avec la page d'affichage de l'utilisateur. La section 12.5 inclut des exercices pour éliminer ces duplications.

12.2.4 Un bouton de suivi standard

Maintenant que nos vues sont en ordre, il est temps de faire fonctionner les boutons suivre/ne plus suivre. Puisque suivre un utilisateur crée une relation, et ne plus suivre un utilisateur détruit cette relation, cela revient à écrire les actions create et destroy pour le contrôleur Relationship. Natuellement, les deux actions devront être protégées ; pour les utilisateurs identifiés, nous utiliserons les méthodes utilitaires follow! et unfollow! définies à la section 12.1.4 pour créer et détruire les relations concernées. Ces exigences conduisent aux tests de l'extrait 12.31.

Extrait 12.31. Tests pour les actions du contrôleur Relationships.
spec/controllers/relationships_controller_spec.rb
require 'spec_helper'

describe RelationshipsController do

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

    it "devrait exiger l'identification pour créer" do
      post :create
      response.should redirect_to(signin_path)
    end

    it "devrait exiger l'identification pour détruire" do
      delete :destroy, :id => 1
      response.should redirect_to(signin_path)
    end
  end

  describe "POST 'create'" do

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

    it "devrait créer une relation" do
      lambda do
        post :create, :relationship => { :followed_id => @followed }
        response.should be_redirect
      end.should change(Relationship, :count).by(1)
    end
  end

  describe "DELETE 'destroy'" do

    before(:each) do
      @user = test_sign_in(Factory(:user))
      @followed = Factory(:user, :email => Factory.next(:email))
      @user.follow!(@followed)
      @relationship = @user.relationships.find_by_followed_id(@followed)
    end

    it "devrait détruire une relation" do
      lambda do
        delete :destroy, :id => @relationship
        response.should be_redirect
      end.should change(Relationship, :count).by(-1)
    end
  end
end

Notez ici comment :

:relationship => { :followed_id => @followed }

… simule la soumission du formulaire avec les champs cachés (hidden) donnés par :

<%= f.hidden_field :followed_id %>

Le code du contrôleur nécessaire pour faire réussir ces tests est remarquablement concis : nous récupérons juste l'utilisateur suivi ou devant être suivi, et suivons ou ne-suivons-plus l'utilisateur en utilisant la méthode utilitaire concernée. L'implémentation complète apparait dans l'extrait 12.32.

Extrait 12.32. Le contrôleur Relationships.
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_filter :authenticate

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    redirect_to @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    redirect_to @user
  end
end

Avec ça, le cœur de la fonctionnalité suivre/ne plus suivre est achevé, et n'importe quel utilisateur peut suivre (ou ne plus suivre) n'importe quel autre utilisateur.

12.2.5 Un bouton de suivi fonctionnel avec Ajax

Bien que l'implémentation du suivi de notre utilisateur soit opérationnelle telle qu'elle est, une dernière petite chose peut être ajoutée avant de travailler sur le « status feed » (l'état de l'alimentation). Vous avez peut-être noté à la section 12.2.4 que les deux actions create et destroy dans le contrôleur Relationships redirigeait simplement (back) vers le profil original. En d'autres termes, un utilisateur commence sur sa page de profil, suit l'utilisateur et est immédiatement redirigé en arrière vers la page initiale. Il est raisonnable de se demander pourquoi l'utilisateur a besoin de quitter cette page de profil.

C'est exactement le genre de problème que peut résoudre Ajax, qui permet aux pages web d'envoyer des requêtes de façon asynchrone à un serveur sans quitter la page.13 Ajouter de l'Ajax à des formulaires web étant plutôt courant aujourd'hui, Rails le rend facile à implémenter. Actualiser les formulaires suivre/ne-plus-suivre des partiels est même particulière aisé ; changez seulement :

form_for

… en :

form_for ..., :remote => true

… et Rails automagiquement utilise Ajax.14 Les partiels actualisés sont présentés dans l'extrait 12.33 et l'extrait 12.34.

Extrait 12.33. Un formulaire pour suivre les utilisateurs en utilisant Ajax.
app/views/users/_follow.html.erb
<%= form_for current_user.relationships.build(:followed_id => @user.id),
             :remote => true do |f| %>
  <div><%= f.hidden_field :followed_id %></div>
  <div class="actions"><%= f.submit "Follow" %></div>
<% end %>
Extrait 12.34. Un formulaire pour ne-plus-suivre l'utilisateur en utilisant Ajax.
app/views/users/_unfollow.html.erb
<%= form_for current_user.relationships.find_by_followed_id(@user),
             :html => { :method => :delete },
             :remote => true do |f| %>
  <div class="actions"><%= f.submit "Unfollow" %></div>
<% end %>

Le code HTML généré par ce ERb n'est pas très expressif, mais par curiosité, jetons-y un coup d'œil :

<form action="/relationships/117" class="edit_relationship" data-remote="true"
      id="edit_relationship_117" method="post">
  .
  .
  .
</form>

Cela définit la variable data-remote="true" à l'intérieur de la balise du formulaire (form), qui demande à Rails de permettre au formulaire d'être traité par JavaScript. En utilisant une simple propriété HTML au lieu d'insérer du code Javascript (comme dans les précédentes versions de Rails), Rails 3 suit la philosophie du JavaScript non intrusif.

Ayant actualisé le formulaire, nous avons besoin maintenant de faire que le contrôleur Relationship réponde aux requêtes Ajax. Nous allons commencer avec une simple paire de tests. Tester Ajax est quelque peu délicat, et le faire à fond est un sujet à part entière, très vaste, mais nous pouvons commencer avec le code de l'extrait 12.35. Il utilise la méthode xhr (pour « XmlHttpRequest ») pour lancer une requête Ajax ; comparez-la aux méthodes get, post, put et delete utilisées dans les précédents tests. Nous vérifions alors que les actions create et destroy fassent les choses attendues quand on lance les requêtes Ajax (pour écrire une suite de tests plus profonde pour les applications utilisant intensivement Ajax, jetez un œil à Selenium et Watir).

Extrait 12.35. Tests pour les réponses du contrôleur Relationships aux requêtes Ajax.
spec/controllers/relationships_controller_spec.rb
describe RelationshipsController do
  .
  .
  .
  describe "POST 'create'" do
    .
    .
    .
    it "devrait créer une relation en utilisant Ajax" do
      lambda do
        xhr :post, :create, :relationship => { :followed_id => @followed }
        response.should be_success
      end.should change(Relationship, :count).by(1)
    end
  end

  describe "DELETE 'destroy'" do
    .
    .
    .
    it "devrait détruire une relation en utilisant Ajax" do
      lambda do
        xhr :delete, :destroy, :id => @relationship
        response.should be_success
      end.should change(Relationship, :count).by(-1)
    end
  end
end

Comme le demande les tests, le code de l'application utilise les mêmes actions create et delete pour répondre aux requêtes Ajax que celles pour répondre aux requêtes HTML ordinaires POST et DELETE. Tout ce que nous avons à faire est de répondre à une requête HTML normale avec une redirection (comme à la section 12.2.4) et répondre à une requête Ajax avec JavaScript.15 Le code du contrôleur est présenté dans l'extrait 12.36 (voyez plus loin à la section 12.5 un exercice montrant une façon même plus compact d'accomplir la même chose).

Extrait 12.36. Répondre à une requête Ajax dans le contrôleur Relationships.
app/controllers/relationships_controller.rb
class RelationshipsController < ApplicationController
  before_filter :authenticate

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    respond_to do |format|
      format.html { redirect_to @user }
      format.js
    end
  end
end

Ce code utilise respond_to pour prendre l'action adaptée au type de requête.16 La syntaxe est potentiellement déroutante, et il est important de comprendre que dans :

respond_to do |format|
  format.html { redirect_to @user }
  format.js
end

… seulement une des lignes sera exécutée (suivant la nature de la requête).

Dans le cas d'une requête Ajax, Rails appelle automatiquement un fichier de code Ruby embarqué JavaScript (.js.erb) portant le même nom que l'action, c'est-à-dire create.js.erb ou destroy.js.erb. Comme vous pouvez vous en douter, les fichiers nous permettent de mixer du JavaScript et du Ruby embarqué pour exécuter des actions sur la page courante. Ce sont ces fichiers que nous devons créer et modifier dans le but d'actualiser la page de profil de l'utilisateur après la demande de suivi (ou de non-suivi).

À l'intérieur d'un fichier JS-ERb, Rails fournit automatiquement les helpers Prototype JavaScript pour manipuler la page en utilisant le Document Object Model (DOM). Le framework Prototype fournit un grand nombre de méthodes pour manipuler le DOM, mais ici nous n'aurons besoin que de deux de ces méthodes. D'abord, nous avons besoin de connaitre la syntaxe Prototype dollar (« $ ») pour accéder à un élément DOM qui se base sur l'id CSS unique. Par exemple, pour manipuler l'élément d'identifiant follow_form, nous devrons utiliser la syntaxe :

$("follow_form")

(Souvenez-vous — extrait 12.23 — que c'est un div qui entoure le formulaire, pas la balise form elle-même.) La seconde méthode dont nous avons besoin est update, qui actualise le code HTML à l'intérieur de l'élément concerné avec le contenu de son argument. Par exemple, pour remplacer entièrement le formulaire de suivi par la chaine de caractère "foober", nous devons écrire :

$("follow_form").update("foobar")

Contrairement aux fichiers en « plain JavaScript », les fichiers JS-ERb permettent aussi d'utiliser du Ruby embarqué, ce que nous appliquons dans le fichier create.js.erb pour actualiser le formulaire de suivi avec le partiel unfollow (qui devrait s'afficher après la réussite d'une définition de suivi) et actualiser le compte de lecteurs. Le résultat est montré dans l'extrait 12.37.

Extrait 12.37. Le code Ruby embarqué JavaScript pour créer une relation de suivi.
app/views/relationships/create.js.erb
$("follow_form").update("<%= escape_javascript(render('users/unfollow')) %>")
$("lecteurs").update('<%= "#{@user.followers.count} followers" %>')

Le fichier destroy.js.erb est analogue (extrait 12.38). Notez que, comme dans l'extrait 12.37, nous devons utiliser escape_javascript pour « échapper » le résultat en insérant le code HTML.

Extrait 12.38. Le code Ruby JavaScript (RJS) pour détruire une relation de suivi.
app/views/relationships/destroy.js.erb
$("follow_form").update("<%= escape_javascript(render('users/follow')) %>")
$("lecteurs").update('<%= "#{@user.followers.count} followers" %>')

Avec cela, nous devrions naviguer vers la page de profil de l'utilisateur et vérifier que vous pouvez suivre ou arrêter de suivre sans que la page n'ait besoin d'être rafraichie.

L'utilisation d'Ajax en Rails est un vaste sujet qui évolue sans cesse, donc nous ne pourrons qu'en effleurer la surface ici, mais (comme pour le reste du matériel abordé dans ce tutoriel) notre traitement vous fournit de bonnes bases pour étudier ensuite des ressources plus avancées. Il est spécialement important de noter que, en addition du framework Prototype, le framework JavaScript jQuery connait beaucoup d'attraction dans la communauté Ruby. L'implémentation des fonctions Ajax de cette section en utilisant jQuery est laissé comme exercice ; voyez la section 12.5.

12.3 L'état de l'alimentation

Nous en arrivons maintenant à l'apogée de notre Application Exemple : l'état de l'alimentation (status feed). S'occuper de l'état de l'alimentation signifie assembler une liste temporelle des micro-messages des utilisateurs suivis par l'utilisateur courant accompagné des propres micro-messages de cet utilisateur. De façon assez logique, cette section contient certaines des notions les plus avancées de tout le tutoriel. Pour accomplir cet exploit, nous aurons en effet besoin de techniques de programmation assez poussées, en Rails, en Ruby et même en SQL.

En prévision des lourdes charges que nous allons avoir à porter, il est spécialement important d'avoir une vision claire de là où nous allons. Une maquette finale de l'état de l'alimentation de l'utilisateur, contruite d'après le proto-feed de la section 11.3.3, est présentée dans l'illustration 12.18.

home_page_feed_mockup
Illustration 12.18: Une maquette de la page d'accueil de l'utilisateur avec un état de l'alimentation (version anglaise). (taille normale)

12.3.1 Motivation et stratégie

L'idée générale derrière l'alimentation est simple. L'illustration 12.19 montre un échantillon de la table microposts de la base de données et l'alimentation en résultant. Le but de l'alimentation est de récupérer les micro-messages dont l'identifiant d'utilisateur (user_id) correspond aux utilisateurs qui sont suivis par l'utilisateur courant (et l'utilisateur courant lui-même), comme indiqué par les flèches dans le diagramme.

user_feed
Illustration 12.19: L'alimentation pour un utilisateur (id 1) suivant les utilisateurs d'id 2, 7, 8 et 10.

Puisque nous avons besoin d'une manière de trouver tous les micro-messages des utilisateurs suivis par un utilisateur donné, nous allons projeter d'implémenter une méthode appelée from_users_followed_by, que nous utiliserons comme suit :

Micropost.from_users_followed_by(user)

Bien que nous ne sachions pas encore comment l'implémenter, nous pouvons déjà écrire des tests pour from_users_followed_by, comme dans l'extrait 12.39.

Extrait 12.39. Tests pour Micropost.from_users_followed_by.
spec/models/micropost_spec.rb
describe Micropost do
  .
  .
  .
  describe "from_users_followed_by" do

    before(:each) do
      @other_user = Factory(:user, :email => Factory.next(:email))
      @third_user = Factory(:user, :email => Factory.next(:email))

      @user_post  = @user.microposts.create!(:content => "foo")
      @other_post = @other_user.microposts.create!(:content => "bar")
      @third_post = @third_user.microposts.create!(:content => "baz")

      @user.follow!(@other_user)
    end

    it "devrait avoir une méthode de classea from_users_followed_by" do
      Micropost.should respond_to(:from_users_followed_by)
    end

    it "devrait inclure les micro-messages des utilisateurs suivis" do
      Micropost.from_users_followed_by(@user).should include(@other_post)
    end

    it "devrait inclure les propres micro-messages de l'utilisateur" do
      Micropost.from_users_followed_by(@user).should include(@user_post)
    end

    it "ne devrait pas inclure les micro-messages des utilisateurs non suivis" do
      Micropost.from_users_followed_by(@user).should_not include(@third_post)
    end
  end
end

La clé ici est de construire des associations dans le bloc before(:each) et ensuite de tester les trois exigences : inclusion des micro-messages des utilisateurs suivis, des micro-messages de l'utilisateur courant et exclusion des micro-messages des utilisateurs non suivis.

L'alimentation elle-même se trouve dans le modèle User (section 11.3.3), donc nous devrons ajouter un test additionnel aux specs du modèle User de l'extrait 11.31, comme montré dans l'extrait 12.40 (notez que nous avons remplacé ici l'utilisation de include? de l'extrait 11.31 par le code plus compact include, convention introduite dans l'extrait 12.12).

Extrait 12.40. Les tests finaux pour l'état de l'alimentation.
spec/models/user_spec.rb
describe User do
  .
  .
  .
  describe "association micro-messages" do
    .
    .
    .
    describe "état de l'alimentation" do

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

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

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

      it "devrait inclure les micro-messages des utilisateurs suivis" do
        followed = Factory(:user, :email => Factory.next(:email))
        mp3 = Factory(:micropost, :user => followed)
        @user.follow!(followed)
        @user.feed.should include(mp3)
      end
    end
    .
    .
    .
  end
end

Implémenter l'alimentation sera facile ; nous la confierons simplement à Micropost.from_users_followed_by, comme le montre l'extrait 12.41.

Extrait 12.41. Ajout de l'alimentation achevée au modèle User.
app/models/user.rb
class User < ActiveRecord::Base
  .
  .
  .
  def feed
    Micropost.from_users_followed_by(self)
  end
  .
  .
  .
end

12.3.2 Une première implémentation de l'alimentation

Il est temps maintenant d'implémenter Micropost.from_users_followed_by, à laquelle, pour la simplicité, je ferai référence simplement par « l'alimentation ». Puisque le résultat final est plutôt complexe, nous allons construire l'implémentation de l'alimentation finale en introduisant les pièces de code les unes après les autres.

La première étape consiste à réfléchir au type de requête dont nous avons besoin. Ce que nous voulons faire, c'est sélectionner de la table microposts tous les micro-messages avec des identifiants de suivi correspondant à l'utilisateur donné (ou l'utilisateur lui-même). Nous pouvons écrire cela, schématiquement, comme suit :

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

En écrivant ce code, nous devinons que SQL supporte un mot-clé IN qui nous permet de tester la définition de l'inclusion (et il le fait !).

Souvenez-vous, d'après la proto-alimentation de la section 11.3.3, que Active Record utilise la méthode where pour accomplir ce type de sélection vue ci-dessus, comme illustré dans l'extrait 11.32. Là, notre sélection est très simple ; nous récupérons juste les micro-messages ayant un user_id (identifiant_dutilisateur) correspondant à l'utilisateur courant :

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

Ici, nous attendons quelque chose de plus compliqué, comme :

where("user_id in (#{followed_ids}) OR user_id = ?", user)

(Ici nous avons utilisé la convention Rails user plutôt que user.id dans la condition ; Rails utilise automatiquement l'attribut id. Nous avons également omis le début Micropost. puisque nous comptons placer cette méthode dans le modèle Micropost lui-même.)

Nous tirons de ces conditions que nous aurons besoin d'un tableau (array) d'ids que l'utilisateur donné suit (ou quelque chose d'équivalent). Une façon de le faire consiste à utiliser la méthode Ruby map, utilisable sur un objet de type « enumerable », c'est-à-dire n'importe quel objet (tel qu'un Array — Tableau — ou un Hash — Table de hachage —) constitué d'une collection d'éléments.17 Nous avons vu un exemple de cette méthode à la section 4.3.2 ; elle fonctionne comme cela :

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Des situations comme celle illustrée ci-dessus, où la même méthode (par exemple to_s) sera invoquée sur chaque élément, est assez courante pour avoir une notation raccourcie utilisant une esperluette « & » et un symbole correspondant à la méthode :18

>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]

Nous pouvons utiliser cette notation pour construire le tableau nécessaire des ids des utilisateurs suivis en appelant id sur chaque élément de user.following. Par exemple, pour le premier utilisateur dans la base de données ce tableau apparait comme suit :

>> User.first.following.map(&:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

À ce stade, vous pouvez deviner que le code :

Micropost.from_users_followed_by(user)

… implique une méthode de classe dans la classe Micropost (une construction vue la dernière fois dans la classe User à la section 7.12). Une mise en œuvre pour aller dans ce sens est présentée dans l'extrait 12.42.

Extrait 12.42. Une première idée de la méthode from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    followed_ids = user.following.map(&:id).join(", ")
    where("user_id IN (#{followed_ids}) OR user_id = ?", user)
  end
end

Bien que les discussions conduisant à l'extrait 12.42 ait été formulées en termes hypothétiques, cela fonctionne, en vérité ! En fait, c'est assez efficace pour la plupart des cas pratiques. Mais ce n'est pas l'implémentation finale ; voyons si vous pouvez deviner pourquoi avant de passer à la section suivante (indice : qu'en est-il si un utilisateur suit 5000 autres utilisateurs ?)

12.3.3 Champs d'application, sous-sélections et lambda

Comme suggéré dans la dernière section, l'implémentation de l'alimentation de la section 12.3.2 ne s'adaptera pas bien à un grand nombre de micro-messages, comme cela se produit lorsqu'un utilisateur suit disons 5000 autres utilisateurs. Dans cette section, nous allons ré-implémenter l'alimentation d'une façon à mieux l'adapter au nombre d'utilisateurs suivis.

Il y a quelques problèmes avec le code de la section 12.3.2. Pour commencer, l'expression :

followed_ids = user.following.map(&:id).join(", ")

… place tous les utilisateurs suivis en mémoire, et crée un tableau de la longueur totale de la liste de suivis. Puisque la condition dans l'extrait 12.42 vérifie en fait seulement l'inclusion dans une définition, il doit exister une façon plus efficace de le faire, et en effet SQL est justement optimisée pour de telles opérations. Ensuite, la méthode de l'extrait 12.42 récupère toujours tous les micro-messages et les fourre dans un tableau Ruby. Bien que ces micro-messages soient paginés dans la vue (extrait 11.33), le tableau est toujours plein.19 Ce que nous voulons réellement, c'est une pagination qui ne récupère que 30 items à la fois.

La solution pour ces deux problèmes implique de convertir l'alimentation de la méthode de classe vers un champ d'application (un scope), qui est une méthode Rails pour restreindre les sélections dans la base de données à certaines conditions. Par exemple, pour qu'une méthode sélectionne tous les utilisateurs administrateurs de notre application, nous pourrions ajouter un champ d'application au modèle User comme suit :

class User < ActiveRecord::Base
  .
  .
  .
  scope :admin, where(:admin => true)
  .
  .
  .
end

Comme résultat de ce champs d'application, le code :

User.admin

… retournerait un tableau de tous les administrateurs du site.

La raison principale qui fait que les champs d'application sont meilleurs que les méthodes de classe « pleines » (plain class methods) est qu'elle peuvent être chainées à d'autres méthodes, de telle sorte que, par exemple :

User.admin.paginate(:page => 1)

… pagine en fait les administrateurs dans la base de données ; si (pour quelque raison bizarre) le site possède 100 administrateurs, le code ci-dessus ne récupérera que les 30 premiers.

Le champ d'application pour l'alimentation est un tout petit peu plus complexe que celui illustré ci-dessus : il a besoin d'un argument, nommément, l'utilisateur dont nous devons générer l'alimentation. Nous pouvons faire cela avec une fonction anonyme, ou fonction lambda (dont nous avons parlé à la section 8.4.2), comme le montre l'extrait 12.43.20

Extrait 12.43. Amélioration de from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  default_scope :order => 'microposts.created_at DESC'

  # Retourne les micro-messages des utilisateurs suivi par un utilisateur donné.
  scope :from_users_followed_by, lambda { |user| followed_by(user) }

  private

    # Retourne une condition SQL pour les utilisateurs suivis par un utilisateur donné.
    # Nous incluons aussi les propres micro-messages de l'utilisateur.
    def self.followed_by(user)
      followed_ids = user.following.map(&:id).join(", ")
      where("user_id IN (#{followed_ids}) OR user_id = :user_id",
            { :user_id => user })
    end
end

Puisque les conditions sur le champ d'application de from_users_followed_by sont plutôt longues, nous avons défini une fonction auxiliaire pour les traiter :

def self.followed_by(user)
  followed_ids = user.following.map(&:id).join(", ")
  where("user_id IN (#{followed_ids}) OR user_id = :user_id",
        { :user_id => user })
end

En prévision de la prochaine étape, nous avons remplacé :

where("... OR user_id = ?", user)

… par le code équivalent :

where("... OR user_id = :user_id", { :user_id => user })

La syntaxe par point d'interrogation est bien, mais quand nous voulons insérer la même variable à différents endroits, la seconde syntaxe, utilisant une table, est plus pratique.

La discussion ci-dessus implique d'ajouter une seconde occurrence de user_id dans la requête SQL, et en effet c'est le cas. Nous pouvons remplacer le code Ruby :

followed_ids = user.following.map(&:id).join(", ")

… par le fragment SQL :

followed_ids = %(SELECT followed_id FROM relationships
                 WHERE follower_id = :user_id)

(Lisez le Box 12.1 pour une explication de la syntaxe %()). Ce code contient une sous-selection SQL et, « internalement », l'entière sélection pour l'utilisateur 1 ressemblerait à quelque chose comme :

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE follower_id = 1)
      OR user_id = 1

Cette sous-sélection s'arrange pour que la définition logique soit imposée à l'intérieur de la base de données, ce qui est plus efficace.21

Sur cette base, nous sommes prêt pour l'implémentation finale de l'alimentation, comme vu dans l'extrait 12.44.

Extrait 12.44. L'implémentation finale de from_users_followed_by.
app/models/micropost.rb
class Micropost < ActiveRecord::Base
  .
  .
  .
  default_scope :order => 'microposts.created_at DESC'

  # Renvoie les micro-messages des utilisateurs suivis par un utilisateur donné.
  scope :from_users_followed_by, lambda { |user| followed_by(user) }

  private

    # Renvoie une condition SQL pour les utilisateurs suivis par l'utilisateur donné.
    # Nous incluons aussi ses propres micro-messages.
    def self.followed_by(user)
      followed_ids = %(SELECT followed_id FROM relationships
                       WHERE follower_id = :user_id)
      where("user_id IN (#{followed_ids}) OR user_id = :user_id",
            { :user_id => user })
    end
end

Ce code met en branle une redoutable combinaison de Rails, Ruby et SQL, mais il accomplit le boulot, et le fait bien.22

12.3.4 Le nouvel état de l'alimentation

Avec le code de l'extrait 12.44, notre état d'alimentation est achevé. Pour mémoire, le code pour la page d'accueil est présenté dans extrait 12.45 ; ce code crée une alimentation paginée des micro-messages à afficher dans la vue, comme le montre l'illustration 12.20.23 Notez que la méthode paginate parvient à tout faire à l'intérieur de la méthode du modèle Micropost de l'extrait 12.44, et s'arrange pour ne récupérer que 30 micro-messages à la fois dans la base de données.24

Extrait 12.45. L'action home avec une alimentation paginée.
app/controllers/pages_controller.rb
class PagesController < ApplicationController

  def home
    @titre = "Accueil"
    if signed_in?
      @micropost = Micropost.new
      @feed_items = current_user.feed.paginate(:page => params[:page])
    end
  end
  .
  .
  .
end
home_page_with_feed
Illustration 12.20: La page d'accueil avec un état d'alimentation fonctionnel (version anglaise). (taille normale)

12.4 Conclusion

Avec l'addition d'un état d'alimentation, nous en avons terminé avec le cœur de l'Application Exemple de ce Tutoriel Ruby on Rails. L'application inclut des exemples de toutes les fonctionnalités principales de Rails, comme les modèles, les vues, les contrôleurs, les templates, les partiels, les filtres, les validations, les fonctions de rappel, les associations has_many/belongs_to et has_many :through, la sécurité, le testing et le déploiement. Malgré cette liste impressionnante, il y a beaucoup plus à apprendre sur Rails. Comme première étape dans la poursuite de cet apprentissage, cette section propose quelques extensions à faire au cœur de votre application, ainsi que des suggestions d'apprentissages ultérieurs.

Avant de commencer à aborder ces extensions de l'application, c'est une bonne idée de fusionner vos changements et de déployer l'application online :

$ git add .
$ git commit -m "Ajout du suivi des utilisateurs"
$ git checkout master
$ git merge following-users
$ git push heroku
$ heroku rake db:migrate

12.4.1 Extensions de l'Application Exemple

Les extensions proposées dans cette section sont principalement inspirées soit par les fonctionnalités courantes des applications web, telles que les rappels de mot de passe et les confirmations d'email, ou des fonctionnalités plus spécifiques à certaines applications, comme la recherche, les réponses et la messagerie. Implémenter l'une ou l'autre de ces extensions vous aidera à faire la transition entre le suivi d'un tutoriel et l'écriture des applications de votre propre cru.

Ne vous étonnez pas si tout semble compliqué au premier abord ; la non connaissance d'une nouvelle fonctionnalité peut être très intimidant. Pour vous aider à commencer, laissez-moi vous donner quelques précieux conseils. Primo, avant d'ajouter une fonctionnalité à une application Rails, jetez une coup d'œil aux archives Railscasts pour voir si Ryan Bates n'aurait pas déjà couvert le sujet.25 Si ce railscast existe, commencer par le regarder peut vous faire économiser un temps considérable. Secondo, faites toujours des recherches Google sur la fonctionnalité que vous vous proposez d'installer pour trouver tous les posts de blog et les tutoriels concernés. Le développement d'application web est difficile, et cela peut aider d'apprendre à partir de l'expérience (et des erreurs) d'autrui.

Beaucoup des fonctionnalités suivantes constituent un véritable challenge, et j'ai donné quelques indices sur les outils dont vous pourriez avoir besoin pour les implémenter. Même avec ces indices, elles sont beaucoup plus compliquées que les exercices concluant les chapitres de ce livre, donc ne vous découragez surtout pas si vous ne parvenez pas à les implémenter qu'au prix d'un effort considérable. Les contraintes du temps ne m'offrent pas la possibilité de faire de l'assistance personnalisée, mais si votre demande peut avoir un intérêt significatif, je pourrais peut-être réaliser dans le futur des articles ou des screencasts autonomes sur certaines de ces extensions ; rendez-vous sur le site web du tutoriel Rails, à l'adresse http://www.railstutorial.org/ et souscrivez à la lettre d'information pour être informé des prochaines actualisations.

Réponses

Twitter permet à ses utilisateurs de faire des « @réponses » (replies), qui sont des micro-messages dont les premiers caractères sont le login de l'utilisateur précédé du signe arobase « @ ». Ces posts n'apparaissent que sur l'alimentation de l'utilisateur en question ou les utilisateurs qui suivent cet utilisateur. Implémentez une version simplifiée, en restreignant l'affichage des @replies à la seule alimentation du receveur (l'utilisateur qui reçoit le message) et de l'émetteur (l'utilisateur qui a écrit la réponse). Cela peut impliquer d'ajouter une colonne in_reply_to dans la table microposts et un champ d'application (scope) including_replies supplémentaire au modèle Micropost.

Puisque le utilisateur de votre application n'ont pas de logins uniques, vous devrez aussi décider d'une façon de les représenter. Une option consiste à utiliser une combinaison de l'id et du nom, tel que @1-michael-hartl. Une autre consiste à ajouter un username unique au processus d'inscription et de l'utiliser ensuite en @replies.

Messagerie

Twitter supporte la messagerie directe (et privée) en préfixant un micro-message avec la lettre « d ». Implémentez cette fonctionnalité à l'Application Exemple. La solution impliquera certainement un modèle Message et une recherche régulière sur les nouveaux micro-messages.

Notifications de suivi

Implémentez une fonctionnalité pour envoyer un mail aux utilisateurs quand ils reçoivent de nouveaux lecteurs. Rendez alors cet envoi optionnel, de telle sorte que les utilisateurs puissent le suspendre s'ils le souhaitent.

Entre autres choses, ajouter cette fonctionnalité requiert d'apprendre à envoyer un mail avec Rails. Il existe un Railscast on sending email pour vous aider à commencer. Soyez informé que la librairie Rails principale pour envoyer des emails, Action Mailer, a connu une importante révision avec Rails 3 comme le montre le Railscast on Action Mailer in Rails 3.

Rappel de mot de passe

En général, si les utilisateurs de votre application oublient leur mot de passe, ils n'ont aucun moyen de le retrouver. À cause du hachage à sens unique du mot de passe sécurisé du chapitre 7, notre application ne peut pas renvoyer par email le mot de passe de l'utilisateur, mais il peut lui envoyer un lien vers un formulaire d'initialisation. Introduisez une ressource PasswordReminders pour implémenter cette fonctionnalité. Pour chaque initialisation, vous devriez créer un « jeton unique » (unique token) et l'envoyer à l'utilisateur par mail. Visiter l'URL avec ce jeton devrait leur permettre de ré-initialiser leur mot de passe avec une valeur de leur choix.

Confirmation de l'inscription

Mise à part la vérification par expression régulière de l'adresse email, l'Application Exemple n'a pour le moment aucun moyen de vérifier la validité d'une adresse mail d'utilisateur. Ajoutez une vérification de cette adresse pour confirmer l'inscription. Cette nouvelle fonctionnalité devrait créer des utilisateurs dans un état « inactif », envoyer une URL d'activation et changer alors leur état vers « actif » à la visite de l'URL. Vous pourriez faire une recherche sur les termes state machines in Rails pour vous aider à exécuter la transition inactif/actif.

Alimentation RSS

Pour chaque utilisateur, implémentez une alimentation RSS pour leurs micro-messages. Implémentez ensuite une alimentation RSS pour leur état d'alimentation, en restreignant optionnellement l'accès à cette alimentation en utilisant un système d'authentification. Le Railscast on generating RSS feeds vous aidera à commencer.

API REST

De nombreux sites proposent une API (Application Programmer Interface, pour « Interface de programmation ») pour qu'une partie tierce puisse obtenir (get), poster (post), placer (put) et effacer (delete) les ressources de l'application. Implémentez une telle API REST pour votre Application Exemple. La solution impliquera d'ajouter des blocs respond_to (section 12.2.5) à de nombreuses actions de contrôleur de l'application ; celles-ci devraient répondre aux requêtes en XML. Attention au problème de sécurité ; l'API devrait être uniquement accessible aux seuls utilisateurs autorisés.

Recherche

Pour le moment, il n'y a pas d'autres moyens pour les utilisateurs de se retrouver que de consulter la page d'index ou de consulter les alimentations des autres utilisateurs. Implémentez une fonctionnalité de recherche pour y remédier. Ajoutez alors une autre fonctionnalité de recherche sur les micro-messages. Le Railscast on simple search forms vous aidera à commencer. Si vous déployez votre application en utilisant un hôte partagé ou un serveur dédié, je vous suggère d'utiliser Thinking Sphinx (en suivant Railscast on Thinking Sphinx). Si vous déployez l'application sur Heroku, vous devriez suivre les instructions de Heroku full text search.

12.4.2 Guide vers d'autres ressources

Il existe une multitude de ressources Rails dans les magasins et sur le web — l'offre est même tellement considérable que ça peut en être écrasant, et il peut être difficile de savoir par où commencer. Maintenant vous savez où commencer — avec ce livre, bien sûr ! Et si vous en êtes arrivé jusqu'ici, vous êtes prêt à tout. Voilà quelques suggestions :

  • Ruby on Rails Tutorial screencasts : j'ai préparé un cours complet par screencast s'appuyant sur ce livre. En addition pour couvrir tout le matériel de ce livre, les screencasts sont plein d'astuces, de trucs, et de démos « voyez-comment-ça-fonctionne » qui sont difficiles de rendre par l'écrit. Ils sont accessibles sur le Ruby on Rails Tutorial website, par Safari Books Online et par InformIT.
  • Railscasts : je ne saurais trop insister sur la grande qualité des Railscasts. Je suggère de commencer en visitant le site Railscasts episode archive et de cliquer sur le premier sujet qui retiendra votre attention.
  • Scaling Rails : l'un des sujets que nous n'avons presque pas abordé dans ce Tutoriel Ruby on Rails concerne la performance, l'optimisation et la mise à l'échelle (scaling). Heureusement, la plupart des sites ne rencontrent pas des problèmes de scaling, et utiliser quoi que ce soit au-delà de Rails serait probablement de l'optimisation prématurée. Si vous rencontrez des problèmes de performance, la série Scaling Rails par Gregg Pollack du Envy Labs est un bon endroit par où commencer. Je recommande aussi de visiter les applications de surveillance de site Scout et New Relic.26 Et, comme vous pouvez vous en douter maintenant, il existe des Railscasts sur ces sujets, parmi lesquels le profilage, la mise en cache, et les tâches de fond.
  • Livres Ruby and Rails : comme mentionné au chapitre 1, je recommande Beginning Ruby par Peter Cooper, The Well-Grounded Rubyist par David A. Black et The Ruby Way par Hal Fulton, ainsi que The Rails 3 Way par Obie Fernandez pour en savoir plus sur Rails.
  • PeepCode : j'ai mentionné plusieurs screencasters commerciaux au chapitre 1, mais le seul dont j'ai une solide expérience est PeepCode. Les screencasts à PeepCode sont de très haute qualité, et je les recommande chaudement.

12.5 Exercices

  1. Ajoutez des tests pour dependent :destroy dans le modèle Relationship (extrait 12.5 et extrait 12.17) en suivant l'exemple de l'extrait 11.11.
  2. La méthode respond_to vu dans l'extrait 12.36 peut en fait hisser des actions à l'intérieur du contrôleur Relationships lui-même, et les blocs respond_to peuvent être remplacés par une méthode Rails appelée respond_with. Démontrez que le code résultant, montré dans l'extrait 12.46, est correct en vérifiant que la suite de tests continue de réussir (pour les détails sur cette méthode, faites une recherche Google sur les termes « rails respond_with »).
  3. Les actions following et followers de l'extrait 12.29 contiennent encore de considérables redondances de code. Vérifiez que la méthode show_follow de l'extrait 12.47 élimine bien cette duplication (voyez si vous pouvez deviner ce que la méthode send fait, comme dans, par exeemple @user.send(:following)).
  4. Restructurez l'extrait 12.30 en ajoutant des partiels au code actuel des pages des lecteurs et des auteurs suivis, de la page d'accueil et de la page d'affichage de l'utilisateur.
  5. En suivant le modèle de l'extrait 12.20, écrivez des tests pour les statistiques dans la page de profil.
  6. Écrivez un test d'intégration pour le suivi et l'arrêt de suivi de l'utilisateur.
  7. Ré-écrivez les méthodes Ajax de la section 12.2.5 en utilisant jQuery à la place de Prototype. Astuce : vous voudrez peut-être lire la section jQuery de Mikel Lindsaar’s blog post about jQuery, RSpec, and Rails 3.
Extrait 12.46. Une restructuration compacte de l'extrait 12.36.
class RelationshipsController < ApplicationController
  before_filter :authenticate

  respond_to :html, :js

  def create
    @user = User.find(params[:relationship][:followed_id])
    current_user.follow!(@user)
    respond_with @user
  end

  def destroy
    @user = Relationship.find(params[:id]).followed
    current_user.unfollow!(@user)
    respond_with @user
  end
end
Extrait 12.47. Actions following et followers restructurées.
app/controllers/users_controller.rb
class UsersController < ApplicationController
  .
  .
  .
  def following
    show_follow(:following)
  end

  def followers
    show_follow(:followers)
  end

  def show_follow(action)
    @titre = action.to_s.capitalize
    @user = User.find(params[:id])
    @users = @user.send(action).paginate(:page => params[:page])
    render 'show_follow'
  end
  .
  .
  .
end
  1. Les photographies des maquettes viennent de http://www.flickr.com/photos/john_lustig/2518452221/ et http://www.flickr.com/photos/30775272@N05/2884963755/
  2. Pour la simplicité, l'illustration 12.6 supprime la colonne id de la table following
  3. Malheureusement, Rails utilise connection pour une connexion la base de données, donc introduire un modèle de nom « Connection » conduit à des bogues plutôt subtils (j'ai appris cela à mes dépends en développant Insoshi). (NdT. Encore une fois, ce problème ne se poserait pas en français, où « connexion » s'écrit avec un « x », pas avec « _ct_ion ») 
  4. En effet, cette construction est tellement caractéristique de Rails que le fameux programmeur Rails Josh Susser l'utilise comme nom de son blog
  5. Techniquement, Rails utilise la méthode underscore pour convertir la classe nom vers un id. Par exemple, "FooBar".underscore est foo_bar, donc la clé étrangère d'un objet FooBar devrait être foo_bar_id (incidemment, l'inverse de underscore est camelize, qui convertit camel_case vers CamelCase). 
  6. Si vous n'avez pas noté que followed_id identifie aussi un utilisateur, et concerne le traitement asymétrique des suivis et des suiveurs, vous êtes hors-jeu. Nous traiterons ce problème à la section 12.1.5
  7. Cette méthode follow! devrait toujours fonctionner, donc (en suivant le modèle de create! et save!) nous indiquons avec l'exclamation qu'une exception (une erreur) sera déclenchée en cas d'échec. 
  8. Une fois que vous aurez une grande expérience de la modélisation d'un domaine particulier, vous pourrez souvent deviner à l'avance de telles méthodes utilitaires, et même quand vous ne pourrez pas, vous trouverez souvent de vous-même que les écrire rend les tests plus clairs. Dans le cas présent, cependant, ça n'est pas si grave si vous ne les aviez pas anticipées. Le développement de logiciel est souvent un processus itératif — vous écrivez du code jusqu'à ce qu'il s'enlaidisse, et alors vous le restructurez — mais la brièveté de la présentation d'un tutoriel doit quelque peu simplifier ce processus. 
  9. La méthode authenticate_with_salt est incluse simplement en vous orientant à l'intérieur du fichier du modèle User. 
  10. La méthode unfollow! ne provoque pas d'erreur à l'échec — en fait, je ne sais même pas comment Rails indique un échec de destruction — mais nous utilisons un point d'exclamation pour maintenant la symétrie follow!/unfollow!
  11. Vous pouvez avoir noté que quelquefois nous accédons à id explicitement, comme dans followed.id et d'autrefois nous utilisons juste followed. J'ai honte d'admettre que mon algorithme habituel pour dire quand savoir lequel utiliser consiste simplement à voir si ça marche sans id et alors de l'ajouter si ça ne fonctionne pas. 
  12. Tout dans l'extrait 12.28 a été couvert quelque part dans ce tutoriel, donc c'est un bon exercice de lire ce code. 
  13. Parce que c'est nominalement un acronyme de asynchronous JavaScript and XML, Ajax est quelquefois écrit « AJAX », même si l'article sur l'Ajax original l'épelle « Ajax » tout au long du texte. 
  14. Cela ne fonctionne bien sûr que si JavaScript est autorisé dans le navigateur, mais Rails s'adapte gracieusement dans le cas contraire, rendant le fonctionnement identique à celui de la section 12.2.4 dans le cas où JavaScript se serait pas autorisé. 
  15. À ce stade, si vous ne l'avez déjà fait, vous aurez à inclure la Librairie JavaScript Prototype par défaut dans votre application Rails comme dans l'extrait 10.39
  16. Il n'y a pas de relation entre ce respond_to et le respond_to utilisé dans les exemples RSpec. 
  17. La principale exigence des ces objets enumerable est qu'ils doivent implémenter la méthode each pour boucler (itérer) sur leur collection d'éléments. 
  18. La notation était en fait une extension Rails faite au cœur du langage Ruby ; elle a été jugée si utile qu'elle a été maintenant incorporée à Ruby lui-même. Elle est pas belle la vie ? 
  19. L'appel de paginate sur un objet Array le convertit en objet WillPaginate::Collection, mais ça ne nous aide pas plus puisque le tableau entier a déjà été créé en mémoire. 
  20. Une fonction « empaquetée » avec une partie de données (un utilisateur dans ce cas) porte le nom de closure, comme nous l'avons vu brièvement au cours d'une discussion sur les blocs à la section 4.3.2
  21. Pour une façon plus avancée de créer la sous-sélection désirée, consultez le post de blog « Hacking a subselect in ActiveRecord ». 
  22. Bien entendu, les sous-sélections ne fonctionneront pas dans tous les cas. Pour des sites plus conséquents, vous devrez probablement générer l'alimentation de façon asynchrone en utilisant une tâche de fond. De telles subtilités dépassent largement le cadre de ce tutoriel, mais le screencast Scaling Rails est une bonne façon de commencer. 
  23. Dans le but de faire une alimentation d'aspect plus sympa pour l'illustration 12.20, j'ai ajouté quelque micro-messages supplémentaires à la main en utilisant la console Rails. 
  24. Vous pouvez le vérifier en examinant les retours SQL dans le fichier journal du serveur de développement (le screencast du Tutoriel Rails couvrira de façon plus approfondie de telles subtilités). 
  25. Ma seule réserve concernant Railscasts est qu'ils omettent souvent les tests. C'est probablement nécessaire pour garder l'épisode sympa et court, mais cela pourrait vous donner une mauvaise idée de l'importance de ces tests. Une fois que vous avez regardé le Railscast concerné pour vous faire une idée de base de comment procéder, je vous suggère d'écrire la nouvelle fonctionnalité en utilisant le « Développement Dirigé par les Tests ». 
  26. En plus d'être une phrase intelligente — nouvelle relique est une contradiction dans les termes (un oxymore. NdT) — New Relic est aussi l'anagramme du nom du créateur de la compagnie, Lew Cirne.