Padrão de campo do modelo Django baseado em outro campo no mesmo modelo

91

Tenho um modelo que gostaria que contivesse o nome do sujeito e suas iniciais (os dados são um tanto anônimos e rastreados por iniciais).

Agora mesmo, eu escrevi

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)

Conforme indicado pela última linha, eu preferiria poder ter as iniciais realmente armazenadas no banco de dados como um campo (independente do nome), mas que seja inicializado com um valor padrão baseado no campo de nome. No entanto, estou tendo problemas porque os modelos de django não parecem ter um 'eu'.

Se eu mudar a linha para subject_init = models.CharField("Subject initials", max_length=2, default=subject_initials), posso fazer o syncdb, mas não consigo criar novos assuntos.

Isso é possível no Django, tendo uma função chamável dar um padrão para algum campo com base no valor de outro campo?

(Para os curiosos, o motivo pelo qual desejo separar as iniciais da minha loja separadamente é em casos raros em que sobrenomes estranhos podem ter diferentes daqueles que estou rastreando. Por exemplo, outra pessoa decidiu que as iniciais do Assunto 1 Chamado "John O'Mallory" são "JM" em vez de "JO" e deseja corrigir, edite-o como administrador.)

dr jimbob
fonte

Respostas:

88

Os modelos certamente têm um "eu"! É que você está tentando definir um atributo de uma classe de modelo como dependente de uma instância de modelo; isso não é possível, já que a instância não existe (e não pode) existir antes de você definir a classe e seus atributos.

Para obter o efeito desejado, substitua o método save () da classe de modelo. Faça as alterações que desejar na instância necessária e, em seguida, chame o método da superclasse para fazer o salvamento real. Aqui está um exemplo rápido.

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

Isso é abordado em Substituindo métodos de modelo na documentação.

Elf Sternberg
fonte
4
Observe que no Python 3 você pode simplesmente chamar super().save(*args, **kwargs)(sem os Subject, selfargumentos) como no exemplo da documentação referenciada.
Kurt Peek de
1
Uma pena que isso não permite definir um valor de atributo de modelo padrão baseado em outro, o que é particularmente útil no lado Admin (por exemplo, para um campo autoincrementado), uma vez que ele o controla ao salvá-lo. Ou estou entendendo mal?
Vadorequest
18

Não sei se existe uma maneira melhor de fazer isso, mas você pode usar um manipulador de sinal para o pre_savesinal :

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
fonte
1
Você importou em post_savevez de pre_save.
Ali Rasim Kocal
1
@arkocal: obrigado pelo aviso, acabei de consertar. Você pode sugerir edições nesses casos, isso ajuda a consertar coisas como esta :)
Gabi Purcaru
1
Parece que está faltando nesta implementação o **kwargsargumento que todas as funções do receptor deveriam ter de acordo com docs.djangoproject.com/en/2.0/topics/signals/… ?
Kurt Peek de
A documentação não recomenda sinais para caminhos de código que você controla . A resposta aceita de substituição save(), talvez alterada para substituição __init__, é melhor porque é mais explícita.
Adam Johnson
7

Usando sinais do Django , isso pode ser feito bem cedo, recebendo o post_initsinal do modelo.

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

O post_initsinal é enviado pela classe assim que ela tiver feito a inicialização na instância. Dessa forma, a instância obtém um valor para nameantes de testar se seus campos não anuláveis ​​estão definidos.

nariz grande
fonte
A documentação não recomenda sinais para caminhos de código que você controla . A resposta aceita de substituição save(), talvez alterada para substituição __init__, é melhor porque é mais explícita.
Adam Johnson
2

Como uma implementação alternativa da resposta de Gabi Purcaru , você também pode se conectar ao pre_savesinal usando o receiverdecorador :

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

Esta função de receptor também aceita os **kwargsargumentos de palavra-chave curinga que todos os manipuladores de sinal devem usar de acordo com https://docs.djangoproject.com/en/2.0/topics/signals/#receiver-functions .

Kurt Peek
fonte