diff --git a/AUTHORS b/AUTHORS index 909149137d..ff7763b2fd 100644 --- a/AUTHORS +++ b/AUTHORS @@ -637,6 +637,7 @@ answer newbie questions, and generally made Django that much better: Paul Bissex Paul Collier Paul Collins + Paul Donohue Paul Lanier Paul McLanahan Paul McMillan diff --git a/django/contrib/admindocs/middleware.py b/django/contrib/admindocs/middleware.py index b5024de72c..4da5604106 100644 --- a/django/contrib/admindocs/middleware.py +++ b/django/contrib/admindocs/middleware.py @@ -2,6 +2,8 @@ from django.conf import settings from django.http import HttpResponse from django.utils.deprecation import MiddlewareMixin +from .utils import get_view_name + class XViewMiddleware(MiddlewareMixin): """ @@ -24,5 +26,5 @@ class XViewMiddleware(MiddlewareMixin): if request.method == 'HEAD' and (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS or (request.user.is_active and request.user.is_staff)): response = HttpResponse() - response['X-View'] = "%s.%s" % (view_func.__module__, view_func.__name__) + response['X-View'] = get_view_name(view_func) return response diff --git a/django/contrib/admindocs/utils.py b/django/contrib/admindocs/utils.py index 3b5b5bb1fc..fa72d60d57 100644 --- a/django/contrib/admindocs/utils.py +++ b/django/contrib/admindocs/utils.py @@ -18,6 +18,12 @@ else: docutils_is_available = True +def get_view_name(view_func): + mod_name = view_func.__module__ + view_name = getattr(view_func, '__qualname__', view_func.__class__.__name__) + return mod_name + '.' + view_name + + def trim_docstring(docstring): """ Uniformly trim leading/trailing whitespace from docstrings. diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py index e332607ce8..2da015b73f 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -23,6 +23,8 @@ from django.utils.inspect import ( from django.utils.translation import gettext as _ from django.views.generic import TemplateView +from .utils import get_view_name + # Exclude methods starting with these strings from documentation MODEL_METHODS_EXCLUDE = ('_', 'add_', 'delete', 'save', 'set_') @@ -124,18 +126,13 @@ class TemplateFilterIndexView(BaseAdminDocsView): class ViewIndexView(BaseAdminDocsView): template_name = 'admin_doc/view_index.html' - @staticmethod - def _get_full_name(func): - mod_name = func.__module__ - return '%s.%s' % (mod_name, func.__qualname__) - def get_context_data(self, **kwargs): views = [] urlconf = import_module(settings.ROOT_URLCONF) view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns) for (func, regex, namespace, name) in view_functions: views.append({ - 'full_name': self._get_full_name(func), + 'full_name': get_view_name(func), 'url': simplify_regex(regex), 'url_name': ':'.join((namespace or []) + (name and [name] or [])), 'namespace': ':'.join((namespace or [])), diff --git a/docs/releases/1.11.13.txt b/docs/releases/1.11.13.txt index b9fd3329ef..f72ccd8fd2 100644 --- a/docs/releases/1.11.13.txt +++ b/docs/releases/1.11.13.txt @@ -12,3 +12,6 @@ Bugfixes * Fixed a regression in Django 1.11.8 where altering a field with a unique constraint may drop and rebuild more foreign keys than necessary (:ticket:`29193`). + +* Fixed crashes in ``django.contrib.admindocs`` when a view is a callable + object, such as ``django.contrib.syndication.views.Feed`` (:ticket:`29296`). diff --git a/docs/releases/2.0.5.txt b/docs/releases/2.0.5.txt index c88135aeb9..7107ba63e7 100644 --- a/docs/releases/2.0.5.txt +++ b/docs/releases/2.0.5.txt @@ -15,3 +15,6 @@ Bugfixes * Fixed a regression in Django 1.11.8 where altering a field with a unique constraint may drop and rebuild more foreign keys than necessary (:ticket:`29193`). + +* Fixed crashes in ``django.contrib.admindocs`` when a view is a callable + object, such as ``django.contrib.syndication.views.Feed`` (:ticket:`29296`). diff --git a/tests/admin_docs/test_middleware.py b/tests/admin_docs/test_middleware.py index 8a818e15d1..ab53716481 100644 --- a/tests/admin_docs/test_middleware.py +++ b/tests/admin_docs/test_middleware.py @@ -40,3 +40,8 @@ class XViewMiddlewareTest(TestDataMixin, AdminDocsTestCase): user.save() response = self.client.head('/xview/class/') self.assertNotIn('X-View', response) + + def test_callable_object_view(self): + self.client.force_login(self.superuser) + response = self.client.head('/xview/callable_object/') + self.assertEqual(response['X-View'], 'admin_docs.views.XViewCallableObject') diff --git a/tests/admin_docs/test_views.py b/tests/admin_docs/test_views.py index be69799d70..c55891a3c0 100644 --- a/tests/admin_docs/test_views.py +++ b/tests/admin_docs/test_views.py @@ -51,6 +51,12 @@ class AdminDocViewTests(TestDataMixin, AdminDocsTestCase): ) self.assertContains(response, 'Views by namespace test') self.assertContains(response, 'Name: test:func.') + self.assertContains( + response, + '

' + '/xview/callable_object_without_xview/

', + html=True, + ) def test_view_index_with_method(self): """ diff --git a/tests/admin_docs/urls.py b/tests/admin_docs/urls.py index 0bcdee4b4b..67c72b249c 100644 --- a/tests/admin_docs/urls.py +++ b/tests/admin_docs/urls.py @@ -13,4 +13,6 @@ urlpatterns = [ url(r'^', include(ns_patterns, namespace='test')), url(r'^xview/func/$', views.xview_dec(views.xview)), url(r'^xview/class/$', views.xview_dec(views.XViewClass.as_view())), + url(r'^xview/callable_object/$', views.xview_dec(views.XViewCallableObject())), + url(r'^xview/callable_object_without_xview/$', views.XViewCallableObject()), ] diff --git a/tests/admin_docs/views.py b/tests/admin_docs/views.py index 31d253f7e2..21fe382bba 100644 --- a/tests/admin_docs/views.py +++ b/tests/admin_docs/views.py @@ -13,3 +13,8 @@ def xview(request): class XViewClass(View): def get(self, request): return HttpResponse() + + +class XViewCallableObject(View): + def __call__(self, request): + return HttpResponse()