Domyślne pole modelu Django oparte na innym polu w tym samym modelu

92

Mam model, w którym chciałbym, aby zawierał nazwiska badanych i ich inicjały (dane są nieco anonimizowane i śledzone według inicjałów).

Właśnie teraz napisałem

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)

Jak wskazano w ostatnim wierszu, wolałbym mieć możliwość zapisania inicjałów w bazie danych jako pola (niezależnie od nazwy), ale jest to inicjalizowane wartością domyślną opartą na polu nazwy. Jednak mam problemy, ponieważ modele django nie wydają się mieć „ja”.

Jeśli zmienię linię na subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), mogę wykonać syncdb, ale nie mogę tworzyć nowych tematów.

Czy jest to możliwe w Django, gdy funkcja wywoływalna daje wartość domyślną dla jakiegoś pola na podstawie wartości innego pola?

(Dla ciekawości powodem, dla którego chcę osobno oddzielić inicjały mojego sklepu są rzadkie przypadki, w których dziwne nazwiska mogą mieć inne niż te, które śledzę. Na przykład ktoś inny zdecydował, że inicjały podmiotu 1 o imieniu „John O'Mallory” są „JM” zamiast „JO” i chce naprawić to jako administrator).

dr jimbob
źródło

Odpowiedzi:

89

Modelki z pewnością mają „ja”! Chodzi o to, że próbujesz zdefiniować atrybut klasy modelu jako zależny od instancji modelu; nie jest to możliwe, ponieważ instancja nie istnieje (i nie może) istnieć przed zdefiniowaniem klasy i jej atrybutów.

Aby uzyskać pożądany efekt, zastąp metodę save () klasy model. Wprowadź potrzebne zmiany w instancji, a następnie wywołaj metodę nadklasy, aby dokonać właściwego zapisu. Oto krótki przykład.

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

Jest to omówione w sekcji Nadpisywanie metod modelu w dokumentacji.

Elf Sternberg
źródło
5
Zauważ, że w Pythonie 3 możesz po prostu wywołać super().save(*args, **kwargs)(bez Subject, selfargumentów), jak w przykładzie w dokumentacji, do której się odwołuje.
Kurt Peek
1
Szkoda, że ​​nie pozwala to na zdefiniowanie domyślnej wartości atrybutu modelu na podstawie innej, co jest szczególnie przydatne po stronie administratora (na przykład dla pola z autoinkrementacją), ponieważ obsługuje ją podczas zapisywania. A może źle to rozumiem?
Vadorequest
18

Nie wiem, czy istnieje lepszy sposób to zrobić, ale można korzystać z funkcji obsługi sygnału dla tej pre_savesygnału :

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
źródło
1
Zaimportowałeś post_savezamiast pre_save.
Ali Rasim Kocal
1
@arkocal: dzięki za ostrzeżenie, właśnie to naprawiłem. W takich przypadkach możesz zasugerować zmiany, pomaga to naprawić takie rzeczy :)
Gabi Purcaru
1
Wygląda na to, że w tej implementacji brakuje **kwargsargumentu, który powinny mieć wszystkie funkcje odbiornika zgodnie z docs.djangoproject.com/en/2.0/topics/signals/… ?
Kurt Peek
Dokumentacja odradza sygnały dla kontrolowanych ścieżek kodu . Zaakceptowana odpowiedź zastępowania save(), być może zmieniona na nadpisywanie __init__, jest lepsza, ponieważ jest bardziej wyraźna.
Adam Johnson
7

Korzystanie sygnały Django , można to zrobić dość wcześnie, przyjąwszy na post_initsygnał od modelu.

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(" ")))

post_initSygnał wysyłany jest przez klasę raz miało to miejsce na przykład inicjalizacji. W ten sposób instancja pobiera wartość nameprzed sprawdzeniem, czy jego pola nie dopuszczające wartości null są ustawione.

duży nos
źródło
Dokumentacja odradza sygnały dla kontrolowanych ścieżek kodu . Zaakceptowana odpowiedź zastępowania save(), być może zmieniona na nadpisywanie __init__, jest lepsza, ponieważ jest bardziej wyraźna.
Adam Johnson
2

Jako alternatywną realizację odpowiedzi Gabi Purcaru możesz też podłączyć się do pre_savesygnału za pomocą receiverdekoratora :

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()

Ta funkcja odbiornika przyjmuje również **kwargsargumenty słów kluczowych wieloznacznych, które wszystkie programy obsługi sygnału muszą przyjąć zgodnie z https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions .

Kurt Peek
źródło