Opened 5 hours ago

Last modified 102 minutes ago

#36128 new Bug

Lack of constraint validation in admin Inline with many-to-many intermediary model leads to IntegrityError

Reported by: Guillaume LEBRETON Owned by:
Component: Documentation Version: 5.1
Severity: Normal Keywords:
Cc: Guillaume LEBRETON Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Following this doc section https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#working-with-many-to-many-intermediary-models
I set an admin tabular inline with an intermediary model.

The provided example is working but does not make that much sense; can a person have a membership to the same group several times ? Probably not, and if you try to do this with a simple many to many inline, without an intermediary model, you will get a validation error on the admin interface, "Please correct the duplicate data for group.".

The solution for the intermediary model is then adding a unique constraint, to avoid duplicated membership. But then, instead of having a validation error, there is a server error "IntegrityError".

To make the example work with the unique constraint, i had to add a custom formset:

models.py

class Person(models.Model):
    name = models.CharField(max_length=128)


class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through="Membership")


class Membership(models.Model):
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

    class Meta:
        constraints = [
            models.UniqueConstraint("person", "group", name="unique_person_group"),
        ]


admins.py

class MembersFormset(forms.models.BaseInlineFormSet):

    def clean(self):
        groups = []

        for form in self.forms:
            if form.cleaned_data:
                groups.append((form.cleaned_data['group'], form.cleaned_data['person']))

        duplicated_groups = [x for x in groups if groups.count(x) > 1]
        if duplicated_groups:
            raise ValidationError(
                'Duplicated values: %(duplicates)s',
                params={'duplicates': ", ".join(group.__str__() for group in set(duplicated_groups))}
            )


class MembershipInline(admin.TabularInline):
    model = Membership
    extra = 1
    formset = MembersFormset

The doc https://docs.djangoproject.com/en/5.1/ref/contrib/admin/#working-with-many-to-many-intermediary-models about inline with intermediary model is quite detailed, but completely lacks hints about UniqueConstraint, while I think most of the time intermediary m2m are designed with a UniqueConstraint.

Therefore, documentation should be updated to hint about the recommended steps to validate unique constraints in Inlines with intermediary m2m.

Change History (2)

comment:1 by Sarah Boyce, 103 minutes ago

Triage Stage: UnreviewedAccepted
Type: UncategorizedBug

Thank you for the report
I'm inclined to think that it should be easier than this and that the admin should handle the constraint errors nicely
But if not, I agree some docs could be beneficial

comment:2 by Sarah Boyce, 102 minutes ago

Summary: IntegrityError in admin Inline with many-to-many intermediary modelLack of constraint validation in admin Inline with many-to-many intermediary model leads to IntegrityError
Note: See TracTickets for help on using tickets.
Back to Top