TabularInline and date range overlap constraint
I use the following formset to avoid overlapping date ranges:
class OccupancyInlineFormset(BaseInlineFormSet): def clean(self): super().clean() for form in self.forms: conflicts = Occupancy.objects.filter(unit=form.cleaned_data['unit'], begin__lte=form.cleaned_data['end'], end__gte=form.cleaned_data['begin']) if any(conflicts): raise ValidationError(_('Overlapping occupancies!'), code='overlap')
This works well in principle, but my current
Occupancy instance always needs to have an end date of 9999-12-31. When I change this date in the admin form for the current instance (and add a new instance with an ende date of 9999-12-31), the
clean() function will always raise an exception based on the values stored in the database. I do not really see how I could avoid this without changing the offending end date (9999-12-31 → 2023-01-31) first in a different (unconstrained) form, which defeats the purpose of the
TabularInline form. Thanks for any help!
After thinking the problem through, I arrived at the following (clunky, but working) solution:
def clean(self): super().clean() infinity = datetime.date(9999,12,31) for form in self.forms: try: current_id = form.cleaned_data['id'].pk except AttributeError: current_id = None unit = form.cleaned_data['unit'] begin = form.cleaned_data['begin'] end = form.cleaned_data['end'] # Exclude the instance corresponding to the form and the current occupancy (infinity end date) from the set of conflicting instances conflicts = Occupancy.objects.filter(unit=unit, begin__lte=end, end__gte=begin).exclude(pk=current_id).exclude(end=infinity) if any(conflicts): raise forms.ValidationError(_('Overlapping occupancies!'), code='overlap') # if the new occupancy has an infinity end date ... elif end == infinity: try: current_occupancy = Occupancy.objects.get(unit=unit, end=datetime.date(9999,12,31)) # ... and is not identical with the current occupancy ... if current_occupancy.pk != current_id: # ... update the current occupancy to end just before the new (infinite) occupancy current_occupancy.end = begin - datetime.timedelta(days=1) current_occupancy.save() except ObjectDoesNotExist: pass
This takes into account that an instance may overlap (at least while cleaning the input) with itself, and that a new occupancy with an infinite end date should replace the former "current" occupancy.