¡La versión 2.0 de Play ya está lista! Ayúdanos a traducir la documentación de la útlima versión y sigue nuestro progreso.

Manuales, tutoriales & referencias

Consulte

Contenidos

Elija la versión

Buscar

Busque con google

Libros

Agregando soporte para tags

A medida que nuestro blog contenga más posts, se tornará cada vez más difícil encontrarlos. Para ayudar a clasificar las entradas del blog según su tema, agregaremos soporte para tags.

El modelo Tag

Agregaremos un objeto más a la definición de los modelos de nuestro blog. La clase Tag en sí misma, será sumamente 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);
    }
}

Dado que queremos agregar cada tag únicamente al momento de ser utilizado, crearemos un método que en caso de no encontrar el tag especificado lo cree, y de existir el mismo simplemente retorne una instancia del tag especificado. Crearemos este método en la clase Tag y lo llamaremos findOrCreateByName(String name):

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

Aplicando tags a los posts

Ahora deberemos vincular el modelo Tag con el modelo Post. Agreguemos las correspondientes relaciones a la clase 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();
}
…

En este caso utilizamos un TreeSet para mantener la lista de tags ordenadas de una manera específica (en orden alfabético, en este caso, debido a la implementación que hicimos de compareTo).

Mantendremos esta relación de manera unidireccional.

Además, agregaremos algunos métodos que nos ayuden a administrar los tags de manera más simple. El primero de ellos simplemente aplicará un Tag a un Post:

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

El siguiente, retornará todos los posts marcados con un tag específico:

…
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();
}
…

Ahora escribiremos un nuevo caso de prueba para probar esta funcionalidad. Reinicie el servidor en modo test mediante el siguiente comando:

$ play test

Ahora agregue un nuevo @Test a la clase 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());
}

Aségurese de que todo funcione correctamente.

Múltiples tags

Bueno, por ahora no lo utilizaremos en nuestro blog, ¿pero qué pasaría si quisiéramos traer los posts marcados con más de tag? Es más complicado de lo que parece.

Le brindamos la consulta JPQL, ya que seguramente le será de utilidad en varios proyectos:

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

El secreto está en que tenemos que usar la sentencia having count para filtrar aquellos posts que tienen exactamente todos los tags de la vista vinculada.

Tenga en cuenta que en este caso no podemos utilizar Post.find("…", tags, tags.count) porque tags ya es un vararg.

Puede verificarlo agregando más pruebas al test anterior:

…
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());
…

La nube de tags

Si tenemos tags, vamos a necesitar una nube de tags. Agreguemos un método a la clase Tag para generar la nube de tags:

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 order by t.name"
    ).fetch();
    return result;
}

Aquí hacemos uso de una funcionalidad de Hibernate que nos permite retornar un objeto a partir de una consulta JPA. En este caso traemos una List que contiene un Map para cada tag con dos claves: tag para el nombre del tag y pound la la cantidad de posts marcados con ese tag.

Verifiquémoslo agregando algunas pruebas a nuestro test de tags:

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

Agreagando tags a la interfaz de usuario de nuestro Blog

Ahora podemos utilizar los tags para agregar nuevas maneras de navegar por nuestro blog. Como de costumbre, para trabajar más cómodos, necesitaremos agregar algunos tags de prueba a nuestro conjunto inicial de datos.

Modifique el archivo /yabe/conf/initial-data.yml para agregar varios tags a nuestros posts de prueba. Por ejemplo:

…
Tag(play):
    name:           Play
 
Tag(architecture):
    name:           Architecture
 
Tag(test):
    name:           Test
 
Tag(mvc):
    name:           MVC
…

Y luego agréguelos a la declaración de los posts:

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

Agregue la declaración de los Tags al principio del archivo YAML, dado que necesitan ser creados antes de que un Post pueda referenciarlos.

Tendrá que reiniciar la aplicación para obligar a play a cargar el nuevo conjunto de datos. Note como Play incluso le advierte si hay problemas en los archivo YAML:

Luego modifique el tag #{display /} para mostrar el conjunto de tags en el modo de vista de post completa. Edite el archivo /yabe/app/views/tags/display.html:

…
#{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}
…

Consultando las entradas según sus tags

Ahora podemos agregar una nueva página para mostrar los posts que están marcados con ciertos tags. En el tag #{display /} que mostramos más arriba, dejamos el href del link en blanco; ahora en su lugar pondremos un link a la nueva acción listTagged:

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

Y crearemos el correspondiente método de acción en el controlador Application:

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

Como siempre, también crearemos una ruta para mantener nuestros URIs prolijos:

GET     /posts/{tag}                    Application.listTagged

Aquí nos encontramos con un problema, dado que esta ruta entra en conflicto con una ya existente. Las siguientes dos rutas coinciden con el mismo URI:

GET     /posts/{id}                     Application.show
GET     /posts/{tag}                    Application.listTagged

Sin embargo, dado que asumiremos que un id es numérico mientras que un tag no lo es, podremos resolver fácilmente este conflicto utilizando una expresión regular para restringir el alcance de la primera ruta:

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

Por último, tan sólo tenemos que crear el template /yabe/app/views/Application/listTagged.html que será utilizado por la nueva acción listTagged:

#{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>

Ahora vaya a Crear una página de administación básica utilizando CRUD.