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:
parent
00bb47b58f
commit
bc1e2d8e8e
1
AUTHORS
1
AUTHORS
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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__
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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`).
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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'),
|
||||||
|
]
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue