L’infrastructure des types de contenus (contenttypes)

Django inclut une application contenttypes qui permet de recenser l’ensemble des modèles installés dans un projet Django, fournissant une interface générique de haut niveau pour travailler avec les modèles.

Aperçu

Le cœur de l’application contenttypes est le modèle ContentType, définir dans django.contrib.contenttypes.models.ContentType. Les instances de ContentType représentent et stockent des informations sur les modèles installés dans un projet ; de nouvelles instances de ContentType sont automatiquement créées à chaque fois que de nouveaux modèles sont installés.

Les instances de ContentType ont des méthodes pour renvoyer les classes de modèle qu’elles représentent et pour interroger les objets de ces modèles. ContentType dispose également d’un gestionnaire personnalisé qui ajoute des méthodes pour travailler avec ContentType et pour obtenir des instances de ContentType pour un modèle particulier.

Les relations entre vos modèles et ContentType peuvent également être utilisées pour établir des relations « génériques » entre une instance d’un de vos modèles et des instances de n’importe quel autre modèle que vous avez installé.

Installation du système contenttypes

Le système contenttypes est inclus par défaut dans la liste INSTALLED_APPS créée par django-admin startproject, mais si vous l’avez supprimé ou si vous définissez manuellement votre liste INSTALLED_APPS, vous pouvez l’activer en ajoutant 'django.contrib.contenttypes' à votre réglage INSTALLED_APPS.

C’est généralement une bonne idée d’avoir l’application contenttypes installée ; plusieurs autres applications fournies avec Django ont en besoin :

  • L’application d’administration l’utilise pour tracer l’historique de chaque objet ajouté ou modifié via l’interface d’administration.
  • Le système d'authentification de Django l’utilise pour associer les permissions des utilisateurs à des modèles spécifiques.

Le modèle ContentType

class ContentType

Chaque instance de ContentType a deux champs qui, pris ensemble, décrivent de manière unique un modèle installé :

app_label

Le nom de l’application dont le modèle fait partie. Il provient de l’attribut app_label du modèle et ne comprend que la dernière partie du chemin d’importation Python de l’application ; par exemple, l’attribut app_label de django.contrib.contenttypes devient contenttypes.

model

Le nom de classe du modèle.

De plus, la propriété suivante est disponible :

name

Le nom lisible du type de contenu. Il provient de l’attribut verbose_name du modèle.

Examinons un exemple pour voir comment cela fonctionne. Si l’application contenttypes est déjà installée, que vous ajoutez ensuite l’application sites à votre réglage INSTALLED_APPS et que vous exécutez manage.py migrate pour l’installer, le modèle django.contrib.sites.models.Site sera installé dans votre base de données. Dans le même temps, une nouvelle instance de ContentType sera créée avec les valeurs suivantes :

  • app_label sera défini à 'sites' (la dernière partie du chemin Python de django.contrib.sites).
  • model sera défini à 'site'.

Méthodes des instances ContentType

Chaque instance ContentType possède des méthodes qui permettent de passer d’une instance ContentType au modèle qu’elle représente, ou de récupérer des objets à partir de ce modèle :

ContentType.get_object_for_this_type(**kwargs)

Accepte un ensemble de paramètres de recherche valides pour le modèle que ContentType représente, et réalise une recherche get() sur ce modèle, renvoyant l’objet correspondant.

ContentType.model_class()

Renvoie la classe du modèle représenté par cette instance ContentType.

Par exemple, nous pourrions chercher le ContentType du modèle User:

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label='auth', model='user')
>>> user_type
<ContentType: user>

Puis l’utiliser pour rechercher un User particulier ou pour accéder à la classe du modèle User:

>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username='Guido')
<User: Guido>

Ensemble, get_object_for_this_type() et model_class() permettent deux cas d’utilisation extrêmement importants :

  1. En utilisant ces méthodes, vous pouvez écrire du code générique de haut niveau qui effectue des requêtes sur n’importe quel modèle installé ; au lieu d’importer et d’utiliser une unique classe de modèle spécifique, vous pouvez passer des variables app_label et model pour chercher un objet ContentType au moment de l’exécution, et travailler ensuite avec la classe de modèle ou récupérer des objets avec.
  2. Vous pouvez associer un autre modèle à un objet ContentType afin de lier ses instances à certaines classes de modèle, et utiliser ces méthodes pour obtenir l’accès à ces classes de modèle.

Plusieurs des applications intégrées à Django font usage de cette dernière technique. Par exemple, le système de permissions dans le système d’authentification de Django utilise un modèle Permission avec une clé étrangère vers ContentType; cela permet à Permission de représenter des concepts tels que « peut ajouter une entrée de blog » ou « peut supprimer une actualité ».

Le gestionnaire ContentTypeManager

class ContentTypeManager

ContentType possède aussi un gestionnaire personnalisé, ContentTypeManager, qui ajoute les méthodes suivantes :

clear_cache()

Efface un cache interne utilisé par ContentType pour garder une trace des modèles pour lesquels il a créé des instances ContentType. Vous n’aurez probablement jamais besoin d’appeler cette méthode de vous-même ; Django l’appelle automatiquement lorsque c’est nécessaire.

get_for_id(id)

Recherche un ContentType par son identifiant. Comme cette méthode utilise le même cache partagé que get_for_model(), il est préférable d’utiliser cette méthode à l’habituel ContentType.objects.get (pk=id).

get_for_model(model, for_concrete_model=True)

Accepte soit une classe de modèle, soit une instance de modèle, et renvoie l’instance ContentType représentant ce modèle. for_concrete_model=False permet de récupérer l’instance ContentType d’un modèle mandataire.

get_for_models(*models, for_concrete_models=True)

Accepte un nombre variable de classes de modèle et renvoie un dictionnaire associant les classes de modèle aux instances ContentType qui les représentent. for_concrete_model=False permet de récupérer les instances ContentType de modèles mandataires.

get_by_natural_key(app_label, model)

Renvoie l’instance ContentType identifiée de façon unique par l’étiquette d’application et le nom du modèle donnés. L’objectif principal de cette méthode est de permettre aux objets ContentType d’être référencés par une clé naturelle pendant la désérialisation.

La méthode get_for_model() est particulièrement utile lorsque vous savez que vous devez travailler avec un ContentType mais que vous ne voulez pas vous soucier d’obtenir les métadonnées du modèle pour effectuer une recherche manuelle :

>>> from django.contrib.auth.models import User
>>> ContentType.objects.get_for_model(User)
<ContentType: user>

Relations génériques

L’ajout d’une clé étrangère depuis l’un de vos propres modèles vers un ContentType permet au modèle de se lier efficacement à une autre classe de modèle, comme dans l’exemple du modèle Permission ci-dessus. Mais il est possible d’aller encore plus loin et d’utiliser ContentType pour permettre de véritables relations génériques (parfois appelées « polymorphes ») entre les modèles.

Un exemple simple est un système d’étiquetage, qui pourrait ressembler à ceci :

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    def __str__(self):
        return self.tag

Une clé ForeignKey normale ne peut « pointer » que vers un seul autre modèle, ce qui signifie que si le modèle TaggedItem utilisait une clé ForeignKey, il devrait choisir un et un seul modèle pour stocker les étiquettes associées. L’application contenttypes fournit un type de champ spécial (GenericForeignKey) qui résout cette problématique et permet que la relation se fasse avec n’importe quel modèle :

class GenericForeignKey

Il y a trois étapes à suivre pour mettre en place une GenericForeignKey:

  1. Ajoutez à votre modèle une clé ForeignKey vers ContentType. Le nom usuel de ce champ est « content_type ».
  2. Ajoutez à votre modèle un champ qui peut stocker des valeurs de clé primaire provenant des modèles qui feront l’objet du lien. Pour la plupart des modèles, cela correspond à un champ PositiveIntegerField. Le nom usuel de ce champ est « object_id ».
  3. Ajoutez à votre modèle une clé GenericForeignKey en lui passant les noms des deux champs décrits ci-dessus. Si ces champs sont nommés « content_type » et « object_id », vous pouvez les omettre, car ce sont les noms de champs par défaut que GenericForeignKey va chercher.
for_concrete_model

Si cet attribut vaut False, le champ sera en mesure de référencer des modèles mandataires. La valeur par défaut est True. Cela fait écho au paramètre for_concrete_model de get_for_model().

Compatibilité de type de clé primaire

Le champ object_id n’a pas besoin d’être du même type que les champs de clé primaire des modèles connexes, mais leurs valeurs de clé primaire doivent pouvoir être transformées dans le même type que le champ object_id par leur méthode get_db_prep_value().

Par exemple, si vous souhaitez autoriser les relations génériques de modèles avec des champs de clé primaire IntegerField ou CharField, vous pouvez utiliser CharField comme type de champ object_id du modèle puisque les nombres entiers peuvent être forcés vers des chaînes de caractères par get_db_prep_value().

Pour une flexibilité maximale, vous pouvez utiliser un champ TextField qui n’a pas de longueur maximale définie, mais cela pourrait entraîner des pénalités de performances significatives en fonction du moteur de base de données.

Il n’y a pas de solution passe-partout concernant le meilleur type de champ. Vous devez évaluer les modèles que vous prévoyez de lier et déterminer quelle solution sera la plus efficace pour votre cas d’utilisation.

Sérialisation des références aux objets ContentType

Si vous sérialisez des données (par exemple, lors de la génération de fixtures) à partir d’un modèle qui met en œuvre les relations génériques, vous devriez probablement utiliser une clé naturelle pour identifier de manière unique les objets ContentType associés. Voir clés naturelles et dumpdata --natural-foreign pour plus d’informations.

Cela activera une API similaire à celle utilisée pour une clé ForeignKey normale ; chaque TaggedItem aura un champ content_object qui renvoie l’objet auquel il est lié, et vous pouvez également attribuer une valeur à ce champ ou l’utiliser lors de la création d’un TaggedItem:

>>> from django.contrib.auth.models import User
>>> guido = User.objects.get(username='Guido')
>>> t = TaggedItem(content_object=guido, tag='bdfl')
>>> t.save()
>>> t.content_object
<User: Guido>

Si l’objet lié est supprimé, les champs content_type et object_id conservent leur valeur originale et le champ GenericForeignKey renvoie None:

>>> guido.delete()
>>> t.content_object  # returns None

En raison de la façon dont GenericForeignKey est implémentée, vous ne pouvez pas utiliser ces champs directement avec les filtres (filter() et exclude(), par exemple) par l’intermédiaire de l’API de base de données. Comme une clé GenericForeignKey n’est pas un objet de champ normal, ces exemples ne fonctionneront pas :

# This will fail
>>> TaggedItem.objects.filter(content_object=guido)
# This will also fail
>>> TaggedItem.objects.get(content_object=guido)

De même, les clés GenericForeignKey n’apparaissent pas dans les formulaires ModelForm.

Relations génériques inverses

class GenericRelation
related_query_name

La relation de l’objet lié vers cet objet n’existe pas par défaut. En définissant related_query_name, la relation est créée de l’objet lié vers celui-ci. Cela permet d’interroger et de filtrer à partir de l’objet lié.

Si vous savez quels seront les modèles que vous utiliserez le plus souvent, vous pouvez également ajouter une relation générique « inverse » pour activer une API supplémentaire. Par exemple :

from django.contrib.contenttypes.fields import GenericRelation
from django.db import models

class Bookmark(models.Model):
    url = models.URLField()
    tags = GenericRelation(TaggedItem)

Les instances Bookmark auront chacune un attribut tags, qui peut être utilisé pour récupérer leurs objets TaggedItems associés :

>>> b = Bookmark(url='https://www.djangoproject.com/')
>>> b.save()
>>> t1 = TaggedItem(content_object=b, tag='django')
>>> t1.save()
>>> t2 = TaggedItem(content_object=b, tag='python')
>>> t2.save()
>>> b.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

En définissant l’attribut related_query_name de GenericRelation, cela permet d’interroger à partir de l’objet lié :

tags = GenericRelation(TaggedItem, related_query_name='bookmark')

Cela permet de filtrer, trier ou d’autres opérations d’interrogation sur Bookmark à partir de TaggedItem:

>>> # Get all tags belonging to bookmarks containing `django` in the url
>>> TaggedItem.objects.filter(bookmark__url__contains='django')
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

Bien sûr, si vous n’ajoutez pas la relation inverse related_query_name, vous pouvez faire les mêmes types de recherches manuellement :

>>> bookmarks = Bookmark.objects.filter(url__contains='django')
>>> bookmark_type = ContentType.objects.get_for_model(Bookmark)
>>> TaggedItem.objects.filter(content_type__pk=bookmark_type.id, object_id__in=bookmarks)
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>

Tout comme GenericForeignKey accepte les noms des champs type de contenu et identifiant d’object en paramètres, il en va de même pour GenericRelation; si le modèle qui a la clé étrangère générique n’utilise pas des noms par défaut pour ces champs, vous devez lui passer les noms des champs lors de la création d’une relation GenericRelation. Par exemple, si le modèle TaggedItem mentionné ci-dessus utilisait des champs nommés content_type_fk et object_primary_key pour créer sa clé étrangère générique, alors une relation GenericRelation inverse devrait être définie comme suit :

tags = GenericRelation(
    TaggedItem,
    content_type_field='content_type_fk',
    object_id_field='object_primary_key',
)

Notez également que si vous supprimez un objet qui possède une relation GenericRelation, tous les objets qui ont une clé GenericForeignKey pointant vers lui seront aussi supprimés. Dans l’exemple ci-dessus, cela signifie que si un objet Bookmark a été supprimé, tous les objets TaggedItem pointant dessus seront supprimés en même temps.

Contrairement à ForeignKey, GenericForeignKey n’accepte pas de paramètre on_delete pour personnaliser ce comportement ; si vous le souhaitez, vous pouvez éviter la cascade de suppression en évitant simplement de définir une relation GenericRelation ; un comportement différent peut être fourni par le signal pre_delete.

Relations génériques et agrégation

L”API d’agrégation de base de données de Django fonctionne avec les relations GenericRelation. Par exemple, vous pouvez savoir combien de « tags » sont définis pour tous les signets :

>>> Bookmark.objects.aggregate(Count('tags'))
{'tags__count': 3}

Relations génériques dans les formulaires

Le module django.contrib.contenttypes.forms fournit :

class BaseGenericInlineFormSet
generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type", fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None, formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False)

Renvoie un GenericInlineFormSet à l’aide de modelformset_factory().

Vous devez fournir ct_field et fk_field s’ils sont différents des valeurs par défaut, respectivement content_type et object_id. Les autres paramètres sont similaires à ceux documentés dans modelformset_factory() et inlineformset_factory().

Le paramètre for_concrete_model correspond au paramètre for_concrete_model de GenericForeignKey.

Relations génériques dans l’interface d’administration

Le module django.contrib.contenttypes.admin fournit GenericTabularInline et GenericStackedInline (sous-classes de GenericInlineModelAdmin)

Ces classes et fonctions permettent l’utilisation de relations génériques dans les formulaires et l’interface d’administration. Voir la documentation sur les formulaires groupés de modèle et l’interface d’administration pour plus d’informations.

class GenericInlineModelAdmin

La classe GenericInlineModelAdmin hérite de toutes les propriétés d’une classe InlineModelAdmin. Cependant, elle en ajoute quelques autres de son cru pour travailler avec la relation générique :

ct_field

Le nom du champ de clé étrangère ContentType sur le modèle. Par défaut, content_type.

ct_fk_field

Le nom du champ nombre entier qui représente l’identifiant de l’objet associé. Par défaut, object_id.

class GenericTabularInline
class GenericStackedInline

Les sous-classes de GenericInlineModelAdmin avec mises en page empilées et tabulaires, respectivement.

Back to Top