Champ de modèle Django par défaut basé sur un autre champ du même modèle

91

J'ai un modèle que je voudrais contenir un nom de sujet et ses initiales (les données sont quelque peu anonymisées et suivies par des initiales).

En ce moment, j'ai écrit

class Subject(models.Model):

    name = models.CharField("Name", max_length=30)
    def subject_initials(self):
        return ''.join(map(lambda x: '' if len(x)==0 else x[0],
                           self.name.split(' ')))
    # Next line is what I want to do (or something equivalent), but doesn't work with
    # NameError: name 'self' is not defined
    subject_init = models.CharField("Subject Initials", max_length=5, default=self.subject_initials)

Comme indiqué par la dernière ligne, je préférerais pouvoir stocker les initiales dans la base de données en tant que champ (indépendant du nom), mais cela est initialisé avec une valeur par défaut basée sur le champ de nom. Cependant, j'ai des problèmes car les modèles django ne semblent pas avoir de «soi».

Si je change la ligne en subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), je peux faire la syncdb, mais je ne peux pas créer de nouveaux sujets.

Est-ce possible dans Django, avoir une fonction appelable donne une valeur par défaut à un champ en fonction de la valeur d'un autre champ?

(Pour les curieux, la raison pour laquelle je veux séparer les initiales de mon magasin séparément est dans de rares cas où les noms de famille bizarres peuvent être différents de ceux que je traque. Par exemple, quelqu'un d'autre a décidé que les initiales du sujet 1 nommé «John O'Mallory» sont "JM" plutôt que "JO" et souhaite le corriger, éditez-le en tant qu'administrateur.)

dr jimbob
la source

Réponses:

88

Les modèles ont certainement un «soi»! C'est juste que vous essayez de définir un attribut d'une classe de modèle comme dépendant d'une instance de modèle; ce n'est pas possible, car l'instance n'existe pas (et ne peut pas) exister avant que vous ne définissiez la classe et ses attributs.

Pour obtenir l'effet souhaité, remplacez la méthode save () de la classe de modèle. Apportez les modifications nécessaires à l'instance, puis appelez la méthode de la superclasse pour effectuer la sauvegarde réelle. Voici un exemple rapide.

def save(self, *args, **kwargs):
    if not self.subject_init:
        self.subject_init = self.subject_initials()
    super(Subject, self).save(*args, **kwargs)

Ceci est traité dans la rubrique Remplacement des méthodes de modèle dans la documentation.

Elfe Sternberg
la source
4
Notez qu'en Python 3 vous pouvez simplement appeler super().save(*args, **kwargs)(sans les Subject, selfarguments) comme dans l'exemple de la documentation référencée.
Kurt Peek
1
Dommage que cela ne permette pas de définir une valeur d'attribut de modèle par défaut basée sur une autre, ce qui est particulièrement utile côté Admin (par exemple, pour un champ auto-incrémenté), car il la gère lors de son enregistrement. Ou suis-je mal compris?
Vadorequest
18

Je ne sais pas s'il existe une meilleure façon de faire cela, mais vous pouvez utiliser un gestionnaire de signal pour le pre_savesignal :

from django.db.models.signals import pre_save

def default_subject(sender, instance, using):
    if not instance.subject_init:
        instance.subject_init = instance.subject_initials()

pre_save.connect(default_subject, sender=Subject)
Gabi Purcaru
la source
1
Vous avez importé post_saveau lieu de pre_save.
Ali Rasim Kocal
1
@arkocal: merci pour la mise au point, je viens de le réparer. Vous pouvez suggérer des modifications vous-même dans de tels cas, cela aide à résoudre des problèmes comme celui-ci :)
Gabi Purcaru
1
Il semble que cette implémentation **kwargsne dispose pas de l' argument que toutes les fonctions de récepteur devraient avoir selon docs.djangoproject.com/en/2.0/topics/signals/... ?
Kurt Peek le
La documentation recommande de ne pas utiliser de signaux pour les chemins de code que vous contrôlez . La réponse acceptée de la priorité save(), peut-être changée en priorité __init__, est meilleure car elle est plus explicite.
Adam Johnson le
7

En utilisant les signaux Django , cela peut être fait assez tôt, en recevant le post_initsignal du modèle.

from django.db import models
import django.dispatch

class LoremIpsum(models.Model):
    name = models.CharField(
        "Name",
        max_length=30,
    )
    subject_initials = models.CharField(
        "Subject Initials",
        max_length=5,
    )

@django.dispatch.receiver(models.signals.post_init, sender=LoremIpsum)
def set_default_loremipsum_initials(sender, instance, *args, **kwargs):
    """
    Set the default value for `subject_initials` on the `instance`.

    :param sender: The `LoremIpsum` class that sent the signal.
    :param instance: The `LoremIpsum` instance that is being
        initialised.
    :return: None.
    """
    if not instance.subject_initials:
        instance.subject_initials = "".join(map(
                (lambda x: x[0] if x else ""),
                instance.name.split(" ")))

Le post_initsignal est envoyé par la classe une fois qu'elle a effectué l'initialisation sur l'instance. De cette façon, l'instance obtient une valeur pour nameavant de tester si ses champs non nullables sont définis.

gros nez
la source
La documentation recommande de ne pas utiliser de signaux pour les chemins de code que vous contrôlez . La réponse acceptée de la priorité save(), peut-être changée en priorité __init__, est meilleure car elle est plus explicite.
Adam Johnson le
2

Comme implémentation alternative de la réponse de Gabi Purcaru , vous pouvez également vous connecter au pre_savesignal à l'aide du receiverdécorateur :

from django.db.models.signals import pre_save
from django.dispatch import receiver


@receiver(pre_save, sender=Subject)
def default_subject(sender, instance, **kwargs):
    if not instance.subject_init:
        instance.subject_init = instance.subject_initials()

Cette fonction de récepteur prend également les **kwargsarguments de mot-clé génériques que tous les gestionnaires de signaux doivent prendre selon https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions .

Kurt Peek
la source