summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHelen Sherwood-Taylor <helen@rrdlabs.co.uk>2016-08-12 19:07:24 +0100
committerTim Graham <timograham@gmail.com>2016-08-20 10:02:22 -0400
commitae0f55eb491255217d6df31296ec8102007224a6 (patch)
treecac9bf607a4ba6483585350d55a74b3f2f8eea1b
parent7c0c3fb6b1d932bc33e7dcc8f16e31cd39992f9b (diff)
[1.10.x] Fixed #27018 -- Fixed admindocs crash with a view in a class.
Generated correct admindocs URLs on Python 3. URLs generate 404s on Python 2, as in older versions of Django. Backport of bc1e2d8e8edde6cc7d2657c68242a13ee65a15b8 from master
-rw-r--r--AUTHORS1
-rw-r--r--django/contrib/admindocs/views.py42
-rw-r--r--django/urls/resolvers.py3
-rw-r--r--docs/releases/1.10.1.txt3
-rw-r--r--tests/admin_docs/tests.py21
-rw-r--r--tests/urlpatterns_reverse/method_view_urls.py18
-rw-r--r--tests/urlpatterns_reverse/tests.py8
7 files changed, 91 insertions, 5 deletions
diff --git a/AUTHORS b/AUTHORS
index 3acb9b89a5..7c1ed70c44 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -289,6 +289,7 @@ answer newbie questions, and generally made Django that much better:
hambaloney
Hannes Struß <x@hannesstruss.de>
Hawkeye
+ Helen Sherwood-Taylor <helen@rrdlabs.co.uk>
Henrique Romano <onaiort@gmail.com>
hipertracker@gmail.com
Hiroki Kiyohara <hirokiky@gmail.com>
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index fb1afc296a..c221789e82 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -13,6 +13,7 @@ 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,
@@ -126,13 +127,23 @@ 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': '%s.%s' % (func.__module__, getattr(func, '__name__', func.__class__.__name__)),
+ 'full_name': self._get_full_name(func),
'url': simplify_regex(regex),
'url_name': ':'.join((namespace or []) + (name and [name] or [])),
'namespace': ':'.join((namespace or [])),
@@ -145,13 +156,34 @@ class ViewIndexView(BaseAdminDocsView):
class ViewDetailView(BaseAdminDocsView):
template_name = 'admin_doc/view_detail.html'
- def get_context_data(self, **kwargs):
- view = self.kwargs['view']
+ @staticmethod
+ def _get_view_func(view):
urlconf = get_urlconf()
if get_resolver(urlconf)._is_callback(view):
mod, func = get_mod_func(view)
- view_func = getattr(import_module(mod), func)
- else:
+ try:
+ # Separate the module and function, e.g.
+ # 'mymodule.views.myview' -> 'mymodule.views', 'myview').
+ return getattr(import_module(mod), func)
+ except ImportError:
+ # Import may fail because view contains a class name, e.g.
+ # 'mymodule.views.ViewContainer.my_view', so mod takes the form
+ # 'mymodule.views.ViewContainer'. Parse it again to separate
+ # the module and class.
+ mod, klass = get_mod_func(mod)
+ return getattr(getattr(import_module(mod), klass), func)
+ except AttributeError:
+ # PY2 generates incorrect paths for views that are methods,
+ # e.g. 'mymodule.views.ViewContainer.my_view' will be
+ # listed as 'mymodule.views.my_view' because the class name
+ # can't be detected. This causes an AttributeError when
+ # trying to resolve the view.
+ return None
+
+ def get_context_data(self, **kwargs):
+ view = self.kwargs['view']
+ view_func = self._get_view_func(view)
+ if view_func is None:
raise Http404
title, body, metadata = utils.parse_docstring(view_func.__doc__)
if title:
diff --git a/django/urls/resolvers.py b/django/urls/resolvers.py
index 6af847caa0..cec960dc1b 100644
--- a/django/urls/resolvers.py
+++ b/django/urls/resolvers.py
@@ -143,7 +143,10 @@ class RegexURLPattern(LocaleRegexProvider):
callback = callback.func
if not hasattr(callback, '__name__'):
return callback.__module__ + "." + callback.__class__.__name__
+ elif six.PY3:
+ return callback.__module__ + "." + callback.__qualname__
else:
+ # PY2 does not support __qualname__
return callback.__module__ + "." + callback.__name__
diff --git a/docs/releases/1.10.1.txt b/docs/releases/1.10.1.txt
index 0094f8137e..e8dc05c7eb 100644
--- a/docs/releases/1.10.1.txt
+++ b/docs/releases/1.10.1.txt
@@ -58,3 +58,6 @@ Bugfixes
* Fixed ``makemigrations`` crash if a database is read-only (:ticket:`27054`).
* Removed duplicated managers in ``Model._meta.managers`` (:ticket:`27073`).
+
+* Fixed ``contrib.admindocs`` crash when a view is in a class, such as some of
+ the admin views (:ticket:`27018`).
diff --git a/tests/admin_docs/tests.py b/tests/admin_docs/tests.py
index e3091738a8..26e9c04d29 100644
--- a/tests/admin_docs/tests.py
+++ b/tests/admin_docs/tests.py
@@ -9,6 +9,7 @@ from django.contrib.sites.models import Site
from django.test import TestCase, modify_settings, override_settings
from django.test.utils import captured_stderr
from django.urls import reverse
+from django.utils import six
from .models import Company, Person
@@ -82,6 +83,18 @@ class AdminDocViewTests(TestDataMixin, AdminDocsTestCase):
self.assertContains(response, 'Views by namespace test')
self.assertContains(response, 'Name: <code>test:func</code>.')
+ @unittest.skipIf(six.PY2, "Python 2 doesn't support __qualname__.")
+ def test_view_index_with_method(self):
+ """
+ Views that are methods are listed correctly.
+ """
+ response = self.client.get(reverse('django-admindocs-views-index'))
+ self.assertContains(
+ response,
+ '<h3><a href="/admindocs/views/django.contrib.admin.sites.AdminSite.index/">/admin/</a></h3>',
+ html=True
+ )
+
def test_view_detail(self):
url = reverse('django-admindocs-views-detail', args=['django.contrib.admindocs.views.BaseAdminDocsView'])
response = self.client.get(url)
@@ -103,6 +116,14 @@ class AdminDocViewTests(TestDataMixin, AdminDocsTestCase):
self.assertEqual(response.status_code, 404)
self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules)
+ def test_view_detail_as_method(self):
+ """
+ Views that are methods can be displayed.
+ """
+ url = reverse('django-admindocs-views-detail', args=['django.contrib.admin.sites.AdminSite.index'])
+ response = self.client.get(url)
+ self.assertEqual(response.status_code, 200 if six.PY3 else 404)
+
def test_model_index(self):
response = self.client.get(reverse('django-admindocs-models-index'))
self.assertContains(
diff --git a/tests/urlpatterns_reverse/method_view_urls.py b/tests/urlpatterns_reverse/method_view_urls.py
new file mode 100644
index 0000000000..d2f24ecffa
--- /dev/null
+++ b/tests/urlpatterns_reverse/method_view_urls.py
@@ -0,0 +1,18 @@
+from django.conf.urls import url
+
+
+class ViewContainer(object):
+ def method_view(self, request):
+ pass
+
+ @classmethod
+ def classmethod_view(cls, request):
+ pass
+
+view_container = ViewContainer()
+
+
+urlpatterns = [
+ url(r'^$', view_container.method_view, name='instance-method-url'),
+ url(r'^$', ViewContainer.classmethod_view, name='instance-method-url'),
+]
diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py
index 67a4370e78..522899e886 100644
--- a/tests/urlpatterns_reverse/tests.py
+++ b/tests/urlpatterns_reverse/tests.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import sys
import threading
+import unittest
from admin_scripts.tests import AdminScriptTestCase
@@ -430,6 +431,13 @@ class ResolverTests(SimpleTestCase):
self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.View3'))
self.assertFalse(resolver._is_callback('urlpatterns_reverse.nested_urls.blub'))
+ @unittest.skipIf(six.PY2, "Python 2 doesn't support __qualname__.")
+ def test_view_detail_as_method(self):
+ # Views which have a class name as part of their path.
+ resolver = get_resolver('urlpatterns_reverse.method_view_urls')
+ self.assertTrue(resolver._is_callback('urlpatterns_reverse.method_view_urls.ViewContainer.method_view'))
+ self.assertTrue(resolver._is_callback('urlpatterns_reverse.method_view_urls.ViewContainer.classmethod_view'))
+
def test_populate_concurrency(self):
"""
RegexURLResolver._populate() can be called concurrently, but not more