[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 bc1e2d8e8e
from master
This commit is contained in:
parent
7c0c3fb6b1
commit
ae0f55eb49
1
AUTHORS
1
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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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__
|
||||
|
||||
|
||||
|
|
|
@ -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`).
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 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
|
||||
|
|
Loading…
Reference in New Issue