Validación de inlines dependientes en django admin

Estoy usando Django 1.4 y quiero establecer reglas de validación que comparen valores de diferentes líneas.

Tengo tres clases simples

En models.py:

class Shopping(models.Model): shop_name = models.CharField(max_length=200) class Item(models.Model): item_name = models.CharField(max_length=200) cost = models.IntegerField() item_shop = models.ForeignKey(Shopping) class Buyer(models.Model): buyer_name = models.CharField(max_length=200) amount = models.IntegerField() buyer_shop = models.ForeignKey(Shopping) 

En admin.py:

 class ItemInline(admin.TabularInline): model = Item class BuyerInline(admin.TabularInline): model = Buyer class ShoppingAdmin(admin.ModelAdmin): inlines = (ItemInline, BuyerInline) 

Así, por ejemplo, es posible comprar una botella de rhum a 10 $ y una de vodka a 8 $. Mike paga 15 $ y Tom paga 3 $.

El objective es evitar que el usuario guarde una instancia con sums que no coincidan: lo que se ha pagado debe ser el mismo que la sum de los costos del artículo (es decir, 10 + 8 = 15 + 3).

Lo intenté:

  • elevando ValidationError en el método Shopping.clean. Pero las inline aún no están actualizadas en limpio, por lo que las sums no son correctas
  • elevar ValidationError en el método ShoppingAdmin.save_related. Pero al generar ValidationError aquí, se obtiene una página de error poco amigable para el usuario en lugar de redirigir a la página de cambio con un buen mensaje de error.

¿Hay alguna solución a este problema? ¿Es la validación del lado del cliente (javascript / ajax) la más simple?

Podría anular el formato Inline para lograr lo que desea. En el método de limpieza del formset, tiene acceso a su instancia de Shopping a través del miembro ‘instancia’. Por lo tanto, podría usar el modelo de Compras para almacenar el total calculado temporalmente y hacer que sus formularios se comuniquen. En models.py:

 class Shopping(models.Model): shop_name = models.CharField(max_length=200) def __init__(self, *args, **kwargs) super(Shopping, self).__init__(*args, **kwargs) self.__total__ = None 

en admin.py:

 from django.forms.models import BaseInlineFormSet class ItemInlineFormSet(BaseInlineFormSet): def clean(self): super(ItemInlineFormSet, self).clean() total = 0 for form in self.forms: if not form.is_valid(): return #other errors exist, so don't bother if form.cleaned_data and not form.cleaned_data.get('DELETE'): total += form.cleaned_data['cost'] self.instance.__total__ = total class BuyerInlineFormSet(BaseInlineFormSet): def clean(self): super(BuyerInlineFormSet, self).clean() total = 0 for form in self.forms: if not form.is_valid(): return #other errors exist, so don't bother if form.cleaned_data and not form.cleaned_data.get('DELETE'): total += form.cleaned_data['cost'] #compare only if Item inline forms were clean as well if self.instance.__total__ is not None and self.instance.__total__ != total: raise ValidationError('Oops!') class ItemInline(admin.TabularInline): model = Item formset = ItemInlineFormSet class BuyerInline(admin.TabularInline): model = Buyer formset = BuyerInlineFormSet 

Esta es la única forma limpia en que puede hacerlo (según mi conocimiento) y todo está ubicado donde debería estar.

EDITAR: Se agregó la verificación * if form.cleaned_data * ya que los formularios también contienen líneas vacías. Por favor, déjame saber cómo funciona esto para ti!

EDIT2: Se agregó la comprobación de formularios que se van a eliminar, como se indica correctamente en los comentarios. Estas formas no deben participar en los cálculos.

Muy bien, tengo una solución. Se trata de editar el código del administrador de django.

En django / contrib / admin / options.py, en los métodos add_view (línea 924) y change_view (línea 1012), localice esta parte:

  [...] if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, True) [...] 

y reemplazarlo con

  if not hasattr(self, 'clean_formsets') or self.clean_formsets(form, formsets): if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, True) 

Ahora en tu ModelAdmin, puedes hacer algo como esto

 class ShoppingAdmin(admin.ModelAdmin): inlines = (ItemInline, BuyerInline) def clean_formsets(self, form, formsets): items_total = 0 buyers_total = 0 for formset in formsets: if formset.is_valid(): if issubclass(formset.model, Item): items_total += formset.cleaned_data[0]['cost'] if issubclass(formset.model, Buyer): buyers_total += formset.cleaned_data[0]['amount'] if items_total != buyers_total: # This is the most ugly part :( if not form._errors.has_key(forms.forms.NON_FIELD_ERRORS): form._errors[forms.forms.NON_FIELD_ERRORS] = [] form._errors[forms.forms.NON_FIELD_ERRORS].append('The totals don\'t match!') return False return True 

Sin embargo, esto es más un hack que una solución adecuada. ¿Alguna sugerencia de mejora? ¿Alguien piensa que esto debería ser una solicitud de función en django?