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 :
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 :
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 :
...
@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 :
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é :
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 :
> play testAjoutons un nouveau @Test dans la classe BasicTest :
@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 :
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 :
...
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 :
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 :
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 :
...
Tag(play):
name: Play
Tag(architecture):
name: Architecture
Tag(test):
name: Test
Tag(mvc):
name: MVC
...Modifions également la donnée d'un billet :
...
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 :
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 :
...
#{if _as != 'full'}
<span class="post-comments">
| ${_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}
...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 :
...
- Tagged
#{list items:_post.tags, as:'tag'}
<a href="@{Application.listTagged(tag.name)}">${tag}</a>${tag_isLast ? '' : ', '}
#{/list}
...Ajoutons la méthode Java :
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 :
GET /posts/{tag} Application.listTaggedNous 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 :
GET /posts/{id} Application.show
GET /posts/{tag} Application.listTaggedToutefois, 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 :
GET /posts/{<[0-9]+>id} Application.show
GET /posts/{tag} Application.listTaggedPour finir, nous n'avons qu'à créer le template /yabe/app/views/Application/listTagged.html qui sera utilisé par cette nouvelle action :
#{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>




