Pull Request: https://github.com/django/django/pull/18612
Description
BaseModelAdminChecks
prevents from setting a many-to-many field in the ModelAdmin fields
list when the many-to-many relationship was configured with a custom through
model (instead of one auto-generated by Django). Example:
# models.py ------------------------------------
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, through='BookAuthor')
class BookAuthor(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# admin.py --------------------------------------
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
fields = ['authors'] # <---- HERE. This would raise an AdminCheck error
Why was this AdminCheck Error set in the first place?
Before #6095 and #9475 were solved, custom through models weren’t initially supported in ModelAdmin and the through
db operations wouldn’t work so we wanted to prevent developers from using a field in an Admin model that couldn’t be possibly be operated with in standard forms.
Why would we want have this new behavior?
fields
when configuring a ModelAdmin
)fields
property allows the m2m relationship to appear as a simple field in the main model's admin form, offering a cleaner, more streamlined user interface.StackedInline
or TabularInline
involves more complex and visually heavier forms, where each related object requires its own row or section in the form. In contrast, with the fields
property, a Many-to-Many field can be rendered as a concise widget (e.g., a multi-select box or autocomplete), which is much easier to manage in scenarios where inline forms might be cumbersome.Extended examples:
There’s a “basic & nice behavior” when a ManyToMany relationship doesn’t have a through=
argument.
Authors:
widget.# models.py ------------------------------------
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self) -> str:
return self.name
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author)
# admin.py ------------------------------------
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
fields = ['name']
list_display = ['name']
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
fields = ['name', 'authors']
SystemCheckError
to alert developers this isn’t supported by Django (yet).# models.py ------------------------------------
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self) -> str:
return self.name
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, through='BookAuthor')
class BookAuthor(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# admin.py ------------------------------------
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
fields = ['name']
list_display = ['name']
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
fields = ['name', 'authors']
# models.py ------------------------------------
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self) -> str:
return self.name
class Book(models.Model):
name = models.CharField(max_length=100)
authors = models.ManyToManyField(Author, through='BookAuthor')
class BookAuthor(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
author = models.ForeignKey(Author, on_delete=models.CASCADE)
# admin.py ------------------------------------
@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
fields = ['name']
list_display = ['name']
# class BookAuthorsInline(admin.StackedInline):
class BookAuthorsInline(admin.TabularInline):
model = Book.authors.through
extra = 1
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
fields = ['name']
list_display = ['name']
inlines = [
BookAuthorsInline,
]