[Django] #36128: IntegrityError in admin Inline with many-to-many intermediary model

6 views
Skip to first unread message

Django

unread,
Jan 23, 2025, 6:59:19 AM1/23/25
to django-...@googlegroups.com
#36128: IntegrityError in admin Inline with many-to-many intermediary model
-------------------------------------+-------------------------------------
Reporter: Guillaume LEBRETON | Type:
| Uncategorized
Status: new | Component:
| Documentation
Version: 5.1 | Severity: Normal
Keywords: | Triage Stage:
| Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
-------------------------------------+-------------------------------------
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.
--
Ticket URL: <https://code.djangoproject.com/ticket/36128>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Jan 23, 2025, 10:05:02 AM1/23/25
to django-...@googlegroups.com
#36128: IntegrityError in admin Inline with many-to-many intermediary model
------------------------------------+------------------------------------
Reporter: Guillaume LEBRETON | Owner: (none)
Type: Bug | Status: new
Component: Documentation | Version: 5.1
Severity: Normal | Resolution:
Keywords: | Triage Stage: Accepted
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 0 | UI/UX: 0
------------------------------------+------------------------------------
Changes (by Sarah Boyce):

* stage: Unreviewed => Accepted
* type: Uncategorized => Bug

Comment:

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
--
Ticket URL: <https://code.djangoproject.com/ticket/36128#comment:1>
Reply all
Reply to author
Forward
0 new messages