diff --git a/AUTHORS b/AUTHORS index 4f92fd3ef7..e29c17953f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -607,6 +607,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 4bdd4f45b5..63dcb5f076 100644 --- a/django/contrib/admindocs/middleware.py +++ b/django/contrib/admindocs/middleware.py @@ -2,6 +2,8 @@ from django import http from django.conf import settings 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 = http.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 b6a23c8849..7275e15707 100644 --- a/django/contrib/admindocs/utils.py +++ b/django/contrib/admindocs/utils.py @@ -5,6 +5,7 @@ from email.errors import HeaderParseError from email.parser import HeaderParser from django.urls import reverse +from django.utils import six from django.utils.encoding import force_bytes from django.utils.safestring import mark_safe @@ -18,6 +19,16 @@ else: docutils_is_available = True +def get_view_name(view_func): + mod_name = view_func.__module__ + if six.PY3: + view_name = getattr(view_func, '__qualname__', view_func.__class__.__name__) + else: + # PY2 does not support __qualname__ + view_name = getattr(view_func, '__name__', 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 5c6701aef2..12f5863228 100644 --- a/django/contrib/admindocs/views.py +++ b/django/contrib/admindocs/views.py @@ -15,7 +15,6 @@ from django.db import models from django.http import Http404 from django.template.engine import Engine from django.urls import get_mod_func, get_resolver, get_urlconf, reverse -from django.utils import six from django.utils.decorators import method_decorator from django.utils.inspect import ( func_accepts_kwargs, func_accepts_var_args, func_has_no_args, @@ -24,6 +23,8 @@ from django.utils.inspect import ( from django.utils.translation import ugettext 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_') @@ -129,23 +130,13 @@ class TemplateFilterIndexView(BaseAdminDocsView): class ViewIndexView(BaseAdminDocsView): template_name = 'admin_doc/view_index.html' - @staticmethod - def _get_full_name(func): - mod_name = func.__module__ - if six.PY3: - return '%s.%s' % (mod_name, func.__qualname__) - else: - # PY2 does not support __qualname__ - func_name = getattr(func, '__name__', func.__class__.__name__) - return '%s.%s' % (mod_name, func_name) - 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/tests/admin_docs/test_middleware.py b/tests/admin_docs/test_middleware.py index 426c78d58f..e5dbd9dfb3 100644 --- a/tests/admin_docs/test_middleware.py +++ b/tests/admin_docs/test_middleware.py @@ -42,3 +42,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 bd483007c7..cf4f9359c7 100644 --- a/tests/admin_docs/test_views.py +++ b/tests/admin_docs/test_views.py @@ -54,6 +54,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, + ) @unittest.skipIf(six.PY2, "Python 2 doesn't support __qualname__.") 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()