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,
+ '',
+ 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()