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.