Fixed #31747 -- Fixed model enumeration via admin URLs.
Co-authored-by: Carlton Gibson <carlton.gibson@noumenal.es>
This commit is contained in:
parent
3071660acf
commit
ba31b01034
|
@ -3,19 +3,23 @@ from functools import update_wrapper
|
||||||
from weakref import WeakSet
|
from weakref import WeakSet
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.admin import ModelAdmin, actions
|
from django.contrib.admin import ModelAdmin, actions
|
||||||
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
|
from django.contrib.admin.views.autocomplete import AutocompleteJsonView
|
||||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.models.base import ModelBase
|
from django.db.models.base import ModelBase
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import (
|
||||||
|
Http404, HttpResponsePermanentRedirect, HttpResponseRedirect,
|
||||||
|
)
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.urls import NoReverseMatch, reverse
|
from django.urls import NoReverseMatch, Resolver404, resolve, reverse
|
||||||
from django.utils.functional import LazyObject
|
from django.utils.functional import LazyObject
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import gettext as _, gettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
|
from django.views.decorators.common import no_append_slash
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
from django.views.i18n import JavaScriptCatalog
|
from django.views.i18n import JavaScriptCatalog
|
||||||
|
|
||||||
|
@ -63,6 +67,8 @@ class AdminSite:
|
||||||
password_change_template = None
|
password_change_template = None
|
||||||
password_change_done_template = None
|
password_change_done_template = None
|
||||||
|
|
||||||
|
final_catch_all_view = True
|
||||||
|
|
||||||
def __init__(self, name='admin'):
|
def __init__(self, name='admin'):
|
||||||
self._registry = {} # model_class class -> admin_class instance
|
self._registry = {} # model_class class -> admin_class instance
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -282,6 +288,10 @@ class AdminSite:
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
re_path(regex, wrap(self.app_index), name='app_list'),
|
re_path(regex, wrap(self.app_index), name='app_list'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if self.final_catch_all_view:
|
||||||
|
urlpatterns.append(re_path(r'(?P<url>.*)$', wrap(self.catch_all_view)))
|
||||||
|
|
||||||
return urlpatterns
|
return urlpatterns
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -406,6 +416,20 @@ class AdminSite:
|
||||||
def autocomplete_view(self, request):
|
def autocomplete_view(self, request):
|
||||||
return AutocompleteJsonView.as_view(admin_site=self)(request)
|
return AutocompleteJsonView.as_view(admin_site=self)(request)
|
||||||
|
|
||||||
|
@no_append_slash
|
||||||
|
def catch_all_view(self, request, url):
|
||||||
|
if settings.APPEND_SLASH and not url.endswith('/'):
|
||||||
|
urlconf = getattr(request, 'urlconf', None)
|
||||||
|
path = '%s/' % request.path_info
|
||||||
|
try:
|
||||||
|
match = resolve(path, urlconf)
|
||||||
|
except Resolver404:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if getattr(match.func, 'should_append_slash', True):
|
||||||
|
return HttpResponsePermanentRedirect(path)
|
||||||
|
raise Http404
|
||||||
|
|
||||||
def _build_app_dict(self, request, label=None):
|
def _build_app_dict(self, request, label=None):
|
||||||
"""
|
"""
|
||||||
Build the app dictionary. The optional `label` parameter filters models
|
Build the app dictionary. The optional `label` parameter filters models
|
||||||
|
|
|
@ -2905,6 +2905,19 @@ Templates can override or extend base admin templates as described in
|
||||||
A boolean value that determines whether to show the navigation sidebar
|
A boolean value that determines whether to show the navigation sidebar
|
||||||
on larger screens. By default, it is set to ``True``.
|
on larger screens. By default, it is set to ``True``.
|
||||||
|
|
||||||
|
.. attribute:: AdminSite.final_catch_all_view
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
A boolean value that determines whether to add a final catch-all view to
|
||||||
|
the admin that redirects unauthenticated users to the login page. By
|
||||||
|
default, it is set to ``True``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Setting this to ``False`` is not recommended as the view protects
|
||||||
|
against a potential model enumeration privacy issue.
|
||||||
|
|
||||||
.. attribute:: AdminSite.login_template
|
.. attribute:: AdminSite.login_template
|
||||||
|
|
||||||
Path to a custom template that will be used by the admin site login view.
|
Path to a custom template that will be used by the admin site login view.
|
||||||
|
|
|
@ -125,6 +125,14 @@ Minor features
|
||||||
<django.db.models.ForeignKey.limit_choices_to>` when searching a related
|
<django.db.models.ForeignKey.limit_choices_to>` when searching a related
|
||||||
model.
|
model.
|
||||||
|
|
||||||
|
* The admin now installs a final catch-all view that redirects unauthenticated
|
||||||
|
users to the login page, regardless or whether the URLs is otherwise valid.
|
||||||
|
This protects against a potential model enumeration privacy issue.
|
||||||
|
|
||||||
|
Although not recommended, you may set the new
|
||||||
|
:attr:`.AdminSite.final_catch_all_view` to ``False`` to disable the
|
||||||
|
catch-all view.
|
||||||
|
|
||||||
:mod:`django.contrib.admindocs`
|
:mod:`django.contrib.admindocs`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -600,6 +608,12 @@ backends.
|
||||||
* Pagination links in the admin are now 1-indexed instead of 0-indexed, i.e.
|
* Pagination links in the admin are now 1-indexed instead of 0-indexed, i.e.
|
||||||
the query string for the first page is ``?p=1`` instead of ``?p=0``.
|
the query string for the first page is ``?p=1`` instead of ``?p=0``.
|
||||||
|
|
||||||
|
* The new admin catch-all view will break URL patterns routed after the admin
|
||||||
|
URLs and matching the admin URL prefix. You can either adjust your URL
|
||||||
|
ordering or, if necessary, set :attr:`AdminSite.final_catch_all_view
|
||||||
|
<django.contrib.admin.AdminSite.final_catch_all_view>` to ``False``,
|
||||||
|
disabling the catch-all view. See :ref:`whats-new-3.2` for more details.
|
||||||
|
|
||||||
:mod:`django.contrib.gis`
|
:mod:`django.contrib.gis`
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,11 @@ from django.core.files.storage import FileSystemStorage
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms.models import BaseModelFormSet
|
from django.forms.models import BaseModelFormSet
|
||||||
from django.http import HttpResponse, StreamingHttpResponse
|
from django.http import HttpResponse, JsonResponse, StreamingHttpResponse
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.views.decorators.common import no_append_slash
|
||||||
|
|
||||||
from .forms import MediaActionForm
|
from .forms import MediaActionForm
|
||||||
from .models import (
|
from .models import (
|
||||||
|
@ -100,7 +101,19 @@ class ArticleForm(forms.ModelForm):
|
||||||
model = Article
|
model = Article
|
||||||
|
|
||||||
|
|
||||||
class ArticleAdmin(admin.ModelAdmin):
|
class ArticleAdminWithExtraUrl(admin.ModelAdmin):
|
||||||
|
def get_urls(self):
|
||||||
|
urlpatterns = super().get_urls()
|
||||||
|
urlpatterns.append(
|
||||||
|
path('extra.json', self.admin_site.admin_view(self.extra_json), name='article_extra_json')
|
||||||
|
)
|
||||||
|
return urlpatterns
|
||||||
|
|
||||||
|
def extra_json(self, request):
|
||||||
|
return JsonResponse({})
|
||||||
|
|
||||||
|
|
||||||
|
class ArticleAdmin(ArticleAdminWithExtraUrl):
|
||||||
list_display = (
|
list_display = (
|
||||||
'content', 'date', callable_year, 'model_year', 'modeladmin_year',
|
'content', 'date', callable_year, 'model_year', 'modeladmin_year',
|
||||||
'model_year_reversed', 'section', lambda obj: obj.title,
|
'model_year_reversed', 'section', lambda obj: obj.title,
|
||||||
|
@ -1181,5 +1194,19 @@ class ArticleAdmin9(admin.ModelAdmin):
|
||||||
return obj is None
|
return obj is None
|
||||||
|
|
||||||
|
|
||||||
|
class ActorAdmin9(admin.ModelAdmin):
|
||||||
|
def get_urls(self):
|
||||||
|
# Opt-out of append slash for single model.
|
||||||
|
urls = super().get_urls()
|
||||||
|
for pattern in urls:
|
||||||
|
pattern.callback = no_append_slash(pattern.callback)
|
||||||
|
return urls
|
||||||
|
|
||||||
|
|
||||||
site9 = admin.AdminSite(name='admin9')
|
site9 = admin.AdminSite(name='admin9')
|
||||||
site9.register(Article, ArticleAdmin9)
|
site9.register(Article, ArticleAdmin9)
|
||||||
|
site9.register(Actor, ActorAdmin9)
|
||||||
|
|
||||||
|
site10 = admin.AdminSite(name='admin10')
|
||||||
|
site10.final_catch_all_view = False
|
||||||
|
site10.register(Article, ArticleAdminWithExtraUrl)
|
||||||
|
|
|
@ -6470,3 +6470,214 @@ class GetFormsetsWithInlinesArgumentTest(TestCase):
|
||||||
post_data = {'name': '2'}
|
post_data = {'name': '2'}
|
||||||
response = self.client.post(reverse('admin:admin_views_implicitlygeneratedpk_change', args=(1,)), post_data)
|
response = self.client.post(reverse('admin:admin_views_implicitlygeneratedpk_change', args=(1,)), post_data)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(ROOT_URLCONF='admin_views.urls')
|
||||||
|
class AdminSiteFinalCatchAllPatternTests(TestCase):
|
||||||
|
"""
|
||||||
|
Verifies the behaviour of the admin catch-all view.
|
||||||
|
|
||||||
|
* Anonynous/non-staff users are redirected to login for all URLs, whether
|
||||||
|
otherwise valid or not.
|
||||||
|
* APPEND_SLASH is applied for staff if needed.
|
||||||
|
* Otherwise Http404.
|
||||||
|
* Catch-all view disabled via AdminSite.final_catch_all_view.
|
||||||
|
"""
|
||||||
|
def test_unknown_url_redirects_login_if_not_authenticated(self):
|
||||||
|
unknown_url = '/test_admin/admin/unknown/'
|
||||||
|
response = self.client.get(unknown_url)
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin:login'), unknown_url))
|
||||||
|
|
||||||
|
def test_unknown_url_404_if_authenticated(self):
|
||||||
|
superuser = User.objects.create_superuser(
|
||||||
|
username='super',
|
||||||
|
password='secret',
|
||||||
|
email='super@example.com',
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
unknown_url = '/test_admin/admin/unknown/'
|
||||||
|
response = self.client.get(unknown_url)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_known_url_redirects_login_if_not_authenticated(self):
|
||||||
|
known_url = reverse('admin:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url)
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin:login'), known_url))
|
||||||
|
|
||||||
|
def test_known_url_missing_slash_redirects_login_if_not_authenticated(self):
|
||||||
|
known_url = reverse('admin:admin_views_article_changelist')[:-1]
|
||||||
|
response = self.client.get(known_url)
|
||||||
|
# Redirects with the next URL also missing the slash.
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin:login'), known_url))
|
||||||
|
|
||||||
|
def test_non_admin_url_shares_url_prefix(self):
|
||||||
|
url = reverse('non_admin')[:-1]
|
||||||
|
response = self.client.get(url)
|
||||||
|
# Redirects with the next URL also missing the slash.
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin:login'), url))
|
||||||
|
|
||||||
|
def test_url_without_trailing_slash_if_not_authenticated(self):
|
||||||
|
url = reverse('admin:article_extra_json')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin:login'), url))
|
||||||
|
|
||||||
|
def test_unkown_url_without_trailing_slash_if_not_authenticated(self):
|
||||||
|
url = reverse('admin:article_extra_json')[:-1]
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin:login'), url))
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_missing_slash_append_slash_true_unknown_url(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
unknown_url = '/test_admin/admin/unknown/'
|
||||||
|
response = self.client.get(unknown_url[:-1])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_missing_slash_append_slash_true(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
known_url = reverse('admin:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertRedirects(response, known_url, status_code=301, target_status_code=403)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_missing_slash_append_slash_true_non_staff_user(self):
|
||||||
|
user = User.objects.create_user(
|
||||||
|
username='user',
|
||||||
|
password='secret',
|
||||||
|
email='user@example.com',
|
||||||
|
is_staff=False,
|
||||||
|
)
|
||||||
|
self.client.force_login(user)
|
||||||
|
known_url = reverse('admin:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertRedirects(response, '/test_admin/admin/login/?next=/test_admin/admin/admin_views/article')
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=False)
|
||||||
|
def test_missing_slash_append_slash_false(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
known_url = reverse('admin:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_single_model_no_append_slash(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
known_url = reverse('admin9:admin_views_actor_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
# Same tests above with final_catch_all_view=False.
|
||||||
|
|
||||||
|
def test_unknown_url_404_if_not_authenticated_without_final_catch_all_view(self):
|
||||||
|
unknown_url = '/test_admin/admin10/unknown/'
|
||||||
|
response = self.client.get(unknown_url)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_unknown_url_404_if_authenticated_without_final_catch_all_view(self):
|
||||||
|
superuser = User.objects.create_superuser(
|
||||||
|
username='super',
|
||||||
|
password='secret',
|
||||||
|
email='super@example.com',
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
unknown_url = '/test_admin/admin10/unknown/'
|
||||||
|
response = self.client.get(unknown_url)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_known_url_redirects_login_if_not_authenticated_without_final_catch_all_view(self):
|
||||||
|
known_url = reverse('admin10:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url)
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin10:login'), known_url))
|
||||||
|
|
||||||
|
def test_known_url_missing_slash_redirects_with_slash_if_not_authenticated_without_final_catch_all_view(self):
|
||||||
|
known_url = reverse('admin10:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertRedirects(response, known_url, status_code=301, fetch_redirect_response=False)
|
||||||
|
|
||||||
|
def test_non_admin_url_shares_url_prefix_without_final_catch_all_view(self):
|
||||||
|
url = reverse('non_admin10')
|
||||||
|
response = self.client.get(url[:-1])
|
||||||
|
self.assertRedirects(response, url, status_code=301)
|
||||||
|
|
||||||
|
def test_url_without_trailing_slash_if_not_authenticated_without_final_catch_all_view(self):
|
||||||
|
url = reverse('admin10:article_extra_json')
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertRedirects(response, '%s?next=%s' % (reverse('admin10:login'), url))
|
||||||
|
|
||||||
|
def test_unkown_url_without_trailing_slash_if_not_authenticated_without_final_catch_all_view(self):
|
||||||
|
url = reverse('admin10:article_extra_json')[:-1]
|
||||||
|
response = self.client.get(url)
|
||||||
|
# Matches test_admin/admin10/admin_views/article/<path:object_id>/
|
||||||
|
self.assertRedirects(response, url + '/', status_code=301, fetch_redirect_response=False)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_missing_slash_append_slash_true_unknown_url_without_final_catch_all_view(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
unknown_url = '/test_admin/admin10/unknown/'
|
||||||
|
response = self.client.get(unknown_url[:-1])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=True)
|
||||||
|
def test_missing_slash_append_slash_true_without_final_catch_all_view(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
known_url = reverse('admin10:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertRedirects(response, known_url, status_code=301, target_status_code=403)
|
||||||
|
|
||||||
|
@override_settings(APPEND_SLASH=False)
|
||||||
|
def test_missing_slash_append_slash_false_without_final_catch_all_view(self):
|
||||||
|
superuser = User.objects.create_user(
|
||||||
|
username='staff',
|
||||||
|
password='secret',
|
||||||
|
email='staff@example.com',
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
self.client.force_login(superuser)
|
||||||
|
known_url = reverse('admin10:admin_views_article_changelist')
|
||||||
|
response = self.client.get(known_url[:-1])
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
# Outside admin.
|
||||||
|
|
||||||
|
def test_non_admin_url_404_if_not_authenticated(self):
|
||||||
|
unknown_url = '/unknown/'
|
||||||
|
response = self.client.get(unknown_url)
|
||||||
|
# Does not redirect to the admin login.
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
|
||||||
from . import admin, custom_has_permission_admin, customadmin, views
|
from . import admin, custom_has_permission_admin, customadmin, views
|
||||||
from .test_autocomplete_view import site as autocomplete_site
|
from .test_autocomplete_view import site as autocomplete_site
|
||||||
|
|
||||||
|
|
||||||
|
def non_admin_view(request):
|
||||||
|
return HttpResponse()
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('test_admin/admin/doc/', include('django.contrib.admindocs.urls')),
|
path('test_admin/admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
path('test_admin/admin/secure-view/', views.secure_view, name='secure_view'),
|
path('test_admin/admin/secure-view/', views.secure_view, name='secure_view'),
|
||||||
|
@ -17,6 +23,10 @@ urlpatterns = [
|
||||||
# All admin views accept `extra_context` to allow adding it like this:
|
# All admin views accept `extra_context` to allow adding it like this:
|
||||||
path('test_admin/admin8/', (admin.site.get_urls(), 'admin', 'admin-extra-context'), {'extra_context': {}}),
|
path('test_admin/admin8/', (admin.site.get_urls(), 'admin', 'admin-extra-context'), {'extra_context': {}}),
|
||||||
path('test_admin/admin9/', admin.site9.urls),
|
path('test_admin/admin9/', admin.site9.urls),
|
||||||
|
path('test_admin/admin10/', admin.site10.urls),
|
||||||
path('test_admin/has_permission_admin/', custom_has_permission_admin.site.urls),
|
path('test_admin/has_permission_admin/', custom_has_permission_admin.site.urls),
|
||||||
path('test_admin/autocomplete_admin/', autocomplete_site.urls),
|
path('test_admin/autocomplete_admin/', autocomplete_site.urls),
|
||||||
|
# Shares the admin URL prefix.
|
||||||
|
path('test_admin/admin/non_admin_view/', non_admin_view, name='non_admin'),
|
||||||
|
path('test_admin/admin10/non_admin_view/', non_admin_view, name='non_admin10'),
|
||||||
]
|
]
|
||||||
|
|
Loading…
Reference in New Issue