Django: ¿Cómo validar relaciones m2m?

Digamos que tengo un modelo de Basket y quiero validar que no se pueden agregar más de 5 Item s:

 class Basket(models.Model): items = models.ManyToManyField('Item') def save(self, *args, **kwargs): self.full_clean() super(Basket, self).save(*args, **kwargs) def clean(self): super(Basket, self).clean() if self.items.count() > 5: raise ValidationError('This basket can\'t have so many items') 

Pero cuando se intenta guardar una Basket se lanza un RuntimeError porque se excede la profundidad máxima de recursión.

El error es el siguiente:

 ValueError: "" needs to have a value for field "basket" before this many-to-many relationship can be used. 

Ocurre en la línea if self.items.count() > 5: .

Al parecer, las complejidades de Django simplemente no le permitirán validar las relaciones de m2m al guardar un modelo. ¿Cómo puedo validarlos entonces?

Nunca se pueden validar las relaciones en el método de limpieza del modelo. Esto se debe a que, en el momento de la limpieza, es posible que el modelo aún no exista, como es el caso de su cesta. Algo que no existe, tampoco puede tener relaciones.

O debe hacer su validación en los datos del formulario como lo indica @bhattravii, o llamar a form.save(commit=False) e implementar un método llamado save_m2m , que implementa el límite.

Para imponer el límite en el nivel del modelo, debe escuchar la señal m2m_changed . Tenga en cuenta que proporcionar comentarios al usuario final es mucho más difícil, pero evita el llenado excesivo de la cesta a través de diferentes medios.

He estado discutiendo esto en la lista de Desarrolladores de Django y de hecho he presentado un método para hacer esto para su consideración en el núcleo de Django de una forma u otra. El método no está totalmente probado ni finalizado, pero los resultados por ahora son muy alentadores y lo estoy empleando en un sitio mío con éxito.

En principio se basa en:

  1. Usando PostgreSQL como su motor de base de datos (estamos bastante seguros de que no funcionará en Lightdb o MySQL, pero estamos interesados ​​en que alguien lo pruebe) ingrese el código aquí

  2. Anulando el método post () de su vista (basada en clase) de manera que:

    1. Abre una transacción atómica.
    2. Guarda el formulario
    3. Guarda todos los formatos si es que hay
    4. Llama a Model.clean () o algo así como Model.full_clean ()
  3. En su Modelo, entonces, en el método llamado 2.4 arriba, verá todas las relaciones de muchos a muchos y de una a muchas. Puede validarlos y lanzar un ValidationError para ver la transacción completa revertida y sin impacto en la base de datos.

Esto está funcionando maravillosamente para mí:

 def post(self, request, *args, **kwargs): # The self.object atttribute MUST exist and be None in a CreateView. self.object = None self.form = self.get_form() self.success_url = reverse_lazy('view', kwargs=self.kwargs) if connection.vendor == 'postgresql': if self.form.is_valid(): try: with transaction.atomic(): self.object = self.form.save() save_related_forms(self) # A separate routine that collects all the formsets in the request and saves them if (hasattr(self.object, 'full_clean') and callable(self.object.full_clean)): self.object.full_clean() except (IntegrityError, ValidationError) as e: if hasattr(e, 'error_dict') and isinstance(e.error_dict, dict): for field, errors in e.error_dict.items(): for error in errors: self.form.add_error(field, error) return self.form_invalid(self.form) return self.form_valid(self.form) else: return self.form_invalid(self.form) else: # The standard Djangop post() method if self.form.is_valid(): self.object = self.form.save() save_related_forms(self) return self.form_valid(self.form) else: return self.form_invalid(self.form) 

Y la conversación en la lista de Desarrolladores está aquí:

https://groups.google.com/forum/#!topic/django-developers/pQ-8LmFhXFg

si desea contribuir con alguna experiencia que obtenga al experimentar con esto (quizás con otras bases de datos).

La única gran advertencia en el enfoque anterior es que delega guardar en el método post () que en la vista predeterminada se realiza en el método form_valid (), por lo que también debe reemplazar form_valid (), de lo contrario, un post () como el Uno de arriba te verá guardando el formulario dos veces. Lo cual es solo una pérdida de tiempo en un UpdateView pero desastroso en un CreateView.