É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(object):
    """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 ?

class Field

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(HandField, self).__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.

Field.__init__()

La méthode __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.

La métaclasse SubfieldBase

class django.db.models.SubfieldBase

Comme indiqué dans l’introduction, les sous-classes de champs sont souvent nécessaires pour deux raisons : soit pour tirer profit d’un type de colonne spécifique à une base de données, soit pour gérer des types Python complexes. Il est aussi imaginable d’avoir les deux contraintes. Si la seule contrainte est un type de colonne spécifique à une base de données et que la représentation Python de vos champs de modèles correspond à un type Python standard directement dérivé du contenu en base de données, cette section ne vous concerne pas.

Si vous devez gérer des types Python personnalisés, comme notre classe Hand, il faut s’assurer que lorsque Django initialise une instance du modèle et attribue au champ personnalisé une valeur provenant de la base de données, cette valeur soit convertie en un objet Python approprié. Les détails internes de ce processus sont un peu complexes, mais le code que vous devez écrire dans votre classe Field est simple : votre sous-classe de champ doit utiliser une métaclasse particulière :

Par exemple, avec Python 2 :

class HandField(models.Field):

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

    __metaclass__ = models.SubfieldBase

    def __init__(self, *args, **kwargs):
        ...

Avec Python 3, au lieu de définir l’attribut __metaclass__, ajoutez metaclass à la définition de classe :

class HandField(models.Field, metaclass=models.SubfieldBase):
    ...

Si vous souhaitez que votre code fonctionne à la fois avec Python 2 et 3, vous pouvez utiliser six.with_metaclass():

from django.utils.six import with_metaclass

class HandField(with_metaclass(models.SubfieldBase, models.Field)):
    ...

This ensures that the to_python() method will always be called when the attribute is initialized.

ModelForms and custom fields

If you use SubfieldBase, to_python() will be called every time an instance of the field is assigned a value (in addition to its usual call when retrieving the value from the database). This means that whenever a value may be assigned to the field, you need to ensure that it will be of the correct datatype, or that you handle any exceptions.

C’est particulièrement important quand vous utilisez des ModelForms. Lors de l’enregistrement du contenu d’un ModelForm, Django utilise les valeurs de formulaire pour créer des instances de modèles. Cependant, si les valeurs de formulaire nettoyées ne peuvent être utilisées comme source de donnée valable pour ce champ, le processus habituel de validation de formulaire échouera.

C’est pour cela que vous devez garantir que le champ de formulaire utilisé pour représenter votre champ personnalisé effectue toute validation de saisie et nettoyage de donnée nécessaire pour convertir les saisies utilisateurs des formulaires en valeurs de champ de modèle compatible avec to_python(). Cela peut parfois nécessiter l’écriture d’un champ de formulaire spécialisé et/ou l’implémentation de la méthode formfield() de votre champ qui renverra une classe de champ de formulaire dont la méthode to_python() renvoie le type de donnée correct.

Documentation du champ personnalisé

Field.description

Comme toujours, vous devriez documenter votre type de champ afin que ses utilisateurs sachent de quoi il s’agit. En plus de la chaîne « docstring » utile aux développeurs, il est aussi possible d’afficher une brève description du type de champ pour les utilisateurs de l’interface d’administration, par l’intermédiaire de l’application django.contrib.admindocs. Pour cela, il suffit de fournir un texte descriptif dans l’attribut de classe description de votre champ personnalisé. Dans l’exemple ci-dessus, la description affichée pour HandField par l’application admindocs 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 et défini la __metaclass__, 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

Field.db_type(connection)

Renvoie le type de donnée de la colonne de la base de données de la classe Field, prenant en compte l’objet connection et les réglages associés.

Disons que vous avez créé un type spécifique à PostgreSQL nommé mytype. Vous pouvez utiliser ce champ avec Django en créant une sous-classe de Field et en surchargeant la méthode db_type() de cette façon :

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'

La méthode db_type() n’est appelée par Django qu’au moment de la génération de la commande CREATE TABLE pour votre application, c’est-à-dire lors de la création initiale des tables. Elle n’est plus appelée ensuite, elle peut donc se permettre d’exécuter du code un tant soit peu complexe, comme pour la consultation 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 django.db.models.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(BetterCharField, self).__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.

Conversion de valeurs de base de données en objets Python

Field.to_python(value)

Convertit la valeur renvoyée par la base de données (ou le sérialiseur) en un objet Python.

L’implémentation par défaut renvoie simplement value, pour le cas fréquent où le moteur de base de données renvoie déjà la valeur dans le bon format (comme chaîne Python, par exemple).

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 faudra alors surcharger cette méthode. En règle générale, la méthode devrait pouvoir traiter correctement des valeurs parmi celles-ci :

  • Une instance ayant le type correct (par exemple Hand dans le cas modèle de ce document).

  • Une chaîne de caractères (par ex. provenant d’un désérialiseur)

  • Une valeur renvoyée habituellement par le type de colonne de base de données que vous utilisez.

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 des instances de Hand dans to_python():

import re

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

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

        # The string case.
        p1 = re.compile('.{26}')
        p2 = re.compile('..')
        args = [p2.findall(x) for x in p1.findall(value)]
        if len(args) != 4:
            raise ValidationError("Invalid input for a Hand instance")
        return Hand(*args)

Remarquez que cette méthode renvoie toujours une instance Hand. C’est le type d’objet Python que nous voulons stocker dans l’attribut du modèle. Si quoi que ce soit se passe mal durant la conversion de la valeur, il faut lever une exception de type ValidationError.

N’oubliez pas : si votre champ personnalisé a besoin que sa méthode to_python() soit appelée lors de sa création, vous devez utiliser la métaclasse SubfieldBase mentionnée précédemment. Sinon, to_python() ne sera pas appelée automatiquement.

Avertissement

Si votre champ personnalisé autorise null=True, toute méthode du champ acceptant value en paramètre, comme to_python() et get_prep_value(), devrait savoir traiter le cas où value vaut None.

Conversion d’objets Python en valeurs de requête

Field.get_prep_value(value)

C’est l’inverse de to_python() lorsqu’il s’agit d’interagir avec les moteurs de base de données (contrairement à la sérialisation). Le paramètre value est la valeur actuelle de l’attribut du modèle (un champ ne contient pas de référence à son modèle conteneur, il ne peut donc pas obtenir lui-même la valeur) et cette méthode devrait renvoyer une valeur dans un format préparé en vue de son utilisation comme paramètre de requête.

Cette conversion ne doit pas inclure de conversion spécifique à une base de données. Si de telles conversions sont nécessaires, elles devraient avoir lieu dans l’appel à get_db_prep_value().

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

Field.get_db_prep_value(value, connection, prepared=False)

Certains types de données (par exemple les dates) doivent être spécialement formatés avant de pouvoir être utilisés par un moteur de base de données. C’est dans la méthode get_db_prep_value() que ces conversions doivent avoir lieu. La connexion spécifique qui sera utilisée pour la requête est transmise dans le paramètre connection. Cela permet d’effectuer des conversions spécifiques à une base de données si c’est nécessaire.

Le paramètre prepared indique si la valeur a déjà passé par des conversions get_prep_value(). Lorsque prepared vaut False, l’implémentation par défaut de get_db_prep_value() appelle get_prep_value() pour effectuer des conversions de données initiales avant de procéder à des conversions spécifiques à la base de données.

Field.get_db_prep_save(value, connection)

Même chose que la méthode ci-dessus, mais appelée lorsque la valeur du champ doit être enregistrée dans la base de données. Comme l’implémentation par défaut ne fait qu’appeler get_db_prep_value(), vous n’avez généralement pas besoin d’implémenter cette méthode, sauf dans les cas où le champ personnalisé doit subir une conversion particulière lors de l’enregistrement qui est différente de la conversion utilisée avant insertion en tant que paramètre de requête (ce qui est implémenté par get_db_prep_value()).

Pré-traitement des valeurs avant enregistrement

Field.pre_save(model_instance, add)

Cette méthode est appelée juste avant get_db_prep_save() et doit renvoyer la valeur de l’attribut correspondant de model_instance pour ce champ. Le nom d’attribut est dans self.attname (défini par Field). Si le modèle est enregistré dans la base de données pour la première fois, le paramètre add vaudra True, sinon il vaudra False.

Cette méthode ne doit être surchargée que s’il est nécessaire de pré-traiter la valeur juste avant l’enregistrement. Par exemple, la classe DateTimeField de Django utilise cette méthode pour définir correctement l’attribut en cas d’utilisation 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.

Préparation des valeurs pour la recherche en base de données

Comme pour la conversion de valeurs, la préparation de valeurs pour des recherches en base de données est un processus en deux phases.

Field.get_prep_lookup(lookup_type, value)

get_prep_lookup() se charge de la première phase de la préparation de la recherche, en effectuant des contrôles génériques de validité des données.

Cette méthode prépare value pour sa transmission à la base de données dans un contexte de recherche (une contrainte SQL WHERE). Le paramètre lookup_type sera l’un des filtres de recherche acceptés par Django : exact, iexact, contains, icontains, gt, gte, lt, lte, in, startswith, istartswith, endswith, iendswith, range, year, month, day, isnull, search, regex et iregex.

Votre méthode doit être prête à gérer toutes ces valeurs de lookup_type et devrait soit lever une erreur ValueError si value est d’un mauvais type (par exemple si c’est une liste alors que vous attendiez un objet), soit une erreur TypeError si votre champ ne prend pas en charge ce type de recherche. Pour beaucoup de champs, il devrait suffire de vous occuper des types de recherche qui ont besoin d’un traitement spécial pour votre champ et pour le reste, appeler la méthode get_db_prep_lookup() de la classe parente.

Si vous avez eu besoin d’implémenter get_db_prep_save(), il sera probablement aussi nécessaire d’implémenter get_prep_lookup(). Si vous ne le faites pas, get_prep_value va être appelé par l’implémentation par défaut, pour gérer les requêtes exact, gt, gte, lt, lte, in et range.

Il peut aussi être utile d’implémenter cette méthode pour limiter le nombre de types de recherches utilisables par votre champ personnalisé.

Remarquez que pour les recherches range et in, get_prep_lookup reçoit une liste d’objets (en principe du bon type) et doit les convertir en une liste d’objets du type accepté par la base de données. La plupart du temps, vous pouvez réutiliser get_prep_value() ou au moins factoriser certaines partie communes.

Par exemple, le code suivant implémente get_prep_lookup pour limiter les recherches acceptées à exact et in:

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

    def get_prep_lookup(self, lookup_type, value):
        # We only handle 'exact' and 'in'. All others are errors.
        if lookup_type == 'exact':
            return self.get_prep_value(value)
        elif lookup_type == 'in':
            return [self.get_prep_value(v) for v in value]
        else:
            raise TypeError('Lookup type %r not supported.' % lookup_type)
Field.get_db_prep_lookup(lookup_type, value, connection, prepared=False)

Effectue toute conversion de donnée spécifique à une base de données requise par une recherche. Tout comme pour get_db_prep_value(), la connexion qui sera utilisée pour la requête est transmise par le paramètre connection. Le paramètre prepared indique si la valeur a déjà été préparée par get_prep_lookup().

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

Field.formfield(form_class=None, choices_form_class=None, **kwargs)

Renvoie le champ de formulaire par défaut qui sera utilisé lorsque ce champ de modèle est affiché dans un formulaire. Cette méthode est appelée par l’intermédiaire de ModelForm.

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.

En poursuivant notre exemple principal, nous pouvons écrire la méthode formfield() comme ceci :

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(HandField, self).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

Field.get_internal_type()

Renvoie une chaîne donnant le nom de la sous-classe de Field que nous émulons au niveau de la base de données. Cette information est utilisée pour déterminer le type de la colonne de base de données dans les cas simples.

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 syncdb et les autres commandes SQL créeront le bon type de colonne pour le stockage d’une chaîne.

If get_internal_type() returns a string that is not known to Django for the database backend you are using – that is, it doesn’t appear in django.db.backends.<db_name>.creation.data_types – the string will still be used by the serializer, but the default db_type() method will return None. See the documentation of db_type() for reasons why this might be useful. Putting a descriptive string in as the type of the field for the serializer is a useful idea if you’re ever going to be using the serializer output in some other place, outside of Django.

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

Field.value_to_string(obj)

Cette méthode est utilisée par les sérialiseurs pour convertir le champ en un résultat chaîne de caractères. La meilleure manière d’obtenir la valeur à sérialiser est d’appeler Field._get_val_from_obj(obj). Par exemple, comme l’objet HandField utilise de toute façon des chaînes pour son stockage de données, nous pouvons réutiliser du code de conversion existant :

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

    def value_to_string(self, obj):
        value = self._get_val_from_obj(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__() ou __unicode__() pour la classe de champ que vous rédigez. À beaucoup d’endroits, le comportement par défaut du code du champ est d’appeler force_text() sur la valeur (dans les exemples de ce document, value serait une instance Hand, pas HandField). Ainsi, si votre méthode __unicode__() (__str__() avec Python 3) 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.