Opened 3 months ago

Closed 5 weeks ago

Last modified 5 weeks ago

#36128 closed Bug (fixed)

Document the necessity of adding unique constraints to related models in intermediary m2m model.

Reported by: Guillaume LEBRETON Owned by: Clifford Gama
Component: Documentation Version: 5.1
Severity: Normal Keywords:
Cc: Guillaume LEBRETON 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

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 (12)

comment:1 by Sarah Boyce, 3 months 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, 3 months 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

comment:3 by Clifford Gama, 2 months ago

Owner: set to Clifford Gama
Status: newassigned

comment:4 by Clifford Gama, 2 months ago

Summary: Lack of constraint validation in admin Inline with many-to-many intermediary model leads to IntegrityErrorDocument the necessity of adding unique constraints to related models in intermediary m2m model.

The lack of constraint validation in admin is a duplicate of #35676 and is solved by Simon Charette's patch in that ticket.

Repurposing this ticket for the documentation.

comment:6 by Jacob Walls, 5 weeks ago

Triage Stage: AcceptedReady for checkin

comment:7 by Sarah Boyce <42296566+sarahboyce@…>, 5 weeks ago

Resolution: fixed
Status: assignedclosed

In ae2736c:

Fixed #36128 -- Clarified auto-generated unique constraint on m2m through models.

comment:8 by Sarah Boyce <42296566+sarahboyce@…>, 5 weeks ago

In 4406ce15:

[5.2.x] Fixed #36128 -- Clarified auto-generated unique constraint on m2m through models.

Backport of ae2736ca3bf4c6a27e23ee95530ad965b550d4cc from main.

comment:9 by Sarah Boyce <42296566+sarahboyce@…>, 5 weeks ago

In cc405e15:

[5.1.x] Fixed #36128 -- Clarified auto-generated unique constraint on m2m through models.

Backport of ae2736ca3bf4c6a27e23ee95530ad965b550d4cc from main.

comment:10 by Guillaume LEBRETON, 5 weeks ago

Resolution: fixed
Status: closednew

I'm sorry to not have taken a look while it was still open, but my point was not so about the lack of integrity constraint on the example, but the lack of proper validation when there is an integrity constraint.
I've looked at the patch and it just add a mention about the unique constraint, but nothing about the validation.

If i add this uniqueness constraint, the result of adding a duplicate will be an integrity error rather than a validation error, and the hassle is writing a complex validation logic like this for a proper validation:

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

Maybe i missed something, but i find the updated doc even more misleading now, before the example was quite incomplete because there is almost always a need for unique constraint in m2m models, but now the provided example is not working properly because of failing validation.

comment:11 by Jacob Walls, 5 weeks ago

Resolution: fixed
Status: newclosed

Isn't the situation captured by comment:4? It would help if you could test the linked PR for #35676 and verify it solves the issue you present.

in reply to:  11 comment:12 by Guillaume LEBRETON, 5 weeks ago

Replying to Jacob Walls:

Isn't the situation captured by comment:4? It would help if you could test the linked PR for #35676 and verify it solves the issue you present.

My bad, i didn't see it i only checked the patch for this PR. I will check properly this evening, sorry.

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