#36128 closed Bug (fixed)
Document the necessity of adding unique constraints to related models in intermediary m2m model.
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 , 3 months ago
Triage Stage: | Unreviewed → Accepted |
---|---|
Type: | Uncategorized → Bug |
comment:2 by , 3 months ago
Summary: | IntegrityError in admin Inline with many-to-many intermediary model → Lack of constraint validation in admin Inline with many-to-many intermediary model leads to IntegrityError |
---|
comment:3 by , 2 months ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:4 by , 2 months ago
Summary: | Lack of constraint validation in admin Inline with many-to-many intermediary model leads to IntegrityError → Document 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 , 5 weeks ago
Triage Stage: | Accepted → Ready for checkin |
---|
comment:10 by , 5 weeks ago
Resolution: | fixed |
---|---|
Status: | closed → new |
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.
follow-up: 12 comment:11 by , 5 weeks ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
comment:12 by , 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.
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