Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validation of dependant inlines in django admin

Tags:

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:

  • raising ValidationError in the Shopping.clean method. But the inlines aren't updated yet in clean so the sums are not correct
  • raising ValidationError in the ShoppingAdmin.save_related method. But raising ValidationError here gives a very user unfriendly error page instead of redirecting to the change page with a nice error message.

Is there any solution to this problem? Is client-side (javascript/ajax) validation the most simple?

like image 883
Rems Avatar asked Nov 23 '12 10:11

Rems


People also ask

How do I increase validation error in Django admin?

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() .

What is Inlines in Django?

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.


1 Answers

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.

like image 186
ppetrid Avatar answered Sep 19 '22 14:09

ppetrid