Ver índice de contenidos del libro

14.2. Sites

El sistema sites de Django es un framework genérico que te permite operar múltiples sitios web desde la misma base de datos, y desde el mismo proyecto de Django. Éste es un concepto abstracto, y puede ser difícil de entender, así que comenzaremos mostrando algunos escenarios en donde sería útil usarlo.

14.2.1. Escenario 1: reuso de los datos en múltiples sitios

Como explicamos en el Capítulo 1, los sitios LJWorld.com y Lawrence.com, que funcionan gracias a Django, son operados por la misma organización de prensa, el diario Lawrence Journal-World de Lawrence, Kansas. LJWorld.com se enfoca en noticias, mientras que Lawrence.com se enfoca en el entretenimiento local. Pero a veces los editores quieren publicar un artículo en ambos sitios.

La forma cabeza dura de resolver el problema sería usar una base de datos para cada sitio, y pedirle a los productores que publiquen la misma nota dos veces: una para LJWorld.com y nuevamente para Lawrence.com. Pero esto es ineficiente para los productores del sitio, y es redundante conservar múltiples copias de la misma nota en las bases de datos.

¿Una solución mejor? Que ambos sitios usen la misma base de datos de artículos, y que un artículo esté asociado con uno o más sitios por una relación de muchos-a-muchos. El framework sites de Django, proporciona la tabla de base de datos que hace que los artículos se puedan relacionar de esta forma. Sirve para asociar datos con uno o más "sitios".

14.2.2. Escenario 2: alojamiento del nombre/dominio de tu sitio en un solo lugar

Los dos sitios LJWorld.com y Lawrence.com, tienen la funcionalidad de alertas por correo electrónico, que les permite a los lectores registrarse para obtener notificaciones. Es bastante básico: un lector se registra en un formulario web, e inmediatamente obtiene un correo electrónico que dice "Gracias por su suscripción".

Sería ineficiente y redundante implementar el código del procesamiento de registros dos veces, así que los sitios usan el mismo código detrás de escena. Pero la noticia "Gracias por su suscripción" debe ser distinta para cada sitio. Empleando objetos Site, podemos abstraer el agradecimiento para usar los valores del nombre y dominio del sitio, variables name (ej. LJWorld.com) y domain (ej. www.ljworld.com).

El framework sites te proporciona un lugar para que puedas almacenar el nombre (name) y el dominio (domain) de cada sitio de tu proyecto, lo que significa que puedes reutilizar estos valores de manera genérica.

14.2.3. Modo de uso del framework sites

Sites más que un framework, es una serie de convenciones. Toda la cosa se basa en dos conceptos simples:

  • el modelo Site, que se halla en django.contrib.sites, tiene los campos domain y name.
  • la opción de configuración SITE_ID especifica el ID de la base de datos del objeto Site asociado con este archivo de configuración en particular.

La manera en que uses estos dos conceptos queda a tu criterio, pero Django los usa de varios modos de manera automática, siguiendo convenciones simples.

Para instalar la aplicación sites, sigue estos pasos:

  1. Agrega 'django.contrib.sites' a tu INSTALLED_APPS.
  2. Ejecuta el comando manage.py syncdb para instalar la tabla django_site en tu base de datos.
  3. Agrega uno o más objetos Site, por medio del sitio de administración de Django, o por medio de la API de Python. Crea un objeto Site para cada sitio/dominio que esté respaldado por este proyecto Django.
  4. Define la opción de configuración SITE_ID en cada uno de tus archivos de configuración (settings). Este valor debería ser el ID de base de datos del objeto Site para el sitio respaldado por el archivo de configuración.

14.2.4. Las capacidades del framework Sites

Las siguientes secciones describen las cosas que puedes hacer con este framework.

14.2.4.1.  Reuso de los datos en múltiples sitios

Para reusar los datos en múltiples sitios, como explicamos en el primer escenario, simplemente debes agregarle un campo muchos-a-muchos, ManyToManyField hacia Site en tus modelos. Por ejemplo:

from django.db import models
from django.contrib.sites.models import Site
 
class Article(models.Model):
    headline = models.CharField(maxlength=200)
    # ...
    sites = models.ManyToManyField(Site)

Esa es toda la infraestructura necesaria para asociar artículos con múltiples sitios en tu base de datos. Con eso en su lugar, puedes reusar el mismo código de vista para múltiples sitios. Continuando con el modelo Article del ejemplo, aquí mostramos cómo luciría una vista article_detail:

from django.conf import settings
 
def article_detail(request, article_id):
    try:
        a = Article.objects.get(id=article_id, sites__id=settings.SITE_ID)
    except Article.DoesNotExist:
        raise Http404
    # ...

esta función de vista es reusable porque chequea el sitio del artículo dinámicamente, según cuál sea el valor de la opción SITE_ID.

Por ejemplo, digamos que el archivo de configuración de LJWorld.com tiene un SITE_ID asignado a 1, y que el de Lawrence.com lo tiene asignado a 2. Si esta vista es llamada cuando el archivo de configuración de LJWorld.com está activado, entonces la búsqueda de artículos se limita a aquellos en que la lista de sitios incluye LJWorld.com.

14.2.4.2. Asociación de contenido con un solo sitio

De manera similar, puedes asociar un modelo con el modelo Site en una relación muchos-a-uno, usando ForeignKey.

Por ejemplo, si un artículo sólo se permite en un sitio, puedes usar un modelo como este:

from django.db import models
from django.contrib.sites.models import Site
 
class Article(models.Model):
    headline = models.CharField(maxlength=200)
    # ...
    site = models.ForeignKey(Site)

Este tiene los mismos beneficios, como se describe en la última sección.

14.2.4.3. Obtención del sitio actual desde las vistas

A un nivel más bajo, puedes usar el framework sites en tus vistas de Django para hacer cosas particulares según el sitio en el cual la vista sea llamada. Por ejemplo:

from django.conf import settings
 
def my_view(request):
    if settings.SITE_ID == 3:
        # Do something.
    else:
        # Do something else.

Por supuesto, es horrible meter en el código el ID del sitio de esa manera. Una forma levemente más limpia de lograr lo mismo, es chequear el dominio actual del sitio:

from django.conf import settings
from django.contrib.sites.models import Site
 
def my_view(request):
    current_site = Site.objects.get(id=settings.SITE_ID)
    if current_site.domain == 'foo.com':
        # Do something
    else:
        # Do something else.

Este fragmento de código usado para obtener el objeto Site según el valor de settings.SITE_ID es tan usado, que el administrador de modelos de Site (Site.objects) tiene un método get_current(). El siguiente ejemplo es equivalente al anterior:

from django.contrib.sites.models import Site
 
def my_view(request):
    current_site = Site.objects.get_current()
    if current_site.domain == 'foo.com':
        # Do something
    else:
        # Do something else.

Nota En este último ejemplo, no hay necesidad de importar django.conf.settings.

14.2.4.4. Obtención del dominio actual para ser mostrado

Una forma DRY (acrónimo del inglés Don't Repeat Yourself, "no te repitas") de guardar el nombre del sitio y del dominio, como explicamos en "Escenario 2: alojamiento del nombre/dominio de tu sitio en un solo lugar", se logra simplemente haciendo referencia a name y a domain del objeto Site actual. Por ejemplo:

from django.contrib.sites.models import Site
from django.core.mail import send_mail
 
def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...
    current_site = Site.objects.get_current()
    send_mail('Thanks for subscribing to %s alerts' % current_site.name,
        'Thanks for your subscription. We appreciate it.\n\n-The %s team.' % current_site.name,
        '[email protected]%s' % current_site.domain,
        [user_email])
    # ...

Continuando con nuestro ejemplo de LJWorld.com y Lawrence.com, en Lawrence.com el correo electrónico tiene como sujeto la línea "Gracias por suscribirse a las alertas de lawrence.com". En LJWorld.com, en cambio, el sujeto es "Gracias por suscribirse a las alertas de LJWorld.com". Este comportamiento específico para cada sitio, también se aplica al cuerpo del correo electrónico.

Una forma aún más flexible (aunque un poco más pesada) de hacer lo mismo, es usando el sistema de plantillas de Django. Asumiendo que Lawrence.com y LJWorld.com tienen distintos directorios de plantillas (TEMPLATE_DIRS), puedes simplemente delegarlo al sistema de plantillas así:

from django.core.mail import send_mail
from django.template import loader, Context
 
def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...
    subject = loader.get_template('alerts/subject.txt').render(Context({}))
    message = loader.get_template('alerts/message.txt').render(Context({}))
    send_mail(subject, message, '[email protected]', [user_email])
    # ...

En este caso, debes crear las plantillas subject.txt y message.txt en ambos directorios de plantillas, el de LJWorld.com y el de Lawrence.com . Como mencionamos anteriormente, eso te da más flexibilidad, pero también es más complejo.

Una buena idea es explotar los objetos Site lo más posible, para que no haya una complejidad y una redundancia innecesarias.

14.2.4.5. Obtención del dominio actual para las URLs completas

La convención de Django de usar get_absolute_url() para obtener las URLs de los objetos sin el dominio, está muy bien. Pero en en algunos casos puedes querer mostrar la URL completa — con http:// y el dominio, y todo — para un objeto. Para hacerlo, puedes usar el framework sites. Este es un ejemplo:

>>> from django.contrib.sites.models import Site
>>> obj = MyModel.objects.get(id=3)
>>> obj.get_absolute_url()
'/mymodel/objects/3/'
>>> Site.objects.get_current().domain
'example.com'
>>> 'http://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'http://example.com/mymodel/objects/3/'

14.2.5. CurrentSiteManager

Si los Site juegan roles importantes en tu aplicación, considera el uso del útil CurrentSiteManager en tu modelo (o modelos). Es un administrador de modelos (consulta el Apéndice B) que filtra automáticamente sus consultas para incluir sólo los objetos asociados al Site actual.

Usa CurrentSiteManager agregándolo a tu modelo explícitamente. Por ejemplo:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
 
class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(maxlength=100)
    pub_date = models.DateField()
    site = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager()

Con este modelo, Photo.objects.all() retorna todos los objetos Photo de la base de datos, pero Photo.on_site.all() retorna sólo los objetos Photo asociados con el sitio actual, de acuerdo a la opción de configuración SITE_ID.

En otras palabras, estas dos sentencias son equivalentes:

Photo.objects.filter(site=settings.SITE_ID)
Photo.on_site.all()

¿Cómo supo CurrentSiteManager cuál campo de Photo era el Site? Por defecto busca un campo llamado site. Si tu modelo tiene un campo ForeignKey o un campo ManyToManyField llamado de otra forma que site, debes pasarlo explícitamente como el parámetro para CurrentSiteManager. El modelo a continuación, que tiene un campo llamado publish_on, lo demuestra:

from django.db import models
from django.contrib.sites.models import Site
from django.contrib.sites.managers import CurrentSiteManager
 
class Photo(models.Model):
    photo = models.FileField(upload_to='/home/photos')
    photographer_name = models.CharField(maxlength=100)
    pub_date = models.DateField()
    publish_on = models.ForeignKey(Site)
    objects = models.Manager()
    on_site = CurrentSiteManager('publish_on')

Si intentas usar CurrentSiteManager y pasarle un nombre de campo que no existe, Django lanzará un ValueError.

Nota Probablemente querrás tener un Manager normal (no específico al sitio) en tu modelo, incluso si usas CurrentSiteManager. Como se explica en el Apéndice B, si defines un manager manualmente, Django no creará automáticamente el manager objects = models.Manager().

Además, algunas partes de Django — el sitio de administración y las vistas genéricas — usan el manager que haya sido definido primero en el modelo. Así que si quieres que el sitio de administración tenga acceso a todos los objetos (no sólo a los específicos al sitio actual), pon un objects = models.Manager() en tu modelo, antes de definir CurrentSiteManager.

14.2.6. El uso que hace Django del framework Sites

Si bien no es necesario que uses el framework sites, es extremadamente recomendado, porque Django toma ventaja de ello en algunos lugares. Incluso si tu instalación de Django está alimentando a un solo sitio, deberías tomarte unos segundos para crear el objeto site con tu domain y name, y apuntar su ID en tu opción de configuración SITE_ID.

Este es el uso que hace Django del framework sites:

  • En el framework redirects (consulta la sección "Redirects" más adelante), cada objeto redirect está asociado con un sitio en particular. Cuando Django busca un redirect, toma en cuenta el SITE_ID actual.
  • En el framework comments, cada comentario está asociado con un sitio en particular. Cuando un comentario es posteado, su site es asignado al SITE_ID actual, y cuando los comentarios son listados con la etiqueta de plantillas apropiada, sólo los comentarios del sitio actual son mostrados.
  • En el framework flatpages (consulta la sección "Flatpages" más adelante), cada página es asociada con un sitio en particular. Cuando una página es creada, tú especificas su site, y el middleware de flatpage chequea el SITE_ID actual cuando se traen páginas para ser mostradas.
  • En el framework syndication (consulta el Capítulo 11), las plantillas para title y description tienen acceso automático a la variable {{ site }}, que es el objeto Site que representa al sitio actual. Además, la conexión para proporcionar las URLs de los elementos usan el domain dede el objeto Site actual si no especificas un fully qualified domain.
  • En el framework authentication (consulta el Capítulo 12), la vista django.contrib.auth.views.login le pasa el nombre del Site actual a la plantilla como {{ site_name }}.