Check if model exist before save to avoid manually handling IntegrityError

I’ve successfully prevented the IntegrityError from surfacing to user level thanks to a solution presented here, sadly that doesn’t give me enough information to present to my users about the error and what can they do to fix it. I’m planning to before saving the model, to check the most obvious cause of problems (one that would reach the ExclusionConstrain) before hand and handling it myself, rather than waiting for the database to return. I’ve been thinking about using a queryset on the Model.clean() method, but I haven’t found a good way to use it there. How can I check a valid model if it’s the combination of many fields? unique_together is not enough because it needs to not have overlaps for the range field. This is the model I’m trying to validate:

class Bins(models.Model):

    class DocumentTypes(models.IntegerChoices):
        BIRTH = 1
        DEATH = 2
        FETAL_DEATH = 3

    class DocumentStatus(models.IntegerChoices):
        EMPTY = 1
        FILLED = 2
        VOID = 3

    document_type = models.IntegerField(
        choices=DocumentTypes.choices)
    document_status = models.IntegerField(
        choices=DocumentStatus.choices,
        default=DocumentStatus.EMPTY)
    created_at = models.DateTimeField(auto_now_add=True)
    series = models.PositiveIntegerField()
    serial_range = IntegerRangeField()
    located_at = models.ForeignKey(
        Warehouse,
        on_delete=models.PROTECT)
    parent_transaction = models.ForeignKey(
        Transactions,
        on_delete=models.PROTECT)

    def save(self, *args, **kwargs):
        if self.serial_range.upper_inc:
            super().save(*args, **kwargs)
            return

        lower = self.serial_range.lower
        upper = self.serial_range.upper
        self.serial_range = NumericRange(
            lower, upper, '[]')
        super().save(*args, **kwargs)

    class Meta:
        verbose_name = _('Bin')
        verbose_name_plural = _('Bins')
        constraints = [
            ExclusionConstraint(
                name='bin_ranges_overlap',
                expressions=[
                    (models.F('document_type'), RangeOperators.EQUAL),
                    ('series', RangeOperators.EQUAL),
                    ('serial_range', RangeOperators.OVERLAPS),
                ],
            ),
        ]

Either Model.clean or Form.clean should be able to do this for you, depending on your precise circumstances.

If you show what you tried, we might be able to assist with that.

That is the purpose of the .clean methods.

My precise circumstances is that I do not want the insert operation to hit the database if the fields already exist. Examples that do not work (on Model.clean()):

def clean(self):
    document_type = self.document_type
    series = self.series
    serial_range = self.serial_range
    if (
        Bins.objects
        .filter(document_type=document_type)
        .filter(series=series)
        .filter(serial_range__overlap=serial_range)
        .exists()
    ):
        raise ValidationError(
            _('Range %(range)s already exist for '
              'the series %(series)s of the type '
              '%(document)s.'),
            params={'series': series,
                    'range': serial_range,
                    'document': document_type},
            code='overlapping_ranges',
        )

No validationerror is captured, save goes through and IntegrityError is raised.

conflicting key value violates exclusion constraint "bin_ranges_overlap"
DETAIL:  Key (document_type, series, serial_range)=(1, 1, [50,301)) conflicts with existing key (document_type, series, serial_range)=(1, 1, [1,234)).

Same if I have it on Forms.clean(), except using clean_data = super().clean() and using the dict. The view calling Model.save() is a straightforward:

if bins_formset.is_valid():
    bins = bins_formset.save(commit=False)
    for bin_instance in bins:
        bin_instance.parent_transaction = transaction_instance
        bin_instance.located_at = transaction_instance.recieving_warehouse
        bin_instance.save()

It looks to me like you’re creating the self.serial_range attribute in your save method.

That appears to me like it’s not going to exist at the time you’re trying to run clean, since the clean would be run first.

You need to build that self.serial_range attribute earlier in your processing, like in the Form.clean method.

The only thing I’m changing with the serial_range field is changing the bounds to include the upper bound:

def save(self, *args, **kwargs):
    if self.serial_range.upper_inc:
        super().save(*args, **kwargs)
        return

    lower = self.serial_range.lower
    upper = self.serial_range.upper
    self.serial_range = NumericRange(
        lower, upper, '[]')
    super().save(*args, **kwargs)

It should return true for the test with my sample data, since more than half of the new serial_range entry is already there. I don’t understand what would be happening here.

Doing the transformation from bounded [) to [] in the clean method also doesn’t seems to have any effect the results.

I’m specifically referring to this line from the code you posted in your previous response:

This assignment of this attribute is happening after clean has already been called.

At the time that is_valid is called, what is self.serial_range going to be for each of the objects represented in the formset?

It will be the same values for lower and upper bounds, with the bounds included. Instead of NumericRange(1, 500, '[)') they will be NumericRange(1, 500, '[]'). This is made to prevent myself from having to mungue with the values and assure that if NumericRange(500, 500, '[]') is tried to be inserted, it counts as overlapping in the ExclusionConstrains. It shouldn’t matter, since the following returns true:

>> models.Bins.objects.filter(Q(document_type=1) & Q(series=1) & Q(serial_range__overlap=NumericRange(50,300,'[]'))).exists()
True

So, my check in the Model.clean() method should trigger the ValidationError. I’m just very confused as to why it doesn’t. Everything points that it should work and I should be getting validation errors every time I try to insert those values, but it doesn’t.

Please post the entire view, along with the form and formset defintions.

@KenWhitesell Here’s the things asked.

Model.py:

class Bins(models.Model):

    class DocumentTypes(models.IntegerChoices):
        BIRTH = 1
        DEATH = 2
        FETAL_DEATH = 3

    class DocumentStatus(models.IntegerChoices):
        EMPTY = 1
        FILLED = 2
        VOID = 3

    document_type = models.IntegerField(
        choices=DocumentTypes.choices)
    document_status = models.IntegerField(
        choices=DocumentStatus.choices,
        default=DocumentStatus.EMPTY)
    created_at = models.DateTimeField(auto_now_add=True)
    series = models.PositiveIntegerField()
    serial_range = IntegerRangeField()
    located_at = models.ForeignKey(
        Warehouse,
        on_delete=models.PROTECT)
    parent_transaction = models.ForeignKey(
        Transactions,
        on_delete=models.PROTECT)

    def close_bounds(unbound_range):
        lower_bound = unbound_range.lower
        upper_bound = unbound_range.upper
        return NumericRange(
            lower_bound, upper_bound, '[]')

    def clean(self):
        document_type = self.document_type
        series = self.series
        lower = self.serial_range.lower
        upper = self.serial_range.upper
        serial_range = NumericRange(
            lower, upper, '[]')
        if (
            Bins.objects.filter(
                Q(document_type=document_type) &
                Q(series=series) &
                Q(serial_range__overlap=serial_range))
                .exists()
        ):
            raise ValidationError(
                _('Range %(range)s already exist for '
                  'the series %(series)s of the type '
                  '%(document)s.'),
                params={'series': series,
                        'range': serial_range,
                        'document': document_type},
                code='overlapping_ranges',
            )


    def save(self, *args, **kwargs):
        if self.serial_range.upper_inc:
            super().save(*args, **kwargs)
            return

        lower = self.serial_range.lower
        upper = self.serial_range.upper
        self.serial_range = NumericRange(
            lower, upper, '[]')
        super().save(*args, **kwargs)

Forms.py

class BinsForm(forms.ModelForm):
    class Meta:
        model = Bins
        fields = ['document_type', 'series',
                  'serial_range']

BinsFormset = inlineformset_factory(
    Transactions, Bins, form=BinsForm,
    extra=6, can_delete=False, max_num=12)

View.py

class DocumentCreationView(View):
    template_name = "manager/document_creation_template.html"
    creating_warehouse = Warehouse.objects.filter(
        can_create=True).first()
    contact_list = Contact.objects.filter(
        deleted_at__isnull=True,
        authorized_warehouse__isnull=True,
    )
    recieving_warehouse = Warehouse.objects.filter(can_recieve=True)

    def get(self, request):
        transaction_form = DocumentCreationForm()
        bins_formset = BinsFormset()
        context = {
            'transaction_form': transaction_form,
            'bins_formset': bins_formset,
        }

        return render(request, self.template_name, context)

    @transaction.atomic
    def handle_forms(self,
                     transaction_form,
                     bins_formset,
                     bins_history_formset):
        if transaction_form.is_valid():
            transaction_instance = transaction_form.save(commit=False)
            transaction_instance.sending_warehouse = self.creating_warehouse
            # Change for user
            transaction_instance.started_by = transaction_instance.sending_contact
            transaction_instance.save()
            bins = None
            if bins_formset.is_valid():
                bins = bins_formset.save(commit=False)
                for bin_instance in bins:
                    bin_instance.parent_transaction = transaction_instance
                    bin_instance.located_at = transaction_instance.recieving_warehouse
                    bin_instance.save()

            if bins_history_formset.is_valid():
                bins_history = bins_history_formset.save(commit=False)
                for bin_history in bins_history:
                    bin_history.parent_transaction = transaction_instance
                    bin_history.save()

            return transaction_instance, bins

    def post(self, request):
        transaction_form = DocumentCreationForm(request.POST)
        bins_formset = BinsFormset(
            request.POST,
            prefix='bins_set')
        bins_history_formset = BinsHistoryFormset(
            request.POST,
            prefix='bins_set')
        try:
            transaction_instance, bins = self.handle_forms(
                transaction_form,
                bins_formset,
                bins_history_formset)
        except IntegrityError as integrity_error:
            print(integrity_error)
            context = {
                'transaction_form': transaction_form,
                'bins_formset': bins_formset,
            }
            return render(
                request, self.template_name, context)

        return HttpResponseRedirect(
            reverse(
                'manager:transaction_details',
                kwargs={'pk': transaction_instance.pk}))

I think I might have an idea here - but I’d like to check something.

Are you getting a complete traceback for this, or just the error? If you’re getting a complete traceback, please post it.

There’s no traceback, because I try … except. I could remove it and copy and pasting.

Yes, that or printing the traceback within your exception handler would be very helpful.

The IntegrityError is the only thing I could extract with the method I’m using, so I allowed it to traceback:

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/documents/create

Django Version: 3.2.12
Python Version: 3.9.2
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'formtools',
 'manager']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

The above exception (conflicting key value violates exclusion constraint "bin_history_ranges_overlap"
DETAIL:  Key (document_type, parent_transaction_id, series, serial_range)=(1, 96, 1, [50,301)) conflicts with existing key (document_type, parent_transaction_id, series, serial_range)=(1, 96, 1, [1,234)).
) was the direct cause of the following exception:
  File "/usr/lib/python3/dist-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/lib/python3/dist-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/python3/dist-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/home/braiam/src/cert_mng/manager/views.py", line 213, in post
    transaction_instance, bins = self.handle_forms(
  File "/usr/lib/python3.9/contextlib.py", line 79, in inner
    return func(*args, **kwds)
  File "/home/braiam/src/cert_mng/manager/views.py", line 201, in handle_forms
    bin_history.save()
  File "/home/braiam/src/cert_mng/manager/models.py", line 234, in save
    super().save(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 739, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 776, in save_base
    updated = self._save_table(
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 881, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 919, in _do_insert
    return manager._insert(
  File "/usr/lib/python3/dist-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/db/models/query.py", line 1270, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/usr/lib/python3/dist-packages/django/db/models/sql/compiler.py", line 1416, in execute_sql
    cursor.execute(sql, params)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 98, in execute
    return super().execute(sql, params)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/lib/python3/dist-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

Exception Type: IntegrityError at /documents/create
Exception Value: conflicting key value violates exclusion constraint "bin_history_ranges_overlap"
DETAIL:  Key (document_type, parent_transaction_id, series, serial_range)=(1, 96, 1, [50,301)) conflicts with existing key (document_type, parent_transaction_id, series, serial_range)=(1, 96, 1, [1,234)).

Request information:

csrfmiddlewaretoken 	

'Wablr7VS6OJSx2HGEeSKAMHIZbmK7wbpxwxumTHB0MuVBG8GK2LMUVb3MBarqMuH'

recieving_warehouse 	

'1'

recieving_contact 	

'1'

sending_contact 	

'2'

bins_set-TOTAL_FORMS 	

'6'

bins_set-INITIAL_FORMS 	

'0'

bins_set-MIN_NUM_FORMS 	

'0'

bins_set-MAX_NUM_FORMS 	

'12'

bins_set-0-document_type 	

'1'

bins_set-0-series 	

'1'

bins_set-0-serial_range_0 	

'1'

bins_set-0-serial_range_1 	

'233'

bins_set-0-id 	

''

bins_set-0-parent_transaction 	

''

bins_set-1-document_type 	

'1'

bins_set-1-series 	

'1'

bins_set-1-serial_range_0 	

'50'

bins_set-1-serial_range_1 	

'300'

bins_set-1-id 	

''

bins_set-1-parent_transaction 	

''

bins_set-2-document_type 	

''

bins_set-2-series 	

''

bins_set-2-serial_range_0 	

''

bins_set-2-serial_range_1 	

''

bins_set-2-id 	

''

bins_set-2-parent_transaction 	

''

bins_set-3-document_type 	

''

bins_set-3-series 	

''

bins_set-3-serial_range_0 	

''

bins_set-3-serial_range_1 	

''

bins_set-3-id 	

''

bins_set-3-parent_transaction 	

''

bins_set-4-document_type 	

''

bins_set-4-series 	

''

bins_set-4-serial_range_0 	

''

bins_set-4-serial_range_1 	

''

bins_set-4-id 	

''

bins_set-4-parent_transaction 	

''

bins_set-5-document_type 	

''

bins_set-5-series 	

''

bins_set-5-serial_range_0 	

''

bins_set-5-serial_range_1 	

''

bins_set-5-id 	

''

bins_set-5-parent_transaction 	

''

Notice that it’s failing on bin_history.save(), not bin_instance.save().

Are you performing this same cleaning process for bin_history?

Yeah, I was expecting that the validation stops all the processing when saving bin, does it not? Anyways, I copied the same clean method to the BinsHistory model, still happening as long as I try to save two overlapping ranges. I think that triggering on bin_history_overlap constrain was a red herring caused by me trying without the transaction.atomic decorator, which allowed partial saving of the bins model. I can cause the same error as before if I try a combination that isn’t partially saved:

Environment:
Request Method: POST
Request URL: http://127.0.0.1:8000/documents/create
Django Version: 3.2.12
Python Version: 3.9.2
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'formtools',
 'manager']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

The above exception (conflicting key value violates exclusion constraint "bin_ranges_overlap"
DETAIL:  Key (document_type, series, serial_range)=(1, 3, [1,101)) conflicts with existing key (document_type, series, serial_range)=(1, 3, [50,301)).
) was the direct cause of the following exception:
  File "/usr/lib/python3/dist-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/lib/python3/dist-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/lib/python3/dist-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/home/braiam/src/cert_mng/manager/views.py", line 213, in post
    transaction_instance, bins = self.handle_forms(
  File "/home/braiam/src/cert_mng/manager/views.py", line 195, in handle_forms
    bin_instance.save()
  File "/home/braiam/src/cert_mng/manager/models.py", line 380, in save
    super().save(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 739, in save
    self.save_base(using=using, force_insert=force_insert,
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 776, in save_base
    updated = self._save_table(
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 881, in _save_table
    results = self._do_insert(cls._base_manager, using, fields, returning_fields, raw)
  File "/usr/lib/python3/dist-packages/django/db/models/base.py", line 919, in _do_insert
    return manager._insert(
  File "/usr/lib/python3/dist-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/lib/python3/dist-packages/django/db/models/query.py", line 1270, in _insert
    return query.get_compiler(using=using).execute_sql(returning_fields)
  File "/usr/lib/python3/dist-packages/django/db/models/sql/compiler.py", line 1416, in execute_sql
    cursor.execute(sql, params)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 98, in execute
    return super().execute(sql, params)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 66, in execute
    return self._execute_with_wrappers(sql, params, many=False, executor=self._execute)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 75, in _execute_with_wrappers
    return executor(sql, params, many, context)
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)
  File "/usr/lib/python3/dist-packages/django/db/utils.py", line 90, in __exit__
    raise dj_exc_value.with_traceback(traceback) from exc_value
  File "/usr/lib/python3/dist-packages/django/db/backends/utils.py", line 84, in _execute
    return self.cursor.execute(sql, params)

Exception Type: IntegrityError at /documents/create
Exception Value: conflicting key value violates exclusion constraint "bin_ranges_overlap"
DETAIL:  Key (document_type, series, serial_range)=(1, 3, [1,101)) conflicts with existing key (document_type, series, serial_range)=(1, 3, [50,301)).

It works fine if I’m not saving multiple models at the same time which violate the constrains between themselves. Example (1, 4, [500, 3000]) and (1, 4, [1, 100]) already exist, and if I try to insert them it informs me. But if instead I try (1, 4, [1, 100]) and (1, 4, [50, 300]), I hit the integrity error.

No, it does not.

In the case of a view processing a form, validation continues - the idea isn’t to “abort” processing at that point, but allow processing to continue so that all errors can be collected and reported back to the person making the request in the manner best decided by the developer.

You may wish to review the docs at Form and field validation | Django documentation | Django for a more detailed explanation of what happens during this process.

You might also want to run this through a debugger, or add a handful of print statements in your code to see exactly what’s happening within this view.

Ok, I understand that, but my original issue remains. I can deal with overlapping if they exist in the database. I can’t deal if the overlapping is withing the form itself (like the last example of my last post inserting (1, 4, [1, 100]) and (1, 4, [50, 300]) in a single operation).

You’re going to have to implement this type of logic yourself in a clean method for the formset. See Overriding clean() on a ModelFormSet
You’ll need to compare the range in each form against the ranges in the rest of the forms to identify an overlap.

I modified my formset, and overwrote the clean() method, still not capturing the ValidationError. How should I handle it in my view? Add it to the try ... except?

Tried something like this:

def post(self, request):
    transaction_form = DocumentCreationForm(request.POST)
    bins_formset = BinsFormset(
        request.POST,
        prefix='bins_set')
    bins_history_formset = BinsHistoryFormset(
        request.POST,
        prefix='bins_set')
    try:
        self.handle_forms(
            transaction_form,
            bins_formset,
            bins_history_formset)
    except ValidationError as validation_error:
        error_message = validation_error
    else:
        error_message = None
    print(error_message)
    context = {
        'transaction_form': transaction_form,
        'bins_formset': bins_formset,
        'error_message': error_message,
    }
    return render(
        request, self.template_name, context)
class OverlappingInlineFormSet(forms.BaseInlineFormSet):
    def clean(self):
        super().clean()
        for form_a, form_b in combinations(self.forms, 2):
            form_a_dt = form_a.cleaned_data['document_type']
            form_b_dt = form_b.cleaned_data['document_type']
            form_a_s = form_a.cleaned_data['series']
            form_b_s = form_b.cleaned_data['series']
            form_a_sr = form_a.cleaned_data['serial_range']
            form_b_sr = form_b.cleaned_data['serial_range']
            form_a_set = set((form_a_sr.lower, form_a_sr.upper))
            form_b_set = set((form_b_sr.lower, form_b_sr.upper))

            if form_a_dt != form_b_dt:
                return
            if form_a_s != form_b_s:
                return
            if not form_a_set.isdisjoint(form_b_set):
                return
            raise ValidationError(
                _('Range %(form_b_range)s is already used for '
                  'the series %(form_a_series)s of the type '
                  '%(form_a_document)s range %(form_b_range)s.'),
                params={'form_a_series': form_a_s,
                        'form_a_range': form_a_sr,
                        'form_a_document': form_a_dt,
                        'form_b_series': form_b_s,
                        'form_b_range': form_a_sr,
                        'form_b_document': form_b_dt},
                code='overlapping_form_ranges',
            )

BinsFormset = forms.inlineformset_factory(
    Transactions, Bins, form=BinsForm,
    extra=6, can_delete=False, max_num=12,
    formset=OverlappingInlineFormSet)

BinsHistoryFormset = forms.inlineformset_factory(
    Transactions, BinsHistory, form=BinHistoryForm,
    extra=60, can_delete=False, max_num=60,
    formset=OverlappingInlineFormSet)

Your test isn’t going to test for an overlapping range - it’s only going to test for the existence of an element of one set being in the other set.

In other words:

a = set((5, 10))
b = set((7, 9))
a.isdisjoint(b)  # True
a = set((5, 10))
b = set((1, 15))
a.isdisjoint(b)  # True
a = set((5, 10))
b = set((1, 5))
a.isdisjoint(b)  # False
a = set((5, 10))
b = set((10, 15))
a.isdisjoint(b)  # False

This doesn’t satisfy your requirements to the degree that I understand them.