Écriture de champs de modèles personnalisés

Introduction

La documentation de référence sur les modèles explique comment utiliser les classes de champs standard de Django : CharField, DateField, etc. Dans la plupart des cas, ces classes sont suffisantes. Cependant, il peut arriver que l’offre de Django ne réponde pas à toutes vos exigences, ou que vous vouliez utiliser un champ totalement différent de ceux mis à disposition par Django.

Les types de champs intégrés de Django ne gèrent pas tous les types possibles de colonnes de bases de données, mais seulement les plus courants comme VARCHAR et INTEGER. Pour des types de colonnes moins usités, comme les polygones géographiques ou même des types définis par les utilisateurs du style des types PostgreSQL personnalisés, vous pouvez définir vos propres sous-classes de Field.

D’un autre côté, vous pourriez avoir un objet Python complexe pouvant être sérialisé d’une manière ou d’une autre dans un type de colonne standard d’une base de données. C’est une autre situation où une sous-classe de Field peut vous aider à utiliser cet objet avec vos modèles.

Notre exemple d’objet

La création de champs personnalisés exige un peu d’attention aux détails. Pour rendre les choses plus faciles à suivre, nous allons utiliser un même exemple tout au long de ce document : encapsuler un objet Python représentant une distribution de cartes dans une main de Bridge. Ne vous inquiétez pas, il n’y a pas besoin de savoir jouer au Bridge pour suivre cet exemple. Il suffit de savoir que les 52 cartes sont distribuées équitablement entre quatre joueurs, traditionnellement appelés nord, est, sud et ouest. Notre classe ressemble à ceci :

class Hand:
    """A hand of cards (bridge style)"""

    def __init__(self, north, east, south, west):
        # Input parameters are lists of cards ('Ah', '9s', etc.)
        self.north = north
        self.east = east
        self.south = south
        self.west = west

    # ... (other possibly useful methods omitted) ...

C’est une classe Python ordinaire sans spécificité Django. Nous aimerions pouvoir faire des choses telles que celles-ci dans nos modèles (en supposant que l’attribut hand du modèle est une instance de Hand) :

example = MyModel.objects.get(pk=1)
print(example.hand.north)

new_hand = Hand(north, east, south, west)
example.hand = new_hand
example.save()

Nous écrivons et lisons dans l’attribut hand de notre modèle comme on le ferait avec tout autre classe Python. L’astuce est d’indiquer à Django comment enregistrer et charger un tel objet.

Pour pouvoir utiliser la classe Hand dans nos modèles, nous ne devons en aucune façon modifier cette classe. C’est idéal, car cela signifie qu’il est facilement possible d’implémenter la fonctionnalité « modèle » pour des classes existantes dont le code source n’est pas modifiable.

Note

Il se peut que vous vouliez uniquement profiter de types personnalisés de colonnes de bases de données et gérer les données comment des types Python standards dans vos modèles ; chaînes ou nombres à virgules, par exemple. Cette situation est semblable à notre exemple Hand et nous signalerons d’éventuelles différences quand elles apparaîtront.

Contexte théorique

Stockage de base de données

Commençons par les champs de modèles. Si on le décompose, un champ de modèle est un moyen de traiter un objet Python normal (une chaîne, une valeur booléenne, un datetime ou un objet plus complexe comme Hand) et de le convertir en un format adapté à l’utilisation avec une base de données (ce format convient également pour la sérialisation, mais nous verrons plus loin que cela se conçoit assez naturellement lorsque la partie base de données est maîtrisée).

D’une manière ou d’une autre, les champs d’un modèle doivent être transformés pour correspondre à un type de colonne d’une base de données. Des bases de données différentes fournissent différents choix de types de colonnes valables, mais la règle est toujours la même : ce sont ces types avec lesquels vous devez travailler. Tout ce que vous voulez stocker dans la base de données doit correspondre à l’un de ces types.

Normalement, soit vous écrivez un champ Django qui correspondra à un type particulier de colonne de base de données, soit il faut trouver une manière de convertir vos données, par exemple en une chaîne.

For our Hand example, we could convert the card data to a string of 104 characters by concatenating all the cards together in a predetermined order – say, all the north cards first, then the east, south and west cards. So Hand objects can be saved to text or character columns in the database.

Que fait une classe de champ ?

Tous les champs Django (et lorsque nous parlons de champs dans ce document, nous parlons toujours de champs de modèles et non pas de champs de formulaires) sont des sous-classes de django.db.models.Field. La plupart des informations que Django conserve sur un champ sont communs à tous les champs (nom, texte d’aide, unicité, etc.). Le stockage de toutes ces informations est géré par Field. Nous verrons plus précisément ce que peut faire Field un peu plus tard ; pour l’instant, contentons-nous de savoir que tout hérite de Field et personnalise les éléments clés du comportement de la classe.

Il est important de réaliser qu’une classe de champ Django n’est pas ce qui est stocké dans les attributs de vos modèles. Les attributs des modèles contiennent des objets Python habituels. Les classes de champs que vous définissez dans un modèle sont en réalité stockés dans la classe Meta au moment de la création de la classe de modèle (il n’est pas important ici de connaître les détails précis de ce processus). Ceci est dû au fait que les classes de champs ne sont pas nécessaires tant qu’il s’agit simplement de créer et modifier des attributs. Par contre, elles fournissent les mécanismes de conversion entre les valeurs d’attributs et ce qui est effectivement stocké dans la base de données ou envoyé au sérialiseur.

Gardez ceci à l’esprit lors de la création de vos propres champs personnalisés. La sous-classe Field Django que vous écrivez fournit les mécanismes de conversion entre vos instances Python et les valeurs de base de données ou de sérialisation de diverses manières (il existe par exemple des différences entre le stockage d’une valeur et son utilisation dans une requête). Si tout cela semble un peu compliqué, ne vous inquiétez pas, cela deviendra plus clair dans les exemples ci-dessous. Rappelez-vous seulement que vous allez souvent créer deux classes lorsque vous voulez créer un champ personnalisé :

  • La première classe constitue l’objet Python que vos utilisateurs vont manipuler. Ils l’utiliseront comme attribut de modèle, ils liront ses valeurs à destination de l’affichage, et ainsi de suite. Il s’agit là de la classe Hand de notre exemple.
  • La seconde classe est la sous-classe de Field. C’est la classe qui sait comment convertir votre première classe de sa forme utile pour le stockage vers sa forme Python et vice versa.

Écriture d’une sous-classe de champ

Lors de la planification de votre sous-classe de Field, réfléchissez d’abord à quelle classe Field existante votre nouveau champ ressemble le plus. Serait-il possible d’hériter d’un champ Django existant et d’économiser un peu de travail ? Si non, vous devrez hériter de la classe Field qui est le parent de toutes les autres.

L’initialisation du nouveau champ consiste à séparer tout paramètre qui est spécifique à votre champ, des paramètres standards, et de transmettre ces derniers à la méthode __init__() de Field (ou d’une autre classe parente).

Dans notre exemple, nous appellerons notre champ HandField (il est conseillé d’appeler votre sous-classe de Field sur le modèle <QuelqueChose>Field afin qu’on distingue rapidement qu’il s’agit d’une sous-classe de Field). Notre champ ne ressemble à aucun autre champ existant, nous allons donc hériter directement de Field:

from django.db import models

class HandField(models.Field):

    description = "A hand of cards (bridge style)"

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super().__init__(*args, **kwargs)

Le champ HandField accepte la plupart des options standards de champs (voir liste ci-dessous), mais nous voulons nous assurer que sa longueur soit fixe, étant donné qu’il ne doit stocker que les valeurs de 52 cartes plus leur couleur ; 104 caractères au total.

Note

Beaucoup de champs de modèles Django acceptent des options qui ne sont pas exploitées. Par exemple, vous pouvez passer à la fois editable et auto_now à django.db.models.DateField et il ignorera le paramètre editable (la définition d”auto_now implique que``editable=False``). Aucune erreur ne survient dans ce cas.

Ce comportement simplifie les classes de champs, car elles n’ont pas besoin de vérifier les options qui ne sont pas nécessaires. Elles transmettent toutes les options à la classe parente et ne les utilisent plus. Il ne tient qu’à vous d’être plus strict sur les options acceptées, ou d’adopter le comportement plus permissif des champs actuels.

La méthode Field.__init__() accepte les paramètres suivants :

Toutes les options sans explications dans la liste ci-dessus ont la même signification que pour les champs Django normaux. Consultez la documentation sur les champs pour des exemples et des détails.

Déconstruction de champ

The counterpoint to writing your __init__() method is writing the deconstruct() method. It’s used during model migrations to tell Django how to take an instance of your new field and reduce it to a serialized form - in particular, what arguments to pass to __init__() to recreate it.

Si vous n’avez pas ajouté d’options supplémentaires au champ dont vous avez hérité, il n’y a alors pas besoin d’écrire une nouvelle méthode deconstruct(). Si toutefois vous modifiez les paramètres passés à __init__() (comme nous l’avons fait avec HandField), il sera nécessaire de compléter les valeurs transmises.

deconstruct() renvoie un tuple de quatre éléments : le nom d’attribut du champ, le chemin d’importation complet de la classe du champ, les paramètres positionnels (sous forme de liste), et les paramètres nommés (sous forme de dictionnaire). Notez la différence avec la méthode deconstruct() des classes personnalisées qui renvoie un tuple de trois éléments.

En tant qu’auteur de champ personnalisé, vous n’avez pas besoin de vous soucier des deux premières valeurs ; la classe de base Field contient tout le code pour déterminer le nom d’attribut et le chemin d’importation du champ. Vous devez cependant vous préoccuper des paramètres positionnels et nommés, puisque ceux-ci font généralement partie des éléments que vous modifiez.

Par exemple, dans notre classe HandField, nous forçons toujours l’utilisation de max_length dans __init__(). La méthode deconstruct() de la classe de base Field le verra et essayera de le renvoyer dans les paramètres nommés ; ainsi, nous pouvons le supprimer des paramètres nommés pour une meilleure lisibilité :

from django.db import models

class HandField(models.Field):

    def __init__(self, *args, **kwargs):
        kwargs['max_length'] = 104
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]
        return name, path, args, kwargs

Si vous ajoutez un nouveau paramètre nommé, vous devez écrire vous-même du code dans deconstruct() qui place sa valeur dans kwargs. Vous devriez aussi omettre la valeur de kwargs lorsqu’il n’est pas nécessaire de reconstruire l’état du champ, comme par exemple lorsque la valeur par défaut est utilisée

from django.db import models

class CommaSepField(models.Field):
    "Implements comma-separated storage of lists"

    def __init__(self, separator=",", *args, **kwargs):
        self.separator = separator
        super().__init__(*args, **kwargs)

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        # Only include kwarg if it's not the default
        if self.separator != ",":
            kwargs['separator'] = self.separator
        return name, path, args, kwargs

Des exemples plus complexes sont au-delà de la portée de ce document, mais rappelez-vous – pour toute configuration de votre instance de champ, deconstruct() doit renvoyer les paramètres que vous pouvez passer à __init__ afin de reconstruire cet état.

Portez une attention toute particulière si vous définissez de nouvelles valeurs par défaut pour les paramètres de la classe parente Field; vous devez vous assurer qu’elles sont toujours incluses, sinon elles pourraient disparaître si elles prennent les anciennes valeurs par défaut.

En outre, essayez d’éviter de renvoyer les valeurs en tant que paramètres positionnels ; autant que possible, renvoyez les valeurs en tant que paramètres nommés pour une compatibilité future maximale. Si vous changez plutôt le nom que la position des éléments dans la liste des paramètres du constructeur, les paramètres positionnels sont préférables, mais gardez à l’esprit que les gens reconstruiront votre champ à partir de la version sérialisée pendant un certain temps (voire des années), selon la durée de vie de vos migrations.

Vous pouvez voir les résultats de la déconstruction en regardant les migrations qui incluent ledit champ, et vous pouvez tester la déconstruction dans les tests unitaires en déconstruisant et reconstruisant le champ :

name, path, args, kwargs = my_field_instance.deconstruct()
new_instance = MyField(*args, **kwargs)
self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)

Field attributes not affecting database column definition

New in Django 4.1.

You can override Field.non_db_attrs to customize attributes of a field that don’t affect a column definition. It’s used during model migrations to detect no-op AlterField operations.

Par exemple :

class CommaSepField(models.Field):

    @property
    def non_db_attrs(self):
        return super().non_db_attrs + ("separator",)

Modification de la classe de base d’un champ personnalisé

Il n’est pas possible de modifier la classe de base d’un champ personnalisé, car Django ne détectera pas le changement et ne créera pas de migration. Par exemple, si vous commencez avec :

class CustomCharField(models.CharField):
    ...

puis que vous décidez de vous baser plutôt sur TextField, vous ne pouvez pas simplement modifier la sous-classe comme ceci :

class CustomCharField(models.TextField):
    ...

Vous devez plutôt créer une nouvelle classe de champ personnalisé et mettre à jour vos modèles pour référencer cette nouvelle classe :

class CustomCharField(models.CharField):
    ...

class CustomTextField(models.TextField):
    ...

Comme discuté dans suppression de champs, vous devez conserver la classe CustomCharField originale tant que vous conservez des migrations qui la référencent.

Documentation du champ personnalisé

Comme toujours, il est souhaitable de documenter votre type de champ, afin que les utilisateurs sachent de quoi il s’agit. En plus de lui fournir une chaîne « docstring » utile pour les développeurs, vous pouvez également permettre aux utilisateurs de l’application d’administration de voir une courte description du type de champ via l’application django.contrib.admindocs. Pour ce faire, fournissez un texte descriptif dans l’attribut de classe description du champ personnalisé. Dans l’exemple ci-dessus, la description affichée par l’application admindocs pour un HandField sera « A hand of cards (bridge style) ».

Dans l’affichage django.contrib.admindocs, la description du champ est interpolée avec field.__dict__, ce qui permet à la description de contenir des paramètres du champ. Par exemple, la description de CharField est :

description = _("String (up to %(max_length)s)")

Méthodes utiles

Après avoir créé la sous-classe de Field, il peut être utile de surcharger quelques méthodes standards, en fonction du comportement du champ. La liste de méthodes ci-dessous est plus ou moins dans l’ordre décroissant d’importance, il faut donc commencer par les premières.

Types de base de données personnalisés

Disons que vous avez créé un type personnalisé PostgreSQL appelé mytype. Vous pouvez dériver Field et implémenter la méthode db_type(), comme suit :

from django.db import models

class MytypeField(models.Field):
    def db_type(self, connection):
        return 'mytype'

Après avoir créé MytypeField, il est possible de l’utiliser dans n’importe quel modèle, comme pour tout autre type Field:

class Person(models.Model):
    name = models.CharField(max_length=80)
    something_else = MytypeField()

Si votre objectif est de créer une application indépendante du choix de la base de données, vous devriez tenir compte des différences de types de colonne de base de données. Par exemple, le type de colonne date/heure dans PostgreSQL est appelé timestamp alors que la même colonne dans MySQL est appelée datetime. Vous pouvez gérer cela dans une méthode db_type() en vous basant sur l’attribut connection.vendor. Les noms actuels des fournisseurs intégrés sont : sqlite, postgresql, mysql et oracle.

Par exemple :

class MyDateField(models.Field):
    def db_type(self, connection):
        if connection.vendor == 'mysql':
            return 'datetime'
        else:
            return 'timestamp'

Les méthodes db_type() et rel_db_type() sont appelées par Django lorsque le système construit les instructions CREATE TABLE pour votre application – c’est-à-dire lors de la création initiale des tables. Elles sont aussi appelées lors de la construction d’une clause WHERE qui comprend le champ de modèle – c’est-à-dire lorsque vous récupérez des données à l’aide de méthodes de QuerySet telles que get(), filter() ou exclude() et que le champ de modèle se trouve en paramètre. Elles ne sont appelées à aucun autre moment, elles peuvent donc se permettre d’exécuter du code un peu complexe, comme la vérification de connection.settings_dict dans l’exemple ci-dessus.

Certains types de colonnes de base de données acceptent des paramètres tels que CHAR(25), où le paramètre 25 représente la taille maximale de la colonne. Dans de tels cas, il est plus souple de définir le paramètre dans le modèle plutôt qu’il soit codé en dur dans la méthode db_type(). Par exemple, ce ne serait pas très raisonnable d’avoir un type CharMaxlength25Field tel que ci-dessous :

# This is a silly example of hard-coded parameters.
class CharMaxlength25Field(models.Field):
    def db_type(self, connection):
        return 'char(25)'

# In the model:
class MyModel(models.Model):
    # ...
    my_field = CharMaxlength25Field()

La meilleure manière de résoudre ce problème serait d’autoriser le paramètre à être défini au moment de l’exécution, c’est-à-dire quand la classe est instanciée. Pour faire cela, implémentez la méthode Field.__init__() comme ceci :

# This is a much more flexible example.
class BetterCharField(models.Field):
    def __init__(self, max_length, *args, **kwargs):
        self.max_length = max_length
        super().__init__(*args, **kwargs)

    def db_type(self, connection):
        return 'char(%s)' % self.max_length

# In the model:
class MyModel(models.Model):
    # ...
    my_field = BetterCharField(25)

Pour terminer, si votre colonne exige une configuration SQL vraiment complexe, renvoyez None depuis db_type(). Cela aura pour conséquence que le code de création SQL de Django va omettre ce champ. Vous êtes alors responsable de créer la colonne dans la bonne table d’une autre manière, mais au moins cela vous permet d’indiquer à Django de ne pas intervenir dans ce processus.

La méthode rel_db_type() est appelée par des champs tels que ForeignKey et OneToOneField qui pointent vers un autre champ pour déterminer leur type de colonne de base de données. Par exemple, si vous avez un UnsignedAutoField, vous avez aussi besoin que les clés étrangères qui pointent vers ce champ utilisent le même type de données :

# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
    def db_type(self, connection):
        return 'integer UNSIGNED AUTO_INCREMENT'

    def rel_db_type(self, connection):
        return 'integer UNSIGNED'

Conversion de valeurs en objets Python

Si votre classe Field personnalisée manipule des structures de données plus complexes que des chaînes, des dates, des entiers ou des nombres à virgule, il peut être nécessaire de surcharger from_db_value() et to_python().

Si elle est présente dans la sous-classe de champ, from_db_value() sera appelée dans tous les cas où des données sont chargées à partir de la base de données, y compris dans les appels d’agrégation et les appels values().

to_python() est appelée par la désérialisation et dans le cadre de la méthode clean() utilisée depuis les formulaires.

En règle générale, to_python() devrait gérer de manière élégante tous les paramètres suivants :

  • Une instance ayant le type correct (par exemple Hand dans le cas modèle de ce document).
  • Une chaîne
  • None (si le champ autorise null=True)

In our HandField class, we’re storing the data as a VARCHAR field in the database, so we need to be able to process strings and None in the from_db_value(). In to_python(), we need to also handle Hand instances:

import re

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _

def parse_hand(hand_string):
    """Takes a string of cards and splits into a full hand."""
    p1 = re.compile('.{26}')
    p2 = re.compile('..')
    args = [p2.findall(x) for x in p1.findall(hand_string)]
    if len(args) != 4:
        raise ValidationError(_("Invalid input for a Hand instance"))
    return Hand(*args)

class HandField(models.Field):
    # ...

    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        return parse_hand(value)

    def to_python(self, value):
        if isinstance(value, Hand):
            return value

        if value is None:
            return value

        return parse_hand(value)

Remarquez que ces méthodes renvoient toujours une instance Hand. C’est le type d’objet Python que nous voulons stocker dans l’attribut du modèle.

Pour to_python(), si quoi que ce soit se passe mal durant la conversion de la valeur, il faut lever une exception de type ValidationError.

Conversion d’objets Python en valeurs de requête

Comme l’utilisation d’une base de données nécessite une conversion dans les deux sens, si vous surchargez from_db_value() vous devrez aussi surcharger get_prep_value() pour convertir les objets Python vers des valeurs de requête.

Par exemple :

class HandField(models.Field):
    # ...

    def get_prep_value(self, value):
        return ''.join([''.join(l) for l in (value.north,
                value.east, value.south, value.west)])

Avertissement

Si votre champ personnalisé utilise les types CHAR, VARCHAR ou TEXT de MySQL, vous devez garantir que la méthode get_prep_value() renvoie toujours un type chaîne. MySQL effectue des correspondances souples mais imprévisibles lorsque des requêtes sur ces types se font avec un nombre entier, ce qui peut faire que les résultats contiendront des objets non prévus. Ce problème ne peut pas se produire si vous renvoyez systématiquement des chaînes dans get_prep_value().

Conversion de valeurs de requête en valeurs de base de données

Certains types de données (par exemple les dates) doivent être dans un format spécifique avant de pouvoir être utilisées par un moteur de base de données. get_db_prep_value() est la méthode où ces conversions doivent être réalisées. La connexion spécifique qui sera utilisée pour la requête est transmise en tant que paramètre connection. Cela vous permet d’utiliser la logique de conversion spécifique au moteur si cela est nécessaire.

Par exemple, Django utilise la méthode suivante pour son champ BinaryField:

def get_db_prep_value(self, value, connection, prepared=False):
    value = super().get_db_prep_value(value, connection, prepared)
    if value is not None:
        return connection.Database.Binary(value)
    return value

Dans le cas où votre champ personnalisé a besoin d’une conversion spéciale lors de son enregistrement qui n’est pas la même que la conversion utilisée pour les paramètres normaux dans une requête, vous pouvez surcharger get_db_prep_save().

Pré-traitement des valeurs avant enregistrement

Si vous souhaitez pré-traiter la valeur juste avant l’enregistrement, vous pouvez utiliser pre_save(). Par exemple, le champ DateTimeField de Django utilise cette méthode pour définir correctement l’attribut dans le cas de auto_now ou auto_now_add.

Si vous surchargez cette méthode, vous devez renvoyez la valeur de l’attribut à la fin. Vous devriez aussi mettre à jour l’attribut du modèle si vous modifiez la valeur, afin que tout code ayant des références sur le modèle voie toujours la valeur correcte.

Sélection du champ de formulaire pour un champ de modèle

Pour personnaliser le champ de formulaire utilisé par ModelForm, vous pouvez surcharger formfield().

La classe de champ de formulaire peut être indiquée via les paramètres form_class et choices_form_class ; ce dernier est utilisé si le champ contient une liste de choix, sinon c’est form_class qui est pris en compte. Si ces paramètres ne sont pas présents, c’est CharField ou TypedChoiceField qui seront utilisés.

Tout le contenu du dictionnaire kwargs est directement transmis à la méthode __init__() du champ de formulaire. En principe, tout ce qu’il y a à faire est de définir une bonne valeur par défaut du paramètre form_class (ou choices_form_class) puis de déléguer le reste à la classe parente. Il est possible que vous deviez écrire un champ de formulaire personnalisé (et même un composant de formulaire). Consultez la documentation sur les formulaires pour plus de détails sur ce sujet.

Poursuivant notre exemple en cours, on peut écrire la méthode formfield() ainsi :

class HandField(models.Field):
    # ...

    def formfield(self, **kwargs):
        # This is a fairly standard way to set up some defaults
        # while letting the caller override them.
        defaults = {'form_class': MyFormField}
        defaults.update(kwargs)
        return super().formfield(**defaults)

Nous partons du principe que la classe de champ MyFormField a été importée (elle possède son propre composant par défaut). Ce document ne traite pas en détails de l’écriture de champs de formulaires personnalisés.

Émulation de types de champs intégrés

Si vous avez créé une méthode db_type(), pas besoin de vous préoccuper de get_internal_type(), elle ne sera que peu utilisée. Il peut cependant arriver que le stockage de votre base de données a un même type qu’un autre champ, vous pouvez donc réutiliser la logique de ce dernier pour créer la bonne colonne.

Par exemple :

class HandField(models.Field):
    # ...

    def get_internal_type(self):
        return 'CharField'

Quel que soit le moteur de base de données utilisé, cela signifie que migrate et les autres commandes SQL créeront le bon type de colonne pour le stockage d’une chaîne.

Si get_internal_type() renvoie une chaîne qui n’est pas connu de Django pour le moteur de base de données que vous utilisez – autrement dit, qu’il n’apparaît pas dans django.db.backends.<db_name>.base.DatabaseWrapper.data_types – la chaîne sera quand même utilisée par le sérialiseur, mais la méthode par défaut db_type() retournera None. Consultez la documentation de db_type() pour les raisons pour lesquelles cela pourrait être utile. Mettre une chaîne descriptive dans le type du champ pour la sérialisation est une idée utile si vous devez un jour utiliser la sortie de sérialisation à un autre endroit, en dehors de Django.

Conversion des données de champs pour la sérialisation

Pour personnaliser la façon dont les valeurs sont sérialisés par un sérialiseur, vous pouvez surcharger value_to_string(). L’appel à value_from_object() est le meilleur moyen d’obtenir la valeur du champ avant sa sérialisation. Par exemple, comme par ailleurs le champ HandField utilise des chaînes pour le stockage de ses données, nous pouvons réutiliser du code existant de conversion :

class HandField(models.Field):
    # ...

    def value_to_string(self, obj):
        value = self.value_from_object(obj)
        return self.get_prep_value(value)

Quelques conseils généraux

L’écriture d’un champ personnalisé peut être un processus fastidieux, particulièrement si vous procédez à des conversions complexes entre vos types Python et les formats de base de données et de sérialisation. Voici quelques astuces qui faciliteront ces opérations :

  1. Observez les champs Django existants (dans django/db/models/fields/__init__.py) comme source d’inspiration. Cherchez un champ qui ressemble à ce que vous voulez faire et complétez ce qui manque, au lieu de créer un tout nouveau champ à partir de zéro.
  2. Écrivez une méthode __str__() pour la classe de champ que vous rédigez. À beaucoup d’endroits, le comportement par défaut du code du champ est d’appeler str() sur la valeur (dans les exemples de ce document, value serait une instance Hand, pas HandField). Ainsi, si votre méthode __str__() convertit automatiquement l’objet Python sous sa forme textuelle, vous économiserez beaucoup de travail.

Écriture d’une sous-classe

En plus des méthodes ci-dessus, les champs qui manipulent des fichiers doivent répondre à quelques exigences supplémentaires dont il faut tenir compte. La plupart des mécanismes fournis par FileField, comme la maîtrise du stockage en base de données et de la récupération des données, peuvent rester tels quels ; il reste donc aux sous-classes la tâche pas toujours triviale de s’occuper de la prise en charge d’un type particulier de fichier.

Django propose une classe File utilisée comme intermédiaire et chargée de s’occuper des contenus des fichiers et de leurs opérations. Elle peut être la base de sous-classes qui peuvent personnaliser l’accès au fichier et les méthodes disponibles. Elle se trouve dans django.db.models.fields.files et son comportement par défaut est expliqué dans la documentation sur les fichiers.

Quand une sous-classe de File a été créée, il s’agit d’indiquer à la nouvelle sous-classe de FileField qu’elle doit l’utiliser. Pour cela, définissez la nouvelle sous-classe de File dans l’attribut spécial attr_class de la sous-classe de FileField.

Quelques suggestions

En plus des détails ci-dessus, voici quelques lignes de conduite qui peuvent grandement améliorer l’efficacité et la lisibilité du code du champ.

  1. Le code source du champ ImageField de Django (dans django/db/models/fields/files.py) est un très bon exemple de la manière dont une sous-classe de FileField peut prendre en charge un type particulier de fichier, car elle tient compte de toutes les techniques décrites ci-dessus.
  2. Mettez si possible en cache les attributs de fichier. Comme les fichiers peuvent être stockés sur des systèmes distants, leur accès peut impliquer des coûts de temps ou même financiers, ce qui n’est pas toujours nécessaire. Dès qu’un fichier est récupéré pour obtenir certaines informations sur son contenu, mettez le plus possible de ces données en cache pour réduire le nombre d’accès au fichier qui pourraient se produire lors d’éventuelles demandes futures pour ces mêmes informations.
Back to Top