Django es el entorno de desarrollo web para perfeccionistas con límites de tiempo

Entradas sobre "modelos":

Agrupar mediante el templatetag {% regroup %} con una función propia

El templatetag {% regroup %} del lenguaje de plantillas de Django nos permite agrupar un listado de objetos por una propiedad. Generalmente utilizamos un campo por el que agrupar pero también podemos utilizar una función propia para agrupar los objetos como queramos.

Vamos a ver un ejemplo sencillo en el que agruparemos un listado de artículos por su fecha de publicación, dividiéndolos en artículos publicados "Esta semana", "La semana pasada" y "Hace X semanas". El siguiente models.py tiene un modelo Articulo con una fecha de publicación que nos servirá para hacer la agrupación. La función semana_group del modelo nos permitirá separar los artículos en grupos distintos según la diferencia de días entre hoy y la fecha de publicación de cada artículo:

from django.db import models
import datetime

class Articulo(models.Model):
    titulo = models.CharField(max_length=250)
    fecha = models.DateField()

    def semana_group(self):
        hoy = datetime.date.today()
        dias = (hoy - self.fecha).days

        if dias <= 7:
            group = 'Esta semana'
        elif dias <= 14:
            group = 'La semana pasada'
        else:
            group = 'Hace %s semanas' % str(dias/7)

        return group

En nuestra vista nos bastaría con listar todos los artículos o filtrar por cualquier atributo. Por ejemplo:

articulos = Articulo.objects.all()

Y en nuestra plantilla es donde utilizaremos el templatetag {% regroup %} agrupando el listado de artículos por el método semana_group que hemos añadido al modelo:

{% regroup articulos by semana_group as articulos_por_semana %}

{% for articulos in articulos_por_semana %}
    <h2>{{ articulos.grouper }}</h2>
    <ul>
    {% for articulo in articulos.list %}
        <li>{{ articulo.titulo }}</li>
    {% endfor %}
    </ul>
{% endfor %}

Publicado por Antonio Melé el Sábado 21 de Agosto de 2010 | 2 comentarios | Categorías: modelos, plantillas, trucos

Aplicar filtros a annotate() al usar Count()

A veces queremos utilizar la función de aggregación annotate() y aplicar filtros al modelo que se encuentra dentro de la misma, pero no es posible hacerlo con el ORM de Django actualmente. En este caso la solución pasa por usar la función extra() del ORM de Django para incluir SQL propio.

Vamos a ver un ejemplo práctico. Supongamos una aplicación llamada ocio y el siguiente models.py con hobbies y personas que los practican:

from django.db import models

class Hobby(models.Model):
    nombre = models.CharField(max_length=250)

SEXO_CHOICES = (
    ('m','Masculino'),
    ('f','Femenino'),
)

class Persona(models.Model):
    nombre = models.CharField(max_length=250)
    sexo = models.CharField(max_length=1, choices=SEXO_CHOICES)
    hobbies = models.ManyToManyField(Persona, related_name='personas')

Gracias a las funciones de agregación de Django podemos obtener fácilmente todos los hobbies y el número de personas que lo practican utilizando annotate(). En el siguiente ejemplo vemos que para cada hobby se contará el número de personas relacionadas y será accesible mediante el atributo personas_count:

from django.db.models import Count

hobbies = Hobby.objects.annotate(personas_count=Count('personas_count'))

for hobby in hobbies:
    print '%s personas practican %s' % (hobby.personas_count, hobby.nombre)

Con esto contamos todas las personas relacionadas con cada hobby, pero a veces nos interesa filtrar las personas que se contabilizan. ¿Y si sólo quisiéramos obtener el número de mujeres que practican cada hobby? En este caso queremos filtrar por un atributo del modelo que se está utilizando en annotate() y de momento no se puede hacer con el ORM de Django.

Para obtener el número de mujeres que practican cada hobby lo primero que haremos será obtener mediante values_list() un listado de los id de las personas que cumplan la condición sexo='f':

# obtenemos lista en python, p. ej: [13, 21, 60]
mujeres_ids = Persona.objects.filter(sexo='f').values_list('id', flat=True)

A continuación convertiremos la lista a un string con id's separados por comas para poder utilizarlos en nuestra sentencia SQL:

# id's separados por comas en string, p. ej: "13,21,60"
lista_ids = ','.join(str(id) for id in mujeres_ids)

Después prepararemos la sentencia SQL que utilizaremos en nuestro queryset(). Recordemos que por defecto Django crea las tablas de la base de datos con la nomenclatura aplicacion_modelo. Por lo que si nuestra aplicación se llama ocio, la tabla para el modelo Persona será ocio_persona y para el modelo Hobby será ocio_hobby. Para la relación M2M Django genera una tabla intermedia que relaciona los id's de personas con los id's de hobbies. Esta tabla en nuestro caso será ocio_persona_hobbies ya que el campo de la relación ManyToMany se llama hobbies y se encuentra en el modelo Persona.

Teniendo en cuenta que la query va a hacerse sobre el modelo Hobby utilizaremos una consulta SELECT que realice un COUNT sobre la tabla que relaciona personas con hobbies de tal forma que por cada hobby (ocio_hobby.id) contemos el número de filas en los que el id de la persona relacionada se encuentre en la lista de id's de mujeres.

'SELECT COUNT(*) FROM ocio_persona_hobbies WHERE ocio_persona_hobbies.hobby_id = ocio_hobby.id AND ocio_persona_hobbies.persona_id IN (%s)' % lista_ids

Por último prepararemos nuestra queryset() utilizando extra() para incluir nuestro código SQL. Así quedará el código completo:

mujeres_ids = Persona.objects.filter(sexo='f').values_list('id', flat=True)
lista_ids = ','.join(str(id) for id in mujeres_ids)

hobbies = Hobby.objects.extra(
    select = {
        'personas_count': 'SELECT COUNT(*) FROM ocio_persona_hobbies WHERE ocio_persona_hobbies.hobby_id = ocio_hobby.id AND ocio_persona_hobbies.persona_id IN (%s)' % lista_ids
    },
)

Si queremos hacer nuestro código independiente de los nombres de las tablas de la base de datos podemos usar Persona._meta.db_table para obtener el nombre de la tabla usada para el modelo Persona y del mismo modo para el modelo Hobby. Para obtener el nombre de la tabla que relaciona personas y hobbies usaremos Persona._meta.get_field('hobbies').m2m_db_table().

El código independiente de las tablas quedaría de la siguiente manera:

mujeres_ids = Persona.objects.filter(sexo='f').values_list('id', flat=True)
lista_ids = ','.join(str(id) for id in mujeres_ids)

personas_hobbies = Persona._meta.get_field('hobbies').m2m_db_table()

hobbies = Hobby.objects.extra(
    select = {
        'personas_count': 'SELECT COUNT(*) FROM %s WHERE %s.hobby_id = %s.id AND %s.persona_id IN (%s)' % (personas_hobbies, personas_hobbies, Hobby._meta.db_table, personas_hobbies, lista_ids)
    },
)

Publicado por Antonio Melé el Viernes 13 de Agosto de 2010 | 0 comentarios | Categorías: modelos, querysets, trucos, tutorial

Actualización de seguridad Django 1.0 y 1.1

Hace un par de días se publicó una importante actualización de seguridad para Django 1.0, 1.1 y la versión de desarrollo. El agujero de seguridad afecta a cualquier aplicación que utiliza campos EmailField ó URLField en sus modelos. El fallo se debe a la validación de direcciones de e-mail y URLs mediante expresiones regulares. Algunas direcciones podían producir un rendimiento negativo en el sistema al aplicarles las expresiones regulares haciendo que el thread correspondiente dejara de responder y se consumieran más recursos de la CPU pudiendo resultar en una denegación de servicio si se hiciera deliberadamente.

Publicado por Antonio Melé el Lunes 12 de Octubre de 2009 | 1 comentario | Categorías: modelos, versiones

Crear una imagen de nuestros modelos con django-command-extensions

Algo interesante que nos aporta django-command-extensions es poder crear una representación gráfica de nuestros modelos (o por decirlo de otro modo nuestro esquema de base de datos) con tan sólo un comando. Esto es posible gracias a GraphViz y el resultado es algo parecido a un diseño UML. Para poder utilizar este comando debemos tener instalado pygraphviz y por supuesto la aplicación django_extensions debe estar incluída en el setting INSTALLED_APPS de nuestro proyecto.

Para instalar pygraphviz en Linux nos bastará con utilizar el comando:

apt-get install python-pygraphviz

Una vez hemos instalado django-command-extensions y pygraphviz podremos crear un archivo dot, formato utilizado por GraphViz ó una imagen. En nuestro caso vamos a crear un archivo PNG que incluya los modelos de todas las aplicaciones de nuestro proyecto. Para ello usamos el comando:

./manage.py graph_models -a -g -o mis_modelos.png

Con el parámetro -o especificamos el archivo de imagen en el que queremos que se almacene el resultado. Si sólo queremos incluir los modelos de algunas aplicaciones podemos hacerlo con el siguiente comando:

./manage.py graph_models app1 app2 app3 -o mis_modelos.png

Este ejemplo es de los modelos de la PyCon-Tech, un framework de gestión de conferencias basado en Django:

Representación gráfica de modelos con django-command-extensions y graphviz

Publicado por Antonio Melé el Domingo 12 de Julio de 2009 | 0 comentarios | Categorías: aplicaciones, imágenes, modelos, pluggables, trucos, tutorial