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,
]

