Développer une application avec le framework Play !


précédentsommairesuivant

VII. Support des tags

Etant donné que notre blog hébergera plusieurs billets, il deviendra de plus en plus difficile de les retrouver. Pour nous aider à les classer selon le sujet qu'ils traitent, nous allons ajouter le support des tags (ou catégorie).

VII-A. L'objet modèle Tag

Nous commençons par ajouter un nouvel objet dans notre modèle. La classe Tag est très simple :

 
Sélectionnez
package models;
 
import java.util.*;
import javax.persistence.*;
 
import play.db.jpa.*;
 
@Entity
public class Tag extends Model implements Comparable<Tag> {
 
    public String name;
    
    private Tag(String name) {
        this.name = name;
    }
    
    public String toString() {
        return name;
    }
    
    public int compareTo(Tag otherTag) {
        return name.compareTo(otherTag.name);
    }
 
}

A chaque fois que nous aurons besoin de récupérer un nouvel objet Tag, nous utiliserons la méthode findOrCreateByName(String name). Ajoutons cette méthode à la classe Tag :

 
Sélectionnez
public static Tag findOrCreateByName(String name) {
    Tag tag = Tag.find("byName", name).first();
    if(tag == null) {
        tag = new Tag(name);
    }
    return tag;
}

VII-B. Tagguer les billets

La nouvelle étape consiste à lier l'objet Post au nouvel objet Tag. Ajoutons cette relation dans la classe Post :

 
Sélectionnez
...
@ManyToMany(cascade=CascadeType.PERSIST)
public Set<Tag> tags;
    
public Post(User author, String title, String content) { 
    this.comments = new ArrayList<Comment>();
    this.tags = new TreeSet<Tag>();
    this.author = author;
    this.title = title;
    this.content = content;
    this.postedAt = new Date();
}
...

Il est à noter que nous utilisons ici un TreeSet afin de conserver les objets dans un ordre spécifique (plus précisément dans l'ordre alphabétique, étant donné notre implémentation de la méthode compareTo).

Notre relation restera unidirectionnelle.

Nous modifions notre classe Post afin d'ajouter des méthodes qui nous aiderons à gérer les tags d'un billet. Tout d'abord, une méthode nous permettant de tagguer un billet :

 
Sélectionnez
public Post tagItWith(String name) {
    tags.add(Tag.findOrCreateByName(name));
    return this;
}

Egalement, une méthode nous permettant de retrouver tous les billets d'un tag donné :

 
Sélectionnez
public static List<Post> findTaggedWith(String tag) {
    return Post.find(
        "select distinct p from Post p join p.tags as t where t.name = ?", tag
    ).fetch();
}

Il est temps d'écrire un nouveau test pour vérifier ce que nous avons écrit. Redémarrons notre serveur en mode test :

 
Sélectionnez
> play test

Ajoutons un nouveau @Test dans la classe BasicTest :

 
Sélectionnez
@Test
public void testTags() {
    // Create a new user and save it
    User bob = new User("bob@gmail.com", "secret", "Bob").save();
 
    // Create a new post
    Post bobPost = new Post(bob, "My first post", "Hello world").save();
    Post anotherBobPost = new Post(bob, "Hop", "Hello world").save();
    
    // Well
    assertEquals(0, Post.findTaggedWith("Red").size());
    
    // Tag it now
    bobPost.tagItWith("Red").tagItWith("Blue").save();
    anotherBobPost.tagItWith("Red").tagItWith("Green").save();
    
    // Check
    assertEquals(2, Post.findTaggedWith("Red").size());        
    assertEquals(1, Post.findTaggedWith("Blue").size());
    assertEquals(1, Post.findTaggedWith("Green").size());
    
}

VII-C. Corsons un peu les choses !

Bien que nous n'allions pas en faire usage pour l'instant, que se passerait-il si nous voulions récupérer les billets qui seraient marqués par plusieurs tags ? Il s'agit d'une tâche plus difficile qu'il n'y parait...

Voici le code JPQL qui nous permet de résoudre ce problème :

 
Sélectionnez
public static List<Post> findTaggedWith(String... tags) {
    return Post.find(
        "select distinct p.id from Post p join p.tags as t where t.name in (:tags) group by p.id having count(t.id) = :size"
    ).bind("tags", tags).bind("size", tags.length).fetch();
}

Le point délicat ici est que nous avons besoin de réaliser un test sur le nombre de tags que possède le billet.

Nous pouvons améliorer notre précédent test en y ajoutant les assertions suivantes :

 
Sélectionnez
...
assertEquals(1, Post.findTaggedWith("Red", "Blue").size());   
assertEquals(1, Post.findTaggedWith("Red", "Green").size());   
assertEquals(0, Post.findTaggedWith("Red", "Green", "Blue").size());  
assertEquals(0, Post.findTaggedWith("Green", "Blue").size());
...

VII-D. Le nuage de tags

Nous avons désormais nos tags, nous voulons maintenant un nuage de tags. Pour ce faire, ajoutons une méthode à notre classe Tag qui génèrera ce nuage :

 
Sélectionnez
public static List<Map> getCloud() {
    List<Map> result = Tag.find(
        "select new map(t.name as tag, count(p.id) as pound) from Post p join p.tags as t group by t.name"
    ).fetch();
    return result;
}

Dans cet exemple, nous utilisons une fonctionnalité d'Hibernate qui retourne un objet particulier à partir d'une requête JPA. Ici, notre méthode retourne une List qui contiendra pour chaque tag une Map contenant deux valeurs : le nom du tag ainsi que son "poids", c'est-à-dire le nombre de billets attachés à ce tag.

Finalisons notre classe de test en y ajoutant ceci :

 
Sélectionnez
List<Map> cloud = Tag.getCloud();
assertEquals(
    "[{tag=Red, pound=2}, {tag=Blue, pound=1}, {tag=Green, pound=1}]", 
    cloud.toString()
);

VII-E. Ajoutons les tags à l'interface graphique

Nous sommes prêts à ajouter la gestion des tags de façon à offrir une nouvelle façon de naviguer parmi les billets de notre blog. Comme toujours, pour rendre notre travail efficace, nous allons ajouter des données propres aux tags dans notre jeu de données de tests.

Modifions le fichier /yabe/conf/initial-data.yml et ajoutons-y des informations sur les tags, comme par exemple :

 
Sélectionnez
... 
Tag(play):
    name:           Play
 
Tag(architecture):
    name:           Architecture
 
Tag(test):
    name:           Test
 
Tag(mvc):
    name:           MVC    
...

Modifions également la donnée d'un billet :

 
Sélectionnez
...
Post(jeffPost):
    title:          The MVC application
    postedAt:       2009-06-06
    author:         jeff
    tags:           
                    - play
                    - architecture
                    - mvc
    content:        >
                    A Play
...

Pensez à ajouter la déclaration des tags au début du fichier, car il est nécessaire qu'ils soient insérés en base avant les posts.

Nous devons forcer le redémarrage de l'application afin que les données initiales soient prises en compte. Nous constatons que Play est aussi à même de détecter les problèmes présents dans le fichier YAML :

Image non disponible

Nous pouvons modifier le tag #{display /} afin d'afficher l'ensemble des tags liés au billet dans sa vue complète. Ouvrons le fichier /yabe/app/views/tags/display/html :

 
Sélectionnez
...
#{if _as != 'full'}
    <span class="post-comments">
        &nbsp;|&nbsp; ${_post.comments.size() ?: 'no'} 
        comment${_post.comments.size().pluralize()}
        #{if _post.comments}
            , latest by ${_post.comments[0].author}
        #{/if}
    </span>
#{/if}
#{elseif _post.tags}
    <span class="post-tags">
        - Tagged 
        #{list items:_post.tags, as:'tag'}
            <a href="#">${tag}</a>${tag_isLast ? '' : ', '}
        #{/list}
    </span>
#{/elseif}
...
Image non disponible

VII-F. La nouvelle page "taggué avec"

Nous avons tout à notre disposition pour pouvoir lister les billets selon leurs tags. Ci-dessus, nous avons laissé le lien vide, remplaçons-le désormais par un lien vers une nouvelle action listTagged :

 
Sélectionnez
...
- Tagged 
#{list items:_post.tags, as:'tag'}
    <a href="@{Application.listTagged(tag.name)}">${tag}</a>${tag_isLast ? '' : ', '}
#{/list}
...

Ajoutons la méthode Java :

 
Sélectionnez
public static void listTagged(String tag) {
    List<Post> posts = Post.findTaggedWith(tag);
    render(tag, posts);
}

N'oublions pas de spécifier la route dans le fichier adéquat :

 
Sélectionnez
GET     /posts/{tag}                    Application.listTagged

Nous avons ici un problème, car une route existante entre en conflit avec cette nouvelle route. Ces deux routes sont liées à la même URI :

 
Sélectionnez
GET     /posts/{id}                     Application.show
GET     /posts/{tag}                    Application.listTagged

Toutefois, comme nous considérons que les ID sont des valeurs numériques contrairement aux tags, nous pouvons résoudre ce problème en utilisant une expression régulière pour restreindre la première route :

 
Sélectionnez
GET     /posts/{<[0-9]+>id}             Application.show
GET     /posts/{tag}                    Application.listTagged

Pour finir, nous n'avons qu'à créer le template /yabe/app/views/Application/listTagged.html qui sera utilisé par cette nouvelle action :

 
Sélectionnez
#{extends 'main.html' /}
#{set title:'Posts tagged with ' + tag /}
 
*{********* Title ********* }*
 
#{if posts.size() > 1}
   <h3>There are ${posts.size()} posts tagged '${tag}'</h3>  
#{/if} 
#{elseif posts}
    <h3>There is 1 post tagged '${tag}'</h3>  
#{/elseif}
#{else}
    <h3>No post tagged '${tag}'</h3>
#{/else}
 
*{********* Posts list *********}*
 
<div class="older-posts">    
    #{list items:posts, as:'post'}
        #{display post:post, as:'teaser' /}
    #{/list}
</div>
Image non disponible

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.