É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 simple classe Python 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

La manière la plus simple d’imaginer un champ de modèle est de considérer un objet acceptant un objet Python normal (une chaîne, une valeur booléenne, un datetime ou un objet plus complexe comme Hand) et qui le convertit dans un format adapté à l’utilisation avec une base de données (y compris 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 existe une manière assez directe de convertir vos données, par exemple en une chaîne.

Pour notre exemple Hand, nous pourrions convertir les données des cartes en une chaîne de 104 caractères en concaténant toutes les cartes dans un ordre prédéterminé, par exemple d’abord toutes les cartes nord, puis toutes les cartes est, sud et ouest. Ainsi les objets Hand peuvent être enregistrés dans des colonnes texte ou caractères de la base de données.

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 simplement 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 ne font que transmettre les options à la classe parente et ne les utilisent pas. Il ne tient qu’à vous d’être plus strict sur les options acceptées, ou d’adopter le comportement plus simple et 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

La contrepartie à l’écriture de la méthode __init __() est l’écriture de la méthode deconstruct(). Cette méthode indique à Django comment prendre une instance du nouveau champ afin de le réduire en format sérialisé ; en particulier, le choix des paramètres à passer à __init __() pour le recréer.

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.

Le contrat de deconstruct() est simple ; il 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é, c’est à vous d’écrire du code pour mettre sa valeur dans kwargs:

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. Bien sûr, 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 simplement 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)

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 simplement 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. La manière la plus simple de gérer cela dans une méthode db_type(), c’est de se baser sur l’attribut connection.settings_dict['ENGINE'].

Par exemple :

class MyDateField(models.Field):
    def db_type(self, connection):
        if connection.settings_dict['ENGINE'] == 'django.db.backends.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, il suffit d’implémenter 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 bien sûr 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)

Dans notre classe HandField, nous stockons les données avec un champ VARCHAR dans la base de données, il faut donc pouvoir gérer des chaînes de caractères et la valeur None dans from_db_value(). Dans``to_python()``, il est aussi nécessaire de gérer les instances de Hand:

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 to_python() 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, il suffit de définir 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