Una de las cosas que los buscadores tienen en cuenta para establecer la relevancia de sus resultados son las palabras que aparecen en las URLs. Por eso conviene que las URLs de nuestros objetos incluyan un slug que los represente.
Un slug es una cadena sin caracteres especiales, en minúsculas y con guiones en lugar de espacios, óptima para ser utilizada en URLs. Con un slug podríamos hacer que una URL tipo /negocios/33/ sea /negocios/mi-negocio/. En este caso el slug sería mi-negocio.
Django tiene un campo SlugField que podemos usar en nuestros modelos precisamente para eso. Si tenemos un campo slug que es único la siguiente URL nos sirve como URL del objeto:
url(r'^negocio/(?P<slug>[-\w]+)/$', mi_vista, name='mi_vista')
Si el slug no es único podemos utilizar la combinación de id y slug. Conviene que el slug aparezca primero ya que los buscadores dan más relevancia a las palabras que aparecen antes en la propia URL:
url(r'^negocio/(?P<slug>[-\w]+)/(?P<id>\d+)/$', mi_vista, name='mi_vista')
Si el slug tiene que generarse dinámicamente de forma transparente para el usuario podemos hacerlo en el método save() del modelo. Podemos utilizar el filtro slugify de django...
from django.db import models
from django.template import defaultfilters
class Negocio(models.Model):
nombre = models.CharField(max_length=100)
slug = models.SlugField(max_length=100)
def save(self, *args, **kwargs):
self.slug = defaultfilters.slugify(self.nombre)
super(Negocio, self).save(*args, **kwargs)
...o bien slughifi que es más cuidadoso al generar slugs transformando caracteres acentuados en sus respectivos caracteres sin acentos.
from slughifi import slughifi
# ...
def save(self, *args, **kwargs):
self.slug = slughifi(self.nombre)
super(Negocio, self).save(*args, **kwargs)
Generar el slug dinámicamente de forma transparente para el usuario tiene un problema: El slug puede variar y por lo tanto la URL puede cambiar. Podemos aplicar dos soluciones:
Generar el slug sólo la primera vez que se guarda el objeto:
def save(self, *args, **kwargs):
if not self.id:
self.slug = slughifi(self.nombre)
super(Negocio, self).save(*args, **kwargs)
La ventaja es que el slug se mantendrá aunque el nombre cambie. La desventaja que el nombre y el slug pueden acabar teniendo poco que ver el uno con el otro.
Regenerar el slug cada vez que se actualice el objeto pero ofrecer la posibilidad de que las URLs antiguas sigan funcionando. Podemos conseguirlo haciendo lo siguiente en nuestra vista:
from django.http import HttpResponsePermanentRedirect
from django.shortcuts import get_object_or_404
def mi_vista(request, slug, id):
try:
negocio = Negocio.objects.get(slug=slug, id=id)
except ObjectDoesNotExist:
negocio = get_object_or_404(Negocio, id=id)
return HttpResponsePermanentRedirect(negocio.get_absolute_url())
# ...
De esta forma el slug podrá cambiar si hace falta y para cualquier enlace que exista hacia la URL antigua se devolverá una redirección permanente a la nueva URL.
Publicado por Antonio Melé el Domingo 29 de Agosto de 2010 | 0 comentarios | Categorías: trucos, urls
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
Vamos a ver cómo guardar un archivo a partir de su URL. Este método nos permitirá descargar y guardar cualquier archivo de Internet y nos servirá tanto para campos ImageField como FileField.
Como ejemplo vamos a ver un modelo con un campo imagen como el siguiente:
from django.db import models
class Foto(models.Model):
imagen = models.ImageField(upload_to='archivos/')
Para guardar cualquier imagen de una URL descargaremos el archivo utilizando urllib y lo guardaremos de la siguiente manera:
import urllib
from django.core.files import File
from models import Foto
url = 'http://www.sitio.com/imagen.jpg'
content = urllib.urlretrieve(url)
nombre = url.split('/')[-1]
f = Foto()
f.imagen.save(nombre, File(open(content[0])), save=True)
Publicado por Antonio Melé el Sábado 14 de Agosto de 2010 | 1 comentario | Categorías: fields, trucos
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
Suscríbete a nuestro feed RSS y al feed de la comunidad para estar al tanto de todo lo que ocurre entorno a Django.
Tú también puedes escribir en éste blog. Para hacerlo basta con que nos digas sobre qué quieres escribir un artículo relacionado con Django.
Utilizar un formulario para modificar 2 modelos
Descubriendo objetos similares por sus etiquetas