mirror of
https://github.com/django/django.git
synced 2025-06-05 11:39:13 +00:00
Fixed django#12203 -- Allow using ManyToManyField with through in Admin.
This commit is contained in:
parent
9ca1f6eff6
commit
a0c6e01f25
@ -438,27 +438,13 @@ class BaseModelAdminChecks:
|
|||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
field = obj.model._meta.get_field(field_name)
|
obj.model._meta.get_field(field_name)
|
||||||
except FieldDoesNotExist:
|
except FieldDoesNotExist:
|
||||||
# If we can't find a field on the model that matches, it could
|
# If we can't find a field on the model that matches, it could
|
||||||
# be an extra field on the form.
|
# be an extra field on the form.
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
if (
|
return []
|
||||||
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 []
|
|
||||||
|
|
||||||
def _check_exclude(self, obj):
|
def _check_exclude(self, obj):
|
||||||
"""Check that exclude is a sequence without duplicates."""
|
"""Check that exclude is a sequence without duplicates."""
|
||||||
@ -536,16 +522,6 @@ class BaseModelAdminChecks:
|
|||||||
return must_be(
|
return must_be(
|
||||||
"a many-to-many field", option=label, obj=obj, id="admin.E020"
|
"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:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -302,10 +302,6 @@ class BaseModelAdmin(metaclass=forms.MediaDefiningClass):
|
|||||||
"""
|
"""
|
||||||
Get a form Field for a ManyToManyField.
|
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")
|
db = kwargs.get("using")
|
||||||
|
|
||||||
if "widget" not in kwargs:
|
if "widget" not in kwargs:
|
||||||
|
@ -660,10 +660,7 @@ with the admin site:
|
|||||||
* **admin.E011**: The value of ``fieldsets[n][1]`` must contain the key
|
* **admin.E011**: The value of ``fieldsets[n][1]`` must contain the key
|
||||||
``fields``.
|
``fields``.
|
||||||
* **admin.E012**: There are duplicate field(s) in ``fieldsets[n][1]``.
|
* **admin.E012**: There are duplicate field(s) in ``fieldsets[n][1]``.
|
||||||
* **admin.E013**: The value of
|
* **admin.E013**: (Removed)
|
||||||
``fields[n]/filter_horizontal[n]/filter_vertical[n]/fieldsets[n][m]`` cannot
|
|
||||||
include the ``ManyToManyField`` ``<field name>``, because that field manually
|
|
||||||
specifies a relationship model.
|
|
||||||
* **admin.E014**: The value of ``exclude`` must be a list or tuple.
|
* **admin.E014**: The value of ``exclude`` must be a list or tuple.
|
||||||
* **admin.E015**: The value of ``exclude`` contains duplicate field(s).
|
* **admin.E015**: The value of ``exclude`` contains duplicate field(s).
|
||||||
* **admin.E016**: The value of ``form`` must inherit from ``BaseModelForm``.
|
* **admin.E016**: The value of ``form`` must inherit from ``BaseModelForm``.
|
||||||
|
@ -856,28 +856,15 @@ class SystemChecksTestCase(SimpleTestCase):
|
|||||||
errors = SongAdmin(Song, AdminSite()).check()
|
errors = SongAdmin(Song, AdminSite()).check()
|
||||||
self.assertEqual(errors, [])
|
self.assertEqual(errors, [])
|
||||||
|
|
||||||
def test_graceful_m2m_fail(self):
|
def test_allow_m2m_with_through(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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class BookAdmin(admin.ModelAdmin):
|
class BookAdmin(admin.ModelAdmin):
|
||||||
fields = ["authors"]
|
fields = ["authors"]
|
||||||
|
|
||||||
errors = BookAdmin(Book, AdminSite()).check()
|
errors = BookAdmin(Book, AdminSite()).check()
|
||||||
expected = [
|
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",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
self.assertEqual(errors, expected)
|
self.assertEqual(errors, expected)
|
||||||
|
|
||||||
def test_cannot_include_through(self):
|
def test_m2m_can_include_through(self):
|
||||||
class FieldsetBookAdmin(admin.ModelAdmin):
|
class FieldsetBookAdmin(admin.ModelAdmin):
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
("Header 1", {"fields": ("name",)}),
|
("Header 1", {"fields": ("name",)}),
|
||||||
@ -885,15 +872,7 @@ class SystemChecksTestCase(SimpleTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
errors = FieldsetBookAdmin(Book, AdminSite()).check()
|
errors = FieldsetBookAdmin(Book, AdminSite()).check()
|
||||||
expected = [
|
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",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
self.assertEqual(errors, expected)
|
self.assertEqual(errors, expected)
|
||||||
|
|
||||||
def test_nested_fields(self):
|
def test_nested_fields(self):
|
||||||
|
@ -201,13 +201,21 @@ class Student(models.Model):
|
|||||||
|
|
||||||
class School(models.Model):
|
class School(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
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")
|
alumni = models.ManyToManyField(Student, related_name="previous_schools")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
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):
|
class Profile(models.Model):
|
||||||
user = models.ForeignKey("auth.User", models.CASCADE, to_field="username")
|
user = models.ForeignKey("auth.User", models.CASCADE, to_field="username")
|
||||||
|
|
||||||
|
@ -376,13 +376,7 @@ class FilterVerticalCheckTests(CheckTestCase):
|
|||||||
class TestModelAdmin(ModelAdmin):
|
class TestModelAdmin(ModelAdmin):
|
||||||
filter_vertical = ["bands"]
|
filter_vertical = ["bands"]
|
||||||
|
|
||||||
self.assertIsInvalid(
|
self.assertIsValid(TestModelAdmin, Artist)
|
||||||
TestModelAdmin,
|
|
||||||
Artist,
|
|
||||||
"The value of 'filter_vertical[0]' cannot include the ManyToManyField "
|
|
||||||
"'bands', because that field manually specifies a relationship model.",
|
|
||||||
"admin.E013",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_valid_case(self):
|
def test_valid_case(self):
|
||||||
class TestModelAdmin(ModelAdmin):
|
class TestModelAdmin(ModelAdmin):
|
||||||
@ -445,7 +439,7 @@ class FilterHorizontalCheckTests(CheckTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@isolate_apps("modeladmin")
|
@isolate_apps("modeladmin")
|
||||||
def test_invalid_m2m_field_with_through(self):
|
def test_valid_m2m_field_with_through(self):
|
||||||
class Artist(Model):
|
class Artist(Model):
|
||||||
bands = ManyToManyField("Band", through="BandArtist")
|
bands = ManyToManyField("Band", through="BandArtist")
|
||||||
|
|
||||||
@ -456,13 +450,7 @@ class FilterHorizontalCheckTests(CheckTestCase):
|
|||||||
class TestModelAdmin(ModelAdmin):
|
class TestModelAdmin(ModelAdmin):
|
||||||
filter_horizontal = ["bands"]
|
filter_horizontal = ["bands"]
|
||||||
|
|
||||||
self.assertIsInvalid(
|
self.assertIsValid(TestModelAdmin, Artist)
|
||||||
TestModelAdmin,
|
|
||||||
Artist,
|
|
||||||
"The value of 'filter_horizontal[0]' cannot include the ManyToManyField "
|
|
||||||
"'bands', because that field manually specifies a relationship model.",
|
|
||||||
"admin.E013",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_valid_case(self):
|
def test_valid_case(self):
|
||||||
class TestModelAdmin(ModelAdmin):
|
class TestModelAdmin(ModelAdmin):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user