From a0c6e01f253d884902df2c8d1585b3570238954c Mon Sep 17 00:00:00 2001 From: Rosana Rufer Date: Wed, 18 Sep 2024 19:41:22 +0100 Subject: [PATCH] Fixed django#12203 -- Allow using ManyToManyField with through in Admin. --- django/contrib/admin/checks.py | 28 ++-------------------------- django/contrib/admin/options.py | 4 ---- docs/ref/checks.txt | 5 +---- tests/admin_checks/tests.py | 29 ++++------------------------- tests/admin_widgets/models.py | 10 +++++++++- tests/modeladmin/test_checks.py | 18 +++--------------- 6 files changed, 19 insertions(+), 75 deletions(-) diff --git a/django/contrib/admin/checks.py b/django/contrib/admin/checks.py index a4d7066d10..57cd8f13fc 100644 --- a/django/contrib/admin/checks.py +++ b/django/contrib/admin/checks.py @@ -438,27 +438,13 @@ class BaseModelAdminChecks: return [] else: try: - field = obj.model._meta.get_field(field_name) + obj.model._meta.get_field(field_name) except FieldDoesNotExist: # If we can't find a field on the model that matches, it could # be an extra field on the form. return [] else: - if ( - isinstance(field, models.ManyToManyField) - and not field.remote_field.through._meta.auto_created - ): - return [ - checks.Error( - "The value of '%s' cannot include the ManyToManyField " - "'%s', because that field manually specifies a " - "relationship model." % (label, field_name), - obj=obj.__class__, - id="admin.E013", - ) - ] - else: - return [] + return [] def _check_exclude(self, obj): """Check that exclude is a sequence without duplicates.""" @@ -536,16 +522,6 @@ class BaseModelAdminChecks: return must_be( "a many-to-many field", option=label, obj=obj, id="admin.E020" ) - elif not field.remote_field.through._meta.auto_created: - return [ - checks.Error( - f"The value of '{label}' cannot include the ManyToManyField " - f"'{field_name}', because that field manually specifies a " - f"relationship model.", - obj=obj.__class__, - id="admin.E013", - ) - ] else: return [] diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 6d5c0708a3..6527284486 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -302,10 +302,6 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass): """ Get a form Field for a ManyToManyField. """ - # If it uses an intermediary model that isn't auto created, don't show - # a field in admin. - if not db_field.remote_field.through._meta.auto_created: - return None db = kwargs.get("using") if "widget" not in kwargs: diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt index d78a6f76b2..4470eaac7b 100644 --- a/docs/ref/checks.txt +++ b/docs/ref/checks.txt @@ -660,10 +660,7 @@ with the admin site: * **admin.E011**: The value of ``fieldsets[n][1]`` must contain the key ``fields``. * **admin.E012**: There are duplicate field(s) in ``fieldsets[n][1]``. -* **admin.E013**: The value of - ``fields[n]/filter_horizontal[n]/filter_vertical[n]/fieldsets[n][m]`` cannot - include the ``ManyToManyField`` ````, because that field manually - specifies a relationship model. +* **admin.E013**: (Removed) * **admin.E014**: The value of ``exclude`` must be a list or tuple. * **admin.E015**: The value of ``exclude`` contains duplicate field(s). * **admin.E016**: The value of ``form`` must inherit from ``BaseModelForm``. diff --git a/tests/admin_checks/tests.py b/tests/admin_checks/tests.py index 6ca5d6d925..a81b693d6a 100644 --- a/tests/admin_checks/tests.py +++ b/tests/admin_checks/tests.py @@ -856,28 +856,15 @@ class SystemChecksTestCase(SimpleTestCase): errors = SongAdmin(Song, AdminSite()).check() self.assertEqual(errors, []) - def test_graceful_m2m_fail(self): - """ - Regression test for #12203/#12237 - Fail more gracefully when a M2M field that - specifies the 'through' option is included in the 'fields' or the 'fieldsets' - ModelAdmin options. - """ - + def test_allow_m2m_with_through(self): class BookAdmin(admin.ModelAdmin): fields = ["authors"] errors = BookAdmin(Book, AdminSite()).check() - expected = [ - checks.Error( - "The value of 'fields' cannot include the ManyToManyField 'authors', " - "because that field manually specifies a relationship model.", - obj=BookAdmin, - id="admin.E013", - ) - ] + expected = [] self.assertEqual(errors, expected) - def test_cannot_include_through(self): + def test_m2m_can_include_through(self): class FieldsetBookAdmin(admin.ModelAdmin): fieldsets = ( ("Header 1", {"fields": ("name",)}), @@ -885,15 +872,7 @@ class SystemChecksTestCase(SimpleTestCase): ) errors = FieldsetBookAdmin(Book, AdminSite()).check() - expected = [ - checks.Error( - "The value of 'fieldsets[1][1][\"fields\"]' cannot include the " - "ManyToManyField 'authors', because that field manually specifies a " - "relationship model.", - obj=FieldsetBookAdmin, - id="admin.E013", - ) - ] + expected = [] self.assertEqual(errors, expected) def test_nested_fields(self): diff --git a/tests/admin_widgets/models.py b/tests/admin_widgets/models.py index 0113ecb7c8..13aa6a5e08 100644 --- a/tests/admin_widgets/models.py +++ b/tests/admin_widgets/models.py @@ -201,13 +201,21 @@ class Student(models.Model): class School(models.Model): name = models.CharField(max_length=255) - students = models.ManyToManyField(Student, related_name="current_schools") + students = models.ManyToManyField( + Student, related_name="current_schools", through="StudentSchool" + ) alumni = models.ManyToManyField(Student, related_name="previous_schools") def __str__(self): return self.name +class StudentSchool(models.Model): + student = models.ForeignKey(Student, models.CASCADE) + school = models.ForeignKey(School, models.CASCADE) + extra_info = models.CharField(max_length=10) + + class Profile(models.Model): user = models.ForeignKey("auth.User", models.CASCADE, to_field="username") diff --git a/tests/modeladmin/test_checks.py b/tests/modeladmin/test_checks.py index 94a80ca006..7c77379e1c 100644 --- a/tests/modeladmin/test_checks.py +++ b/tests/modeladmin/test_checks.py @@ -376,13 +376,7 @@ class FilterVerticalCheckTests(CheckTestCase): class TestModelAdmin(ModelAdmin): filter_vertical = ["bands"] - self.assertIsInvalid( - TestModelAdmin, - Artist, - "The value of 'filter_vertical[0]' cannot include the ManyToManyField " - "'bands', because that field manually specifies a relationship model.", - "admin.E013", - ) + self.assertIsValid(TestModelAdmin, Artist) def test_valid_case(self): class TestModelAdmin(ModelAdmin): @@ -445,7 +439,7 @@ class FilterHorizontalCheckTests(CheckTestCase): ) @isolate_apps("modeladmin") - def test_invalid_m2m_field_with_through(self): + def test_valid_m2m_field_with_through(self): class Artist(Model): bands = ManyToManyField("Band", through="BandArtist") @@ -456,13 +450,7 @@ class FilterHorizontalCheckTests(CheckTestCase): class TestModelAdmin(ModelAdmin): filter_horizontal = ["bands"] - self.assertIsInvalid( - TestModelAdmin, - Artist, - "The value of 'filter_horizontal[0]' cannot include the ManyToManyField " - "'bands', because that field manually specifies a relationship model.", - "admin.E013", - ) + self.assertIsValid(TestModelAdmin, Artist) def test_valid_case(self): class TestModelAdmin(ModelAdmin):