Maxim
Maxim

Reputation: 507

dependent multi object validation in django admin

Each "time range" entry of the TimeClass is dependent on each other.

They cannot overlap and start_time < end_time.

models.py

class Xyz(models.Model):
    ...   

class TimeRangeClass(models.Model)
    start_time = models.TimeField()
    end_time = models.TimeField()
    xyz = models.ForeignKey(Xyz)
    # other fields here

    def clean(self):
        # Here I loop through TimeRangeClass.objects.all() and 
        # check for conflicts through my custom "my_validator_method".
        # If there is a conflict I throw an error
        #(I've since modified it to just be one single query as per Titusz advice)             
        for each in TimeRangeClass.objects.filter(xyz=self.xyz).exclude(id=self.id):
            my_validator_method(start_time1=self.start_time, 
                                end_time1=self.end_time, 
                                start_time2=each.start_time, 
                                end_time2=each.end_time)

admin.py

from .models import TimeRangeClass, Xyz
class TimeRangeClassInLine(admin.TabularInline):
    model = TimeRangeClass
    extra = 3

@admin.register(Xyz)
class Xyz(admin.ModelAdmin):
    exclude = []
    inlines = [TimeRangeClassInLine]

Problem: I can edit/add multiple TimeRangeClass's at once through the admin. But given that the models.Model clean method only evaluates 1 change at a time I can't validate multiple edits against each other.

Example:

  1. Save an Entry1 & Entry2 without conflict

  2. Change Entry2 to produce a validation error

  3. Adjust Entry1 (instead of #2) so they do not overlap

  4. This doesn't register because neither changes are written to the db.

I'm looking for a workaround.

Upvotes: 0

Views: 852

Answers (1)

Titusz
Titusz

Reputation: 1477

Some hints on the problem:

You should not iterate over the full table when checking for overlapping rows. Just filter for the problematic rows... something like:

overlaps = TimeRangeClass.objects.filter(
    Q(start_time__gte=self.start_time, start_time__lt=self.end_time) | 
    Q(end_time__gt=self.start_time, end_time__lte=self.end_time)
)

overlaps is now a queryset that evaluates when you iterate over it and only returns the conflicting objects.

If you are using Django with postgres you should check out https://docs.djangoproject.com/es/1.9/ref/contrib/postgres/fields/#datetimerangefield.

Once you have the conflicting objects you should be able to change their start and end times within the function and save the changes. Model.save() will not automatically call the model.clean() method. But be aware, if you save an object from the Django admin it will call the model.clean() method before saving.

So something like that:

def clean():
    overlaps = TimeRangeClass.overlaps.for_trc(self)
    for trc_object in overlaps:
        fixed_object = fix_start_end(trc_object, self)
        fixed_object.save()

If you feel brave you should also read up on transactions to make the mutation of multiple objects in the database all succeed or all fail and nothing in between.

def clean():
    with transaction.atomic():
        # do your multi object magic here ...

Update on clarified question:

If you want to validate or pre/process data that comes from admin inlines you have to hook into the corresponding ModelAdmin method(s). There are multiple ways to approach this. I guess the easiest would be to override ModelAdmin.save_fromset. Here you have access to all the inlineforms before they have been saved.

Upvotes: 1

Related Questions