¿Por qué no llama model.save () de django full_clean ()?

150

Tengo curiosidad por saber si alguien sabe si hay una buena razón por la cual el orm de django no llama 'full_clean' en un modelo a menos que se guarde como parte de un formulario de modelo.

Tenga en cuenta que no se llamará automáticamente a full_clean () cuando llame al método save () de su modelo. Deberá llamarlo manualmente cuando desee ejecutar la validación de modelo de un solo paso para sus propios modelos creados manualmente. el documento completo de django

(NOTA: la cita actualizada para Django 1.6 ... los documentos anteriores de django también tenían una advertencia sobre ModelForms).

¿Hay buenas razones por las cuales la gente no querría este comportamiento? Creo que si se tomara el tiempo de agregar validación a un modelo, desearía que esa validación se ejecute cada vez que se guarde el modelo.

Sé cómo hacer que todo funcione correctamente, solo estoy buscando una explicación.

Aaron
fuente
11
Muchas gracias por esta pregunta, me impidió golpear mi cabeza contra la pared mucho más tiempo. Creé un mixin que podría ayudar a otros. Echa un vistazo a la esencia: gist.github.com/glarrain/5448253
glarrain
Y finalmente uso la señal para atrapar el pre_savegancho y hacerlo full_cleanen todos los modelos atrapados.
Alfred Huang

Respuestas:

59

AFAIK, esto se debe a la compatibilidad con versiones anteriores. También hay problemas con ModelForms con campos excluidos, modelos con valores predeterminados, señales pre_save (), etc.

Fuentes en las que podría estar interesado:

lqc
fuente
3
El extracto más útil (en mi humilde opinión) de la segunda referencia: "Desarrollar una opción de validación" automática "que sea lo suficientemente simple como para ser realmente útil y lo suficientemente robusta para manejar todos los casos límite es, si es posible, mucho más que se puede lograr en el marco de tiempo 1.2. Por lo tanto, por ahora, Django no tiene tal cosa, y no lo tendrá en 1.2. Si cree que puede hacer que funcione para 1.3, su mejor opción es elaborar un propuesta, que incluye al menos un código de muestra, junto con una explicación de cómo lo mantendrá simple y robusto ".
Josh
30

Debido a la compatibilidad que se considera, la limpieza automática al guardar no está habilitada en el núcleo de django.

Si estamos comenzando un nuevo proyecto y queremos que el savemétodo predeterminado en el Modelo se pueda limpiar automáticamente, podemos usar la siguiente señal para limpiar antes de guardar cada modelo.

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
fuente
2
¿Por qué es esto mejor (o peor) que anular el método de guardar en algunos BaseModel (del cual todos los demás heredarán) para llamar a full_clean primero y luego llamar a super ()?
J__
77
Veo dos problemas con este enfoque 1) en el caso de que full_clean () de ModelForm se llamaría dos veces: por el formulario y por la señal 2) Si el formulario excluye algunos campos, la señal aún los validaría.
mehmet
1
Así que puede ser @mehmet puede agregar éstos if send == somemodel, then exclude some fieldsenpre_save_handler
Simin Jie
44
Para aquellos que usan o consideran usar este enfoque: tenga en cuenta que este enfoque no es oficialmente compatible con Django y no lo será en el futuro previsible (vea este comentario en el rastreador de errores de Django: code.djangoproject.com/ticket/ 29655 # comentario: 3 ), por lo que es probable que tropiece con algunas imperfecciones como que la autenticación deje de funcionar ( code.djangoproject.com/ticket/29655 ) si habilita la validación para todos los modelos. Tendrás que lidiar con esos problemas tú mismo. Sin embargo, no hay mejor enfoque atm.
Evgeny A.
2
A partir de Django 2.2.3, esto causa un problema con el sistema básico de autenticación. Obtendrás un ValidationError: Session with this Session key already exists. Para evitar esto, debe agregar una instrucción if para sender in list_of_model_classesevitar que la señal anule los modelos de autenticación predeterminados de Django. Define list_of_model_classescomo elijas
Addison Klinke
15

La forma más sencilla de llamar al full_cleanmétodo es simplemente anular el savemétodo en su model:

def save(self, *args, **kwargs):
    self.full_clean()
    return super(YourModel, self).save(*args, **kwargs)
M.Void
fuente
¿Por qué es esto mejor (o peor) que usar una señal?
J__
66
Veo dos problemas con este enfoque 1) en caso de que se llame dos veces a full_clean () de ModelForm: por el formulario y por el guardado 2) Si el formulario excluye algunos campos, el guardado aún los validaría.
mehmet
3

En lugar de insertar un fragmento de código que declara un receptor, podemos usar una aplicación como INSTALLED_APPSsección ensettings.py

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

Antes de eso, es posible que necesite instalar django-fullcleanusando PyPI:

pip install django-fullclean
Alfred Huang
fuente
13
¿Por qué usaría pip installalguna aplicación con 4 líneas de código (verifique el código fuente ) en lugar de escribir estas líneas usted mismo?
David D.
Otra biblioteca que no he probado yo mismo: github.com/danielgatis/django-smart-save
Flimm el
2

Si tiene un modelo que desea garantizar que tenga al menos una relación FK, y no desea usarlo null=Falseporque eso requiere establecer un FK predeterminado (que sería información basura), la mejor manera que he encontrado es para agregar personalizados .clean()y .save()métodos. .clean()plantea el error de validación y .save()llama a la limpieza. De esta forma, la integridad se aplica tanto desde los formularios como desde otro código de llamada, la línea de comandos y las pruebas. Sin esto, (AFAICT) no hay forma de escribir una prueba que garantice que un modelo tenga una relación FK con otro modelo elegido específicamente (no predeterminado).

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
fuente
1

Comentando la respuesta de @Alfred Huang y sus comentarios. Uno podría bloquear el enlace pre_save en una aplicación definiendo una lista de clases en el módulo actual (models.py) y verificando en el enlace 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
fuente