I am using Django 1.4 and I want to set validation rules that compare values of different inlines.
I have three simple classes
In 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)
In admin.py:
class ItemInline(admin.TabularInline): model = Item class BuyerInline(admin.TabularInline): model = Buyer class ShoppingAdmin(admin.ModelAdmin): inlines = (ItemInline, BuyerInline)
So for example it is possible to buy a bottle of rhum at 10$ and one of vodka at 8$. Mike pays 15$ and Tom pays 3$.
The goal is to prevent the user from saving an instance with sums that don't match: what has been paid must be the same as the sum of the item costs (ie 10+8 = 15+3).
I tried:
Is there any solution to this problem? Is client-side (javascript/ajax) validation the most simple?
In django 1.2, model validation has been added. You can now add a "clean" method to your models which raise ValidationError exceptions, and it will be called automatically when using the django admin. The clean() method is called when using the django admin, but NOT called on save() .
The admin interface is also customizable in many ways. This post is going to focus on one such customization, something called inlines. When two Django models share a foreign key relation, inlines can be used to expose the related model on the parent model page. This can be extremely useful for many applications.
You could override your Inline formset to achieve what you want. In the clean method of the formset you have access to your Shopping instance through the 'instance' member. Therefore you could use the Shopping model to store the calculated total temporarily and make your formsets communicate. In 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
in 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
This is the only clean way you can do it (to the best of my knowledge) and everything is placed where it should be.
EDIT: Added the *if form.cleaned_data* check since forms contain empty inlines as well. Please let me know how this works for you!
EDIT2: Added the check for forms about to be deleted, as correctly pointed out in the comments. These forms should not participate in the calculations.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With