Por que model.save () do django não chama full_clean ()?

150

Estou curioso para saber se alguém sabe se há uma boa razão pela qual o orm do django não chama 'full_clean' em um modelo, a menos que esteja sendo salvo como parte de um formulário de modelo.

Observe que full_clean () não será chamado automaticamente quando você chamar o método save () do seu modelo. Você precisará chamá-lo manualmente quando desejar executar a validação de modelo em uma etapa para seus próprios modelos criados manualmente. doc limpo completo do django

(NOTA: a cotação atualizada para o Django 1.6 ... os documentos anteriores do django também tinham uma advertência sobre o ModelForms.)

Existem boas razões para as pessoas não quererem esse comportamento? Eu acho que se você reservasse algum tempo para adicionar validação a um modelo, desejaria que a validação fosse executada toda vez que o modelo for salvo.

Eu sei como fazer tudo funcionar corretamente, só estou procurando uma explicação.

Aaron
fonte
11
Muito obrigado por esta pergunta, me impediu de bater minha cabeça contra a parede por muito mais tempo. Eu criei um mixin que pode ajudar os outros. Confira o gist: gist.github.com/glarrain/5448253
glarrain:
E finalmente uso o sinal para pegar o pre_savegancho e fazer full_cleanem todos os modelos capturados.
Alfred Huang

Respostas:

59

AFAIK, isso ocorre devido à compatibilidade com versões anteriores. Também há problemas com ModelForms com campos excluídos, modelos com valores padrão, sinais pre_save () etc.

Fontes nas quais você pode se interessar:

lqc
fonte
3
O trecho mais útil (IMHO) da segunda referência: "Desenvolvendo uma opção de validação" automática "que seja simples o suficiente para ser realmente útil e robusta o suficiente para lidar com todos os casos extremos é - se é que é possível - muito mais do que pode ser realizado no período 1.2.Portanto, por enquanto, o Django não tem nada disso e não o terá no 1.2.Se você acha que pode fazê-lo funcionar no 1.3, sua melhor aposta é trabalhar com um proposta, incluindo pelo menos algum código de exemplo, além de uma explicação de como você o manterá simples e robusto ".
21412 Josh
30

Por causa da compatibilidade, a limpeza automática ao salvar não é ativada no kernel do django.

Se estamos iniciando um novo projeto e queremos que o savemétodo padrão no Model seja limpo automaticamente, podemos usar o seguinte sinal para fazer a limpeza antes de cada modelo ser salvo.

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

@receiver(pre_save)
def pre_save_handler(sender, instance, *args, **kwargs):
    instance.full_clean()
Alfred Huang
fonte
2
Por que isso é melhor (ou pior) do que substituir o método save em algum BaseModel (do qual todos os outros herdarão) para chamar full_clean primeiro e depois chamar super ()?
J__
7
Eu vejo dois problemas com essa abordagem 1) no caso de full_clean () do ModelForm ser chamado duas vezes: pelo formulário e pelo sinal 2) Se o formulário excluir alguns campos, eles ainda serão validados pelo sinal.
mehmet
1
@mehmet Então, pode ser que você pode adicioná-los if send == somemodel, then exclude some fieldsempre_save_handler
Simin Jie
4
Para aqueles que estão usando ou considerando usar esta abordagem: lembre-se de que essa abordagem não é oficialmente suportada pelo Django e não será suportada em um futuro próximo (veja este comentário no rastreador de erros do Django: code.djangoproject.com/ticket/ 29655 # comment: 3 ), é provável que você depare com algumas imperfeições, como a autenticação para de funcionar ( code.djangoproject.com/ticket/29655 ), se você habilitar a validação para todos os modelos. Você terá que lidar com esses problemas você mesmo. No entanto, não há melhor abordagem atm.
Evgeny A.
2
A partir do Django 2.2.3, isso causa um problema no sistema de autenticação básica. Você receberá um ValidationError: Session with this Session key already exists. Para evitar isso, você precisa adicionar uma instrução if para sender in list_of_model_classesevitar que o sinal substitua os modelos de autenticação padrão do Django. Defina list_of_model_classescomo quiser
Addison Klinke
15

A maneira mais simples para chamar o full_cleanmétodo é apenas para substituição savemétodo na sua model:

def save(self, *args, **kwargs):
    self.full_clean()
    return super(YourModel, self).save(*args, **kwargs)
M.Void
fonte
Por que isso é melhor (ou pior) do que usar um sinal?
J__
6
Eu vejo dois problemas com essa abordagem 1) no caso de full_clean () do ModelForm ser chamado duas vezes: pelo formulário e pelo save 2) Se o formulário excluir alguns campos, eles ainda serão validados pelo save.
precisa
3

Em vez de inserir um pedaço de código que declara um receptor, podemos usar um aplicativo como INSTALLED_APPSseção emsettings.py

INSTALLED_APPS = [
    # ...
    'django_fullclean',
    # your apps here,
]

Antes disso, você pode precisar instalar django-fullcleanusando o PyPI:

pip install django-fullclean
Alfred Huang
fonte
13
Por que você aplicaria pip installalgum aplicativo com quatro linhas de código (verifique o código-fonte ) em vez de escrever essas linhas?
David D.
Outra biblioteca que eu não tenha me tentado: github.com/danielgatis/django-smart-save
Flimm
2

Se você tem um modelo que deseja garantir que tenha pelo menos um relacionamento FK e não deseja usá-lo, null=Falseporque isso requer a configuração de um FK padrão (que seria dados de lixo), a melhor maneira de criar isso é para adicionar métodos .clean()e customizados .save(). .clean()gera o erro de validação e .save()chama a limpeza. Dessa forma, a integridade é imposta a partir de formulários e de outro código de chamada, linha de comando e testes. Sem isso, (AFAICT) não há como escrever um teste que garanta que um modelo tenha uma relação FK com outro modelo escolhido (não padrão).

class Payer(models.Model):

    name = models.CharField(blank=True, max_length=100)
    # Nullable, but will enforce FK in clean/save:
    payer_group = models.ForeignKey(PayerGroup, null=True, blank=True,)

    def clean(self):
        # Ensure every Payer is in a PayerGroup (but only via forms)
        if not self.payer_group:
            raise ValidationError(
                {'payer_group': 'Each Payer must belong to a PayerGroup.'})

    def save(self, *args, **kwargs):
        self.full_clean()
        return super().save(*args, **kwargs)

    def __str__(self):
        return self.name
shacker
fonte
1

Comentando a resposta de @Alfred Huang e comentários sobre ela. Pode-se bloquear o gancho pre_save em um aplicativo, definindo uma lista de classes no módulo atual (models.py) e comparando-o no gancho pre_save:

CUSTOM_CLASSES = [obj for name, obj in
        inspect.getmembers(sys.modules[__name__])
        if inspect.isclass(obj)]

@receiver(pre_save)
def pre_save_handler(sender, instance, **kwargs):
    if type(instance) in CUSTOM_CLASSES:
        instance.full_clean()
Peter Shannon
fonte