Opened 3 weeks ago
Last modified 2 days ago
#36128 assigned Bug
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: | Accepted |
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 (5)
comment:1 by , 3 weeks ago
Triage Stage: | Unreviewed → Accepted |
---|---|
Type: | Uncategorized → Bug |
comment:2 by , 3 weeks 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 , 4 days ago
Owner: | set to |
---|---|
Status: | new → assigned |
comment:4 by , 4 days 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.
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