Opened 8 years ago

Closed 8 years ago

#25987 closed Bug (fixed)

Creating new object in admin with child model inlines and unique_together raises IntegrityError

Reported by: Anton Owned by: Simon Charette
Component: Forms Version: dev
Severity: Normal Keywords: admin, create object, unique_together, IntegrityError, validate_unique
Cc: Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I'm getting an IntegrityError when trying to create a model (ParentModel) with inlines for ChildModel which have unique_together constraint on (ParentModel, some_field).

When ParentModel object already exists and I try to add duplicates for ChildModel, BaseModelFormSet.validate_unique() nicely prevents me to save data and prints the standard django adminsite error "Please correct the duplicate values below" with concrete message for duplicate inline rows "'Please correct the duplicate data for some_field".

models.py:

from django.db import models


class ParentModel(models.Model):
    title = models.CharField(max_length=100, verbose_name='Title')


class ChildModel(models.Model):
    parent = models.ForeignKey(ParentModel, verbose_name='Link to parent')
    some_field = models.IntegerField(verbose_name='Some numeric field for unique_together')

    class Meta:
        unique_together = (('parent', 'some_field'), )

admin.py:

from django.contrib import admin
from .models import *


class ChildModelFormInline(admin.TabularInline):
    model = ChildModel


class ParentModelAdmin(admin.ModelAdmin):
    inlines = (ChildModelFormInline, )


class ChildModelAdmin(admin.ModelAdmin):
    pass


admin.site.register(ParentModel, ParentModelAdmin)
admin.site.register(ChildModel, ChildModelAdmin)

I think it's important issue. If our "ParentModel" has many "ChildModels" with many fields and inlines, people don't want lose their data on http 500 due to IntegriryError. Googling gives me workarounds only.

Done this on Django 1.7.11, 1.8.7 and 1.9 with SQLite (3.8.2 2013-12-06) and PostgreSQL (9.4.3)

Change History (8)

comment:1 by Anton, 8 years ago

Keywords: admin create object unique_together IntegrityError validate_unique added

comment:2 by Anton, 8 years ago

I went deeper in formsets and wrote this for ChildModel:

class ChildModelFormSet(BaseInlineFormSet):
    def clean(self):
        super().clean()
        self.validate_unique_on_create()

    def validate_unique_on_create(self):
        """ Additional validation of unique
        Copy-paste from BaseModelFormSet.validate_unique(), but removed date checks and added "or 0" condition in row_data list comprehension to prevent "None" values for not yet saved objects.
        """
        # Collect unique_checks and date_checks to run from all the forms.
        all_unique_checks = set()
        forms_to_delete = self.deleted_forms
        valid_forms = [form for form in self.forms if form.is_valid() and form not in forms_to_delete]
        for form in valid_forms:
            exclude = form._get_validation_exclusions()
            unique_checks, _ = form.instance._get_unique_checks(exclude=exclude)
            all_unique_checks = all_unique_checks.union(set(unique_checks))

        errors = []
        # Do each of the unique checks (unique and unique_together)
        for uclass, unique_check in all_unique_checks:
            seen_data = set()
            for form in valid_forms:
                # get data for each field of each of unique_check
                row_data = (form.cleaned_data[field]
                            for field in unique_check if field in form.cleaned_data)
                # Reduce Model instances to their primary key values
                row_data = tuple(d._get_pk_val() or 0 if hasattr(d, '_get_pk_val') else d
                                 for d in row_data)
                if row_data and None not in row_data:
                    # if we've already seen it then we have a uniqueness failure
                    if row_data in seen_data:
                        # poke error messages into the right places and mark
                        # the form as invalid
                        errors.append(self.get_unique_error_message(unique_check))
                        form._errors[NON_FIELD_ERRORS] = self.error_class([self.get_form_error()])
                        # remove the data from the cleaned_data dict since it was invalid
                        for field in unique_check:
                            if field in form.cleaned_data:
                                del form.cleaned_data[field]
                    # mark the data as seen
                    seen_data.add(row_data)
        if errors:
            raise ValidationError(errors)

It works fine and gracefully as I think. Workaround or not?

comment:3 by Tim Graham, 8 years ago

Summary: Creating new object on adminsite with child model inlines and unique_together raises IntegrityErrorCreating new object in admin with child model inlines and unique_together raises IntegrityError
Triage Stage: UnreviewedAccepted

From a quick look, I'm not sure if the ticket component should be "Forms" or "contrib.admin" -- can the issue be reproduced outside the admin?

comment:4 by Anton, 8 years ago

In my humble opinion of django's middle developer, the issue can be reproduced in all cases when we use BaseModelFormSet or its descendants. In most cases it's used in admin (and without overriding of buggy method).

comment:5 by Simon Charette, 8 years ago

Owner: changed from nobody to Simon Charette
Status: newassigned
Version: master

I just hit this issue using the admin but I can reproduce using basic inlines. I'll submit a patch with tests shortly.

comment:7 by Tim Graham, 8 years ago

Triage Stage: AcceptedReady for checkin

comment:8 by Simon Charette <charette.s@…>, 8 years ago

Resolution: fixed
Status: assignedclosed

In 1a403aa7:

Fixed #25987 -- Made inline formset validation respect unique_together with an unsaved parent object.

Thanks Anton Kuzmichev for the report and Tim for the review.

Note: See TracTickets for help on using tickets.
Back to Top