Skip to content

Commit

Permalink
Fixed django#9602 -- Added AdminSite.get_model_admin().
Browse files Browse the repository at this point in the history
This allows retrieving an admin class for the given model class without
using internal attributes.
  • Loading branch information
felixxm committed Jul 6, 2023
1 parent 95cdf9d commit def5b0a
Show file tree
Hide file tree
Showing 15 changed files with 121 additions and 65 deletions.
34 changes: 19 additions & 15 deletions django/contrib/admin/checks.py
Expand Up @@ -220,6 +220,8 @@ def _check_autocomplete_fields_item(self, obj, field_name, label):
ManyToManyField and that the item has a related ModelAdmin with
search_fields defined.
"""
from django.contrib.admin.sites import NotRegistered

try:
field = obj.model._meta.get_field(field_name)
except FieldDoesNotExist:
Expand All @@ -234,8 +236,9 @@ def _check_autocomplete_fields_item(self, obj, field_name, label):
obj=obj,
id="admin.E038",
)
related_admin = obj.admin_site._registry.get(field.remote_field.model)
if related_admin is None:
try:
related_admin = obj.admin_site.get_model_admin(field.remote_field.model)
except NotRegistered:
return [
checks.Error(
'An admin for model "%s" has to be registered '
Expand All @@ -248,19 +251,20 @@ def _check_autocomplete_fields_item(self, obj, field_name, label):
id="admin.E039",
)
]
elif not related_admin.search_fields:
return [
checks.Error(
'%s must define "search_fields", because it\'s '
"referenced by %s.autocomplete_fields."
% (
related_admin.__class__.__name__,
type(obj).__name__,
),
obj=obj.__class__,
id="admin.E040",
)
]
else:
if not related_admin.search_fields:
return [
checks.Error(
'%s must define "search_fields", because it\'s '
"referenced by %s.autocomplete_fields."
% (
related_admin.__class__.__name__,
type(obj).__name__,
),
obj=obj.__class__,
id="admin.E040",
)
]
return []

def _check_raw_id_fields(self, obj):
Expand Down
12 changes: 8 additions & 4 deletions django/contrib/admin/filters.py
Expand Up @@ -257,10 +257,14 @@ def field_admin_ordering(self, field, request, model_admin):
"""
Return the model admin's ordering for related field, if provided.
"""
related_admin = model_admin.admin_site._registry.get(field.remote_field.model)
if related_admin is not None:
return related_admin.get_ordering(request)
return ()
from django.contrib.admin.sites import NotRegistered

try:
return model_admin.admin_site.get_model_admin(
field.remote_field.model
).get_ordering(request)
except NotRegistered:
return ()

def field_choices(self, field, request, model_admin):
ordering = self.field_admin_ordering(field, request, model_admin)
Expand Down
37 changes: 24 additions & 13 deletions django/contrib/admin/options.py
Expand Up @@ -160,6 +160,8 @@ def formfield_for_dbfield(self, db_field, request, **kwargs):
If kwargs are given, they're passed to the form Field's constructor.
"""
from django.contrib.admin.sites import NotRegistered

# If the field specifies choices, we don't need to look for special
# admin widgets - we just need to use a select widget of some kind.
if db_field.choices:
Expand All @@ -185,23 +187,27 @@ def formfield_for_dbfield(self, db_field, request, **kwargs):
# rendered output. formfield can be None if it came from a
# OneToOneField with parent_link=True or a M2M intermediary.
if formfield and db_field.name not in self.raw_id_fields:
related_modeladmin = self.admin_site._registry.get(
db_field.remote_field.model
)
wrapper_kwargs = {}
if related_modeladmin:
wrapper_kwargs.update(
can_add_related=related_modeladmin.has_add_permission(request),
can_change_related=related_modeladmin.has_change_permission(
try:
related_modeladmin = self.admin_site.get_model_admin(
db_field.remote_field.model
)
except NotRegistered:
wrapper_kwargs = {}
else:
wrapper_kwargs = {
"can_add_related": related_modeladmin.has_add_permission(
request
),
can_delete_related=related_modeladmin.has_delete_permission(
"can_change_related": related_modeladmin.has_change_permission(
request
),
can_view_related=related_modeladmin.has_view_permission(
"can_delete_related": related_modeladmin.has_delete_permission(
request
),
)
"can_view_related": related_modeladmin.has_view_permission(
request
),
}
formfield.widget = widgets.RelatedFieldWidgetWrapper(
formfield.widget,
db_field.remote_field,
Expand Down Expand Up @@ -246,8 +252,13 @@ def get_field_queryset(self, db, db_field, request):
ordering. Otherwise don't specify the queryset, let the field decide
(return None in that case).
"""
related_admin = self.admin_site._registry.get(db_field.remote_field.model)
if related_admin is not None:
from django.contrib.admin.sites import NotRegistered

try:
related_admin = self.admin_site.get_model_admin(db_field.remote_field.model)
except NotRegistered:
return None
else:
ordering = related_admin.get_ordering(request)
if ordering is not None and ordering != ():
return db_field.remote_field.model._default_manager.using(db).order_by(
Expand Down
8 changes: 7 additions & 1 deletion django/contrib/admin/sites.py
Expand Up @@ -121,7 +121,7 @@ def register(self, model_or_iterable, admin_class=None, **options):
)

if self.is_registered(model):
registered_admin = str(self._registry[model])
registered_admin = str(self.get_model_admin(model))
msg = "The model %s is already registered " % model.__name__
if registered_admin.endswith(".ModelAdmin"):
# Most likely registered without a ModelAdmin subclass.
Expand Down Expand Up @@ -166,6 +166,12 @@ def is_registered(self, model):
"""
return model in self._registry

def get_model_admin(self, model):
try:
return self._registry[model]
except KeyError:
raise NotRegistered(f"The model {model.__name__} is not registered.")

def add_action(self, action, name=None):
"""
Register an action to be available globally.
Expand Down
4 changes: 3 additions & 1 deletion django/contrib/admin/utils.py
Expand Up @@ -144,7 +144,9 @@ def format_callback(obj):
no_edit_link = "%s: %s" % (capfirst(opts.verbose_name), obj)

if admin_site.is_registered(model):
if not admin_site._registry[model].has_delete_permission(request, obj):
if not admin_site.get_model_admin(model).has_delete_permission(
request, obj
):
perms_needed.add(opts.verbose_name)
try:
admin_url = reverse(
Expand Down
6 changes: 4 additions & 2 deletions django/contrib/admin/views/autocomplete.py
Expand Up @@ -74,6 +74,8 @@ def process_request(self, request):
Raise Http404 if the target model admin is not configured properly with
search_fields.
"""
from django.contrib.admin.sites import NotRegistered

term = request.GET.get("term", "")
try:
app_label = request.GET["app_label"]
Expand All @@ -97,8 +99,8 @@ def process_request(self, request):
except AttributeError as e:
raise PermissionDenied from e
try:
model_admin = self.admin_site._registry[remote_model]
except KeyError as e:
model_admin = self.admin_site.get_model_admin(remote_model)
except NotRegistered as e:
raise PermissionDenied from e

# Validate suitability of objects.
Expand Down
7 changes: 7 additions & 0 deletions docs/ref/contrib/admin/index.txt
Expand Up @@ -3024,6 +3024,13 @@ Templates can override or extend base admin templates as described in
Raises ``django.contrib.admin.sites.NotRegistered`` if a model isn't
already registered.

.. method:: AdminSite.get_model_admin(model)

.. versionadded:: 5.0

Returns an admin class for the given model class. Raises
``django.contrib.admin.sites.NotRegistered`` if a model isn't registered.

.. method:: AdminSite.get_log_entries(request)

.. versionadded:: 5.0
Expand Down
3 changes: 3 additions & 0 deletions docs/releases/5.0.txt
Expand Up @@ -145,6 +145,9 @@ Minor features

* ``XRegExp`` is upgraded from version 3.2.0 to 5.1.1.

* The new :meth:`.AdminSite.get_model_admin` method returns an admin class for
the given model class.

:mod:`django.contrib.admindocs`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
4 changes: 2 additions & 2 deletions tests/admin_changelist/tests.py
Expand Up @@ -888,7 +888,7 @@ def test_dynamic_list_display(self):
user_parents = self._create_superuser("parents")

# Test with user 'noparents'
m = custom_site._registry[Child]
m = custom_site.get_model_admin(Child)
request = self._mocked_authenticated_request("/child/", user_noparents)
response = m.changelist_view(request)
self.assertNotContains(response, "Parent object")
Expand All @@ -913,7 +913,7 @@ def test_dynamic_list_display(self):

# Test default implementation
custom_site.register(Child, ChildAdmin)
m = custom_site._registry[Child]
m = custom_site.get_model_admin(Child)
request = self._mocked_authenticated_request("/child/", user_noparents)
response = m.changelist_view(request)
self.assertContains(response, "Parent object")
Expand Down
3 changes: 1 addition & 2 deletions tests/admin_checks/tests.py
Expand Up @@ -276,8 +276,7 @@ def test_allows_checks_relying_on_other_modeladmins(self):
class MyBookAdmin(admin.ModelAdmin):
def check(self, **kwargs):
errors = super().check(**kwargs)
author_admin = self.admin_site._registry.get(Author)
if author_admin is None:
if not self.admin_site.is_registered(Author):
errors.append("AuthorAdmin missing!")
return errors

Expand Down
4 changes: 2 additions & 2 deletions tests/admin_ordering/tests.py
Expand Up @@ -152,10 +152,10 @@ def tearDown(self):
site.unregister(Band)

def check_ordering_of_field_choices(self, correct_ordering):
fk_field = site._registry[Song].formfield_for_foreignkey(
fk_field = site.get_model_admin(Song).formfield_for_foreignkey(
Song.band.field, request=None
)
m2m_field = site._registry[Song].formfield_for_manytomany(
m2m_field = site.get_model_admin(Song).formfield_for_manytomany(
Song.other_interpreters.field, request=None
)
self.assertEqual(list(fk_field.queryset), correct_ordering)
Expand Down
42 changes: 28 additions & 14 deletions tests/admin_registration/tests.py
Expand Up @@ -22,13 +22,13 @@ def setUp(self):

def test_bare_registration(self):
self.site.register(Person)
self.assertIsInstance(self.site._registry[Person], admin.ModelAdmin)
self.assertIsInstance(self.site.get_model_admin(Person), admin.ModelAdmin)
self.site.unregister(Person)
self.assertEqual(self.site._registry, {})

def test_registration_with_model_admin(self):
self.site.register(Person, NameAdmin)
self.assertIsInstance(self.site._registry[Person], NameAdmin)
self.assertIsInstance(self.site.get_model_admin(Person), NameAdmin)
self.site.unregister(Person)
self.assertEqual(self.site._registry, {})

Expand Down Expand Up @@ -57,22 +57,28 @@ def test_unregister_unregistered_model(self):

def test_registration_with_star_star_options(self):
self.site.register(Person, search_fields=["name"])
self.assertEqual(self.site._registry[Person].search_fields, ["name"])
self.assertEqual(self.site.get_model_admin(Person).search_fields, ["name"])

def test_get_model_admin_unregister_model(self):
msg = "The model Person is not registered."
with self.assertRaisesMessage(admin.sites.NotRegistered, msg):
self.site.get_model_admin(Person)

def test_star_star_overrides(self):
self.site.register(
Person, NameAdmin, search_fields=["name"], list_display=["__str__"]
)
self.assertEqual(self.site._registry[Person].search_fields, ["name"])
self.assertEqual(self.site._registry[Person].list_display, ["__str__"])
self.assertTrue(self.site._registry[Person].save_on_top)
person_admin = self.site.get_model_admin(Person)
self.assertEqual(person_admin.search_fields, ["name"])
self.assertEqual(person_admin.list_display, ["__str__"])
self.assertIs(person_admin.save_on_top, True)

def test_iterable_registration(self):
self.site.register([Person, Place], search_fields=["name"])
self.assertIsInstance(self.site._registry[Person], admin.ModelAdmin)
self.assertEqual(self.site._registry[Person].search_fields, ["name"])
self.assertIsInstance(self.site._registry[Place], admin.ModelAdmin)
self.assertEqual(self.site._registry[Place].search_fields, ["name"])
self.assertIsInstance(self.site.get_model_admin(Person), admin.ModelAdmin)
self.assertEqual(self.site.get_model_admin(Person).search_fields, ["name"])
self.assertIsInstance(self.site.get_model_admin(Place), admin.ModelAdmin)
self.assertEqual(self.site.get_model_admin(Place).search_fields, ["name"])
self.site.unregister([Person, Place])
self.assertEqual(self.site._registry, {})

Expand Down Expand Up @@ -116,18 +122,26 @@ def setUp(self):

def test_basic_registration(self):
register(Person)(NameAdmin)
self.assertIsInstance(self.default_site._registry[Person], admin.ModelAdmin)
self.assertIsInstance(
self.default_site.get_model_admin(Person), admin.ModelAdmin
)
self.default_site.unregister(Person)

def test_custom_site_registration(self):
register(Person, site=self.custom_site)(NameAdmin)
self.assertIsInstance(self.custom_site._registry[Person], admin.ModelAdmin)
self.assertIsInstance(
self.custom_site.get_model_admin(Person), admin.ModelAdmin
)

def test_multiple_registration(self):
register(Traveler, Place)(NameAdmin)
self.assertIsInstance(self.default_site._registry[Traveler], admin.ModelAdmin)
self.assertIsInstance(
self.default_site.get_model_admin(Traveler), admin.ModelAdmin
)
self.default_site.unregister(Traveler)
self.assertIsInstance(self.default_site._registry[Place], admin.ModelAdmin)
self.assertIsInstance(
self.default_site.get_model_admin(Place), admin.ModelAdmin
)
self.default_site.unregister(Place)

def test_wrapped_class_not_a_model_admin(self):
Expand Down
8 changes: 6 additions & 2 deletions tests/admin_views/test_autocomplete_view.py
Expand Up @@ -3,6 +3,7 @@
from contextlib import contextmanager

from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
from django.contrib.admin.tests import AdminSeleniumTestCase
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
from django.contrib.auth.models import Permission, User
Expand Down Expand Up @@ -61,8 +62,11 @@ class BookAdmin(admin.ModelAdmin):

@contextmanager
def model_admin(model, model_admin, admin_site=site):
org_admin = admin_site._registry.get(model)
if org_admin:
try:
org_admin = admin_site.get_model_admin(model)
except NotRegistered:
org_admin = None
else:
admin_site.unregister(model)
admin_site.register(model, model_admin)
try:
Expand Down

0 comments on commit def5b0a

Please sign in to comment.