Поле модели Django по умолчанию основано на другом поле в той же модели

92

У меня есть модель, в которой я хотел бы содержать имена субъектов и их инициалы (данные несколько анонимны и отслеживаются по инициалам).

Прямо сейчас я написал

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)

Как указано в последней строке, я бы предпочел, чтобы инициалы фактически сохранялись в базе данных в виде поля (независимо от имени), но оно инициализируется значением по умолчанию на основе поля имени. Однако у меня возникают проблемы, так как модели django, похоже, не имеют «я».

Если я изменю строку на subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), то смогу выполнить syncdb, но не смогу создавать новые темы.

Возможно ли это в Django, если вызываемая функция дает значение по умолчанию для некоторого поля на основе значения другого поля?

(Для любопытных, причина, по которой я хочу разделить инициалы своего магазина отдельно, заключается в редких случаях, когда странные фамилии могут отличаться от тех, которые я отслеживаю. Например, кто-то другой решил, что инициалы Субъекта 1 с именем «Джон О'Мэллори» являются "JM", а не "JO", и хочет исправить, отредактируйте его как администратор.)

доктор джимбоб
источник

Ответы:

89

У моделей действительно есть «я»! Просто вы пытаетесь определить атрибут класса модели как зависящий от экземпляра модели; это невозможно, поскольку экземпляр не существует (и не может) существовать до того, как вы определите класс и его атрибуты.

Чтобы получить желаемый эффект, переопределите метод save () класса модели. Внесите в экземпляр необходимые изменения, а затем вызовите метод суперкласса, чтобы выполнить фактическое сохранение. Вот небольшой пример.

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

Это описано в документации по переопределению методов модели .

Эльф Штернберг
источник
5
Обратите внимание, что в Python 3 вы можете просто вызвать super().save(*args, **kwargs)(без Subject, selfаргументов), как в примере в указанной документации.
Курт Пик
1
Жаль, что это не позволяет определить значение атрибута модели по умолчанию на основе другого, что особенно полезно на стороне администратора (например, для автоматически увеличиваемого поля), поскольку оно обрабатывает его при сохранении. Или я это неправильно понимаю?
Vadorequest
18

Я не знаю , если есть лучший способ сделать это, но вы можете использовать обработчик сигнала для по pre_saveсигналу :

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)
Габи Пуркару
источник
1
Вы импортировали post_saveвместо pre_save.
Али Расим Кокал
1
@arkocal: спасибо за внимание, только что исправил. В таких случаях вы можете сами предлагать правки, это помогает исправить такие вещи :)
Габи Пуркару
1
Кажется, что в этой реализации отсутствует **kwargsаргумент, который должен иметь все функции-получатель согласно docs.djangoproject.com/en/2.0/topics/signals/… ?
Курт Пик
В документации не рекомендуются сигналы для управляемых вами кодовых путей . Принятый ответ переопределения save(), возможно, измененный на переопределение __init__, лучше, поскольку он более явный.
Адам Джонсон,
7

Используя сигналы Django , это может быть сделано достаточно рано, получив на post_initсигнале от модели.

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_initСигнал посылается по классу , как только он сделал инициализации на экземпляре. Таким образом, экземпляр получает значение, nameпрежде чем проверять, установлены ли его поля, не допускающие значения NULL.

большой нос
источник
В документации не рекомендуются сигналы для управляемых вами кодовых путей . Принятый ответ переопределения save(), возможно, измененный на переопределение __init__, лучше, поскольку он более явный.
Адам Джонсон,
2

В качестве альтернативной реализации ответа Габи Пуркару вы также можете подключиться к pre_saveсигналу с помощью receiverдекоратора :

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

Эта функция приемника также принимает **kwargsаргументы ключевого слова с подстановочными знаками, которые должны принимать все обработчики сигналов в соответствии с https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions .

Курт Пик
источник