Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Validate Django model data by calculating the sum of multiple rows

Tags:

django

I have two models:

class User(models.Model):
    name = models.CharField(max_length=32)


class Referral(models.Model):
    referring_user = models.ForeignKey(User, related_name="referrals")
    referred_user  = models.ForeignKey(User, related_name="referrers")
    percentage     = models.PositiveIntegerField()

The idea is that every user has n referrers, and should have at least one. Each referrer has a percentage value which should add up to 100% when added to the other referrers.

So User "Alice" might have referrers "Bob" (50%) and "Cynthia" (50%), and User "Donald" might have one referrer: "Erin" (100%).

The problem I have is with validation. Is there a way (preferably one that plays nice with the Django admin using admin.TabularInline) that I can have validation reject the saving of a User if the sum of Refferrals != 100%?

Ideally I want this to happen at the form/admin level and not by overriding User.save(), but at this point I don't know where to start. Most of Django's validation code appears to be atomic, and validation across multiple rows is not something I've done in Django before.

like image 221
Daniel Quinn Avatar asked Apr 11 '14 19:04

Daniel Quinn


2 Answers

After Jerry Meng suggested I look into the data property and not cleaned_data, I started poking around admin.ModelAdmin to see how I might access that method. I found get_form which appears to return a form class, so I overrode that method to capture the returning class, subclass it, and override .clean() in there.

Once inside, I looped over self.data, using a regex to find the relevant fields and then literally did the math.

import re
from django import forms
from django.contrib import admin

class UserAdmin(admin.ModelAdmin):

    # ...

    def get_form(self, request, obj=None, **kwargs):

        parent = admin.ModelAdmin.get_form(self, request, obj=None, **kwargs)

        class PercentageSummingForm(parent):
            def clean(self):

                cleaned_data = parent.clean(self)

                total_percentage = 0
                regex = re.compile(r"^referrers-(\d+)-percentage$")

                for k, v in self.data.items():
                    match = re.match(regex, k)
                    if match:
                        try:
                            total_percentage += int(v)
                        except ValueError:
                            raise forms.ValidationError(
                                "Percentage values must be integers"
                            )

                if not total_percentage == 100:
                    raise forms.ValidationError(
                        "Percentage values must add up to 100"
                    )

                return cleaned_data

        return PercentageSummingForm
like image 74
Daniel Quinn Avatar answered Oct 04 '22 18:10

Daniel Quinn


As per the Django docs, clean() is the official function to implement for your purposes. You could imagine a function that looks like this:

from django.core.exceptions import ValidationError

def clean(self):
    total_percentage = 0
    for referrer in self.referrers.all():
        total_percentage += referrer.percentage
    if total_percentage !== 100:
        raise ValidationError("Referrer percentage does not equal 100")
like image 43
Craig Labenz Avatar answered Oct 04 '22 19:10

Craig Labenz