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.
This commit is contained in:
Helen Sherwood-Taylor 2016-08-12 19:07:24 +01:00 committed by Tim Graham
parent 00bb47b58f
commit bc1e2d8e8e
7 changed files with 91 additions and 5 deletions

View File

@ -290,6 +290,7 @@ answer newbie questions, and generally made Django that much better:
hambaloney hambaloney
Hannes Struß <x@hannesstruss.de> Hannes Struß <x@hannesstruss.de>
Hawkeye Hawkeye
Helen Sherwood-Taylor <helen@rrdlabs.co.uk>
Henrique Romano <onaiort@gmail.com> Henrique Romano <onaiort@gmail.com>
hipertracker@gmail.com hipertracker@gmail.com
Hiroki Kiyohara <hirokiky@gmail.com> Hiroki Kiyohara <hirokiky@gmail.com>

View File

@ -13,6 +13,7 @@ from django.db import models
from django.http import Http404 from django.http import Http404
from django.template.engine import Engine from django.template.engine import Engine
from django.urls import get_mod_func, get_resolver, get_urlconf, reverse 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.decorators import method_decorator
from django.utils.inspect import ( from django.utils.inspect import (
func_accepts_kwargs, func_accepts_var_args, func_has_no_args, func_accepts_kwargs, func_accepts_var_args, func_has_no_args,
@ -126,13 +127,23 @@ class TemplateFilterIndexView(BaseAdminDocsView):
class ViewIndexView(BaseAdminDocsView): class ViewIndexView(BaseAdminDocsView):
template_name = 'admin_doc/view_index.html' 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): def get_context_data(self, **kwargs):
views = [] views = []
urlconf = import_module(settings.ROOT_URLCONF) urlconf = import_module(settings.ROOT_URLCONF)
view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns) view_functions = extract_views_from_urlpatterns(urlconf.urlpatterns)
for (func, regex, namespace, name) in view_functions: for (func, regex, namespace, name) in view_functions:
views.append({ 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': simplify_regex(regex),
'url_name': ':'.join((namespace or []) + (name and [name] or [])), 'url_name': ':'.join((namespace or []) + (name and [name] or [])),
'namespace': ':'.join((namespace or [])), 'namespace': ':'.join((namespace or [])),
@ -145,13 +156,34 @@ class ViewIndexView(BaseAdminDocsView):
class ViewDetailView(BaseAdminDocsView): class ViewDetailView(BaseAdminDocsView):
template_name = 'admin_doc/view_detail.html' template_name = 'admin_doc/view_detail.html'
def get_context_data(self, **kwargs): @staticmethod
view = self.kwargs['view'] def _get_view_func(view):
urlconf = get_urlconf() urlconf = get_urlconf()
if get_resolver(urlconf)._is_callback(view): if get_resolver(urlconf)._is_callback(view):
mod, func = get_mod_func(view) mod, func = get_mod_func(view)
view_func = getattr(import_module(mod), func) try:
else: # 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 raise Http404
title, body, metadata = utils.parse_docstring(view_func.__doc__) title, body, metadata = utils.parse_docstring(view_func.__doc__)
if title: if title:

View File

@ -143,7 +143,10 @@ class RegexURLPattern(LocaleRegexProvider):
callback = callback.func callback = callback.func
if not hasattr(callback, '__name__'): if not hasattr(callback, '__name__'):
return callback.__module__ + "." + callback.__class__.__name__ return callback.__module__ + "." + callback.__class__.__name__
elif six.PY3:
return callback.__module__ + "." + callback.__qualname__
else: else:
# PY2 does not support __qualname__
return callback.__module__ + "." + callback.__name__ return callback.__module__ + "." + callback.__name__

View File

@ -58,3 +58,6 @@ Bugfixes
* Fixed ``makemigrations`` crash if a database is read-only (:ticket:`27054`). * Fixed ``makemigrations`` crash if a database is read-only (:ticket:`27054`).
* Removed duplicated managers in ``Model._meta.managers`` (:ticket:`27073`). * 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`).

View File

@ -9,6 +9,7 @@ from django.contrib.sites.models import Site
from django.test import TestCase, modify_settings, override_settings from django.test import TestCase, modify_settings, override_settings
from django.test.utils import captured_stderr from django.test.utils import captured_stderr
from django.urls import reverse from django.urls import reverse
from django.utils import six
from .models import Company, Person from .models import Company, Person
@ -82,6 +83,18 @@ class AdminDocViewTests(TestDataMixin, AdminDocsTestCase):
self.assertContains(response, 'Views by namespace test') self.assertContains(response, 'Views by namespace test')
self.assertContains(response, 'Name: <code>test:func</code>.') 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): def test_view_detail(self):
url = reverse('django-admindocs-views-detail', args=['django.contrib.admindocs.views.BaseAdminDocsView']) url = reverse('django-admindocs-views-detail', args=['django.contrib.admindocs.views.BaseAdminDocsView'])
response = self.client.get(url) response = self.client.get(url)
@ -103,6 +116,14 @@ class AdminDocViewTests(TestDataMixin, AdminDocsTestCase):
self.assertEqual(response.status_code, 404) self.assertEqual(response.status_code, 404)
self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules) 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): def test_model_index(self):
response = self.client.get(reverse('django-admindocs-models-index')) response = self.client.get(reverse('django-admindocs-models-index'))
self.assertContains( self.assertContains(

View File

@ -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'),
]

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import sys import sys
import threading import threading
import unittest
from admin_scripts.tests import AdminScriptTestCase from admin_scripts.tests import AdminScriptTestCase
@ -430,6 +431,13 @@ class ResolverTests(SimpleTestCase):
self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.View3')) self.assertTrue(resolver._is_callback('urlpatterns_reverse.nested_urls.View3'))
self.assertFalse(resolver._is_callback('urlpatterns_reverse.nested_urls.blub')) 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): def test_populate_concurrency(self):
""" """
RegexURLResolver._populate() can be called concurrently, but not more RegexURLResolver._populate() can be called concurrently, but not more