L’infrastructure des « sites »

Django est livré avec un système facultatif de « sites ». C’est un point d’extension pour associer des objets et des fonctionnalités à certains sites Web, et c’est un lieu de regroupement pour les noms de domaine et les noms « explicites » de vos sites Django.

Utilisez-le si une seule installation de Django est utilisée pour plus d’un site Web et que vous avez besoin de différencier ces sites sur certains points.

L’infrastructure de sites est basée principalement sur un modèle simple :

class models.Site

Un modèle pour le stockage des attributs domain et name d’un site Web.

domain

Le nom de domaine pleinement qualifié associé au site Web. Par exemple, www.example.com.

Changed in Django 1.9:

Le champ domain a été défini comme unique.

name

Un nom lisible « explicite » pour le site Web.

Le réglage SITE_ID indique l’identifiant de base de données de l’objet Site associé à ce fichier de réglages particulier. Si ce réglage est absent, la fonction get_current_site() essaie d’obtenir le site actuel en comparant le domain au nom d’hôte provenant de la méthode request.get_host().

La manière de l’utiliser est à votre convenance, mais Django l’utilise automatiquement dans un certain nombre de situations à l’aide de conventions simples.

Exemple d’utilisation

Pourquoi auriez-vous besoin du système de sites ? Des exemples offrent la meilleure des explications.

Association de contenu à plusieurs sites différents

Les sites Django LJWorld.com et Lawrence.com sont exploités par la même agence de presse – le journal Lawrence Journal-World à Lawrence, Kansas. LJWorld.com se concentre sur l’actualité, tandis que Lawrence.com met l’accent sur le divertissement local. Mais parfois, les éditeurs veulent publier un article sur les deux sites.

La manière naïve de résoudre ce problème serait d’exiger de chaque rédacteur de site de publier la même histoire deux fois : une fois pour LJWorld.com et une autre fois pour Lawrence.com. Mais c’est vraiment inefficace pour les producteurs des sites, sans compter qu’il est redondant de stocker plusieurs copies de la même histoire dans la base de données.

La meilleure solution est simple : les deux sites utilisent le même article dans la base de données et chaque article est associé à un ou plusieurs sites. Dans la terminologie des modèles Django, ceci est représenté par un champ ManyToManyField dans le modèle Article:

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    sites = models.ManyToManyField(Site)

Ceci effectue plusieurs choses de manière élégante :

  • Les producteurs de sites peuvent éditer tous les contenus, pour les deux sites, dans une même interface (l’administration de Django).

  • La même histoire n’a pas besoin d’être publiée deux fois dans la base de données ; elle constitue un seul enregistrement de base de données.

  • Les développeurs des sites peuvent utiliser le même code de vue Django pour les deux sites. Le code de vue qui affiche une histoire donnée n’a qu’à vérifier que l’histoire demandée se trouve bien sur le site actuel. Cela donne quelque chose comme :

    from django.contrib.sites.shortcuts import get_current_site
    
    def article_detail(request, article_id):
        try:
            a = Article.objects.get(id=article_id, sites__id=get_current_site(request).id)
        except Article.DoesNotExist:
            raise Http404("Article does not exist on this site")
        # ...
    

Association de contenu à un seul site

De même, vous pouvez associer un modèle au modèle Site par une relation plusieurs-à-un, avec une clé ForeignKey.

Par exemple, si un article ne doit apparaître que sur un seul site, vous pourriez utiliser un modèle tel que celui-ci :

from django.db import models
from django.contrib.sites.models import Site

class Article(models.Model):
    headline = models.CharField(max_length=200)
    # ...
    site = models.ForeignKey(Site, on_delete=models.CASCADE)

Les bénéfices sont les mêmes que ceux de la section précédente.

Interception du site actuel dans les vues

Vous pouvez exploiter l’infrastructure des sites dans les vues Django pour effectuer des opérations particulières sur la base du site pour lequel la vue est appelée. Par exemple :

from django.conf import settings

def my_view(request):
    if settings.SITE_ID == 3:
        # Do something.
        pass
    else:
        # Do something else.
        pass

Naturellement, ce n’est pas du tout élégant de figer des identifiants de site dans le code de cette manière. Ce genre de pratique devrait être réservé à des corrections exceptionnelles quand on est très pressé. La façon propre de faire la même chose est de se baser sur le nom de domaine du site actuel :

from django.contrib.sites.shortcuts import get_current_site

def my_view(request):
    current_site = get_current_site(request)
    if current_site.domain == 'foo.com':
        # Do something
        pass
    else:
        # Do something else.
        pass

Cela présente aussi l’avantage de vérifier si l’infrastructure des sites est installée et de renvoyer une instance RequestSite quand ce n’est pas le cas.

Si vous n’avez pas accès à l’objet de requête, vous pouvez utiliser la méthode get_current() du gestionnaire du modèle Site. Vous devez ensuite vous assurer que le fichier des réglages contient bien le réglage SITE_ID. Cet exemple est équivalent au précédent :

from django.contrib.sites.models import Site

def my_function_without_request():
    current_site = Site.objects.get_current()
    if current_site.domain == 'foo.com':
        # Do something
        pass
    else:
        # Do something else.
        pass

Accès au domaine actuel pour l’affichage

LJWorld.com et Lawrence.com ont tous deux une fonction d’alerte par courriel, ce qui permet aux lecteurs de s’inscrire pour recevoir des notifications lorsque des actualités apparaissent. C’est assez simple : un lecteur s’inscrit avec un formulaire Web et reçoit immédiatement un courriel disant « Merci pour votre inscription ».

Il serait inefficace et redondant d’implémenter deux fois le code de ce processus d’inscription, ce qui fait que les sites utilisent le même code en arrière-plan. Mais le message « merci pour votre inscription » doit être différent pour chaque site. En utilisant des objets Site, il est possible d’abstraire le message « merci » afin d’utiliser les valeurs name et domain correspondant au site actuel.

Voici un exemple qui pourrait constituer le code de gestion du formulaire dans la vue :

from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import send_mail

def register_for_newsletter(request):
    # Check form values, etc., and subscribe the user.
    # ...

    current_site = get_current_site(request)
    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,
        ),
        'editor@%s' % current_site.domain,
        [user.email],
    )

    # ...

Pour Lawrence.com, l’objet du courriel correspond à « Thanks for subscribing to lawrence.com alerts. ». Pour LJWorld.com, l’objet du courriel correspond à « Thanks for subscribing to LJWorld.com alerts. ». De même pour les corps des courriels.

Notez qu’une manière encore plus souple (mais plus lourde) de faire cela serait d’utiliser le système des gabarits de Django. En supposant que les sites Lawrence.com et LJWorld.com ont des répertoires de gabarits différents (DIRS), il suffirait de confier la personnalisation des messages au système des gabarits, comme ceci :

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, 'editor@ljworld.com', [user.email])

    # ...

Dans ce cas, il faudrait encore créer les fichiers de gabarit subject.txt et message.txt dans les deux répertoires de gabarits LJWorld.com et Lawrence.com. Cela apporte un maximum de flexibilité, mais est aussi plus complexe.

Il est recommandé d’exploiter au maximum les objets Site afin de supprimer de la complexité inutile et de la redondance.

Accès au domaine actuel pour des URL complètes

La convention get_absolute_url() de Django est pratique pour obtenir les URL des objets sans le nom de domaine, mais dans certains cas, il peut être souhaitable d’afficher une URL complète d’un objet, avec http:// et le domaine. Pour cela, vous pouvez utiliser l’infrastructure des sites. Un exemple simple :

>>> 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'
>>> 'https://%s%s' % (Site.objects.get_current().domain, obj.get_absolute_url())
'https://example.com/mymodel/objects/3/'

Activation de l’infrastructure des sites

Pour activer l’infrastructure des sites, suivez ces étapes :

  1. Ajoutez 'django.contrib.sites' à votre réglage INSTALLED_APPS.

  2. Définissez un réglage SITE_ID:

    SITE_ID = 1
    
  3. Exécutez migrate.

django.contrib.sites inscrit un gestionnaire de signal post_migrate qui crée un site par défaut nommé example.com avec le domaine example.com. Ce site est aussi créé à la suite de la création de la base de données de test par Django. Pour définir le nom et le domaine corrects pour votre projet, vous pouvez utiliser une migration de données.

Afin de servir différents sites en production, il s’agit de créer un fichier de réglages différent pour chacun avec un SITE_ID spécifique (en important peut-être d’un fichier de réglages commun pour éviter de dupliquer les réglages partagés), puis de définir la valeur DJANGO_SETTINGS_MODULE appropriée pour chaque site.

Mise en cache de l’objet Site actuel

Comme le site actuel est stocké en base de données, chaque appel à Site.objects.get_current() pourrait aboutir à une requête de base de données. Mais Django est un peu plus malin que cela : lors de la première requête, le site actuel est mis en cache et tout appel subséquent à cette méthode renvoie les données en cache plutôt que de recourir à la base de données.

Si pour une raison quelconque vous souhaitez forcer une nouvelle requête à la base de données, vous pouvez demander à Django d’effacer le contenu en cache en utilisant Site.objects.clear_cache():

# First call; current site fetched from database.
current_site = Site.objects.get_current()
# ...

# Second call; current site fetched from cache.
current_site = Site.objects.get_current()
# ...

# Force a database query for the third call.
Site.objects.clear_cache()
current_site = Site.objects.get_current()

Le gestionnaire CurrentSiteManager

class managers.CurrentSiteManager

Si Site joue un rôle majeur dans votre application, considérer l’utilisation du gestionnaire CurrentSiteManager dans vos modèles. Il s’agit d’un gestionnaire de modèle qui filtre automatiquement ses requêtes pour n’inclure que les objets associés au Site actuel.

SITE_ID obligatoire

CurrentSiteManager n’est utilisable qu’à la condition que le réglage SITE_ID soit défini dans vos réglages.

Utilisez CurrentSiteManager en l’ajoutant explicitement à vos modèles. Par exemple :

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(max_length=100)
    pub_date = models.DateField()
    site = models.ForeignKey(Site, on_delete=models.CASCADE)
    objects = models.Manager()
    on_site = CurrentSiteManager()

Avec ce modèle, Photo.objects.all() renvoie tous les objets Photo de la base de données, mais Photo.on_site.all() ne renvoie que les objets Photo associés au site actuel, en fonction du réglage SITE_ID.

Autrement dit, ces deux instructions sont équivalentes :

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

Comment CurrentSiteManager sait-il quel champ de Photo indique le Site? Par défaut, CurrentSiteManager cherche à filtrer soit selon une clé ForeignKey nommée site, soit selon un champ ManyToManyField nommé sites. Si vous utilisez un champ nommé différemment de``site`` ou sites pour désigner les objets Site auxquels vos objets sont reliés, il est alors nécessaire de passer explicitement le nom de champ personnalisé comme paramètre à CurrentSiteManager dans le modèle. Le modèle suivant, qui possède un champ nommé publish_on, démontre cela :

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(max_length=100)
    pub_date = models.DateField()
    publish_on = models.ForeignKey(Site, on_delete=models.CASCADE)
    objects = models.Manager()
    on_site = CurrentSiteManager('publish_on')

Si vous essayez d’utiliser CurrentSiteManager et que vous lui indiquez un nom de champ qui n’existe pas, Django génère une exception ValueError.

Finalement, notez qu’il est recommandé de conserver un Manager normal (non lié aux sites) pour vos modèles, même si vous utilisez CurrentSiteManager. Comme expliqué dans la documentation des gestionnaires, si vous définissez manuellement un gestionnaire, Django ne crée pas le gestionnaire automatique objects = models.Manager() à votre place. Sachez également que certaines parties de Django, particulièrement le site d’administration et les vues génériques, utilisent le premier gestionnaire défini pour un modèle, donc si vous voulez que le site d’administration puisse accéder à tous les objets (et pas seulement ceux d’un site particulier), ajoutez objects = models.Manager() dans vos modèles avant de définir CurrentSiteManager.

Intergiciel de site

Si vous utilisez fréquemment ce motif :

from django.contrib.sites.models import Site

def my_view(request):
    site = Site.objects.get_current()
    ...

il existe une manière simple d’éviter les répétitions. Ajoutez django.contrib.sites.middleware.CurrentSiteMiddleware à MIDDLEWARE_CLASSES. Cet intergiciel définit l’attribut site pour chaque objet de requête, ce qui permet d’appeler request.site pour accéder au site actuel.

Utilisation de l’infrastructure des sites par Django

Même s’il n’est pas obligatoire d’utiliser l’infrastructure des sites, elle est fortement encouragée car Django en tire profit à plusieurs endroits. Même si votre installation de Django n’est utilisée que par un seul site, prenez les quelques secondes nécessaires à la définition de l’objet site avec les bonnes valeurs domain et name, puis faites pointer le réglage SITE_ID vers l’identifiant de cet objet.

Voici comment Django utilise l’infrastructure des sites :

  • Dans l’application de redirection, chaque objet de redirection est associé à un site particulier. Lorsque Django recherche une redirection, il prend en compte le site actuel.

  • Dans l’application des pages statiques, chaque page statique est associée à un site particulier. Lorsqu’une page statique est créée, il faut indiquer le Site, et l’intergiciel FlatpageFallbackMiddleware vérifie le site actuel lorsqu’il recherche des pages statiques à afficher.

  • Dans l’infrastructure de syndication, les gabarits pour title et description ont automatiquement accès à une variable {{ site }}, qui est l’objet Site représentant le site actuel. De même, le point d’entrée pour fournir les URL des éléments utilisent l’attribut domain de l’objet Site actuel si vous n’indiquez pas un domaine pleinement qualifié.

  • Dans l’infrastructure d'authentification, la vue django.contrib.auth.views.login() passe le nom de l’objet Site actuel au gabarit sous forme de {{ site_name }}.

  • La vue de raccourci (django.contrib.contenttypes.views.shortcut) utilise le domaine de l’objet Site actuel lors de la construction de l’URL d’un objet.

  • Dans le site d’administration, le lien « voir sur le site » utilise l’objet Site actuel pour connaître le domaine du site vers lequel la redirection s’opère.

Les objets RequestSite

Certaines applications django.contrib s’appuient sur l’infrastructure des sites mais sont conçues pour ne pas dépendre de l’installation de l’application sites dans la base de données. (Certaines personnes ne souhaitent pas ou n’ont simplement pas la possibilité d’installer la table de base de données supplémentaire que demande l’infrastructure des sites.) Pour ces situations, l’application fournit une classe django.contrib.sites.requests.RequestSite qui peut être utilisée comme solution de repli lorsque le complément en base de données de l’infrastructure des sites n’est pas disponible.

class requests.RequestSite

Une classe qui partage l’interface principale de Site (c’est-à-dire qu’elle possède les attributs domain et name), mais qui reçoit ses données à partir d’un objet HttpRequest de Django plutôt qu’à partir de la base de données.

__init__(request)

Définit les attributs name et domain selon les valeurs obtenues de get_host().

Un objet RequestSite possède une interface similaire à un objet Site normal, sauf que sa méthode __init__() accepte un objet HttpRequest. Il est capable d’en déduire le domaine et le nom en examinant le domaine de la requête. Il possède des méthodes save() et delete() pour refléter l’interface de Site, mais ces méthodes générent l’exception NotImplementedError.

Le raccourci get_current_site

Finalement, pour éviter du code redondant, le système fournit une fonction django.contrib.sites.shortcuts.get_current_site().

shortcuts.get_current_site(request)

Une fonction qui vérifie si django.contrib.sites est installé et qui renvoie soit l’objet Site en cours, soit un objet RequestSite défini à partir de la requête. Il détermine le site actuel sur la base de request.get_host() si le réglage SITE_ID n’est pas défini.

request.get_host() peut renvoyer à la fois un domaine et un port lorsque l’en-tête Host contient un numéro de port explicite, par exemple example.com:80. Dans de tels cas, si la recherche échoue parce que l’hôte ne correspond pas à un enregistrement dans la base de données, le port est enlevé et la recherche est relancée uniquement avec la partie du domaine. Ceci ne concerne pas RequestSite qui utilise toujours l’hôte non modifié.

Changed in Django 1.8:

La détermination du site actuel basée sur request.get_host() a été ajoutée.

Changed in Django 1.9:

Un second essai de détermination du site actuel sans le port a été ajouté.

Back to Top