Fixed #28593 -- Added a simplified URL routing syntax per DEP 0201.
Thanks Aymeric Augustin for shepherding the DEP and patch review. Thanks Marten Kenbeek and Tim Graham for contributing to the code. Thanks Tom Christie, Shai Berger, and Tim Graham for the docs.
This commit is contained in:
parent
c4c128d67c
commit
df41b5a05d
1
AUTHORS
1
AUTHORS
|
@ -732,6 +732,7 @@ answer newbie questions, and generally made Django that much better:
|
|||
Simon Meers <simon@simonmeers.com>
|
||||
Simon Williams
|
||||
Simon Willison <simon@simonwillison.net>
|
||||
Sjoerd Job Postmus
|
||||
Slawek Mikula <slawek dot mikula at gmail dot com>
|
||||
sloonz <simon.lipp@insa-lyon.fr>
|
||||
smurf@smurf.noris.de
|
||||
|
|
|
@ -5,17 +5,17 @@ The `urlpatterns` list routes URLs to views. For more information please see:
|
|||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from django.urls import RegexURLPattern, RegexURLResolver, include
|
||||
from django.urls import include, re_path
|
||||
from django.views import defaults
|
||||
|
||||
__all__ = ['handler400', 'handler403', 'handler404', 'handler500', 'include', 'url']
|
||||
|
@ -10,11 +10,4 @@ handler500 = defaults.server_error
|
|||
|
||||
|
||||
def url(regex, view, kwargs=None, name=None):
|
||||
if isinstance(view, (list, tuple)):
|
||||
# For include(...) processing.
|
||||
urlconf_module, app_name, namespace = view
|
||||
return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
|
||||
elif callable(view):
|
||||
return RegexURLPattern(regex, view, kwargs, name)
|
||||
else:
|
||||
raise TypeError('view must be a callable or a list/tuple in the case of include().')
|
||||
return re_path(regex, view, kwargs, name)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import functools
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.urls import LocaleRegexURLResolver, get_resolver
|
||||
from django.urls import LocalePrefixPattern, URLResolver, get_resolver, path
|
||||
from django.views.i18n import set_language
|
||||
|
||||
|
||||
|
@ -13,7 +12,12 @@ def i18n_patterns(*urls, prefix_default_language=True):
|
|||
"""
|
||||
if not settings.USE_I18N:
|
||||
return list(urls)
|
||||
return [LocaleRegexURLResolver(list(urls), prefix_default_language=prefix_default_language)]
|
||||
return [
|
||||
URLResolver(
|
||||
LocalePrefixPattern(prefix_default_language=prefix_default_language),
|
||||
list(urls),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
|
@ -25,11 +29,11 @@ def is_language_prefix_patterns_used(urlconf):
|
|||
)
|
||||
"""
|
||||
for url_pattern in get_resolver(urlconf).url_patterns:
|
||||
if isinstance(url_pattern, LocaleRegexURLResolver):
|
||||
return True, url_pattern.prefix_default_language
|
||||
if isinstance(url_pattern.pattern, LocalePrefixPattern):
|
||||
return True, url_pattern.pattern.prefix_default_language
|
||||
return False, False
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^setlang/$', set_language, name='set_language'),
|
||||
path('setlang/', set_language, name='set_language'),
|
||||
]
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import re_path
|
||||
from django.views.static import serve
|
||||
|
||||
|
||||
|
@ -23,5 +23,5 @@ def static(prefix, view=serve, **kwargs):
|
|||
# No-op if not in debug mode or a non-local prefix.
|
||||
return []
|
||||
return [
|
||||
url(r'^%s(?P<path>.*)$' % re.escape(prefix.lstrip('/')), view, kwargs=kwargs),
|
||||
re_path(r'^%s(?P<path>.*)$' % re.escape(prefix.lstrip('/')), view, kwargs=kwargs),
|
||||
]
|
||||
|
|
|
@ -567,7 +567,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
return inline_instances
|
||||
|
||||
def get_urls(self):
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
def wrap(view):
|
||||
def wrapper(*args, **kwargs):
|
||||
|
@ -578,14 +578,14 @@ class ModelAdmin(BaseModelAdmin):
|
|||
info = self.model._meta.app_label, self.model._meta.model_name
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', wrap(self.changelist_view), name='%s_%s_changelist' % info),
|
||||
url(r'^add/$', wrap(self.add_view), name='%s_%s_add' % info),
|
||||
url(r'^autocomplete/$', wrap(self.autocomplete_view), name='%s_%s_autocomplete' % info),
|
||||
url(r'^(.+)/history/$', wrap(self.history_view), name='%s_%s_history' % info),
|
||||
url(r'^(.+)/delete/$', wrap(self.delete_view), name='%s_%s_delete' % info),
|
||||
url(r'^(.+)/change/$', wrap(self.change_view), name='%s_%s_change' % info),
|
||||
path('', wrap(self.changelist_view), name='%s_%s_changelist' % info),
|
||||
path('add/', wrap(self.add_view), name='%s_%s_add' % info),
|
||||
path('autocomplete/', wrap(self.autocomplete_view), name='%s_%s_autocomplete' % info),
|
||||
path('<path:object_id>/history/', wrap(self.history_view), name='%s_%s_history' % info),
|
||||
path('<path:object_id>/delete/', wrap(self.delete_view), name='%s_%s_delete' % info),
|
||||
path('<path:object_id>/change/', wrap(self.change_view), name='%s_%s_change' % info),
|
||||
# For backwards compatibility (was the change url before 1.9)
|
||||
url(r'^(.+)/$', wrap(RedirectView.as_view(
|
||||
path('<path:object_id>/', wrap(RedirectView.as_view(
|
||||
pattern_name='%s:%s_%s_change' % ((self.admin_site.name,) + info)
|
||||
))),
|
||||
]
|
||||
|
@ -1173,8 +1173,7 @@ class ModelAdmin(BaseModelAdmin):
|
|||
opts = obj._meta
|
||||
to_field = request.POST.get(TO_FIELD_VAR)
|
||||
attr = str(to_field) if to_field else opts.pk.attname
|
||||
# Retrieve the `object_id` from the resolved pattern arguments.
|
||||
value = request.resolver_match.args[0]
|
||||
value = request.resolver_match.kwargs['object_id']
|
||||
new_value = obj.serializable_value(attr)
|
||||
popup_response_data = json.dumps({
|
||||
'action': 'change',
|
||||
|
|
|
@ -196,11 +196,11 @@ class AdminSite:
|
|||
class MyAdminSite(AdminSite):
|
||||
|
||||
def get_urls(self):
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
urls = super().get_urls()
|
||||
urls += [
|
||||
url(r'^my_view/$', self.admin_view(some_view))
|
||||
path('my_view/', self.admin_view(some_view))
|
||||
]
|
||||
return urls
|
||||
|
||||
|
@ -230,7 +230,7 @@ class AdminSite:
|
|||
return update_wrapper(inner, view)
|
||||
|
||||
def get_urls(self):
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import include, path, re_path
|
||||
# Since this module gets imported in the application's root package,
|
||||
# it cannot import models from other applications at the module level,
|
||||
# and django.contrib.contenttypes.views imports ContentType.
|
||||
|
@ -244,15 +244,21 @@ class AdminSite:
|
|||
|
||||
# Admin-site-wide views.
|
||||
urlpatterns = [
|
||||
url(r'^$', wrap(self.index), name='index'),
|
||||
url(r'^login/$', self.login, name='login'),
|
||||
url(r'^logout/$', wrap(self.logout), name='logout'),
|
||||
url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'),
|
||||
url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True),
|
||||
name='password_change_done'),
|
||||
url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
|
||||
url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut),
|
||||
name='view_on_site'),
|
||||
path('', wrap(self.index), name='index'),
|
||||
path('login/', self.login, name='login'),
|
||||
path('logout/', wrap(self.logout), name='logout'),
|
||||
path('password_change/', wrap(self.password_change, cacheable=True), name='password_change'),
|
||||
path(
|
||||
'password_change/done/',
|
||||
wrap(self.password_change_done, cacheable=True),
|
||||
name='password_change_done',
|
||||
),
|
||||
path('jsi18n/', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
|
||||
path(
|
||||
'r/<int:content_type_id>/<path:object_id>/',
|
||||
wrap(contenttype_views.shortcut),
|
||||
name='view_on_site',
|
||||
),
|
||||
]
|
||||
|
||||
# Add in each model's views, and create a list of valid URLS for the
|
||||
|
@ -260,7 +266,7 @@ class AdminSite:
|
|||
valid_app_labels = []
|
||||
for model, model_admin in self._registry.items():
|
||||
urlpatterns += [
|
||||
url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
|
||||
path('%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)),
|
||||
]
|
||||
if model._meta.app_label not in valid_app_labels:
|
||||
valid_app_labels.append(model._meta.app_label)
|
||||
|
@ -270,7 +276,7 @@ class AdminSite:
|
|||
if valid_app_labels:
|
||||
regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$'
|
||||
urlpatterns += [
|
||||
url(regex, wrap(self.app_index), name='app_list'),
|
||||
re_path(regex, wrap(self.app_index), name='app_list'),
|
||||
]
|
||||
return urlpatterns
|
||||
|
||||
|
|
|
@ -1,32 +1,50 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib.admindocs import views
|
||||
from django.urls import path, re_path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$',
|
||||
path(
|
||||
'',
|
||||
views.BaseAdminDocsView.as_view(template_name='admin_doc/index.html'),
|
||||
name='django-admindocs-docroot'),
|
||||
url(r'^bookmarklets/$',
|
||||
name='django-admindocs-docroot',
|
||||
),
|
||||
path(
|
||||
'bookmarklets/',
|
||||
views.BookmarkletsView.as_view(),
|
||||
name='django-admindocs-bookmarklets'),
|
||||
url(r'^tags/$',
|
||||
name='django-admindocs-bookmarklets',
|
||||
),
|
||||
path(
|
||||
'tags/',
|
||||
views.TemplateTagIndexView.as_view(),
|
||||
name='django-admindocs-tags'),
|
||||
url(r'^filters/$',
|
||||
name='django-admindocs-tags',
|
||||
),
|
||||
path(
|
||||
'filters/',
|
||||
views.TemplateFilterIndexView.as_view(),
|
||||
name='django-admindocs-filters'),
|
||||
url(r'^views/$',
|
||||
name='django-admindocs-filters',
|
||||
),
|
||||
path(
|
||||
'views/',
|
||||
views.ViewIndexView.as_view(),
|
||||
name='django-admindocs-views-index'),
|
||||
url(r'^views/(?P<view>[^/]+)/$',
|
||||
name='django-admindocs-views-index',
|
||||
),
|
||||
path(
|
||||
'views/<view>/',
|
||||
views.ViewDetailView.as_view(),
|
||||
name='django-admindocs-views-detail'),
|
||||
url(r'^models/$',
|
||||
name='django-admindocs-views-detail',
|
||||
),
|
||||
path(
|
||||
'models/',
|
||||
views.ModelIndexView.as_view(),
|
||||
name='django-admindocs-models-index'),
|
||||
url(r'^models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$',
|
||||
name='django-admindocs-models-index',
|
||||
),
|
||||
re_path(
|
||||
r'^models/(?P<app_label>[^\.]+)\.(?P<model_name>[^/]+)/$',
|
||||
views.ModelDetailView.as_view(),
|
||||
name='django-admindocs-models-detail'),
|
||||
url(r'^templates/(?P<template>.*)/$',
|
||||
name='django-admindocs-models-detail',
|
||||
),
|
||||
path(
|
||||
'templates/<path:template>/',
|
||||
views.TemplateDetailView.as_view(),
|
||||
name='django-admindocs-templates'),
|
||||
name='django-admindocs-templates',
|
||||
),
|
||||
]
|
||||
|
|
|
@ -401,13 +401,12 @@ def extract_views_from_urlpatterns(urlpatterns, base='', namespace=None):
|
|||
continue
|
||||
views.extend(extract_views_from_urlpatterns(
|
||||
patterns,
|
||||
base + p.regex.pattern,
|
||||
base + str(p.pattern),
|
||||
(namespace or []) + (p.namespace and [p.namespace] or [])
|
||||
))
|
||||
elif hasattr(p, 'callback'):
|
||||
try:
|
||||
views.append((p.callback, base + p.regex.pattern,
|
||||
namespace, p.name))
|
||||
views.append((p.callback, base + str(p.pattern), namespace, p.name))
|
||||
except ViewDoesNotExist:
|
||||
continue
|
||||
else:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin.options import IS_POPUP_VAR
|
||||
from django.contrib.admin.utils import unquote
|
||||
|
@ -12,7 +11,7 @@ from django.core.exceptions import PermissionDenied
|
|||
from django.db import router, transaction
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.urls import path, reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
|
@ -81,8 +80,8 @@ class UserAdmin(admin.ModelAdmin):
|
|||
|
||||
def get_urls(self):
|
||||
return [
|
||||
url(
|
||||
r'^(.+)/password/$',
|
||||
path(
|
||||
'<id>/password/',
|
||||
self.admin_site.admin_view(self.user_change_password),
|
||||
name='auth_user_password_change',
|
||||
),
|
||||
|
|
|
@ -3,19 +3,18 @@
|
|||
# It is also provided as a convenience to those who want to deploy these URLs
|
||||
# elsewhere.
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth import views
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^login/$', views.LoginView.as_view(), name='login'),
|
||||
url(r'^logout/$', views.LogoutView.as_view(), name='logout'),
|
||||
path('login/', views.LoginView.as_view(), name='login'),
|
||||
path('logout/', views.LogoutView.as_view(), name='logout'),
|
||||
|
||||
url(r'^password_change/$', views.PasswordChangeView.as_view(), name='password_change'),
|
||||
url(r'^password_change/done/$', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
|
||||
path('password_change/', views.PasswordChangeView.as_view(), name='password_change'),
|
||||
path('password_change/done/', views.PasswordChangeDoneView.as_view(), name='password_change_done'),
|
||||
|
||||
url(r'^password_reset/$', views.PasswordResetView.as_view(), name='password_reset'),
|
||||
url(r'^password_reset/done/$', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
|
||||
url(r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
url(r'^reset/done/$', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
|
||||
path('password_reset/', views.PasswordResetView.as_view(), name='password_reset'),
|
||||
path('password_reset/done/', views.PasswordResetDoneView.as_view(), name='password_reset_done'),
|
||||
path('reset/<uidb64>/<token>/', views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
|
||||
path('reset/done/', views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib.flatpages import views
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<url>.*)$', views.flatpage, name='django.contrib.flatpages.views.flatpage'),
|
||||
path('<path:url>', views.flatpage, name='django.contrib.flatpages.views.flatpage'),
|
||||
]
|
||||
|
|
|
@ -81,13 +81,13 @@ def get_warning_for_invalid_pattern(pattern):
|
|||
"have a prefix string as the first element.".format(pattern)
|
||||
)
|
||||
elif isinstance(pattern, tuple):
|
||||
hint = "Try using url() instead of a tuple."
|
||||
hint = "Try using path() instead of a tuple."
|
||||
else:
|
||||
hint = None
|
||||
|
||||
return [Error(
|
||||
"Your URL pattern {!r} is invalid. Ensure that urlpatterns is a list "
|
||||
"of url() instances.".format(pattern),
|
||||
"of path() and/or re_path() instances.".format(pattern),
|
||||
hint=hint,
|
||||
id="urls.E004",
|
||||
)]
|
||||
|
|
|
@ -1329,7 +1329,7 @@ def url(parser, token):
|
|||
|
||||
{% url "url_name" name1=value1 name2=value2 %}
|
||||
|
||||
The first argument is a django.conf.urls.url() name. Other arguments are
|
||||
The first argument is a URL pattern name. Other arguments are
|
||||
space-separated values that will be filled in place of positional and
|
||||
keyword arguments in the URL. Don't mix positional and keyword arguments.
|
||||
All arguments for the URL must be present.
|
||||
|
@ -1337,12 +1337,12 @@ def url(parser, token):
|
|||
For example, if you have a view ``app_name.views.client_details`` taking
|
||||
the client's id and the corresponding line in a URLconf looks like this::
|
||||
|
||||
url('^client/(\d+)/$', views.client_details, name='client-detail-view')
|
||||
path('client/<int:id>/', views.client_details, name='client-detail-view')
|
||||
|
||||
and this app's URLconf is included into the project's URLconf under some
|
||||
path::
|
||||
|
||||
url('^clients/', include('app_name.urls'))
|
||||
path('clients/', include('app_name.urls'))
|
||||
|
||||
then in a template you can create a link for a certain client like this::
|
||||
|
||||
|
@ -1359,7 +1359,7 @@ def url(parser, token):
|
|||
"""
|
||||
bits = token.split_contents()
|
||||
if len(bits) < 2:
|
||||
raise TemplateSyntaxError("'%s' takes at least one argument, the name of a url()." % bits[0])
|
||||
raise TemplateSyntaxError("'%s' takes at least one argument, a URL pattern name." % bits[0])
|
||||
viewname = parser.compile_filter(bits[1])
|
||||
args = []
|
||||
kwargs = {}
|
||||
|
|
|
@ -3,19 +3,21 @@ from .base import (
|
|||
is_valid_path, resolve, reverse, reverse_lazy, set_script_prefix,
|
||||
set_urlconf, translate_url,
|
||||
)
|
||||
from .conf import include
|
||||
from .conf import include, path, re_path
|
||||
from .converters import register_converter
|
||||
from .exceptions import NoReverseMatch, Resolver404
|
||||
from .resolvers import (
|
||||
LocaleRegexProvider, LocaleRegexURLResolver, RegexURLPattern,
|
||||
RegexURLResolver, ResolverMatch, get_ns_resolver, get_resolver,
|
||||
LocalePrefixPattern, ResolverMatch, URLPattern, URLResolver,
|
||||
get_ns_resolver, get_resolver,
|
||||
)
|
||||
from .utils import get_callable, get_mod_func
|
||||
|
||||
__all__ = [
|
||||
'LocaleRegexProvider', 'LocaleRegexURLResolver', 'NoReverseMatch',
|
||||
'RegexURLPattern', 'RegexURLResolver', 'Resolver404', 'ResolverMatch',
|
||||
'clear_script_prefix', 'clear_url_caches', 'get_callable', 'get_mod_func',
|
||||
'get_ns_resolver', 'get_resolver', 'get_script_prefix', 'get_urlconf',
|
||||
'include', 'is_valid_path', 'resolve', 'reverse', 'reverse_lazy',
|
||||
'set_script_prefix', 'set_urlconf', 'translate_url',
|
||||
'LocalePrefixPattern', 'NoReverseMatch', 'URLPattern',
|
||||
'URLResolver', 'Resolver404', 'ResolverMatch', 'clear_script_prefix',
|
||||
'clear_url_caches', 'get_callable', 'get_mod_func', 'get_ns_resolver',
|
||||
'get_resolver', 'get_script_prefix', 'get_urlconf', 'include',
|
||||
'is_valid_path', 'path', 're_path', 'register_converter', 'resolve',
|
||||
'reverse', 'reverse_lazy', 'set_script_prefix', 'set_urlconf',
|
||||
'translate_url',
|
||||
]
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""Functions for use in URLsconfs."""
|
||||
from functools import partial
|
||||
from importlib import import_module
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from .resolvers import LocaleRegexURLResolver
|
||||
from .resolvers import (
|
||||
LocalePrefixPattern, RegexPattern, RoutePattern, URLPattern, URLResolver,
|
||||
)
|
||||
|
||||
|
||||
def include(arg, namespace=None):
|
||||
|
@ -43,8 +46,32 @@ def include(arg, namespace=None):
|
|||
# testcases will break).
|
||||
if isinstance(patterns, (list, tuple)):
|
||||
for url_pattern in patterns:
|
||||
if isinstance(url_pattern, LocaleRegexURLResolver):
|
||||
pattern = getattr(url_pattern, 'pattern', None)
|
||||
if isinstance(pattern, LocalePrefixPattern):
|
||||
raise ImproperlyConfigured(
|
||||
'Using i18n_patterns in an included URLconf is not allowed.'
|
||||
)
|
||||
return (urlconf_module, app_name, namespace)
|
||||
|
||||
|
||||
def _path(route, view, kwargs=None, name=None, Pattern=None):
|
||||
if isinstance(view, (list, tuple)):
|
||||
# For include(...) processing.
|
||||
pattern = Pattern(route, is_endpoint=False)
|
||||
urlconf_module, app_name, namespace = view
|
||||
return URLResolver(
|
||||
pattern,
|
||||
urlconf_module,
|
||||
kwargs,
|
||||
app_name=app_name,
|
||||
namespace=namespace,
|
||||
)
|
||||
elif callable(view):
|
||||
pattern = Pattern(route, name=name, is_endpoint=True)
|
||||
return URLPattern(pattern, view, kwargs, name)
|
||||
else:
|
||||
raise TypeError('view must be a callable or a list/tuple in the case of include().')
|
||||
|
||||
|
||||
path = partial(_path, Pattern=RoutePattern)
|
||||
re_path = partial(_path, Pattern=RegexPattern)
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import uuid
|
||||
|
||||
from django.utils import lru_cache
|
||||
|
||||
|
||||
class IntConverter:
|
||||
regex = '[0-9]+'
|
||||
|
||||
def to_python(self, value):
|
||||
return int(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return str(value)
|
||||
|
||||
|
||||
class StringConverter:
|
||||
regex = '[^/]+'
|
||||
|
||||
def to_python(self, value):
|
||||
return value
|
||||
|
||||
def to_url(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class UUIDConverter:
|
||||
regex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
|
||||
|
||||
def to_python(self, value):
|
||||
return uuid.UUID(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return str(value)
|
||||
|
||||
|
||||
class SlugConverter(StringConverter):
|
||||
regex = '[-a-zA-Z0-9_]+'
|
||||
|
||||
|
||||
class PathConverter(StringConverter):
|
||||
regex = '.+'
|
||||
|
||||
|
||||
DEFAULT_CONVERTERS = {
|
||||
'int': IntConverter(),
|
||||
'path': PathConverter(),
|
||||
'slug': SlugConverter(),
|
||||
'str': StringConverter(),
|
||||
'uuid': UUIDConverter(),
|
||||
}
|
||||
|
||||
|
||||
REGISTERED_CONVERTERS = {}
|
||||
|
||||
|
||||
def register_converter(converter, type_name):
|
||||
REGISTERED_CONVERTERS[type_name] = converter()
|
||||
get_converters.cache_clear()
|
||||
|
||||
|
||||
@lru_cache.lru_cache(maxsize=None)
|
||||
def get_converters():
|
||||
converters = {}
|
||||
converters.update(DEFAULT_CONVERTERS)
|
||||
converters.update(REGISTERED_CONVERTERS)
|
||||
return converters
|
||||
|
||||
|
||||
def get_converter(raw_converter):
|
||||
return get_converters()[raw_converter]
|
|
@ -21,6 +21,7 @@ from django.utils.http import RFC3986_SUBDELIMS
|
|||
from django.utils.regex_helper import normalize
|
||||
from django.utils.translation import get_language
|
||||
|
||||
from .converters import get_converter
|
||||
from .exceptions import NoReverseMatch, Resolver404
|
||||
from .utils import get_callable
|
||||
|
||||
|
@ -64,7 +65,7 @@ def get_resolver(urlconf=None):
|
|||
if urlconf is None:
|
||||
from django.conf import settings
|
||||
urlconf = settings.ROOT_URLCONF
|
||||
return RegexURLResolver(r'^/', urlconf)
|
||||
return URLResolver(RegexPattern(r'^/'), urlconf)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
|
@ -72,11 +73,14 @@ def get_ns_resolver(ns_pattern, resolver):
|
|||
# Build a namespaced resolver for the given parent URLconf pattern.
|
||||
# This makes it possible to have captured parameters in the parent
|
||||
# URLconf pattern.
|
||||
ns_resolver = RegexURLResolver(ns_pattern, resolver.url_patterns)
|
||||
return RegexURLResolver(r'^/', [ns_resolver])
|
||||
ns_resolver = URLResolver(RegexPattern(ns_pattern), resolver.url_patterns)
|
||||
return URLResolver(RegexPattern(r'^/'), [ns_resolver])
|
||||
|
||||
|
||||
class LocaleRegexDescriptor:
|
||||
def __init__(self, attr):
|
||||
self.attr = attr
|
||||
|
||||
def __get__(self, instance, cls=None):
|
||||
"""
|
||||
Return a compiled regular expression based on the active language.
|
||||
|
@ -86,46 +90,23 @@ class LocaleRegexDescriptor:
|
|||
# As a performance optimization, if the given regex string is a regular
|
||||
# string (not a lazily-translated string proxy), compile it once and
|
||||
# avoid per-language compilation.
|
||||
if isinstance(instance._regex, str):
|
||||
instance.__dict__['regex'] = self._compile(instance._regex)
|
||||
pattern = getattr(instance, self.attr)
|
||||
if isinstance(pattern, str):
|
||||
instance.__dict__['regex'] = instance._compile(pattern)
|
||||
return instance.__dict__['regex']
|
||||
language_code = get_language()
|
||||
if language_code not in instance._regex_dict:
|
||||
instance._regex_dict[language_code] = self._compile(str(instance._regex))
|
||||
instance._regex_dict[language_code] = instance._compile(str(pattern))
|
||||
return instance._regex_dict[language_code]
|
||||
|
||||
def _compile(self, regex):
|
||||
"""
|
||||
Compile and return the given regular expression.
|
||||
"""
|
||||
try:
|
||||
return re.compile(regex)
|
||||
except re.error as e:
|
||||
raise ImproperlyConfigured(
|
||||
'"%s" is not a valid regular expression: %s' % (regex, e)
|
||||
)
|
||||
|
||||
|
||||
class LocaleRegexProvider:
|
||||
"""
|
||||
A mixin to provide a default regex property which can vary by active
|
||||
language.
|
||||
"""
|
||||
def __init__(self, regex):
|
||||
# regex is either a string representing a regular expression, or a
|
||||
# translatable string (using gettext_lazy) representing a regular
|
||||
# expression.
|
||||
self._regex = regex
|
||||
self._regex_dict = {}
|
||||
|
||||
regex = LocaleRegexDescriptor()
|
||||
|
||||
class CheckURLMixin:
|
||||
def describe(self):
|
||||
"""
|
||||
Format the URL pattern for display in warning messages.
|
||||
"""
|
||||
description = "'{}'".format(self.regex.pattern)
|
||||
if getattr(self, 'name', False):
|
||||
description = "'{}'".format(self)
|
||||
if self.name:
|
||||
description += " [name='{}']".format(self.name)
|
||||
return description
|
||||
|
||||
|
@ -138,9 +119,9 @@ class LocaleRegexProvider:
|
|||
# Skip check as it can be useful to start a URL pattern with a slash
|
||||
# when APPEND_SLASH=False.
|
||||
return []
|
||||
if (regex_pattern.startswith('/') or regex_pattern.startswith('^/')) and not regex_pattern.endswith('/'):
|
||||
if any(regex_pattern.startswith(x) for x in ('/', '^/', '^\/')) and not regex_pattern.endswith('/'):
|
||||
warning = Warning(
|
||||
"Your URL pattern {} has a regex beginning with a '/'. Remove this "
|
||||
"Your URL pattern {} has a route beginning with a '/'. Remove this "
|
||||
"slash as it is unnecessary. If this pattern is targeted in an "
|
||||
"include(), ensure the include() pattern has a trailing '/'.".format(
|
||||
self.describe()
|
||||
|
@ -152,37 +133,17 @@ class LocaleRegexProvider:
|
|||
return []
|
||||
|
||||
|
||||
class RegexURLPattern(LocaleRegexProvider):
|
||||
def __init__(self, regex, callback, default_args=None, name=None):
|
||||
LocaleRegexProvider.__init__(self, regex)
|
||||
self.callback = callback # the view
|
||||
self.default_args = default_args or {}
|
||||
class RegexPattern(CheckURLMixin):
|
||||
regex = LocaleRegexDescriptor('_regex')
|
||||
|
||||
def __init__(self, regex, name=None, is_endpoint=False):
|
||||
self._regex = regex
|
||||
self._regex_dict = {}
|
||||
self._is_endpoint = is_endpoint
|
||||
self.name = name
|
||||
self.converters = {}
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s %s>' % (self.__class__.__name__, self.name, self.regex.pattern)
|
||||
|
||||
def check(self):
|
||||
warnings = self._check_pattern_name()
|
||||
if not warnings:
|
||||
warnings = self._check_pattern_startswith_slash()
|
||||
return warnings
|
||||
|
||||
def _check_pattern_name(self):
|
||||
"""
|
||||
Check that the pattern name does not contain a colon.
|
||||
"""
|
||||
if self.name is not None and ":" in self.name:
|
||||
warning = Warning(
|
||||
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
|
||||
"avoid ambiguous namespace references.".format(self.describe()),
|
||||
id="urls.W003",
|
||||
)
|
||||
return [warning]
|
||||
else:
|
||||
return []
|
||||
|
||||
def resolve(self, path):
|
||||
def match(self, path):
|
||||
match = self.regex.search(path)
|
||||
if match:
|
||||
# If there are any named groups, use those as kwargs, ignoring
|
||||
|
@ -190,9 +151,190 @@ class RegexURLPattern(LocaleRegexProvider):
|
|||
# positional arguments.
|
||||
kwargs = match.groupdict()
|
||||
args = () if kwargs else match.groups()
|
||||
# In both cases, pass any extra_kwargs as **kwargs.
|
||||
return path[match.end():], args, kwargs
|
||||
return None
|
||||
|
||||
def check(self):
|
||||
warnings = []
|
||||
warnings.extend(self._check_pattern_startswith_slash())
|
||||
if not self._is_endpoint:
|
||||
warnings.extend(self._check_include_trailing_dollar())
|
||||
return warnings
|
||||
|
||||
def _check_include_trailing_dollar(self):
|
||||
regex_pattern = self.regex.pattern
|
||||
if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
|
||||
return [Warning(
|
||||
"Your URL pattern {} uses include with a route ending with a '$'. "
|
||||
"Remove the dollar from the route to avoid problems including "
|
||||
"URLs.".format(self.describe()),
|
||||
id='urls.W001',
|
||||
)]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _compile(self, regex):
|
||||
"""Compile and return the given regular expression."""
|
||||
try:
|
||||
return re.compile(regex)
|
||||
except re.error as e:
|
||||
raise ImproperlyConfigured(
|
||||
'"%s" is not a valid regular expression: %s' % (regex, e)
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self._regex
|
||||
|
||||
|
||||
_PATH_PARAMETER_COMPONENT_RE = re.compile(
|
||||
'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
|
||||
)
|
||||
|
||||
|
||||
def _route_to_regex(route, is_endpoint=False):
|
||||
"""
|
||||
Convert a path pattern into a regular expression. Return the regular
|
||||
expression and a dictionary mapping the capture names to the converters.
|
||||
For example, 'foo/<int:pk>' returns '^foo\\/(?P<pk>[0-9]+)'
|
||||
and {'pk': <django.urls.converters.IntConverter>}.
|
||||
"""
|
||||
original_route = route
|
||||
parts = ['^']
|
||||
converters = {}
|
||||
while True:
|
||||
match = _PATH_PARAMETER_COMPONENT_RE.search(route)
|
||||
if not match:
|
||||
parts.append(re.escape(route))
|
||||
break
|
||||
parts.append(re.escape(route[:match.start()]))
|
||||
route = route[match.end():]
|
||||
parameter = match.group('parameter')
|
||||
if not parameter.isidentifier():
|
||||
raise ImproperlyConfigured(
|
||||
"URL route '%s' uses parameter name %r which isn't a valid "
|
||||
"Python identifier." % (original_route, parameter)
|
||||
)
|
||||
raw_converter = match.group('converter')
|
||||
if raw_converter is None:
|
||||
# If a converter isn't specified, the default is `str`.
|
||||
raw_converter = 'str'
|
||||
try:
|
||||
converter = get_converter(raw_converter)
|
||||
except KeyError as e:
|
||||
raise ImproperlyConfigured(
|
||||
"URL route '%s' uses invalid converter %s." % (original_route, e)
|
||||
)
|
||||
converters[parameter] = converter
|
||||
parts.append('(?P<' + parameter + '>' + converter.regex + ')')
|
||||
if is_endpoint:
|
||||
parts.append('$')
|
||||
return ''.join(parts), converters
|
||||
|
||||
|
||||
class RoutePattern(CheckURLMixin):
|
||||
regex = LocaleRegexDescriptor('_route')
|
||||
|
||||
def __init__(self, route, name=None, is_endpoint=False):
|
||||
self._route = route
|
||||
self._regex_dict = {}
|
||||
self._is_endpoint = is_endpoint
|
||||
self.name = name
|
||||
self.converters = _route_to_regex(str(route), is_endpoint)[1]
|
||||
|
||||
def match(self, path):
|
||||
match = self.regex.search(path)
|
||||
if match:
|
||||
# RoutePattern doesn't allow non-named groups so args are ignored.
|
||||
kwargs = match.groupdict()
|
||||
for key, value in kwargs.items():
|
||||
converter = self.converters[key]
|
||||
try:
|
||||
kwargs[key] = converter.to_python(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return path[match.end():], (), kwargs
|
||||
return None
|
||||
|
||||
def check(self):
|
||||
return self._check_pattern_startswith_slash()
|
||||
|
||||
def _compile(self, route):
|
||||
return re.compile(_route_to_regex(route, self._is_endpoint)[0])
|
||||
|
||||
def __str__(self):
|
||||
return self._route
|
||||
|
||||
|
||||
class LocalePrefixPattern:
|
||||
def __init__(self, prefix_default_language=True):
|
||||
self.prefix_default_language = prefix_default_language
|
||||
self.converters = {}
|
||||
|
||||
@property
|
||||
def regex(self):
|
||||
# This is only used by reverse() and cached in _reverse_dict.
|
||||
return re.compile(self.language_prefix)
|
||||
|
||||
@property
|
||||
def language_prefix(self):
|
||||
language_code = get_language() or settings.LANGUAGE_CODE
|
||||
if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
|
||||
return ''
|
||||
else:
|
||||
return '%s/' % language_code
|
||||
|
||||
def match(self, path):
|
||||
language_prefix = self.language_prefix
|
||||
if path.startswith(language_prefix):
|
||||
return path[len(language_prefix):], (), {}
|
||||
return None
|
||||
|
||||
def check(self):
|
||||
return []
|
||||
|
||||
def describe(self):
|
||||
return "'{}'".format(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.language_prefix
|
||||
|
||||
|
||||
class URLPattern:
|
||||
def __init__(self, pattern, callback, default_args=None, name=None):
|
||||
self.pattern = pattern
|
||||
self.callback = callback # the view
|
||||
self.default_args = default_args or {}
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s>' % (self.__class__.__name__, self.pattern.describe())
|
||||
|
||||
def check(self):
|
||||
warnings = self._check_pattern_name()
|
||||
warnings.extend(self.pattern.check())
|
||||
return warnings
|
||||
|
||||
def _check_pattern_name(self):
|
||||
"""
|
||||
Check that the pattern name does not contain a colon.
|
||||
"""
|
||||
if self.pattern.name is not None and ":" in self.pattern.name:
|
||||
warning = Warning(
|
||||
"Your URL pattern {} has a name including a ':'. Remove the colon, to "
|
||||
"avoid ambiguous namespace references.".format(self.pattern.describe()),
|
||||
id="urls.W003",
|
||||
)
|
||||
return [warning]
|
||||
else:
|
||||
return []
|
||||
|
||||
def resolve(self, path):
|
||||
match = self.pattern.match(path)
|
||||
if match:
|
||||
new_path, args, kwargs = match
|
||||
# Pass any extra_kwargs as **kwargs.
|
||||
kwargs.update(self.default_args)
|
||||
return ResolverMatch(self.callback, args, kwargs, self.name)
|
||||
return ResolverMatch(self.callback, args, kwargs, self.pattern.name)
|
||||
|
||||
@cached_property
|
||||
def lookup_str(self):
|
||||
|
@ -210,9 +352,9 @@ class RegexURLPattern(LocaleRegexProvider):
|
|||
return callback.__module__ + "." + callback.__qualname__
|
||||
|
||||
|
||||
class RegexURLResolver(LocaleRegexProvider):
|
||||
def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
|
||||
LocaleRegexProvider.__init__(self, regex)
|
||||
class URLResolver:
|
||||
def __init__(self, pattern, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
|
||||
self.pattern = pattern
|
||||
# urlconf_name is the dotted Python path to the module defining
|
||||
# urlpatterns. It may also be an object with an urlpatterns attribute
|
||||
# or urlpatterns itself.
|
||||
|
@ -238,33 +380,17 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
urlconf_repr = repr(self.urlconf_name)
|
||||
return '<%s %s (%s:%s) %s>' % (
|
||||
self.__class__.__name__, urlconf_repr, self.app_name,
|
||||
self.namespace, self.regex.pattern,
|
||||
self.namespace, self.pattern.describe(),
|
||||
)
|
||||
|
||||
def check(self):
|
||||
warnings = self._check_include_trailing_dollar()
|
||||
warnings = []
|
||||
for pattern in self.url_patterns:
|
||||
warnings.extend(check_resolver(pattern))
|
||||
if not warnings:
|
||||
warnings = self._check_pattern_startswith_slash()
|
||||
warnings = self.pattern.check()
|
||||
return warnings
|
||||
|
||||
def _check_include_trailing_dollar(self):
|
||||
"""
|
||||
Check that include is not used with a regex ending with a dollar.
|
||||
"""
|
||||
regex_pattern = self.regex.pattern
|
||||
if regex_pattern.endswith('$') and not regex_pattern.endswith(r'\$'):
|
||||
warning = Warning(
|
||||
"Your URL pattern {} uses include with a regex ending with a '$'. "
|
||||
"Remove the dollar from the regex to avoid problems including "
|
||||
"URLs.".format(self.describe()),
|
||||
id="urls.W001",
|
||||
)
|
||||
return [warning]
|
||||
else:
|
||||
return []
|
||||
|
||||
def _populate(self):
|
||||
# Short-circuit if called recursively in this thread to prevent
|
||||
# infinite recursion. Concurrent threads may call this at the same
|
||||
|
@ -277,46 +403,51 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
namespaces = {}
|
||||
apps = {}
|
||||
language_code = get_language()
|
||||
for pattern in reversed(self.url_patterns):
|
||||
if isinstance(pattern, RegexURLPattern):
|
||||
self._callback_strs.add(pattern.lookup_str)
|
||||
p_pattern = pattern.regex.pattern
|
||||
try:
|
||||
for url_pattern in reversed(self.url_patterns):
|
||||
p_pattern = url_pattern.pattern.regex.pattern
|
||||
if p_pattern.startswith('^'):
|
||||
p_pattern = p_pattern[1:]
|
||||
if isinstance(pattern, RegexURLResolver):
|
||||
if pattern.namespace:
|
||||
namespaces[pattern.namespace] = (p_pattern, pattern)
|
||||
if pattern.app_name:
|
||||
apps.setdefault(pattern.app_name, []).append(pattern.namespace)
|
||||
if isinstance(url_pattern, URLPattern):
|
||||
self._callback_strs.add(url_pattern.lookup_str)
|
||||
bits = normalize(url_pattern.pattern.regex.pattern)
|
||||
lookups.appendlist(
|
||||
url_pattern.callback,
|
||||
(bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
|
||||
)
|
||||
if url_pattern.name is not None:
|
||||
lookups.appendlist(
|
||||
url_pattern.name,
|
||||
(bits, p_pattern, url_pattern.default_args, url_pattern.pattern.converters)
|
||||
)
|
||||
else: # url_pattern is a URLResolver.
|
||||
url_pattern._populate()
|
||||
if url_pattern.app_name:
|
||||
apps.setdefault(url_pattern.app_name, []).append(url_pattern.namespace)
|
||||
namespaces[url_pattern.namespace] = (p_pattern, url_pattern)
|
||||
else:
|
||||
parent_pat = pattern.regex.pattern
|
||||
for name in pattern.reverse_dict:
|
||||
for matches, pat, defaults in pattern.reverse_dict.getlist(name):
|
||||
new_matches = normalize(parent_pat + pat)
|
||||
for name in url_pattern.reverse_dict:
|
||||
for matches, pat, defaults, converters in url_pattern.reverse_dict.getlist(name):
|
||||
new_matches = normalize(p_pattern + pat)
|
||||
lookups.appendlist(
|
||||
name,
|
||||
(
|
||||
new_matches,
|
||||
p_pattern + pat,
|
||||
dict(defaults, **pattern.default_kwargs),
|
||||
dict(defaults, **url_pattern.default_kwargs),
|
||||
dict(self.pattern.converters, **converters)
|
||||
)
|
||||
)
|
||||
for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
|
||||
for namespace, (prefix, sub_pattern) in url_pattern.namespace_dict.items():
|
||||
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
|
||||
for app_name, namespace_list in pattern.app_dict.items():
|
||||
for app_name, namespace_list in url_pattern.app_dict.items():
|
||||
apps.setdefault(app_name, []).extend(namespace_list)
|
||||
if not getattr(pattern._local, 'populating', False):
|
||||
pattern._populate()
|
||||
self._callback_strs.update(pattern._callback_strs)
|
||||
else:
|
||||
bits = normalize(p_pattern)
|
||||
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
|
||||
if pattern.name is not None:
|
||||
lookups.appendlist(pattern.name, (bits, p_pattern, pattern.default_args))
|
||||
self._reverse_dict[language_code] = lookups
|
||||
self._callback_strs.update(url_pattern._callback_strs)
|
||||
self._namespace_dict[language_code] = namespaces
|
||||
self._app_dict[language_code] = apps
|
||||
self._reverse_dict[language_code] = lookups
|
||||
self._populated = True
|
||||
finally:
|
||||
self._local.populating = False
|
||||
|
||||
@property
|
||||
|
@ -348,9 +479,9 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
def resolve(self, path):
|
||||
path = str(path) # path may be a reverse_lazy object
|
||||
tried = []
|
||||
match = self.regex.search(path)
|
||||
match = self.pattern.match(path)
|
||||
if match:
|
||||
new_path = path[match.end():]
|
||||
new_path, args, kwargs = match
|
||||
for pattern in self.url_patterns:
|
||||
try:
|
||||
sub_match = pattern.resolve(new_path)
|
||||
|
@ -363,15 +494,14 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
else:
|
||||
if sub_match:
|
||||
# Merge captured arguments in match with submatch
|
||||
sub_match_dict = dict(match.groupdict(), **self.default_kwargs)
|
||||
sub_match_dict = dict(kwargs, **self.default_kwargs)
|
||||
# Update the sub_match_dict with the kwargs from the sub_match.
|
||||
sub_match_dict.update(sub_match.kwargs)
|
||||
|
||||
# If there are *any* named groups, ignore all non-named groups.
|
||||
# Otherwise, pass all non-named arguments as positional arguments.
|
||||
sub_match_args = sub_match.args
|
||||
if not sub_match_dict:
|
||||
sub_match_args = match.groups() + sub_match.args
|
||||
|
||||
sub_match_args = args + sub_match.args
|
||||
return ResolverMatch(
|
||||
sub_match.func,
|
||||
sub_match_args,
|
||||
|
@ -421,20 +551,18 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
|
||||
if args and kwargs:
|
||||
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
|
||||
text_args = [str(v) for v in args]
|
||||
text_kwargs = {k: str(v) for (k, v) in kwargs.items()}
|
||||
|
||||
if not self._populated:
|
||||
self._populate()
|
||||
|
||||
possibilities = self.reverse_dict.getlist(lookup_view)
|
||||
|
||||
for possibility, pattern, defaults in possibilities:
|
||||
for possibility, pattern, defaults, converters in possibilities:
|
||||
for result, params in possibility:
|
||||
if args:
|
||||
if len(args) != len(params):
|
||||
continue
|
||||
candidate_subs = dict(zip(params, text_args))
|
||||
candidate_subs = dict(zip(params, args))
|
||||
else:
|
||||
if set(kwargs).symmetric_difference(params).difference(defaults):
|
||||
continue
|
||||
|
@ -445,16 +573,23 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
break
|
||||
if not matches:
|
||||
continue
|
||||
candidate_subs = text_kwargs
|
||||
candidate_subs = kwargs
|
||||
# Convert the candidate subs to text using Converter.to_url().
|
||||
text_candidate_subs = {}
|
||||
for k, v in candidate_subs.items():
|
||||
if k in converters:
|
||||
text_candidate_subs[k] = converters[k].to_url(v)
|
||||
else:
|
||||
text_candidate_subs[k] = str(v)
|
||||
# WSGI provides decoded URLs, without %xx escapes, and the URL
|
||||
# resolver operates on such URLs. First substitute arguments
|
||||
# without quoting to build a decoded URL and look for a match.
|
||||
# Then, if we have a match, redo the substitution with quoted
|
||||
# arguments in order to return a properly encoded URL.
|
||||
candidate_pat = _prefix.replace('%', '%%') + result
|
||||
if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % candidate_subs):
|
||||
if re.search('^%s%s' % (re.escape(_prefix), pattern), candidate_pat % text_candidate_subs):
|
||||
# safe characters from `pchar` definition of RFC 3986
|
||||
url = quote(candidate_pat % candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
|
||||
url = quote(candidate_pat % text_candidate_subs, safe=RFC3986_SUBDELIMS + '/~:@')
|
||||
# Don't allow construction of scheme relative urls.
|
||||
if url.startswith('//'):
|
||||
url = '/%%2F%s' % url[2:]
|
||||
|
@ -468,7 +603,7 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
else:
|
||||
lookup_view_s = lookup_view
|
||||
|
||||
patterns = [pattern for (possibility, pattern, defaults) in possibilities]
|
||||
patterns = [pattern for (_, pattern, _, _) in possibilities]
|
||||
if patterns:
|
||||
if args:
|
||||
arg_msg = "arguments '%s'" % (args,)
|
||||
|
@ -486,29 +621,3 @@ class RegexURLResolver(LocaleRegexProvider):
|
|||
"a valid view function or pattern name." % {'view': lookup_view_s}
|
||||
)
|
||||
raise NoReverseMatch(msg)
|
||||
|
||||
|
||||
class LocaleRegexURLResolver(RegexURLResolver):
|
||||
"""
|
||||
A URL resolver that always matches the active language code as URL prefix.
|
||||
|
||||
Rather than taking a regex argument, we just override the ``regex``
|
||||
function to always return the active language-code as regex.
|
||||
"""
|
||||
def __init__(
|
||||
self, urlconf_name, default_kwargs=None, app_name=None, namespace=None,
|
||||
prefix_default_language=True,
|
||||
):
|
||||
super().__init__(None, urlconf_name, default_kwargs, app_name, namespace)
|
||||
self.prefix_default_language = prefix_default_language
|
||||
|
||||
@property
|
||||
def regex(self):
|
||||
language_code = get_language() or settings.LANGUAGE_CODE
|
||||
if language_code not in self._regex_dict:
|
||||
if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
|
||||
regex_string = ''
|
||||
else:
|
||||
regex_string = '^%s/' % language_code
|
||||
self._regex_dict[language_code] = re.compile(regex_string)
|
||||
return self._regex_dict[language_code]
|
||||
|
|
|
@ -612,7 +612,7 @@ def _date_from_string(year, year_format, month='', month_format='', day='', day_
|
|||
(only year is mandatory). Raise a 404 for an invalid date.
|
||||
"""
|
||||
format = year_format + delim + month_format + delim + day_format
|
||||
datestr = year + delim + month + delim + day
|
||||
datestr = str(year) + delim + str(month) + delim + str(day)
|
||||
try:
|
||||
return datetime.datetime.strptime(datestr, format).date()
|
||||
except ValueError:
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
{% for pattern in urlpatterns %}
|
||||
<li>
|
||||
{% for pat in pattern %}
|
||||
{{ pat.regex.pattern }}
|
||||
{{ pat.pattern }}
|
||||
{% if forloop.last and pat.name %}[name='{{ pat.name }}']{% endif %}
|
||||
{% endfor %}
|
||||
</li>
|
||||
|
|
|
@ -147,7 +147,7 @@ details on these changes.
|
|||
``django.utils.feedgenerator.RssFeed`` will be removed in favor of
|
||||
``content_type``.
|
||||
|
||||
* The ``app_name`` argument to :func:`~django.conf.urls.include()` will be
|
||||
* The ``app_name`` argument to ``django.conf.urls.include()`` will be
|
||||
removed.
|
||||
|
||||
* Support for passing a 3-tuple as the first argument to ``include()`` will
|
||||
|
@ -786,10 +786,9 @@ details on these changes.
|
|||
``django.contrib.gis.utils`` will be removed.
|
||||
|
||||
* ``django.conf.urls.defaults`` will be removed. The functions
|
||||
:func:`~django.conf.urls.include`, ``patterns()`` and
|
||||
:func:`~django.conf.urls.url` plus :data:`~django.conf.urls.handler404`,
|
||||
:data:`~django.conf.urls.handler500`, are now available through
|
||||
:mod:`django.conf.urls` .
|
||||
``include()``, ``patterns()``, and ``url()``, plus
|
||||
:data:`~django.conf.urls.handler404` and :data:`~django.conf.urls.handler500`
|
||||
are now available through ``django.conf.urls``.
|
||||
|
||||
* The functions ``setup_environ()`` and ``execute_manager()`` will be removed
|
||||
from :mod:`django.core.management`. This also means that the old (pre-1.4)
|
||||
|
|
|
@ -191,31 +191,30 @@ example above:
|
|||
.. snippet::
|
||||
:filename: mysite/news/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^articles/([0-9]{4})/$', views.year_archive),
|
||||
url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
|
||||
url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
|
||||
path('articles/<int:year>/', views.year_archive),
|
||||
path('articles/<int:year>/<int:month>/', views.month_archive),
|
||||
path('articles/<int:year>/<int:month>/<int:pk>/', views.article_detail),
|
||||
]
|
||||
|
||||
The code above maps URLs, as simple :ref:`regular expressions <regex-howto>`,
|
||||
to the location of Python callback functions ("views"). The regular expressions
|
||||
use parenthesis to "capture" values from the URLs. When a user requests a page,
|
||||
Django runs through each pattern, in order, and stops at the first one that
|
||||
matches the requested URL. (If none of them matches, Django calls a
|
||||
special-case 404 view.) This is blazingly fast, because the regular expressions
|
||||
are compiled at load time.
|
||||
The code above maps URL paths to Python callback functions ("views"). The path
|
||||
strings use parameter tags to "capture" values from the URLs. When a user
|
||||
requests a page, Django runs through each path, in order, and stops at the
|
||||
first one that matches the requested URL. (If none of them matches, Django
|
||||
calls a special-case 404 view.) This is blazingly fast, because the paths are
|
||||
compiled into regular expressions at load time.
|
||||
|
||||
Once one of the regexes matches, Django calls the given view, which is a Python
|
||||
function. Each view gets passed a request object -- which contains request
|
||||
metadata -- and the values captured in the regex.
|
||||
Once one of the URL patterns matches, Django calls the given view, which is a
|
||||
Python function. Each view gets passed a request object -- which contains
|
||||
request metadata -- and the values captured in the pattern.
|
||||
|
||||
For example, if a user requested the URL "/articles/2005/05/39323/", Django
|
||||
would call the function ``news.views.article_detail(request,
|
||||
'2005', '05', '39323')``.
|
||||
year=2005, month=5, pk=39323)``.
|
||||
|
||||
Write your views
|
||||
================
|
||||
|
|
|
@ -165,7 +165,7 @@ this. For a small app like polls, this process isn't too difficult.
|
|||
|
||||
2. Include the polls URLconf in your project urls.py like this::
|
||||
|
||||
url(r'^polls/', include('polls.urls')),
|
||||
path('polls/', include('polls.urls')),
|
||||
|
||||
3. Run `python manage.py migrate` to create the polls models.
|
||||
|
||||
|
|
|
@ -274,55 +274,45 @@ In the ``polls/urls.py`` file include the following code:
|
|||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.index, name='index'),
|
||||
path('', views.index, name='index'),
|
||||
]
|
||||
|
||||
The next step is to point the root URLconf at the ``polls.urls`` module. In
|
||||
``mysite/urls.py``, add an import for ``django.conf.urls.include`` and insert
|
||||
an :func:`~django.conf.urls.include` in the ``urlpatterns`` list, so you have:
|
||||
``mysite/urls.py``, add an import for ``django.urls.include`` and insert an
|
||||
:func:`~django.urls.include` in the ``urlpatterns`` list, so you have:
|
||||
|
||||
.. snippet::
|
||||
:filename: mysite/urls.py
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
from django.contrib import admin
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^polls/', include('polls.urls')),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
path('polls/', include('polls.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
The :func:`~django.conf.urls.include` function allows referencing other
|
||||
URLconfs. Note that the regular expressions for the
|
||||
:func:`~django.conf.urls.include` function doesn't have a ``$`` (end-of-string
|
||||
match character) but rather a trailing slash. Whenever Django encounters
|
||||
:func:`~django.conf.urls.include`, it chops off whatever part of the URL
|
||||
matched up to that point and sends the remaining string to the included URLconf
|
||||
for further processing.
|
||||
The :func:`~django.urls.include` function allows referencing other URLconfs.
|
||||
Whenever Django encounters :func:`~django.urls.include`, it chops off whatever
|
||||
part of the URL matched up to that point and sends the remaining string to the
|
||||
included URLconf for further processing.
|
||||
|
||||
The idea behind :func:`~django.conf.urls.include` is to make it easy to
|
||||
The idea behind :func:`~django.urls.include` is to make it easy to
|
||||
plug-and-play URLs. Since polls are in their own URLconf
|
||||
(``polls/urls.py``), they can be placed under "/polls/", or under
|
||||
"/fun_polls/", or under "/content/polls/", or any other path root, and the
|
||||
app will still work.
|
||||
|
||||
.. admonition:: When to use :func:`~django.conf.urls.include()`
|
||||
.. admonition:: When to use :func:`~django.urls.include()`
|
||||
|
||||
You should always use ``include()`` when you include other URL patterns.
|
||||
``admin.site.urls`` is the only exception to this.
|
||||
|
||||
.. admonition:: Doesn't match what you see?
|
||||
|
||||
If you're seeing ``include(admin.site.urls)`` instead of just
|
||||
``admin.site.urls``, you're probably using a version of Django that
|
||||
doesn't match this tutorial version. You'll want to either switch to the
|
||||
older tutorial or the newer Django version.
|
||||
|
||||
You have now wired an ``index`` view into the URLconf. Lets verify it's
|
||||
working, run the following command:
|
||||
|
||||
|
@ -334,56 +324,39 @@ Go to http://localhost:8000/polls/ in your browser, and you should see the
|
|||
text "*Hello, world. You're at the polls index.*", which you defined in the
|
||||
``index`` view.
|
||||
|
||||
The :func:`~django.conf.urls.url` function is passed four arguments, two
|
||||
required: ``regex`` and ``view``, and two optional: ``kwargs``, and ``name``.
|
||||
The :func:`~django.urls.path` function is passed four arguments, two required:
|
||||
``route`` and ``view``, and two optional: ``kwargs``, and ``name``.
|
||||
At this point, it's worth reviewing what these arguments are for.
|
||||
|
||||
:func:`~django.conf.urls.url` argument: regex
|
||||
:func:`~django.urls.path` argument: ``route``
|
||||
---------------------------------------------
|
||||
|
||||
The term "regex" is a commonly used short form meaning "regular expression",
|
||||
which is a syntax for matching patterns in strings, or in this case, url
|
||||
patterns. Django starts at the first regular expression and makes its way down
|
||||
the list, comparing the requested URL against each regular expression until it
|
||||
finds one that matches.
|
||||
``route`` is a string that contains a URL pattern. When processing a request,
|
||||
Django starts at the first pattern in ``urlpatterns`` and makes its way down
|
||||
the list, comparing the requested URL against each pattern until it finds one
|
||||
that matches.
|
||||
|
||||
Note that these regular expressions do not search GET and POST parameters, or
|
||||
the domain name. For example, in a request to
|
||||
``https://www.example.com/myapp/``, the URLconf will look for ``myapp/``. In a
|
||||
request to ``https://www.example.com/myapp/?page=3``, the URLconf will also
|
||||
look for ``myapp/``.
|
||||
Patterns don't search GET and POST parameters, or the domain name. For example,
|
||||
in a request to ``https://www.example.com/myapp/``, the URLconf will look for
|
||||
``myapp/``. In a request to ``https://www.example.com/myapp/?page=3``, the
|
||||
URLconf will also look for ``myapp/``.
|
||||
|
||||
If you need help with regular expressions, see `Wikipedia's entry`_ and the
|
||||
documentation of the :mod:`re` module. Also, the O'Reilly book "Mastering
|
||||
Regular Expressions" by Jeffrey Friedl is fantastic. In practice, however,
|
||||
you don't need to be an expert on regular expressions, as you really only need
|
||||
to know how to capture simple patterns. In fact, complex regexes can have poor
|
||||
lookup performance, so you probably shouldn't rely on the full power of regexes.
|
||||
|
||||
Finally, a performance note: these regular expressions are compiled the first
|
||||
time the URLconf module is loaded. They're super fast (as long as the lookups
|
||||
aren't too complex as noted above).
|
||||
|
||||
.. _Wikipedia's entry: https://en.wikipedia.org/wiki/Regular_expression
|
||||
|
||||
:func:`~django.conf.urls.url` argument: view
|
||||
:func:`~django.urls.path` argument: ``view``
|
||||
--------------------------------------------
|
||||
|
||||
When Django finds a regular expression match, Django calls the specified view
|
||||
function, with an :class:`~django.http.HttpRequest` object as the first
|
||||
argument and any “captured” values from the regular expression as other
|
||||
arguments. If the regex uses simple captures, values are passed as positional
|
||||
arguments; if it uses named captures, values are passed as keyword arguments.
|
||||
We'll give an example of this in a bit.
|
||||
When Django finds a matching pattern, it calls the specified view function with
|
||||
an :class:`~django.http.HttpRequest` object as the first argument and any
|
||||
"captured" values from the route as keyword arguments. We'll give an example
|
||||
of this in a bit.
|
||||
|
||||
:func:`~django.conf.urls.url` argument: kwargs
|
||||
:func:`~django.urls.path` argument: ``kwargs``
|
||||
----------------------------------------------
|
||||
|
||||
Arbitrary keyword arguments can be passed in a dictionary to the target view. We
|
||||
aren't going to use this feature of Django in the tutorial.
|
||||
|
||||
:func:`~django.conf.urls.url` argument: name
|
||||
---------------------------------------------
|
||||
:func:`~django.urls.path` argument: ``name``
|
||||
--------------------------------------------
|
||||
|
||||
Naming your URL lets you refer to it unambiguously from elsewhere in Django,
|
||||
especially from within templates. This powerful feature allows you to make
|
||||
|
|
|
@ -53,10 +53,10 @@ A URL pattern is simply the general form of a URL - for example:
|
|||
``/newsarchive/<year>/<month>/``.
|
||||
|
||||
To get from a URL to a view, Django uses what are known as 'URLconfs'. A
|
||||
URLconf maps URL patterns (described as regular expressions) to views.
|
||||
URLconf maps URL patterns to views.
|
||||
|
||||
This tutorial provides basic instruction in the use of URLconfs, and you can
|
||||
refer to :mod:`django.urls` for more information.
|
||||
refer to :doc:`/topics/http/urls` for more information.
|
||||
|
||||
Writing more views
|
||||
==================
|
||||
|
@ -78,24 +78,24 @@ slightly different, because they take an argument:
|
|||
return HttpResponse("You're voting on question %s." % question_id)
|
||||
|
||||
Wire these new views into the ``polls.urls`` module by adding the following
|
||||
:func:`~django.conf.urls.url` calls:
|
||||
:func:`~django.urls.path` calls:
|
||||
|
||||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# ex: /polls/
|
||||
url(r'^$', views.index, name='index'),
|
||||
path('', views.index, name='index'),
|
||||
# ex: /polls/5/
|
||||
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
|
||||
path('<int:question_id>/', views.detail, name='detail'),
|
||||
# ex: /polls/5/results/
|
||||
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
|
||||
path('<int:question_id>/results/', views.results, name='results'),
|
||||
# ex: /polls/5/vote/
|
||||
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
|
||||
path('<int:question_id>/vote/', views.vote, name='vote'),
|
||||
]
|
||||
|
||||
Take a look in your browser, at "/polls/34/". It'll run the ``detail()``
|
||||
|
@ -106,26 +106,24 @@ placeholder results and voting pages.
|
|||
When somebody requests a page from your website -- say, "/polls/34/", Django
|
||||
will load the ``mysite.urls`` Python module because it's pointed to by the
|
||||
:setting:`ROOT_URLCONF` setting. It finds the variable named ``urlpatterns``
|
||||
and traverses the regular expressions in order. After finding the match at
|
||||
``'^polls/'``, it strips off the matching text (``"polls/"``) and sends the
|
||||
remaining text -- ``"34/"`` -- to the 'polls.urls' URLconf for further
|
||||
processing. There it matches ``r'^(?P<question_id>[0-9]+)/$'``, resulting in a
|
||||
call to the ``detail()`` view like so::
|
||||
and traverses the patterns in order. After finding the match at ``'polls/'``,
|
||||
it strips off the matching text (``"polls/"``) and sends the remaining text --
|
||||
``"34/"`` -- to the 'polls.urls' URLconf for further processing. There it
|
||||
matches ``'<int:question_id>/'``, resulting in a call to the ``detail()`` view
|
||||
like so::
|
||||
|
||||
detail(request=<HttpRequest object>, question_id='34')
|
||||
detail(request=<HttpRequest object>, question_id=34)
|
||||
|
||||
The ``question_id='34'`` part comes from ``(?P<question_id>[0-9]+)``. Using parentheses
|
||||
around a pattern "captures" the text matched by that pattern and sends it as an
|
||||
argument to the view function; ``?P<question_id>`` defines the name that will
|
||||
be used to identify the matched pattern; and ``[0-9]+`` is a regular expression to
|
||||
match a sequence of digits (i.e., a number).
|
||||
The ``question_id=34`` part comes from ``<int:question_id>``. Using angle
|
||||
brackets "captures" part of the URL and sends it as a keyword argument to the
|
||||
view function. The ``:question_id>`` part of the string defines the name that
|
||||
will be used to identify the matched pattern, and the ``<int:`` part is a
|
||||
converter that determines what patterns should match this part of the URL path.
|
||||
|
||||
Because the URL patterns are regular expressions, there really is no limit on
|
||||
what you can do with them. And there's no need to add URL cruft such as
|
||||
``.html`` -- unless you want to, in which case you can do something like
|
||||
this::
|
||||
There's no need to add URL cruft such as ``.html`` -- unless you want to, in
|
||||
which case you can do something like this::
|
||||
|
||||
url(r'^polls/latest\.html$', views.index),
|
||||
path('polls/latest.html', views.index),
|
||||
|
||||
But, don't do that. It's silly.
|
||||
|
||||
|
@ -388,7 +386,7 @@ template, the link was partially hardcoded like this:
|
|||
|
||||
The problem with this hardcoded, tightly-coupled approach is that it becomes
|
||||
challenging to change URLs on projects with a lot of templates. However, since
|
||||
you defined the name argument in the :func:`~django.conf.urls.url` functions in
|
||||
you defined the name argument in the :func:`~django.urls.path` functions in
|
||||
the ``polls.urls`` module, you can remove a reliance on specific URL paths
|
||||
defined in your url configurations by using the ``{% url %}`` template tag:
|
||||
|
||||
|
@ -402,7 +400,7 @@ defined below::
|
|||
|
||||
...
|
||||
# the 'name' value as called by the {% url %} template tag
|
||||
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
|
||||
path('<int:question_id>/', views.detail, name='detail'),
|
||||
...
|
||||
|
||||
If you want to change the URL of the polls detail view to something else,
|
||||
|
@ -411,7 +409,7 @@ template (or templates) you would change it in ``polls/urls.py``::
|
|||
|
||||
...
|
||||
# added the word 'specifics'
|
||||
url(r'^specifics/(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
|
||||
path('specifics/<int:question_id>/', views.detail, name='detail'),
|
||||
...
|
||||
|
||||
Namespacing URL names
|
||||
|
@ -430,16 +428,16 @@ file, go ahead and add an ``app_name`` to set the application namespace:
|
|||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'polls'
|
||||
urlpatterns = [
|
||||
url(r'^$', views.index, name='index'),
|
||||
url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
|
||||
url(r'^(?P<question_id>[0-9]+)/results/$', views.results, name='results'),
|
||||
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
|
||||
path('', views.index, name='index'),
|
||||
path('<int:question_id>/', views.detail, name='detail'),
|
||||
path('<int:question_id>/results/', views.results, name='results'),
|
||||
path('<int:question_id>/vote/', views.vote, name='vote'),
|
||||
]
|
||||
|
||||
Now change your ``polls/index.html`` template from:
|
||||
|
|
|
@ -61,7 +61,7 @@ created a URLconf for the polls application that includes this line:
|
|||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
|
||||
path('<int:question_id>/vote/', views.vote, name='vote'),
|
||||
|
||||
We also created a dummy implementation of the ``vote()`` function. Let's
|
||||
create a real version. Add the following to ``polls/views.py``:
|
||||
|
@ -237,20 +237,20 @@ First, open the ``polls/urls.py`` URLconf and change it like so:
|
|||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'polls'
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^(?P<pk>[0-9]+)/$', views.DetailView.as_view(), name='detail'),
|
||||
url(r'^(?P<pk>[0-9]+)/results/$', views.ResultsView.as_view(), name='results'),
|
||||
url(r'^(?P<question_id>[0-9]+)/vote/$', views.vote, name='vote'),
|
||||
path('', views.IndexView.as_view(), name='index'),
|
||||
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
|
||||
path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
|
||||
path('<int:question_id>/vote/', views.vote, name='vote'),
|
||||
]
|
||||
|
||||
Note that the name of the matched pattern in the regexes of the second and third
|
||||
patterns has changed from ``<question_id>`` to ``<pk>``.
|
||||
Note that the name of the matched pattern in the path strings of the second and
|
||||
third patterns has changed from ``<question_id>`` to ``<pk>``.
|
||||
|
||||
Amend views
|
||||
-----------
|
||||
|
|
|
@ -444,18 +444,18 @@ URLs
|
|||
The following checks are performed on your URL configuration:
|
||||
|
||||
* **urls.W001**: Your URL pattern ``<pattern>`` uses
|
||||
:func:`~django.conf.urls.include` with a ``regex`` ending with a
|
||||
``$``. Remove the dollar from the ``regex`` to avoid problems
|
||||
including URLs.
|
||||
* **urls.W002**: Your URL pattern ``<pattern>`` has a ``regex``
|
||||
beginning with a ``/``. Remove this slash as it is unnecessary.
|
||||
If this pattern is targeted in an :func:`~django.conf.urls.include`, ensure
|
||||
the :func:`~django.conf.urls.include` pattern has a trailing ``/``.
|
||||
:func:`~django.urls.include` with a ``route`` ending with a ``$``. Remove the
|
||||
dollar from the ``route`` to avoid problems including URLs.
|
||||
* **urls.W002**: Your URL pattern ``<pattern>`` has a ``route`` beginning with
|
||||
a ``/``. Remove this slash as it is unnecessary. If this pattern is targeted
|
||||
in an :func:`~django.urls.include`, ensure the :func:`~django.urls.include`
|
||||
pattern has a trailing ``/``.
|
||||
* **urls.W003**: Your URL pattern ``<pattern>`` has a ``name``
|
||||
including a ``:``. Remove the colon, to avoid ambiguous namespace
|
||||
references.
|
||||
* **urls.E004**: Your URL pattern ``<pattern>`` is invalid. Ensure that
|
||||
``urlpatterns`` is a list of :func:`~django.conf.urls.url()` instances.
|
||||
``urlpatterns`` is a list of :func:`~django.urls.path` and/or
|
||||
:func:`~django.urls.re_path` instances.
|
||||
* **urls.W005**: URL namespace ``<namespace>`` isn't unique. You may not be
|
||||
able to reverse all URLs in this namespace.
|
||||
* **urls.E006**: The :setting:`MEDIA_URL`/ :setting:`STATIC_URL` setting must
|
||||
|
|
|
@ -40,12 +40,12 @@ MRO is an acronym for Method Resolution Order.
|
|||
|
||||
**Example urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import MyView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^mine/$', MyView.as_view(), name='my-view'),
|
||||
path('mine/', MyView.as_view(), name='my-view'),
|
||||
]
|
||||
|
||||
**Attributes**
|
||||
|
@ -144,12 +144,12 @@ MRO is an acronym for Method Resolution Order.
|
|||
|
||||
**Example urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import HomePageView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', HomePageView.as_view(), name='home'),
|
||||
path('', HomePageView.as_view(), name='home'),
|
||||
]
|
||||
|
||||
**Context**
|
||||
|
@ -208,15 +208,15 @@ MRO is an acronym for Method Resolution Order.
|
|||
|
||||
**Example urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from article.views import ArticleCounterRedirectView, ArticleDetail
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^counter/(?P<pk>[0-9]+)/$', ArticleCounterRedirectView.as_view(), name='article-counter'),
|
||||
url(r'^details/(?P<pk>[0-9]+)/$', ArticleDetail.as_view(), name='article-detail'),
|
||||
url(r'^go-to-django/$', RedirectView.as_view(url='https://djangoproject.com'), name='go-to-django'),
|
||||
path('counter/<int:pk>/', ArticleCounterRedirectView.as_view(), name='article-counter'),
|
||||
path('details/<int:pk>/', ArticleDetail.as_view(), name='article-detail'),
|
||||
path('go-to-django/', RedirectView.as_view(url='https://djangoproject.com'), name='go-to-django'),
|
||||
]
|
||||
|
||||
**Attributes**
|
||||
|
|
|
@ -63,13 +63,13 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from django.views.generic.dates import ArchiveIndexView
|
||||
|
||||
from myapp.models import Article
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^archive/$',
|
||||
path('archive/',
|
||||
ArchiveIndexView.as_view(model=Article, date_field="pub_date"),
|
||||
name="article_archive"),
|
||||
]
|
||||
|
@ -162,12 +162,12 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import ArticleYearArchiveView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<year>[0-9]{4})/$',
|
||||
path('<int:year>/',
|
||||
ArticleYearArchiveView.as_view(),
|
||||
name="article_year_archive"),
|
||||
]
|
||||
|
@ -254,19 +254,19 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import ArticleMonthArchiveView
|
||||
|
||||
urlpatterns = [
|
||||
# Example: /2012/aug/
|
||||
url(r'^(?P<year>[0-9]{4})/(?P<month>[-\w]+)/$',
|
||||
ArticleMonthArchiveView.as_view(),
|
||||
name="archive_month"),
|
||||
# Example: /2012/08/
|
||||
url(r'^(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$',
|
||||
path('<int:year>/<int:month>/',
|
||||
ArticleMonthArchiveView.as_view(month_format='%m'),
|
||||
name="archive_month_numeric"),
|
||||
# Example: /2012/aug/
|
||||
path('<int:year>/<str:month>/',
|
||||
ArticleMonthArchiveView.as_view(),
|
||||
name="archive_month"),
|
||||
]
|
||||
|
||||
**Example myapp/article_archive_month.html**:
|
||||
|
@ -356,13 +356,13 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import ArticleWeekArchiveView
|
||||
|
||||
urlpatterns = [
|
||||
# Example: /2012/week/23/
|
||||
url(r'^(?P<year>[0-9]{4})/week/(?P<week>[0-9]+)/$',
|
||||
path('<int:year>/week/<int:week>/',
|
||||
ArticleWeekArchiveView.as_view(),
|
||||
name="archive_week"),
|
||||
]
|
||||
|
@ -468,13 +468,13 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import ArticleDayArchiveView
|
||||
|
||||
urlpatterns = [
|
||||
# Example: /2012/nov/10/
|
||||
url(r'^(?P<year>[0-9]{4})/(?P<month>[-\w]+)/(?P<day>[0-9]+)/$',
|
||||
path('<int:year>/<str:month>/<int:day>/',
|
||||
ArticleDayArchiveView.as_view(),
|
||||
name="archive_day"),
|
||||
]
|
||||
|
@ -541,12 +541,12 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.views import ArticleTodayArchiveView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^today/$',
|
||||
path('today/',
|
||||
ArticleTodayArchiveView.as_view(),
|
||||
name="archive_today"),
|
||||
]
|
||||
|
@ -591,11 +591,11 @@ views for displaying drilldown pages for date-based data.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from django.views.generic.dates import DateDetailView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<year>[0-9]{4})/(?P<month>[-\w]+)/(?P<day>[0-9]+)/(?P<pk>[0-9]+)/$',
|
||||
path('<int:year>/<str:month>/<int:day>/<int:pk>/',
|
||||
DateDetailView.as_view(model=Article, date_field="pub_date"),
|
||||
name="archive_date_detail"),
|
||||
]
|
||||
|
|
|
@ -54,12 +54,12 @@ many projects they are typically the most commonly used views.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from article.views import ArticleDetailView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<slug>[-\w]+)/$', ArticleDetailView.as_view(), name='article-detail'),
|
||||
path('<slug>/', ArticleDetailView.as_view(), name='article-detail'),
|
||||
]
|
||||
|
||||
**Example myapp/article_detail.html**:
|
||||
|
@ -123,12 +123,12 @@ many projects they are typically the most commonly used views.
|
|||
|
||||
**Example myapp/urls.py**::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from article.views import ArticleListView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', ArticleListView.as_view(), name='article-list'),
|
||||
path('', ArticleListView.as_view(), name='article-list'),
|
||||
]
|
||||
|
||||
**Example myapp/article_list.html**:
|
||||
|
|
|
@ -26,7 +26,7 @@ A class-based view is deployed into a URL pattern using the
|
|||
:meth:`~django.views.generic.base.View.as_view()` classmethod::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^view/$', MyView.as_view(size=42)),
|
||||
path('view/', MyView.as_view(size=42)),
|
||||
]
|
||||
|
||||
.. admonition:: Thread safety with view arguments
|
||||
|
|
|
@ -15,7 +15,7 @@ Multiple object mixins
|
|||
* Use the ``page`` parameter in the URLconf. For example, this is what
|
||||
your URLconf might look like::
|
||||
|
||||
url(r'^objects/page(?P<page>[0-9]+)/$', PaginatedView.as_view()),
|
||||
path('objects/page<int:page>/', PaginatedView.as_view()),
|
||||
|
||||
* Pass the page number via the ``page`` query-string parameter. For
|
||||
example, a URL would look like this::
|
||||
|
|
|
@ -19,9 +19,9 @@ To activate the :mod:`~django.contrib.admindocs`, you will need to do
|
|||
the following:
|
||||
|
||||
* Add :mod:`django.contrib.admindocs` to your :setting:`INSTALLED_APPS`.
|
||||
* Add ``url(r'^admin/doc/', include('django.contrib.admindocs.urls'))`` to
|
||||
* Add ``path('admin/doc/', include('django.contrib.admindocs.urls'))`` to
|
||||
your ``urlpatterns``. Make sure it's included *before* the
|
||||
``r'^admin/'`` entry, so that requests to ``/admin/doc/`` don't get
|
||||
``'admin/'`` entry, so that requests to ``/admin/doc/`` don't get
|
||||
handled by the latter entry.
|
||||
* Install the docutils Python module (http://docutils.sf.net/).
|
||||
* **Optional:** Using the admindocs bookmarklets requires
|
||||
|
|
|
@ -1587,11 +1587,15 @@ templates used by the :class:`ModelAdmin` views:
|
|||
that ModelAdmin in the same way as a URLconf. Therefore you can extend
|
||||
them as documented in :doc:`/topics/http/urls`::
|
||||
|
||||
from django.contrib import admin
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import path
|
||||
|
||||
class MyModelAdmin(admin.ModelAdmin):
|
||||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
my_urls = [
|
||||
url(r'^my_view/$', self.my_view),
|
||||
path('my_view/', self.my_view),
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
|
@ -1643,13 +1647,13 @@ templates used by the :class:`ModelAdmin` views:
|
|||
def get_urls(self):
|
||||
urls = super().get_urls()
|
||||
my_urls = [
|
||||
url(r'^my_view/$', self.admin_site.admin_view(self.my_view))
|
||||
path('my_view/', self.admin_site.admin_view(self.my_view))
|
||||
]
|
||||
return my_urls + urls
|
||||
|
||||
Notice the wrapped view in the fifth line above::
|
||||
|
||||
url(r'^my_view/$', self.admin_site.admin_view(self.my_view))
|
||||
path('my_view/', self.admin_site.admin_view(self.my_view))
|
||||
|
||||
This wrapping will protect ``self.my_view`` from unauthorized access and
|
||||
will apply the :func:`django.views.decorators.cache.never_cache` decorator to
|
||||
|
@ -1659,7 +1663,7 @@ templates used by the :class:`ModelAdmin` views:
|
|||
performed, you can pass a ``cacheable=True`` argument to
|
||||
``AdminSite.admin_view()``::
|
||||
|
||||
url(r'^my_view/$', self.admin_site.admin_view(self.my_view, cacheable=True))
|
||||
path('my_view/', self.admin_site.admin_view(self.my_view, cacheable=True))
|
||||
|
||||
``ModelAdmin`` views have ``model_admin`` attributes. Other
|
||||
``AdminSite`` views have ``admin_site`` attributes.
|
||||
|
@ -2767,17 +2771,17 @@ Hooking ``AdminSite`` instances into your URLconf
|
|||
The last step in setting up the Django admin is to hook your ``AdminSite``
|
||||
instance into your URLconf. Do this by pointing a given URL at the
|
||||
``AdminSite.urls`` method. It is not necessary to use
|
||||
:func:`~django.conf.urls.include()`.
|
||||
:func:`~django.urls.include()`.
|
||||
|
||||
In this example, we register the default ``AdminSite`` instance
|
||||
``django.contrib.admin.site`` at the URL ``/admin/`` ::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
.. _customizing-adminsite:
|
||||
|
@ -2809,12 +2813,12 @@ update :file:`myproject/urls.py` to reference your :class:`AdminSite` subclass.
|
|||
.. snippet::
|
||||
:filename: myproject/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from myapp.admin import admin_site
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^myadmin/', admin_site.urls),
|
||||
path('myadmin/', admin_site.urls),
|
||||
]
|
||||
|
||||
Note that you may not want autodiscovery of ``admin`` modules when using your
|
||||
|
@ -2838,12 +2842,12 @@ separate versions of the admin site -- using the ``AdminSite`` instances
|
|||
respectively::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from myproject.admin import basic_site, advanced_site
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^basic-admin/', basic_site.urls),
|
||||
url(r'^advanced-admin/', advanced_site.urls),
|
||||
path('basic-admin/', basic_site.urls),
|
||||
path('advanced-admin/', advanced_site.urls),
|
||||
]
|
||||
|
||||
``AdminSite`` instances take a single argument to their constructor, their
|
||||
|
@ -2879,23 +2883,23 @@ your URLconf. Specifically, add these four patterns::
|
|||
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
url(
|
||||
r'^admin/password_reset/$',
|
||||
path(
|
||||
'admin/password_reset/',
|
||||
auth_views.PasswordResetView.as_view(),
|
||||
name='admin_password_reset',
|
||||
),
|
||||
url(
|
||||
r'^admin/password_reset/done/$',
|
||||
path(
|
||||
'admin/password_reset/done/',
|
||||
auth_views.PasswordResetDoneView.as_view(),
|
||||
name='password_reset_done',
|
||||
),
|
||||
url(
|
||||
r'^reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>.+)/$',
|
||||
path(
|
||||
'reset/<uidb64>/<token>/',
|
||||
auth_views.PasswordResetConfirmView.as_view(),
|
||||
name='password_reset_confirm',
|
||||
),
|
||||
url(
|
||||
r'^reset/done/$',
|
||||
path(
|
||||
'reset/done/',
|
||||
auth_views.PasswordResetCompleteView.as_view(),
|
||||
name='password_reset_complete',
|
||||
),
|
||||
|
|
|
@ -47,7 +47,7 @@ Then either:
|
|||
3. Add an entry in your URLconf. For example::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^pages/', include('django.contrib.flatpages.urls')),
|
||||
path('pages/', include('django.contrib.flatpages.urls')),
|
||||
]
|
||||
|
||||
or:
|
||||
|
@ -74,7 +74,7 @@ There are several ways to include the flat pages in your URLconf. You can
|
|||
dedicate a particular path to flat pages::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^pages/', include('django.contrib.flatpages.urls')),
|
||||
path('pages/', include('django.contrib.flatpages.urls')),
|
||||
]
|
||||
|
||||
You can also set it up as a "catchall" pattern. In this case, it is important
|
||||
|
@ -84,7 +84,7 @@ to place the pattern at the end of the other urlpatterns::
|
|||
|
||||
# Your other patterns here
|
||||
urlpatterns += [
|
||||
url(r'^(?P<url>.*/)$', views.flatpage),
|
||||
path('<path:url>', views.flatpage),
|
||||
]
|
||||
|
||||
.. warning::
|
||||
|
@ -100,8 +100,8 @@ tag::
|
|||
from django.contrib.flatpages import views
|
||||
|
||||
urlpatterns += [
|
||||
url(r'^about-us/$', views.flatpage, {'url': '/about-us/'}, name='about'),
|
||||
url(r'^license/$', views.flatpage, {'url': '/license/'}, name='license'),
|
||||
path('about-us/', views.flatpage, {'url': '/about-us/'}, name='about'),
|
||||
path('license/', views.flatpage, {'url': '/license/'}, name='license'),
|
||||
]
|
||||
|
||||
Using the middleware
|
||||
|
@ -345,15 +345,15 @@ Example
|
|||
|
||||
Here's an example of a URLconf using :class:`FlatPageSitemap`::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.flatpages.sitemaps import FlatPageSitemap
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
|
||||
# the sitemap
|
||||
url(r'^sitemap\.xml$', sitemap,
|
||||
path('sitemap.xml', sitemap,
|
||||
{'sitemaps': {'flatpages': FlatPageSitemap}},
|
||||
name='django.contrib.sitemaps.views.sitemap'),
|
||||
]
|
||||
|
|
|
@ -716,11 +716,11 @@ Let's dive right in. Create a file called ``admin.py`` inside the
|
|||
|
||||
Next, edit your ``urls.py`` in the ``geodjango`` application folder as follows::
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib.gis import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
||||
|
||||
Create an admin user:
|
||||
|
|
|
@ -56,7 +56,7 @@ To activate sitemap generation on your Django site, add this line to your
|
|||
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
|
||||
url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
|
||||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps},
|
||||
name='django.contrib.sitemaps.views.sitemap')
|
||||
|
||||
This tells Django to build a sitemap when a client accesses :file:`/sitemap.xml`.
|
||||
|
@ -283,9 +283,9 @@ Example
|
|||
Here's an example of a :doc:`URLconf </topics/http/urls>` using
|
||||
:class:`GenericSitemap`::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.sitemaps import GenericSitemap
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.urls import path
|
||||
from blog.models import Entry
|
||||
|
||||
info_dict = {
|
||||
|
@ -298,7 +298,7 @@ Here's an example of a :doc:`URLconf </topics/http/urls>` using
|
|||
# ...
|
||||
|
||||
# the sitemap
|
||||
url(r'^sitemap\.xml$', sitemap,
|
||||
path('sitemap.xml', sitemap,
|
||||
{'sitemaps': {'blog': GenericSitemap(info_dict, priority=0.6)}},
|
||||
name='django.contrib.sitemaps.views.sitemap'),
|
||||
]
|
||||
|
@ -328,8 +328,8 @@ the ``location`` method of the sitemap. For example::
|
|||
return reverse(item)
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.contrib.sitemaps.views import sitemap
|
||||
from django.urls import path
|
||||
|
||||
from .sitemaps import StaticViewSitemap
|
||||
from . import views
|
||||
|
@ -339,11 +339,11 @@ the ``location`` method of the sitemap. For example::
|
|||
}
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.main, name='main'),
|
||||
url(r'^about/$', views.about, name='about'),
|
||||
url(r'^license/$', views.license, name='license'),
|
||||
path('', views.main, name='main'),
|
||||
path('about/', views.about, name='about'),
|
||||
path('license/', views.license, name='license'),
|
||||
# ...
|
||||
url(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
|
||||
path('sitemap.xml', sitemap, {'sitemaps': sitemaps},
|
||||
name='django.contrib.sitemaps.views.sitemap')
|
||||
]
|
||||
|
||||
|
@ -367,8 +367,8 @@ Here's what the relevant URLconf lines would look like for the example above::
|
|||
from django.contrib.sitemaps import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sitemap\.xml$', views.index, {'sitemaps': sitemaps}),
|
||||
url(r'^sitemap-(?P<section>.+)\.xml$', views.sitemap, {'sitemaps': sitemaps},
|
||||
path('sitemap.xml', views.index, {'sitemaps': sitemaps}),
|
||||
path('sitemap-<section>.xml', views.sitemap, {'sitemaps': sitemaps},
|
||||
name='django.contrib.sitemaps.views.sitemap'),
|
||||
]
|
||||
|
||||
|
@ -389,10 +389,10 @@ with a caching decorator -- you must name your sitemap view and pass
|
|||
from django.views.decorators.cache import cache_page
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sitemap\.xml$',
|
||||
path('sitemap.xml',
|
||||
cache_page(86400)(sitemaps_views.index),
|
||||
{'sitemaps': sitemaps, 'sitemap_url_name': 'sitemaps'}),
|
||||
url(r'^sitemap-(?P<section>.+)\.xml$',
|
||||
path('sitemap-<section>.xml',
|
||||
cache_page(86400)(sitemaps_views.sitemap),
|
||||
{'sitemaps': sitemaps}, name='sitemaps'),
|
||||
]
|
||||
|
@ -408,11 +408,11 @@ parameter to the ``sitemap`` and ``index`` views via the URLconf::
|
|||
from django.contrib.sitemaps import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^custom-sitemap\.xml$', views.index, {
|
||||
path('custom-sitemap.xml', views.index, {
|
||||
'sitemaps': sitemaps,
|
||||
'template_name': 'custom_sitemap.html'
|
||||
}),
|
||||
url(r'^custom-sitemap-(?P<section>.+)\.xml$', views.sitemap, {
|
||||
path('custom-sitemap-<section>.xml', views.sitemap, {
|
||||
'sitemaps': sitemaps,
|
||||
'template_name': 'custom_sitemap.html'
|
||||
}, name='django.contrib.sitemaps.views.sitemap'),
|
||||
|
|
|
@ -462,10 +462,11 @@ primary URL configuration::
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import views
|
||||
from django.urls import re_path
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
url(r'^static/(?P<path>.*)$', views.serve),
|
||||
re_path(r'^static/(?P<path>.*)$', views.serve),
|
||||
]
|
||||
|
||||
Note, the beginning of the pattern (``r'^static/'``) should be your
|
||||
|
|
|
@ -77,12 +77,12 @@ a feed of the latest five news items::
|
|||
To connect a URL to this feed, put an instance of the Feed object in
|
||||
your :doc:`URLconf </topics/http/urls>`. For example::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from myproject.feeds import LatestEntriesFeed
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'^latest/feed/$', LatestEntriesFeed()),
|
||||
path('latest/feed/', LatestEntriesFeed()),
|
||||
# ...
|
||||
]
|
||||
|
||||
|
@ -217,7 +217,7 @@ The police beat feeds could be accessible via URLs like this:
|
|||
|
||||
These can be matched with a :doc:`URLconf </topics/http/urls>` line such as::
|
||||
|
||||
url(r'^beats/(?P<beat_id>[0-9]+)/rss/$', BeatFeed()),
|
||||
path('beats/<int:beat_id>/rss/', BeatFeed()),
|
||||
|
||||
Like a view, the arguments in the URL are passed to the ``get_object()``
|
||||
method along with the request object.
|
||||
|
@ -366,13 +366,13 @@ Here's a full example::
|
|||
|
||||
And the accompanying URLconf::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'^sitenews/rss/$', RssSiteNewsFeed()),
|
||||
url(r'^sitenews/atom/$', AtomSiteNewsFeed()),
|
||||
path('sitenews/rss/', RssSiteNewsFeed()),
|
||||
path('sitenews/atom/', AtomSiteNewsFeed()),
|
||||
# ...
|
||||
]
|
||||
|
||||
|
|
|
@ -1115,11 +1115,11 @@ hard-code URLs in your templates::
|
|||
|
||||
{% url 'some-url-name' v1 v2 %}
|
||||
|
||||
The first argument is a :func:`~django.conf.urls.url` ``name``. It can be a
|
||||
quoted literal or any other context variable. Additional arguments are optional
|
||||
and should be space-separated values that will be used as arguments in the URL.
|
||||
The example above shows passing positional arguments. Alternatively you may
|
||||
use keyword syntax::
|
||||
The first argument is a :ref:`URL pattern name <naming-url-patterns>`. It can
|
||||
be a quoted literal or any other context variable. Additional arguments are
|
||||
optional and should be space-separated values that will be used as arguments in
|
||||
the URL. The example above shows passing positional arguments. Alternatively
|
||||
you may use keyword syntax::
|
||||
|
||||
{% url 'some-url-name' arg1=v1 arg2=v2 %}
|
||||
|
||||
|
@ -1132,14 +1132,14 @@ takes a client ID (here, ``client()`` is a method inside the views file
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
('^client/([0-9]+)/$', app_views.client, name='app-views-client')
|
||||
path('client/<int:id>/', app_views.client, name='app-views-client')
|
||||
|
||||
If this app's URLconf is included into the project's URLconf under a path
|
||||
such as this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
('^clients/', include('project_name.app_name.urls'))
|
||||
path('clients/', include('project_name.app_name.urls'))
|
||||
|
||||
...then, in a template, you can create a link to this view like this::
|
||||
|
||||
|
@ -1179,8 +1179,8 @@ by the context as to the current application.
|
|||
|
||||
.. warning::
|
||||
|
||||
Don't forget to put quotes around the :func:`~django.conf.urls.url`
|
||||
``name``, otherwise the value will be interpreted as a context variable!
|
||||
Don't forget to put quotes around the URL pattern ``name``, otherwise the
|
||||
value will be interpreted as a context variable!
|
||||
|
||||
.. templatetag:: verbatim
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ callable view object. For example, given the following ``url``::
|
|||
|
||||
from news import views
|
||||
|
||||
url(r'^archive/$', views.archive, name='news-archive')
|
||||
path('archive/', views.archive, name='news-archive')
|
||||
|
||||
you can use any of the following to reverse the URL::
|
||||
|
||||
|
|
|
@ -5,7 +5,79 @@
|
|||
.. module:: django.urls.conf
|
||||
:synopsis: Functions for use in URLconfs.
|
||||
|
||||
.. currentmodule:: django.conf.urls
|
||||
.. currentmodule:: django.urls
|
||||
|
||||
``path()``
|
||||
==========
|
||||
|
||||
.. function:: path(route, view, kwargs=None, name=None)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Returns an element for inclusion in ``urlpatterns``. For example::
|
||||
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('index/', views.index, name='main-view'),
|
||||
path('bio/<username>/', views.bio, name='bio'),
|
||||
path('articles/<slug:title>/', views.article, name='article-detail'),
|
||||
path('articles/<slug:title>/<int:section>/', views.section, name='article-section'),
|
||||
path('weblog/', include('blog.urls')),
|
||||
...
|
||||
]
|
||||
|
||||
The ``route`` argument should be a string or
|
||||
:func:`~django.utils.translation.gettext_lazy()` (see
|
||||
:ref:`translating-urlpatterns`) that contains a URL pattern. The string
|
||||
may contain angle brackets (like ``<username>`` above) to capture part of the
|
||||
URL and send it as a keyword argument to the view. The angle brackets may
|
||||
include a converter specification (like the ``int`` part of ``<int:section>``)
|
||||
which limits the characters matched and may also change the type of the
|
||||
variable passed to the view. For example, ``<int:section>`` matches a string
|
||||
of decimal digits and converts the value to an ``int``. See
|
||||
:ref:`how-django-processes-a-request` for more details.
|
||||
|
||||
The ``view`` argument is a view function or the result of
|
||||
:meth:`~django.views.generic.base.View.as_view` for class-based views. It can
|
||||
also be an :func:`django.urls.include`.
|
||||
|
||||
The ``kwargs`` argument allows you to pass additional arguments to the view
|
||||
function or method. See :ref:`views-extra-options` for an example.
|
||||
|
||||
See :ref:`Naming URL patterns <naming-url-patterns>` for why the ``name``
|
||||
argument is useful.
|
||||
|
||||
``re_path()``
|
||||
=============
|
||||
|
||||
.. function:: re_path(route, view, kwargs=None, name=None)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Returns an element for inclusion in ``urlpatterns``. For example::
|
||||
|
||||
from django.urls import include, re_path
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'^index/$', views.index, name='index'),
|
||||
re_path(r'^bio/(?P<username>\w+)/$', views.bio, name='bio'),
|
||||
re_path(r'^weblog/', include('blog.urls')),
|
||||
...
|
||||
]
|
||||
|
||||
The ``route`` argument should be a string or
|
||||
:func:`~django.utils.translation.gettext_lazy()` (see
|
||||
:ref:`translating-urlpatterns`) that contains a regular expression compatible
|
||||
with Python's :py:mod:`re` module. Strings typically use raw string syntax
|
||||
(``r''``) so that they can contain sequences like ``\d`` without the need to
|
||||
escape the backslash with another backslash. When a match is made, captured
|
||||
groups from the regular expression are passed to the view -- as named arguments
|
||||
if the groups are named, and as positional arguments otherwise. The values are
|
||||
passed as strings, without any type conversion.
|
||||
|
||||
The ``view``, ``kwargs`` and ``name`` arguments are the same as for
|
||||
:func:`~django.urls.path()`.
|
||||
|
||||
``include()``
|
||||
=============
|
||||
|
@ -30,7 +102,7 @@
|
|||
:arg module: URLconf module (or module name)
|
||||
:arg namespace: Instance namespace for the URL entries being included
|
||||
:type namespace: string
|
||||
:arg pattern_list: Iterable of :func:`django.conf.urls.url` instances
|
||||
:arg pattern_list: Iterable of :func:`~django.urls.path` and/or :func:`~django.urls.re_path` instances.
|
||||
:arg app_namespace: Application namespace for the URL entries being included
|
||||
:type app_namespace: string
|
||||
:arg instance_namespace: Instance namespace for the URL entries being included
|
||||
|
@ -43,6 +115,20 @@ See :ref:`including-other-urlconfs` and :ref:`namespaces-and-include`.
|
|||
In older versions, this function is located in ``django.conf.urls``. The
|
||||
old location still works for backwards compatibility.
|
||||
|
||||
``register_converter()``
|
||||
========================
|
||||
|
||||
.. function:: register_converter(converter, type_name)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
The function for registering a converter for use in :func:`~django.urls.path()`
|
||||
``route``\s.
|
||||
|
||||
The ``converter`` argument is a converter class, and ``type_name`` is the
|
||||
converter name to use in path patterns. See
|
||||
:ref:`registering-custom-path-converters` for an example.
|
||||
|
||||
==================================================
|
||||
``django.conf.urls`` functions for use in URLconfs
|
||||
==================================================
|
||||
|
@ -68,32 +154,8 @@ Helper function to return a URL pattern for serving files in debug mode::
|
|||
|
||||
.. function:: url(regex, view, kwargs=None, name=None)
|
||||
|
||||
``urlpatterns`` should be a list of ``url()`` instances. For example::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^index/$', index_view, name='main-view'),
|
||||
url(r'^weblog/', include('blog.urls')),
|
||||
...
|
||||
]
|
||||
|
||||
The ``regex`` parameter should be a string or
|
||||
:func:`~django.utils.translation.gettext_lazy()` (see
|
||||
:ref:`translating-urlpatterns`) that contains a regular expression compatible
|
||||
with Python's :py:mod:`re` module. Strings typically use raw string syntax
|
||||
(``r''``) so that they can contain sequences like ``\d`` without the need to
|
||||
escape the backslash with another backslash.
|
||||
|
||||
The ``view`` parameter is a view function or the result of
|
||||
:meth:`~django.views.generic.base.View.as_view` for class-based views. It can
|
||||
also be an :func:`include`.
|
||||
|
||||
The ``kwargs`` parameter allows you to pass additional arguments to the view
|
||||
function or method. See :ref:`views-extra-options` for an example.
|
||||
|
||||
See :ref:`Naming URL patterns <naming-url-patterns>` for why the ``name``
|
||||
parameter is useful.
|
||||
This function is an alias to :func:`django.urls.re_path()`. It's likely to be
|
||||
deprecated in a future release.
|
||||
|
||||
``handler400``
|
||||
==============
|
||||
|
|
|
@ -823,7 +823,7 @@ appropriate entities.
|
|||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
urlpatterns = [
|
||||
url(format_lazy(r'{person}/(?P<pk>\d+)/$', person=pgettext_lazy('URL', 'person')),
|
||||
path(format_lazy('{person}/<int:pk>/', person=pgettext_lazy('URL', 'person')),
|
||||
PersonDetailView.as_view()),
|
||||
]
|
||||
|
||||
|
|
|
@ -26,13 +26,14 @@ built-in handling for user-uploaded files, but you can have Django serve your
|
|||
:setting:`MEDIA_ROOT` by appending something like this to your URLconf::
|
||||
|
||||
from django.conf import settings
|
||||
from django.urls import re_path
|
||||
from django.views.static import serve
|
||||
|
||||
# ... the rest of your URLconf goes here ...
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += [
|
||||
url(r'^media/(?P<path>.*)$', serve, {
|
||||
re_path(r'^media/(?P<path>.*)$', serve, {
|
||||
'document_root': settings.MEDIA_ROOT,
|
||||
}),
|
||||
]
|
||||
|
|
|
@ -1229,9 +1229,8 @@ disable this backward-compatibility shim and deprecation warning.
|
|||
``django.conf.urls.defaults``
|
||||
-----------------------------
|
||||
|
||||
Until Django 1.3, the functions :func:`~django.conf.urls.include`,
|
||||
``patterns()`` and :func:`~django.conf.urls.url` plus
|
||||
:data:`~django.conf.urls.handler404`, :data:`~django.conf.urls.handler500`
|
||||
Until Django 1.3, the ``include()``, ``patterns()``, and ``url()`` functions,
|
||||
plus :data:`~django.conf.urls.handler404` and :data:`~django.conf.urls.handler500`
|
||||
were located in a ``django.conf.urls.defaults`` module.
|
||||
|
||||
In Django 1.4, they live in :mod:`django.conf.urls`.
|
||||
|
|
|
@ -657,7 +657,7 @@ URLs
|
|||
* The application namespace can now be set using an ``app_name`` attribute
|
||||
on the included module or object. It can also be set by passing a 2-tuple
|
||||
of (<list of patterns>, <application namespace>) as the first argument to
|
||||
:func:`~django.conf.urls.include`.
|
||||
``include()``.
|
||||
|
||||
* System checks have been added for common URL pattern mistakes.
|
||||
|
||||
|
@ -1233,8 +1233,8 @@ extending. This change necessitated a new template loader API. The old
|
|||
Details about the new API can be found :ref:`in the template loader
|
||||
documentation <custom-template-loaders>`.
|
||||
|
||||
Passing a 3-tuple or an ``app_name`` to :func:`~django.conf.urls.include()`
|
||||
---------------------------------------------------------------------------
|
||||
Passing a 3-tuple or an ``app_name`` to ``include()``
|
||||
-----------------------------------------------------
|
||||
|
||||
The instance namespace part of passing a tuple as an argument to ``include()``
|
||||
has been replaced by passing the ``namespace`` argument to ``include()``. For
|
||||
|
|
|
@ -46,6 +46,32 @@ be compatible with Django 2.0.
|
|||
What's new in Django 2.0
|
||||
========================
|
||||
|
||||
Simplified URL routing syntax
|
||||
-----------------------------
|
||||
|
||||
The new :func:`django.urls.path()` function allows a simpler, more readable URL
|
||||
routing syntax. For example, this example from previous Django releases::
|
||||
|
||||
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
|
||||
|
||||
could be written as::
|
||||
|
||||
path('articles/<int:year>/', views.year_archive),
|
||||
|
||||
The new syntax supports type coercion of URL parameters. In the example, the
|
||||
view will receive the ``year`` keyword argument as an integer rather than as
|
||||
a string.
|
||||
|
||||
The ``django.conf.urls.url()`` function from previous versions is now available
|
||||
as :func:`django.urls.re_path`, however, the old location remains for backwards
|
||||
compatibility, without an imminent deprecation. The old
|
||||
``django.conf.urls.include()`` function is now importable from ``django.urls``
|
||||
so you can use ``from django.urls import include, path, re_path`` in your
|
||||
URLconfs.
|
||||
|
||||
The :doc:`/topics/http/urls` document is rewritten to feature the new syntax
|
||||
and provide more details.
|
||||
|
||||
Mobile-friendly ``contrib.admin``
|
||||
---------------------------------
|
||||
|
||||
|
|
|
@ -502,7 +502,7 @@ The ``login_required`` decorator
|
|||
|
||||
from django.contrib.auth import views as auth_views
|
||||
|
||||
url(r'^accounts/login/$', auth_views.LoginView.as_view()),
|
||||
path('accounts/login/', auth_views.LoginView.as_view()),
|
||||
|
||||
The :setting:`settings.LOGIN_URL <LOGIN_URL>` also accepts view function
|
||||
names and :ref:`named URL patterns <naming-url-patterns>`. This allows you
|
||||
|
@ -896,7 +896,7 @@ easiest way is to include the provided URLconf in ``django.contrib.auth.urls``
|
|||
in your own URLconf, for example::
|
||||
|
||||
urlpatterns = [
|
||||
url('^', include('django.contrib.auth.urls')),
|
||||
path('', include('django.contrib.auth.urls')),
|
||||
]
|
||||
|
||||
This will include the following URL patterns::
|
||||
|
@ -919,7 +919,7 @@ your URLconf::
|
|||
from django.contrib.auth import views as auth_views
|
||||
|
||||
urlpatterns = [
|
||||
url('^change-password/$', auth_views.PasswordChangeView.as_view()),
|
||||
path('change-password/', auth_views.PasswordChangeView.as_view()),
|
||||
]
|
||||
|
||||
The views have optional arguments you can use to alter the behavior of the
|
||||
|
@ -928,8 +928,8 @@ provide the ``template_name`` argument. A way to do this is to provide keyword
|
|||
arguments in the URLconf, these will be passed on to the view. For example::
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
'^change-password/$',
|
||||
path(
|
||||
'change-password/',
|
||||
auth_views.PasswordChangeView.as_view(template_name='change-password.html'),
|
||||
),
|
||||
]
|
||||
|
@ -1035,7 +1035,7 @@ implementation details see :ref:`using-the-views`.
|
|||
the ``as_view`` method in your URLconf. For example, this URLconf line would
|
||||
use :file:`myapp/login.html` instead::
|
||||
|
||||
url(r'^accounts/login/$', auth_views.LoginView.as_view(template_name='myapp/login.html')),
|
||||
path('accounts/login/', auth_views.LoginView.as_view(template_name='myapp/login.html')),
|
||||
|
||||
You can also specify the name of the ``GET`` field which contains the URL
|
||||
to redirect to after login using ``redirect_field_name``. By default, the
|
||||
|
|
|
@ -591,7 +591,7 @@ multiple URLs point at the same view, each URL will be cached separately.
|
|||
Continuing the ``my_view`` example, if your URLconf looks like this::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^foo/([0-9]{1,2})/$', my_view),
|
||||
path('foo/<int:code>/', my_view),
|
||||
]
|
||||
|
||||
then requests to ``/foo/1/`` and ``/foo/23/`` will be cached separately, as
|
||||
|
@ -637,7 +637,7 @@ Doing so is easy: simply wrap the view function with ``cache_page`` when you
|
|||
refer to it in the URLconf. Here's the old URLconf from earlier::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^foo/([0-9]{1,2})/$', my_view),
|
||||
path('foo/<int:code>/', my_view),
|
||||
]
|
||||
|
||||
Here's the same thing, with ``my_view`` wrapped in ``cache_page``::
|
||||
|
@ -645,7 +645,7 @@ Here's the same thing, with ``my_view`` wrapped in ``cache_page``::
|
|||
from django.views.decorators.cache import cache_page
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^foo/([0-9]{1,2})/$', cache_page(60 * 15)(my_view)),
|
||||
path('foo/<int:code>/', cache_page(60 * 15)(my_view)),
|
||||
]
|
||||
|
||||
.. templatetag:: cache
|
||||
|
|
|
@ -117,11 +117,11 @@ Now we need to define a view::
|
|||
Finally hook that view into your urls::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from books.views import PublisherList
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^publishers/$', PublisherList.as_view()),
|
||||
path('publishers/', PublisherList.as_view()),
|
||||
]
|
||||
|
||||
That's all the Python code we need to write. We still need to write a template,
|
||||
|
@ -332,11 +332,11 @@ various useful things are stored on ``self``; as well as the request
|
|||
Here, we have a URLconf with a single captured group::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from books.views import PublisherBookList
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^books/([\w-]+)/$', PublisherBookList.as_view()),
|
||||
path('books/<publisher>/', PublisherBookList.as_view()),
|
||||
]
|
||||
|
||||
Next, we'll write the ``PublisherBookList`` view itself::
|
||||
|
@ -351,7 +351,7 @@ Next, we'll write the ``PublisherBookList`` view itself::
|
|||
template_name = 'books/books_by_publisher.html'
|
||||
|
||||
def get_queryset(self):
|
||||
self.publisher = get_object_or_404(Publisher, name=self.args[0])
|
||||
self.publisher = get_object_or_404(Publisher, name=self.kwargs['publisher'])
|
||||
return Book.objects.filter(publisher=self.publisher)
|
||||
|
||||
As you can see, it's quite easy to add more logic to the queryset selection;
|
||||
|
@ -398,12 +398,12 @@ updated.
|
|||
First, we'd need to add an author detail bit in the URLconf to point to a
|
||||
custom view::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from books.views import AuthorDetailView
|
||||
|
||||
urlpatterns = [
|
||||
#...
|
||||
url(r'^authors/(?P<pk>[0-9]+)/$', AuthorDetailView.as_view(), name='author-detail'),
|
||||
path('authors/<int:pk>/', AuthorDetailView.as_view(), name='author-detail'),
|
||||
]
|
||||
|
||||
Then we'd write our new view -- ``get_object`` is the method that retrieves the
|
||||
|
|
|
@ -149,14 +149,14 @@ Finally, we hook these new views into the URLconf:
|
|||
.. snippet::
|
||||
:filename: urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from myapp.views import AuthorCreate, AuthorUpdate, AuthorDelete
|
||||
|
||||
urlpatterns = [
|
||||
# ...
|
||||
url(r'author/add/$', AuthorCreate.as_view(), name='author-add'),
|
||||
url(r'author/(?P<pk>[0-9]+)/$', AuthorUpdate.as_view(), name='author-update'),
|
||||
url(r'author/(?P<pk>[0-9]+)/delete/$', AuthorDelete.as_view(), name='author-delete'),
|
||||
path('author/add/', AuthorCreate.as_view(), name='author-add'),
|
||||
path('author/<int:pk>/', AuthorUpdate.as_view(), name='author-update'),
|
||||
path('author/<int:pk>/delete/', AuthorDelete.as_view(), name='author-delete'),
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
|
|
@ -38,11 +38,11 @@ URLconf. If you're only changing a few simple attributes on a class-based view,
|
|||
you can simply pass them into the
|
||||
:meth:`~django.views.generic.base.View.as_view` method call itself::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^about/$', TemplateView.as_view(template_name="about.html")),
|
||||
path('about/', TemplateView.as_view(template_name="about.html")),
|
||||
]
|
||||
|
||||
Any arguments passed to :meth:`~django.views.generic.base.View.as_view` will
|
||||
|
@ -75,11 +75,11 @@ class method instead, which provides a function-like entry to class-based
|
|||
views::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from some_app.views import AboutView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^about/$', AboutView.as_view()),
|
||||
path('about/', AboutView.as_view()),
|
||||
]
|
||||
|
||||
|
||||
|
@ -100,11 +100,11 @@ preferable to ask the API when the most recent book was published.
|
|||
|
||||
We map the URL to book list view in the URLconf::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from books.views import BookListView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^books/$', BookListView.as_view()),
|
||||
path('books/', BookListView.as_view()),
|
||||
]
|
||||
|
||||
And the view::
|
||||
|
|
|
@ -89,11 +89,11 @@ request to a matching method if one is defined, or raises
|
|||
:class:`~django.http.HttpResponseNotAllowed` if not::
|
||||
|
||||
# urls.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from myapp.views import MyView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^about/$', MyView.as_view()),
|
||||
path('about/', MyView.as_view()),
|
||||
]
|
||||
|
||||
|
||||
|
@ -130,7 +130,7 @@ Another option is to configure class attributes as keyword arguments to the
|
|||
:meth:`~django.views.generic.base.View.as_view` call in the URLconf::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^about/$', GreetingView.as_view(greeting="G'day")),
|
||||
path('about/', GreetingView.as_view(greeting="G'day")),
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
@ -245,8 +245,8 @@ The easiest place to do this is in the URLconf where you deploy your view::
|
|||
from .views import VoteView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^about/$', login_required(TemplateView.as_view(template_name="secret.html"))),
|
||||
url(r'^vote/$', permission_required('polls.can_vote')(VoteView.as_view())),
|
||||
path('about/', login_required(TemplateView.as_view(template_name="secret.html"))),
|
||||
path('vote/', permission_required('polls.can_vote')(VoteView.as_view())),
|
||||
]
|
||||
|
||||
This approach applies the decorator on a per-instance basis. If you
|
||||
|
|
|
@ -258,12 +258,12 @@ We can hook this into our URLs easily enough:
|
|||
.. snippet::
|
||||
:filename: urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from books.views import RecordInterest
|
||||
|
||||
urlpatterns = [
|
||||
#...
|
||||
url(r'^author/(?P<pk>[0-9]+)/interest/$', RecordInterest.as_view(), name='author-interest'),
|
||||
path('author/<int:pk>/interest/', RecordInterest.as_view(), name='author-interest'),
|
||||
]
|
||||
|
||||
Note the ``pk`` named group, which
|
||||
|
|
|
@ -19,8 +19,7 @@ Overview
|
|||
|
||||
To design URLs for an app, you create a Python module informally called a
|
||||
**URLconf** (URL configuration). This module is pure Python code and is a
|
||||
simple mapping between URL patterns (simple regular expressions) to Python
|
||||
functions (your views).
|
||||
mapping between URL path expressions to Python functions (your views).
|
||||
|
||||
This mapping can be as short or as long as needed. It can reference other
|
||||
mappings. And, because it's pure Python code, it can be constructed
|
||||
|
@ -45,25 +44,26 @@ algorithm the system follows to determine which Python code to execute:
|
|||
:setting:`ROOT_URLCONF` setting.
|
||||
|
||||
2. Django loads that Python module and looks for the variable
|
||||
``urlpatterns``. This should be a Python list of :func:`django.conf.urls.url`
|
||||
instances.
|
||||
``urlpatterns``. This should be a Python list of :func:`django.urls.path`
|
||||
and/or :func:`django.urls.re_path` instances.
|
||||
|
||||
3. Django runs through each URL pattern, in order, and stops at the first
|
||||
one that matches the requested URL.
|
||||
|
||||
4. Once one of the regexes matches, Django imports and calls the given view,
|
||||
which is a simple Python function (or a :doc:`class-based view
|
||||
4. Once one of the URL patterns matches, Django imports and calls the given
|
||||
view, which is a simple Python function (or a :doc:`class-based view
|
||||
</topics/class-based-views/index>`). The view gets passed the following
|
||||
arguments:
|
||||
|
||||
* An instance of :class:`~django.http.HttpRequest`.
|
||||
* If the matched regular expression returned no named groups, then the
|
||||
* If the matched URL pattern returned no named groups, then the
|
||||
matches from the regular expression are provided as positional arguments.
|
||||
* The keyword arguments are made up of any named groups matched by the
|
||||
regular expression, overridden by any arguments specified in the optional
|
||||
``kwargs`` argument to :func:`django.conf.urls.url`.
|
||||
* The keyword arguments are made up of any named parts matched by the
|
||||
path expression, overridden by any arguments specified in the optional
|
||||
``kwargs`` argument to :func:`django.urls.path` or
|
||||
:func:`django.urls.re_path`.
|
||||
|
||||
5. If no regex matches, or if an exception is raised during any
|
||||
5. If no URL pattern matches, or if an exception is raised during any
|
||||
point in this process, Django invokes an appropriate
|
||||
error-handling view. See `Error handling`_ below.
|
||||
|
||||
|
@ -72,36 +72,33 @@ Example
|
|||
|
||||
Here's a sample URLconf::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^articles/2003/$', views.special_case_2003),
|
||||
url(r'^articles/([0-9]{4})/$', views.year_archive),
|
||||
url(r'^articles/([0-9]{4})/([0-9]{2})/$', views.month_archive),
|
||||
url(r'^articles/([0-9]{4})/([0-9]{2})/([0-9]+)/$', views.article_detail),
|
||||
path('articles/2003/', views.special_case_2003),
|
||||
path('articles/<int:year>/', views.year_archive),
|
||||
path('articles/<int:year>/<int:month>/', views.month_archive),
|
||||
path('articles/<int:year>/<int:month>/<slug>/', views.article_detail),
|
||||
]
|
||||
|
||||
Notes:
|
||||
|
||||
* To capture a value from the URL, just put parenthesis around it.
|
||||
* To capture a value from the URL, use angle brackets.
|
||||
|
||||
* Captured values can optionally include a converter type. For example, use
|
||||
``<int:name>`` to capture an integer parameter. If a converter isn't included,
|
||||
any string, excluding a ``/`` character, is matched.
|
||||
|
||||
* There's no need to add a leading slash, because every URL has that. For
|
||||
example, it's ``^articles``, not ``^/articles``.
|
||||
|
||||
* The ``'r'`` in front of each regular expression string is optional but
|
||||
recommended. It tells Python that a string is "raw" -- that nothing in
|
||||
the string should be escaped. See `Dive Into Python's explanation`_.
|
||||
example, it's ``articles``, not ``/articles``.
|
||||
|
||||
Example requests:
|
||||
|
||||
* A request to ``/articles/2005/03/`` would match the third entry in the
|
||||
list. Django would call the function
|
||||
``views.month_archive(request, '2005', '03')``.
|
||||
|
||||
* ``/articles/2005/3/`` would not match any URL patterns, because the
|
||||
third entry in the list requires two digits for the month.
|
||||
``views.month_archive(request, year=2005, month=3)``.
|
||||
|
||||
* ``/articles/2003/`` would match the first pattern in the list, not the
|
||||
second one, because the patterns are tested in order, and the first one
|
||||
|
@ -112,66 +109,163 @@ Example requests:
|
|||
* ``/articles/2003`` would not match any of these patterns, because each
|
||||
pattern requires that the URL end with a slash.
|
||||
|
||||
* ``/articles/2003/03/03/`` would match the final pattern. Django would call
|
||||
the function ``views.article_detail(request, '2003', '03', '03')``.
|
||||
* ``/articles/2003/03/building-a-django-site/`` would match the final
|
||||
pattern. Django would call the function
|
||||
``views.article_detail(request, year=2003, month=3, slug="building-a-django-site")``.
|
||||
|
||||
.. _Dive Into Python's explanation: http://www.diveintopython3.net/regular-expressions.html#streetaddresses
|
||||
Path converters
|
||||
===============
|
||||
|
||||
Named groups
|
||||
============
|
||||
The following path converters are available by default:
|
||||
|
||||
The above example used simple, *non-named* regular-expression groups (via
|
||||
parenthesis) to capture bits of the URL and pass them as *positional* arguments
|
||||
to a view. In more advanced usage, it's possible to use *named*
|
||||
regular-expression groups to capture URL bits and pass them as *keyword*
|
||||
arguments to a view.
|
||||
* ``str`` - Matches any non-empty string, excluding the path separator, ``'/'``.
|
||||
This is the default if a converter isn't included in the expression.
|
||||
|
||||
In Python regular expressions, the syntax for named regular-expression groups
|
||||
* ``int`` - Matches zero or any positive integer. Returns an `int`.
|
||||
|
||||
* ``slug`` - Matches any slug string consisting of ASCII letters or numbers,
|
||||
plus the hyphen and underscore characters. For example,
|
||||
``building-your-1st-django-site``.
|
||||
|
||||
* ``uuid`` - Matches a formatted UUID. For example,
|
||||
``075194d3-6885-417e-a8a8-6c931e272f00``. Returns a :class:`~uuid.UUID`
|
||||
instance.
|
||||
|
||||
* ``path`` - Matches any non-empty string, including the path separator,
|
||||
``'/'``. This allows you to match against a complete URL path rather than
|
||||
just a segment of a URL path as with ``str``.
|
||||
|
||||
.. _registering-custom-path-converters:
|
||||
|
||||
Registering custom path converters
|
||||
==================================
|
||||
|
||||
For more complex matching requirements, you can define your own path converters.
|
||||
|
||||
A converter is a class that includes the following:
|
||||
|
||||
* A ``regex`` class attribute, as a string.
|
||||
|
||||
* A ``to_python(self, value)`` method, which handles converting the matched
|
||||
string into the type that should be passed to the view function. It should
|
||||
raise ``ValueError`` if it can't convert the given value.
|
||||
|
||||
* A ``to_url(self, value)`` method, which handles converting the Python type
|
||||
into a string to be used in the URL.
|
||||
|
||||
For example::
|
||||
|
||||
class FourDigitYearConverter:
|
||||
regex = '[0-9]{4}'
|
||||
|
||||
def to_python(self, value):
|
||||
return int(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return '%04d' % value
|
||||
|
||||
Register custom converter classes in your URLconf using
|
||||
:func:`~django.urls.register_converter`::
|
||||
|
||||
from django.urls import register_converter, path
|
||||
|
||||
from . import converters, views
|
||||
|
||||
register_converter(converters.FourDigitYearConverter, 'yyyy')
|
||||
|
||||
urlpatterns = [
|
||||
path('articles/2003/', views.special_case_2003),
|
||||
path('articles/<yyyy:year>/', views.year_archive),
|
||||
...
|
||||
]
|
||||
|
||||
Using regular expressions
|
||||
=========================
|
||||
|
||||
If the paths and converters syntax isn't sufficient for defining your URL
|
||||
patterns, you can also use regular expressions. To do so, use
|
||||
:func:`~django.urls.re_path` instead of :func:`~django.urls.path`.
|
||||
|
||||
In Python regular expressions, the syntax for named regular expression groups
|
||||
is ``(?P<name>pattern)``, where ``name`` is the name of the group and
|
||||
``pattern`` is some pattern to match.
|
||||
|
||||
Here's the above example URLconf, rewritten to use named groups::
|
||||
Here's the example URLconf from earlier, rewritten using regular expressions::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^articles/2003/$', views.special_case_2003),
|
||||
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
|
||||
url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive),
|
||||
url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<day>[0-9]{2})/$', views.article_detail),
|
||||
path('articles/2003/', views.special_case_2003),
|
||||
re_path('articles/(?P<year>[0-9]{4})/', views.year_archive),
|
||||
re_path('articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/', views.month_archive),
|
||||
re_path('articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/(?P<slug>[^/]+)/', views.article_detail),
|
||||
]
|
||||
|
||||
This accomplishes exactly the same thing as the previous example, with one
|
||||
subtle difference: The captured values are passed to view functions as keyword
|
||||
arguments rather than positional arguments. For example:
|
||||
This accomplishes roughly the same thing as the previous example, except:
|
||||
|
||||
* A request to ``/articles/2005/03/`` would call the function
|
||||
``views.month_archive(request, year='2005', month='03')``, instead
|
||||
of ``views.month_archive(request, '2005', '03')``.
|
||||
* The exact URLs that will match are slightly more constrained. For example,
|
||||
the year 10000 will no longer match since the year integers are constrained
|
||||
to be exactly four digits long.
|
||||
|
||||
* A request to ``/articles/2003/03/03/`` would call the function
|
||||
``views.article_detail(request, year='2003', month='03', day='03')``.
|
||||
* Each captured argument is sent to the view as a string, regardless of what
|
||||
sort of match the regular expression makes.
|
||||
|
||||
In practice, this means your URLconfs are slightly more explicit and less prone
|
||||
to argument-order bugs -- and you can reorder the arguments in your views'
|
||||
function definitions. Of course, these benefits come at the cost of brevity;
|
||||
some developers find the named-group syntax ugly and too verbose.
|
||||
When switching from using :func:`~django.urls.path` to
|
||||
:func:`~django.urls.re_path` or vice versa, it's particularly important to be
|
||||
aware that the type of the view arguments may change, and so you may need to
|
||||
adapt your views.
|
||||
|
||||
The matching/grouping algorithm
|
||||
-------------------------------
|
||||
Using unnamed regular expression groups
|
||||
---------------------------------------
|
||||
|
||||
Here's the algorithm the URLconf parser follows, with respect to named groups
|
||||
vs. non-named groups in a regular expression:
|
||||
As well as the named group syntax, e.g. ``(?P<year>[0-9]{4})``, you can
|
||||
also use the shorter unnamed group, e.g. ``([0-9]{4})``.
|
||||
|
||||
1. If there are any named arguments, it will use those, ignoring non-named
|
||||
arguments.
|
||||
This usage isn't particularly recommended as it makes it easier to accidentally
|
||||
introduce errors between the intended meaning of a match and the arguments
|
||||
of the view.
|
||||
|
||||
2. Otherwise, it will pass all non-named arguments as positional arguments.
|
||||
In either case, using only one style within an given regex is recommended. When
|
||||
both styles are mixed, any unnamed groups are ignored and only named groups are
|
||||
passed to the view function.
|
||||
|
||||
In both cases, any extra keyword arguments that have been given as per `Passing
|
||||
extra options to view functions`_ (below) will also be passed to the view.
|
||||
Nested arguments
|
||||
----------------
|
||||
|
||||
Regular expressions allow nested arguments, and Django will resolve them and
|
||||
pass them to the view. When reversing, Django will try to fill in all outer
|
||||
captured arguments, ignoring any nested captured arguments. Consider the
|
||||
following URL patterns which optionally take a page argument::
|
||||
|
||||
from django.urls import re_path
|
||||
|
||||
urlpatterns = [
|
||||
re_path(r'blog/(page-(\d+)/)?$', blog_articles), # bad
|
||||
re_path(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments), # good
|
||||
]
|
||||
|
||||
Both patterns use nested arguments and will resolve: for example,
|
||||
``blog/page-2/`` will result in a match to ``blog_articles`` with two
|
||||
positional arguments: ``page-2/`` and ``2``. The second pattern for
|
||||
``comments`` will match ``comments/page-2/`` with keyword argument
|
||||
``page_number`` set to 2. The outer argument in this case is a non-capturing
|
||||
argument ``(?:...)``.
|
||||
|
||||
The ``blog_articles`` view needs the outermost captured argument to be reversed,
|
||||
``page-2/`` or no arguments in this case, while ``comments`` can be reversed
|
||||
with either no arguments or a value for ``page_number``.
|
||||
|
||||
Nested captured arguments create a strong coupling between the view arguments
|
||||
and the URL as illustrated by ``blog_articles``: the view receives part of the
|
||||
URL (``page-2/``) instead of only the value the view is interested in. This
|
||||
coupling is even more pronounced when reversing, since to reverse the view we
|
||||
need to pass the piece of URL instead of the page number.
|
||||
|
||||
As a rule of thumb, only capture the values the view needs to work with and
|
||||
use non-capturing arguments when the regular expression needs an argument but
|
||||
the view ignores it.
|
||||
|
||||
What the URLconf searches against
|
||||
=================================
|
||||
|
@ -189,18 +283,6 @@ The URLconf doesn't look at the request method. In other words, all request
|
|||
methods -- ``POST``, ``GET``, ``HEAD``, etc. -- will be routed to the same
|
||||
function for the same URL.
|
||||
|
||||
Captured arguments are always strings
|
||||
=====================================
|
||||
|
||||
Each captured argument is sent to the view as a plain Python string, regardless
|
||||
of what sort of match the regular expression makes. For example, in this
|
||||
URLconf line::
|
||||
|
||||
url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive),
|
||||
|
||||
...the ``year`` argument passed to ``views.year_archive()`` will be a string,
|
||||
not an integer, even though the ``[0-9]{4}`` will only match integer strings.
|
||||
|
||||
Specifying defaults for view arguments
|
||||
======================================
|
||||
|
||||
|
@ -208,25 +290,25 @@ A convenient trick is to specify default parameters for your views' arguments.
|
|||
Here's an example URLconf and view::
|
||||
|
||||
# URLconf
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^blog/$', views.page),
|
||||
url(r'^blog/page(?P<num>[0-9]+)/$', views.page),
|
||||
path('blog/', views.page),
|
||||
path('blog/page<int:num>/', views.page),
|
||||
]
|
||||
|
||||
# View (in blog/views.py)
|
||||
def page(request, num="1"):
|
||||
def page(request, num=1):
|
||||
# Output the appropriate page of blog entries, according to num.
|
||||
...
|
||||
|
||||
In the above example, both URL patterns point to the same view --
|
||||
``views.page`` -- but the first pattern doesn't capture anything from the
|
||||
URL. If the first pattern matches, the ``page()`` function will use its
|
||||
default argument for ``num``, ``"1"``. If the second pattern matches,
|
||||
``page()`` will use whatever ``num`` value was captured by the regex.
|
||||
default argument for ``num``, ``1``. If the second pattern matches,
|
||||
``page()`` will use whatever ``num`` value was captured.
|
||||
|
||||
Performance
|
||||
===========
|
||||
|
@ -237,14 +319,14 @@ accessed. This makes the system blazingly fast.
|
|||
Syntax of the ``urlpatterns`` variable
|
||||
======================================
|
||||
|
||||
``urlpatterns`` should be a Python list of :func:`~django.conf.urls.url`
|
||||
instances.
|
||||
``urlpatterns`` should be a Python list of :func:`~django.urls.path` and/or
|
||||
:func:`~django.urls.re_path` instances.
|
||||
|
||||
Error handling
|
||||
==============
|
||||
|
||||
When Django can't find a regex matching the requested URL, or when an
|
||||
exception is raised, Django will invoke an error-handling view.
|
||||
When Django can't find a match for the requested URL, or when an exception is
|
||||
raised, Django invokes an error-handling view.
|
||||
|
||||
The views to use for these cases are specified by four variables. Their
|
||||
default values should suffice for most projects, but further customization is
|
||||
|
@ -277,39 +359,37 @@ essentially "roots" a set of URLs below other ones.
|
|||
For example, here's an excerpt of the URLconf for the `Django website`_
|
||||
itself. It includes a number of other URLconfs::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
# ... snip ...
|
||||
url(r'^community/', include('django_website.aggregator.urls')),
|
||||
url(r'^contact/', include('django_website.contact.urls')),
|
||||
path('community/', include('aggregator.urls')),
|
||||
path('contact/', include('contact.urls')),
|
||||
# ... snip ...
|
||||
]
|
||||
|
||||
Note that the regular expressions in this example don't have a ``$``
|
||||
(end-of-string match character) but do include a trailing slash. Whenever
|
||||
Django encounters ``include()`` (:func:`django.conf.urls.include()`), it chops
|
||||
off whatever part of the URL matched up to that point and sends the remaining
|
||||
Whenever Django encounters :func:`~django.urls.include()`, it chops off
|
||||
whatever part of the URL matched up to that point and sends the remaining
|
||||
string to the included URLconf for further processing.
|
||||
|
||||
Another possibility is to include additional URL patterns by using a list of
|
||||
:func:`~django.conf.urls.url` instances. For example, consider this URLconf::
|
||||
:func:`~django.urls.path` instances. For example, consider this URLconf::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
from apps.main import views as main_views
|
||||
from credit import views as credit_views
|
||||
|
||||
extra_patterns = [
|
||||
url(r'^reports/$', credit_views.report),
|
||||
url(r'^reports/(?P<id>[0-9]+)/$', credit_views.report),
|
||||
url(r'^charge/$', credit_views.charge),
|
||||
path('reports/', credit_views.report),
|
||||
path('reports/<int:id>/', credit_views.report),
|
||||
path('charge/', credit_views.charge),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', main_views.homepage),
|
||||
url(r'^help/', include('apps.help.urls')),
|
||||
url(r'^credit/', include(extra_patterns)),
|
||||
path('', main_views.homepage),
|
||||
path('help/', include('apps.help.urls')),
|
||||
path('credit/', include(extra_patterns)),
|
||||
]
|
||||
|
||||
In this example, the ``/credit/reports/`` URL will be handled by the
|
||||
|
@ -318,28 +398,28 @@ In this example, the ``/credit/reports/`` URL will be handled by the
|
|||
This can be used to remove redundancy from URLconfs where a single pattern
|
||||
prefix is used repeatedly. For example, consider this URLconf::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/history/$', views.history),
|
||||
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/edit/$', views.edit),
|
||||
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/discuss/$', views.discuss),
|
||||
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/permissions/$', views.permissions),
|
||||
path('<page_slug>-<page_id>/history/', views.history),
|
||||
path('<page_slug>-<page_id>/edit/', views.edit),
|
||||
path('<page_slug>-<page_id>/discuss/', views.discuss),
|
||||
path('<page_slug>-<page_id>/permissions/', views.permissions),
|
||||
]
|
||||
|
||||
We can improve this by stating the common path prefix only once and grouping
|
||||
the suffixes that differ::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<page_slug>[\w-]+)-(?P<page_id>\w+)/', include([
|
||||
url(r'^history/$', views.history),
|
||||
url(r'^edit/$', views.edit),
|
||||
url(r'^discuss/$', views.discuss),
|
||||
url(r'^permissions/$', views.permissions),
|
||||
path('<page_slug>-<page_id>/', include([
|
||||
path('history/', views.history),
|
||||
path('edit/', views.edit),
|
||||
path('discuss/', views.discuss),
|
||||
path('permissions/', views.permissions),
|
||||
])),
|
||||
]
|
||||
|
||||
|
@ -352,60 +432,24 @@ An included URLconf receives any captured parameters from parent URLconfs, so
|
|||
the following example is valid::
|
||||
|
||||
# In settings/urls/main.py
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^(?P<username>\w+)/blog/', include('foo.urls.blog')),
|
||||
path('<username>/blog/', include('foo.urls.blog')),
|
||||
]
|
||||
|
||||
# In foo/urls/blog.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.blog.index),
|
||||
url(r'^archive/$', views.blog.archive),
|
||||
path('', views.blog.index),
|
||||
path('archive/', views.blog.archive),
|
||||
]
|
||||
|
||||
In the above example, the captured ``"username"`` variable is passed to the
|
||||
included URLconf, as expected.
|
||||
|
||||
Nested arguments
|
||||
================
|
||||
|
||||
Regular expressions allow nested arguments, and Django will resolve them and
|
||||
pass them to the view. When reversing, Django will try to fill in all outer
|
||||
captured arguments, ignoring any nested captured arguments. Consider the
|
||||
following URL patterns which optionally take a page argument::
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
urlpatterns = [
|
||||
url(r'blog/(page-(\d+)/)?$', blog_articles), # bad
|
||||
url(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments), # good
|
||||
]
|
||||
|
||||
Both patterns use nested arguments and will resolve: for example,
|
||||
``blog/page-2/`` will result in a match to ``blog_articles`` with two
|
||||
positional arguments: ``page-2/`` and ``2``. The second pattern for
|
||||
``comments`` will match ``comments/page-2/`` with keyword argument
|
||||
``page_number`` set to 2. The outer argument in this case is a non-capturing
|
||||
argument ``(?:...)``.
|
||||
|
||||
The ``blog_articles`` view needs the outermost captured argument to be reversed,
|
||||
``page-2/`` or no arguments in this case, while ``comments`` can be reversed
|
||||
with either no arguments or a value for ``page_number``.
|
||||
|
||||
Nested captured arguments create a strong coupling between the view arguments
|
||||
and the URL as illustrated by ``blog_articles``: the view receives part of the
|
||||
URL (``page-2/``) instead of only the value the view is interested in. This
|
||||
coupling is even more pronounced when reversing, since to reverse the view we
|
||||
need to pass the piece of URL instead of the page number.
|
||||
|
||||
As a rule of thumb, only capture the values the view needs to work with and
|
||||
use non-capturing arguments when the regular expression needs an argument but
|
||||
the view ignores it.
|
||||
|
||||
.. _views-extra-options:
|
||||
|
||||
Passing extra options to view functions
|
||||
|
@ -414,21 +458,21 @@ Passing extra options to view functions
|
|||
URLconfs have a hook that lets you pass extra arguments to your view functions,
|
||||
as a Python dictionary.
|
||||
|
||||
The :func:`django.conf.urls.url` function can take an optional third argument
|
||||
The :func:`~django.urls.path` function can take an optional third argument
|
||||
which should be a dictionary of extra keyword arguments to pass to the view
|
||||
function.
|
||||
|
||||
For example::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^blog/(?P<year>[0-9]{4})/$', views.year_archive, {'foo': 'bar'}),
|
||||
path('blog/<int:year>/', views.year_archive, {'foo': 'bar'}),
|
||||
]
|
||||
|
||||
In this example, for a request to ``/blog/2005/``, Django will call
|
||||
``views.year_archive(request, year='2005', foo='bar')``.
|
||||
``views.year_archive(request, year=2005, foo='bar')``.
|
||||
|
||||
This technique is used in the
|
||||
:doc:`syndication framework </ref/contrib/syndication>` to pass metadata and
|
||||
|
@ -444,46 +488,45 @@ options to views.
|
|||
Passing extra options to ``include()``
|
||||
--------------------------------------
|
||||
|
||||
Similarly, you can pass extra options to :func:`~django.conf.urls.include`.
|
||||
When you pass extra options to ``include()``, *each* line in the included
|
||||
URLconf will be passed the extra options.
|
||||
Similarly, you can pass extra options to :func:`~django.urls.include` and
|
||||
each line in the included URLconf will be passed the extra options.
|
||||
|
||||
For example, these two URLconf sets are functionally identical:
|
||||
|
||||
Set one::
|
||||
|
||||
# main.py
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^blog/', include('inner'), {'blogid': 3}),
|
||||
path('blog/', include('inner'), {'blog_id': 3}),
|
||||
]
|
||||
|
||||
# inner.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from mysite import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^archive/$', views.archive),
|
||||
url(r'^about/$', views.about),
|
||||
path('archive/', views.archive),
|
||||
path('about/', views.about),
|
||||
]
|
||||
|
||||
Set two::
|
||||
|
||||
# main.py
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
from mysite import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^blog/', include('inner')),
|
||||
path('blog/', include('inner')),
|
||||
]
|
||||
|
||||
# inner.py
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^archive/$', views.archive, {'blogid': 3}),
|
||||
url(r'^about/$', views.about, {'blogid': 3}),
|
||||
path('archive/', views.archive, {'blog_id': 3}),
|
||||
path('about/', views.about, {'blog_id': 3}),
|
||||
]
|
||||
|
||||
Note that extra options will *always* be passed to *every* line in the included
|
||||
|
@ -543,18 +586,18 @@ Examples
|
|||
|
||||
Consider again this URLconf entry::
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
#...
|
||||
url(r'^articles/([0-9]{4})/$', views.year_archive, name='news-year-archive'),
|
||||
path('articles/<int:year>/', views.year_archive, name='news-year-archive'),
|
||||
#...
|
||||
]
|
||||
|
||||
According to this design, the URL for the archive corresponding to year *nnnn*
|
||||
is ``/articles/nnnn/``.
|
||||
is ``/articles/<nnnn>/``.
|
||||
|
||||
You can obtain these in template code by using:
|
||||
|
||||
|
@ -720,24 +763,24 @@ displaying polls.
|
|||
.. snippet::
|
||||
:filename: urls.py
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^author-polls/', include('polls.urls', namespace='author-polls')),
|
||||
url(r'^publisher-polls/', include('polls.urls', namespace='publisher-polls')),
|
||||
path('author-polls/', include('polls.urls', namespace='author-polls')),
|
||||
path('publisher-polls/', include('polls.urls', namespace='publisher-polls')),
|
||||
]
|
||||
|
||||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'polls'
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
|
||||
path('', views.IndexView.as_view(), name='index'),
|
||||
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
|
||||
...
|
||||
]
|
||||
|
||||
|
@ -783,60 +826,61 @@ Application namespaces of included URLconfs can be specified in two ways.
|
|||
|
||||
Firstly, you can set an ``app_name`` attribute in the included URLconf module,
|
||||
at the same level as the ``urlpatterns`` attribute. You have to pass the actual
|
||||
module, or a string reference to the module, to
|
||||
:func:`~django.conf.urls.include`, not the list of ``urlpatterns`` itself.
|
||||
module, or a string reference to the module, to :func:`~django.urls.include`,
|
||||
not the list of ``urlpatterns`` itself.
|
||||
|
||||
.. snippet::
|
||||
:filename: polls/urls.py
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = 'polls'
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
|
||||
path('', views.IndexView.as_view(), name='index'),
|
||||
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
|
||||
...
|
||||
]
|
||||
|
||||
.. snippet::
|
||||
:filename: urls.py
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^polls/', include('polls.urls')),
|
||||
path('polls/', include('polls.urls')),
|
||||
]
|
||||
|
||||
The URLs defined in ``polls.urls`` will have an application namespace ``polls``.
|
||||
|
||||
Secondly, you can include an object that contains embedded namespace data. If
|
||||
you ``include()`` a list of :func:`~django.conf.urls.url` instances,
|
||||
the URLs contained in that object will be added to the global namespace.
|
||||
However, you can also ``include()`` a 2-tuple containing::
|
||||
you ``include()`` a list of :func:`~django.urls.path` or
|
||||
:func:`~django.urls.re_path` instances, the URLs contained in that object
|
||||
will be added to the global namespace. However, you can also ``include()`` a
|
||||
2-tuple containing::
|
||||
|
||||
(<list of url() instances>, <application namespace>)
|
||||
(<list of path()/re_path() instances>, <application namespace>)
|
||||
|
||||
For example::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, path
|
||||
|
||||
from . import views
|
||||
|
||||
polls_patterns = ([
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^(?P<pk>\d+)/$', views.DetailView.as_view(), name='detail'),
|
||||
path('', views.IndexView.as_view(), name='index'),
|
||||
path('<int:pk>/', views.DetailView.as_view(), name='detail'),
|
||||
], 'polls')
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^polls/', include(polls_patterns)),
|
||||
path('polls/', include(polls_patterns)),
|
||||
]
|
||||
|
||||
This will include the nominated URL patterns into the given application
|
||||
namespace.
|
||||
|
||||
The instance namespace can be specified using the ``namespace`` argument to
|
||||
:func:`~django.conf.urls.include`. If the instance namespace is not specified,
|
||||
:func:`~django.urls.include`. If the instance namespace is not specified,
|
||||
it will default to the included URLconf's application namespace. This means
|
||||
it will also be the default instance for that namespace.
|
||||
|
|
|
@ -992,13 +992,13 @@ The ``JavaScriptCatalog`` view
|
|||
from django.views.i18n import JavaScriptCatalog
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
]
|
||||
|
||||
**Example with custom packages**::
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^jsi18n/myapp/$',
|
||||
path('jsi18n/myapp/',
|
||||
JavaScriptCatalog.as_view(packages=['your.app.label']),
|
||||
name='javascript-catalog'),
|
||||
]
|
||||
|
@ -1012,7 +1012,7 @@ The ``JavaScriptCatalog`` view
|
|||
from django.conf.urls.i18n import i18n_patterns
|
||||
|
||||
urlpatterns = i18n_patterns(
|
||||
url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
|
||||
)
|
||||
|
||||
The precedence of translations is such that the packages appearing later in the
|
||||
|
@ -1235,7 +1235,7 @@ URL::
|
|||
|
||||
# The value returned by get_version() must change when translations change.
|
||||
urlpatterns = [
|
||||
url(r'^jsi18n/$',
|
||||
path('jsi18n/',
|
||||
cache_page(86400, key_prefix='js18n-%s' % get_version())(JavaScriptCatalog.as_view()),
|
||||
name='javascript-catalog'),
|
||||
]
|
||||
|
@ -1253,7 +1253,7 @@ whenever you restart your application server::
|
|||
last_modified_date = timezone.now()
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^jsi18n/$',
|
||||
path('jsi18n/',
|
||||
last_modified(lambda req, **kw: last_modified_date)(JavaScriptCatalog.as_view()),
|
||||
name='javascript-catalog'),
|
||||
]
|
||||
|
@ -1302,26 +1302,26 @@ translations to existing site so that the current URLs won't change.
|
|||
|
||||
Example URL patterns::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.urls import include, url
|
||||
|
||||
from about import views as about_views
|
||||
from news import views as news_views
|
||||
from sitemap.views import sitemap
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sitemap\.xml$', sitemap, name='sitemap-xml'),
|
||||
path('sitemap.xml', sitemap, name='sitemap-xml'),
|
||||
]
|
||||
|
||||
news_patterns = ([
|
||||
url(r'^$', news_views.index, name='index'),
|
||||
url(r'^category/(?P<slug>[\w-]+)/$', news_views.category, name='category'),
|
||||
url(r'^(?P<slug>[\w-]+)/$', news_views.details, name='detail'),
|
||||
path('', news_views.index, name='index'),
|
||||
path('category/<slug>/', news_views.category, name='category'),
|
||||
path('<slug>/', news_views.details, name='detail'),
|
||||
], 'news')
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
url(r'^about/$', about_views.main, name='about'),
|
||||
url(r'^news/', include(news_patterns, namespace='news')),
|
||||
path('about/', about_views.main, name='about'),
|
||||
path('news/', include(news_patterns, namespace='news')),
|
||||
)
|
||||
|
||||
After defining these URL patterns, Django will automatically add the
|
||||
|
@ -1371,8 +1371,8 @@ Translating URL patterns
|
|||
URL patterns can also be marked translatable using the
|
||||
:func:`~django.utils.translation.gettext_lazy` function. Example::
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.urls import include, path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from about import views as about_views
|
||||
|
@ -1380,18 +1380,18 @@ URL patterns can also be marked translatable using the
|
|||
from sitemaps.views import sitemap
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^sitemap\.xml$', sitemap, name='sitemap-xml'),
|
||||
path('sitemap.xml', sitemap, name='sitemap-xml'),
|
||||
]
|
||||
|
||||
news_patterns = ([
|
||||
url(r'^$', news_views.index, name='index'),
|
||||
url(_(r'^category/(?P<slug>[\w-]+)/$'), news_views.category, name='category'),
|
||||
url(r'^(?P<slug>[\w-]+)/$', news_views.details, name='detail'),
|
||||
path('', news_views.index, name='index'),
|
||||
path(_('category/<slug>/'), news_views.category, name='category'),
|
||||
path('<slug>/', news_views.details, name='detail'),
|
||||
], 'news')
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
url(_(r'^about/$'), about_views.main, name='about'),
|
||||
url(_(r'^news/'), include(news_patterns, namespace='news')),
|
||||
path(_('about/'), about_views.main, name='about'),
|
||||
path(_('news/'), include(news_patterns, namespace='news')),
|
||||
)
|
||||
|
||||
After you've created the translations, the :func:`~django.urls.reverse`
|
||||
|
@ -1750,7 +1750,7 @@ back to the previous page.
|
|||
|
||||
Activate this view by adding the following line to your URLconf::
|
||||
|
||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
|
||||
(Note that this example makes the view available at ``/i18n/setlang/``.)
|
||||
|
||||
|
|
|
@ -32,8 +32,11 @@ class CheckUrlConfigTests(SimpleTestCase):
|
|||
self.assertEqual(len(result), 1)
|
||||
warning = result[0]
|
||||
self.assertEqual(warning.id, 'urls.W001')
|
||||
expected_msg = "Your URL pattern '^include-with-dollar$' uses include with a regex ending with a '$'."
|
||||
self.assertIn(expected_msg, warning.msg)
|
||||
self.assertEqual(warning.msg, (
|
||||
"Your URL pattern '^include-with-dollar$' uses include with a "
|
||||
"route ending with a '$'. Remove the dollar from the route to "
|
||||
"avoid problems including URLs."
|
||||
))
|
||||
|
||||
@override_settings(ROOT_URLCONF='check_framework.urls.contains_tuple')
|
||||
def test_contains_tuple_not_url_instance(self):
|
||||
|
@ -42,23 +45,33 @@ class CheckUrlConfigTests(SimpleTestCase):
|
|||
self.assertEqual(warning.id, 'urls.E004')
|
||||
self.assertRegex(warning.msg, (
|
||||
r"^Your URL pattern \('\^tuple/\$', <function <lambda> at 0x(\w+)>\) is "
|
||||
r"invalid. Ensure that urlpatterns is a list of url\(\) instances.$"
|
||||
r"invalid. Ensure that urlpatterns is a list of path\(\) and/or re_path\(\) "
|
||||
r"instances\.$"
|
||||
))
|
||||
|
||||
@override_settings(ROOT_URLCONF='check_framework.urls.include_contains_tuple')
|
||||
def test_contains_included_tuple(self):
|
||||
result = check_url_config(None)
|
||||
warning = result[0]
|
||||
self.assertEqual(warning.id, 'urls.E004')
|
||||
self.assertRegex(warning.msg, (
|
||||
r"^Your URL pattern \('\^tuple/\$', <function <lambda> at 0x(\w+)>\) is "
|
||||
r"invalid. Ensure that urlpatterns is a list of path\(\) and/or re_path\(\) "
|
||||
r"instances\.$"
|
||||
))
|
||||
|
||||
@override_settings(ROOT_URLCONF='check_framework.urls.beginning_with_slash')
|
||||
def test_beginning_with_slash(self):
|
||||
result = check_url_config(None)
|
||||
self.assertEqual(len(result), 1)
|
||||
warning = result[0]
|
||||
self.assertEqual(warning.id, 'urls.W002')
|
||||
expected_msg = (
|
||||
"Your URL pattern '/starting-with-slash/$' has a regex beginning "
|
||||
"with a '/'. Remove this slash as it is unnecessary. If this "
|
||||
"pattern is targeted in an include(), ensure the include() pattern "
|
||||
"has a trailing '/'."
|
||||
msg = (
|
||||
"Your URL pattern '%s' has a route beginning with a '/'. Remove "
|
||||
"this slash as it is unnecessary. If this pattern is targeted in "
|
||||
"an include(), ensure the include() pattern has a trailing '/'."
|
||||
)
|
||||
|
||||
self.assertIn(expected_msg, warning.msg)
|
||||
warning1, warning2 = check_url_config(None)
|
||||
self.assertEqual(warning1.id, 'urls.W002')
|
||||
self.assertEqual(warning1.msg, msg % '/path-starting-with-slash/')
|
||||
self.assertEqual(warning2.id, 'urls.W002')
|
||||
self.assertEqual(warning2.msg, msg % '/url-starting-with-slash/$')
|
||||
|
||||
@override_settings(
|
||||
ROOT_URLCONF='check_framework.urls.beginning_with_slash',
|
||||
|
@ -95,7 +108,7 @@ class CheckUrlConfigTests(SimpleTestCase):
|
|||
|
||||
def test_get_warning_for_invalid_pattern_tuple(self):
|
||||
warning = get_warning_for_invalid_pattern((r'^$', lambda x: x))[0]
|
||||
self.assertEqual(warning.hint, "Try using url() instead of a tuple.")
|
||||
self.assertEqual(warning.hint, "Try using path() instead of a tuple.")
|
||||
|
||||
def test_get_warning_for_invalid_pattern_other(self):
|
||||
warning = get_warning_for_invalid_pattern(object())[0]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
url(r'/starting-with-slash/$', lambda x: x),
|
||||
path('/path-starting-with-slash/', lambda x: x),
|
||||
url(r'/url-starting-with-slash/$', lambda x: x),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path('', include([(r'^tuple/$', lambda x: x)])),
|
||||
]
|
|
@ -1,6 +1,6 @@
|
|||
from django.conf.urls import url
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import path, re_path
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
@ -9,288 +9,212 @@ from .models import Book
|
|||
|
||||
urlpatterns = [
|
||||
# TemplateView
|
||||
url(r'^template/no_template/$',
|
||||
TemplateView.as_view()),
|
||||
url(r'^template/login_required/$',
|
||||
login_required(TemplateView.as_view())),
|
||||
url(r'^template/simple/(?P<foo>\w+)/$',
|
||||
TemplateView.as_view(template_name='generic_views/about.html')),
|
||||
url(r'^template/custom/(?P<foo>\w+)/$',
|
||||
views.CustomTemplateView.as_view(template_name='generic_views/about.html')),
|
||||
url(r'^template/content_type/$',
|
||||
TemplateView.as_view(template_name='generic_views/robots.txt', content_type='text/plain')),
|
||||
|
||||
url(r'^template/cached/(?P<foo>\w+)/$',
|
||||
cache_page(2.0)(TemplateView.as_view(template_name='generic_views/about.html'))),
|
||||
url(r'^template/extra_context/$',
|
||||
TemplateView.as_view(template_name='generic_views/about.html', extra_context={'title': 'Title'})),
|
||||
path('template/no_template/', TemplateView.as_view()),
|
||||
path('template/login_required/', login_required(TemplateView.as_view())),
|
||||
path('template/simple/<foo>/', TemplateView.as_view(template_name='generic_views/about.html')),
|
||||
path('template/custom/<foo>/', views.CustomTemplateView.as_view(template_name='generic_views/about.html')),
|
||||
path(
|
||||
'template/content_type/',
|
||||
TemplateView.as_view(template_name='generic_views/robots.txt', content_type='text/plain'),
|
||||
),
|
||||
path(
|
||||
'template/cached/<foo>/',
|
||||
cache_page(2.0)(TemplateView.as_view(template_name='generic_views/about.html')),
|
||||
),
|
||||
path(
|
||||
'template/extra_context/',
|
||||
TemplateView.as_view(template_name='generic_views/about.html', extra_context={'title': 'Title'}),
|
||||
),
|
||||
|
||||
# DetailView
|
||||
url(r'^detail/obj/$',
|
||||
views.ObjectDetail.as_view()),
|
||||
url(r'^detail/artist/(?P<pk>[0-9]+)/$',
|
||||
views.ArtistDetail.as_view(),
|
||||
name="artist_detail"),
|
||||
url(r'^detail/author/(?P<pk>[0-9]+)/$',
|
||||
views.AuthorDetail.as_view(),
|
||||
name="author_detail"),
|
||||
url(r'^detail/author/bycustompk/(?P<foo>[0-9]+)/$',
|
||||
views.AuthorDetail.as_view(pk_url_kwarg='foo')),
|
||||
url(r'^detail/author/byslug/(?P<slug>[\w-]+)/$',
|
||||
views.AuthorDetail.as_view()),
|
||||
url(r'^detail/author/bycustomslug/(?P<foo>[\w-]+)/$',
|
||||
views.AuthorDetail.as_view(slug_url_kwarg='foo')),
|
||||
url(r'^detail/author/bypkignoreslug/(?P<pk>[0-9]+)-(?P<slug>[\w-]+)/$',
|
||||
views.AuthorDetail.as_view()),
|
||||
url(r'^detail/author/bypkandslug/(?P<pk>[0-9]+)-(?P<slug>[\w-]+)/$',
|
||||
views.AuthorDetail.as_view(query_pk_and_slug=True)),
|
||||
url(r'^detail/author/(?P<pk>[0-9]+)/template_name_suffix/$',
|
||||
views.AuthorDetail.as_view(template_name_suffix='_view')),
|
||||
url(r'^detail/author/(?P<pk>[0-9]+)/template_name/$',
|
||||
views.AuthorDetail.as_view(template_name='generic_views/about.html')),
|
||||
url(r'^detail/author/(?P<pk>[0-9]+)/context_object_name/$',
|
||||
views.AuthorDetail.as_view(context_object_name='thingy')),
|
||||
url(r'^detail/author/(?P<pk>[0-9]+)/custom_detail/$',
|
||||
views.AuthorCustomDetail.as_view()),
|
||||
url(r'^detail/author/(?P<pk>[0-9]+)/dupe_context_object_name/$',
|
||||
views.AuthorDetail.as_view(context_object_name='object')),
|
||||
url(r'^detail/page/(?P<pk>[0-9]+)/field/$',
|
||||
views.PageDetail.as_view()),
|
||||
url(r'^detail/author/invalid/url/$',
|
||||
views.AuthorDetail.as_view()),
|
||||
url(r'^detail/author/invalid/qs/$',
|
||||
views.AuthorDetail.as_view(queryset=None)),
|
||||
url(r'^detail/nonmodel/1/$',
|
||||
views.NonModelDetail.as_view()),
|
||||
url(r'^detail/doesnotexist/(?P<pk>[0-9]+)/$',
|
||||
views.ObjectDoesNotExistDetail.as_view()),
|
||||
path('detail/obj/', views.ObjectDetail.as_view()),
|
||||
path('detail/artist/<int:pk>/', views.ArtistDetail.as_view(), name='artist_detail'),
|
||||
path('detail/author/<int:pk>/', views.AuthorDetail.as_view(), name='author_detail'),
|
||||
path('detail/author/bycustompk/<foo>/', views.AuthorDetail.as_view(pk_url_kwarg='foo')),
|
||||
path('detail/author/byslug/<slug>/', views.AuthorDetail.as_view()),
|
||||
path('detail/author/bycustomslug/<foo>/', views.AuthorDetail.as_view(slug_url_kwarg='foo')),
|
||||
path('detail/author/bypkignoreslug/<int:pk>-<slug>/', views.AuthorDetail.as_view()),
|
||||
path('detail/author/bypkandslug/<int:pk>-<slug>/', views.AuthorDetail.as_view(query_pk_and_slug=True)),
|
||||
path('detail/author/<int:pk>/template_name_suffix/', views.AuthorDetail.as_view(template_name_suffix='_view')),
|
||||
path(
|
||||
'detail/author/<int:pk>/template_name/',
|
||||
views.AuthorDetail.as_view(template_name='generic_views/about.html'),
|
||||
),
|
||||
path('detail/author/<int:pk>/context_object_name/', views.AuthorDetail.as_view(context_object_name='thingy')),
|
||||
path('detail/author/<int:pk>/custom_detail/', views.AuthorCustomDetail.as_view()),
|
||||
path('detail/author/<int:pk>/dupe_context_object_name/', views.AuthorDetail.as_view(context_object_name='object')),
|
||||
path('detail/page/<int:pk>/field/', views.PageDetail.as_view()),
|
||||
path(r'detail/author/invalid/url/', views.AuthorDetail.as_view()),
|
||||
path('detail/author/invalid/qs/', views.AuthorDetail.as_view(queryset=None)),
|
||||
path('detail/nonmodel/1/', views.NonModelDetail.as_view()),
|
||||
path('detail/doesnotexist/<pk>/', views.ObjectDoesNotExistDetail.as_view()),
|
||||
# FormView
|
||||
url(r'^contact/$',
|
||||
views.ContactView.as_view()),
|
||||
url(r'^late-validation/$',
|
||||
views.LateValidationView.as_view()),
|
||||
path('contact/', views.ContactView.as_view()),
|
||||
path('late-validation/', views.LateValidationView.as_view()),
|
||||
|
||||
# Create/UpdateView
|
||||
url(r'^edit/artists/create/$',
|
||||
views.ArtistCreate.as_view()),
|
||||
url(r'^edit/artists/(?P<pk>[0-9]+)/update/$',
|
||||
views.ArtistUpdate.as_view()),
|
||||
path('edit/artists/create/', views.ArtistCreate.as_view()),
|
||||
path('edit/artists/<int:pk>/update/', views.ArtistUpdate.as_view()),
|
||||
|
||||
url(r'^edit/authors/create/naive/$',
|
||||
views.NaiveAuthorCreate.as_view()),
|
||||
url(r'^edit/authors/create/redirect/$',
|
||||
views.NaiveAuthorCreate.as_view(success_url='/edit/authors/create/')),
|
||||
url(r'^edit/authors/create/interpolate_redirect/$',
|
||||
views.NaiveAuthorCreate.as_view(success_url='/edit/author/{id}/update/')),
|
||||
url(r'^edit/authors/create/interpolate_redirect_nonascii/$',
|
||||
views.NaiveAuthorCreate.as_view(success_url='/%C3%A9dit/author/{id}/update/')),
|
||||
url(r'^edit/authors/create/restricted/$',
|
||||
views.AuthorCreateRestricted.as_view()),
|
||||
url(r'^[eé]dit/authors/create/$',
|
||||
views.AuthorCreate.as_view()),
|
||||
url(r'^edit/authors/create/special/$',
|
||||
views.SpecializedAuthorCreate.as_view()),
|
||||
path('edit/authors/create/naive/', views.NaiveAuthorCreate.as_view()),
|
||||
path('edit/authors/create/redirect/', views.NaiveAuthorCreate.as_view(success_url='/edit/authors/create/')),
|
||||
path(
|
||||
'edit/authors/create/interpolate_redirect/',
|
||||
views.NaiveAuthorCreate.as_view(success_url='/edit/author/{id}/update/'),
|
||||
),
|
||||
path(
|
||||
'edit/authors/create/interpolate_redirect_nonascii/',
|
||||
views.NaiveAuthorCreate.as_view(success_url='/%C3%A9dit/author/{id}/update/'),
|
||||
),
|
||||
path('edit/authors/create/restricted/', views.AuthorCreateRestricted.as_view()),
|
||||
re_path('^[eé]dit/authors/create/$', views.AuthorCreate.as_view()),
|
||||
path('edit/authors/create/special/', views.SpecializedAuthorCreate.as_view()),
|
||||
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/update/naive/$',
|
||||
views.NaiveAuthorUpdate.as_view()),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/update/redirect/$',
|
||||
views.NaiveAuthorUpdate.as_view(success_url='/edit/authors/create/')),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/update/interpolate_redirect/$',
|
||||
views.NaiveAuthorUpdate.as_view(success_url='/edit/author/{id}/update/')),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/update/interpolate_redirect_nonascii/$',
|
||||
views.NaiveAuthorUpdate.as_view(success_url='/%C3%A9dit/author/{id}/update/')),
|
||||
url(r'^[eé]dit/author/(?P<pk>[0-9]+)/update/$',
|
||||
views.AuthorUpdate.as_view()),
|
||||
url(r'^edit/author/update/$',
|
||||
views.OneAuthorUpdate.as_view()),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/update/special/$',
|
||||
views.SpecializedAuthorUpdate.as_view()),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/delete/naive/$',
|
||||
views.NaiveAuthorDelete.as_view()),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/delete/redirect/$',
|
||||
views.NaiveAuthorDelete.as_view(success_url='/edit/authors/create/')),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/delete/interpolate_redirect/$',
|
||||
views.NaiveAuthorDelete.as_view(success_url='/edit/authors/create/?deleted={id}')),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/delete/interpolate_redirect_nonascii/$',
|
||||
views.NaiveAuthorDelete.as_view(success_url='/%C3%A9dit/authors/create/?deleted={id}')),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/delete/$',
|
||||
views.AuthorDelete.as_view()),
|
||||
url(r'^edit/author/(?P<pk>[0-9]+)/delete/special/$',
|
||||
views.SpecializedAuthorDelete.as_view()),
|
||||
path('edit/author/<int:pk>/update/naive/', views.NaiveAuthorUpdate.as_view()),
|
||||
path(
|
||||
'edit/author/<int:pk>/update/redirect/',
|
||||
views.NaiveAuthorUpdate.as_view(success_url='/edit/authors/create/')
|
||||
),
|
||||
path(
|
||||
'edit/author/<int:pk>/update/interpolate_redirect/',
|
||||
views.NaiveAuthorUpdate.as_view(success_url='/edit/author/{id}/update/')
|
||||
),
|
||||
path(
|
||||
'edit/author/<int:pk>/update/interpolate_redirect_nonascii/',
|
||||
views.NaiveAuthorUpdate.as_view(success_url='/%C3%A9dit/author/{id}/update/'),
|
||||
),
|
||||
re_path('^[eé]dit/author/(?P<pk>[0-9]+)/update/$', views.AuthorUpdate.as_view()),
|
||||
path('edit/author/update/', views.OneAuthorUpdate.as_view()),
|
||||
path('edit/author/<int:pk>/update/special/', views.SpecializedAuthorUpdate.as_view()),
|
||||
path('edit/author/<int:pk>/delete/naive/', views.NaiveAuthorDelete.as_view()),
|
||||
path(
|
||||
'edit/author/<int:pk>/delete/redirect/',
|
||||
views.NaiveAuthorDelete.as_view(success_url='/edit/authors/create/'),
|
||||
),
|
||||
path(
|
||||
'edit/author/<int:pk>/delete/interpolate_redirect/',
|
||||
views.NaiveAuthorDelete.as_view(success_url='/edit/authors/create/?deleted={id}')
|
||||
),
|
||||
path(
|
||||
'edit/author/<int:pk>/delete/interpolate_redirect_nonascii/',
|
||||
views.NaiveAuthorDelete.as_view(success_url='/%C3%A9dit/authors/create/?deleted={id}')
|
||||
),
|
||||
path('edit/author/<int:pk>/delete/', views.AuthorDelete.as_view()),
|
||||
path('edit/author/<int:pk>/delete/special/', views.SpecializedAuthorDelete.as_view()),
|
||||
|
||||
# ArchiveIndexView
|
||||
url(r'^dates/books/$',
|
||||
views.BookArchive.as_view()),
|
||||
url(r'^dates/books/context_object_name/$',
|
||||
views.BookArchive.as_view(context_object_name='thingies')),
|
||||
url(r'^dates/books/allow_empty/$',
|
||||
views.BookArchive.as_view(allow_empty=True)),
|
||||
url(r'^dates/books/template_name/$',
|
||||
views.BookArchive.as_view(template_name='generic_views/list.html')),
|
||||
url(r'^dates/books/template_name_suffix/$',
|
||||
views.BookArchive.as_view(template_name_suffix='_detail')),
|
||||
url(r'^dates/books/invalid/$',
|
||||
views.BookArchive.as_view(queryset=None)),
|
||||
url(r'^dates/books/paginated/$',
|
||||
views.BookArchive.as_view(paginate_by=10)),
|
||||
url(r'^dates/books/reverse/$',
|
||||
views.BookArchive.as_view(queryset=Book.objects.order_by('pubdate'))),
|
||||
url(r'^dates/books/by_month/$',
|
||||
views.BookArchive.as_view(date_list_period='month')),
|
||||
url(r'^dates/booksignings/$',
|
||||
views.BookSigningArchive.as_view()),
|
||||
url(r'^dates/books/sortedbyname/$',
|
||||
views.BookArchive.as_view(ordering='name')),
|
||||
url(r'^dates/books/sortedbynamedec/$',
|
||||
views.BookArchive.as_view(ordering='-name')),
|
||||
path('dates/books/', views.BookArchive.as_view()),
|
||||
path('dates/books/context_object_name/', views.BookArchive.as_view(context_object_name='thingies')),
|
||||
path('dates/books/allow_empty/', views.BookArchive.as_view(allow_empty=True)),
|
||||
path('dates/books/template_name/', views.BookArchive.as_view(template_name='generic_views/list.html')),
|
||||
path('dates/books/template_name_suffix/', views.BookArchive.as_view(template_name_suffix='_detail')),
|
||||
path('dates/books/invalid/', views.BookArchive.as_view(queryset=None)),
|
||||
path('dates/books/paginated/', views.BookArchive.as_view(paginate_by=10)),
|
||||
path('dates/books/reverse/', views.BookArchive.as_view(queryset=Book.objects.order_by('pubdate'))),
|
||||
path('dates/books/by_month/', views.BookArchive.as_view(date_list_period='month')),
|
||||
path('dates/booksignings/', views.BookSigningArchive.as_view()),
|
||||
path('dates/books/sortedbyname/', views.BookArchive.as_view(ordering='name')),
|
||||
path('dates/books/sortedbynamedec/', views.BookArchive.as_view(ordering='-name')),
|
||||
|
||||
|
||||
# ListView
|
||||
url(r'^list/dict/$',
|
||||
views.DictList.as_view()),
|
||||
url(r'^list/dict/paginated/$',
|
||||
views.DictList.as_view(paginate_by=1)),
|
||||
url(r'^list/artists/$',
|
||||
views.ArtistList.as_view(),
|
||||
name="artists_list"),
|
||||
url(r'^list/authors/$',
|
||||
views.AuthorList.as_view(),
|
||||
name="authors_list"),
|
||||
url(r'^list/authors/paginated/$',
|
||||
views.AuthorList.as_view(paginate_by=30)),
|
||||
url(r'^list/authors/paginated/(?P<page>[0-9]+)/$',
|
||||
views.AuthorList.as_view(paginate_by=30)),
|
||||
url(r'^list/authors/paginated-orphaned/$',
|
||||
views.AuthorList.as_view(paginate_by=30, paginate_orphans=2)),
|
||||
url(r'^list/authors/notempty/$',
|
||||
views.AuthorList.as_view(allow_empty=False)),
|
||||
url(r'^list/authors/notempty/paginated/$',
|
||||
views.AuthorList.as_view(allow_empty=False, paginate_by=2)),
|
||||
url(r'^list/authors/template_name/$',
|
||||
views.AuthorList.as_view(template_name='generic_views/list.html')),
|
||||
url(r'^list/authors/template_name_suffix/$',
|
||||
views.AuthorList.as_view(template_name_suffix='_objects')),
|
||||
url(r'^list/authors/context_object_name/$',
|
||||
views.AuthorList.as_view(context_object_name='author_list')),
|
||||
url(r'^list/authors/dupe_context_object_name/$',
|
||||
views.AuthorList.as_view(context_object_name='object_list')),
|
||||
url(r'^list/authors/invalid/$',
|
||||
views.AuthorList.as_view(queryset=None)),
|
||||
url(r'^list/authors/paginated/custom_class/$',
|
||||
views.AuthorList.as_view(paginate_by=5, paginator_class=views.CustomPaginator)),
|
||||
url(r'^list/authors/paginated/custom_page_kwarg/$',
|
||||
views.AuthorList.as_view(paginate_by=30, page_kwarg='pagina')),
|
||||
url(r'^list/authors/paginated/custom_constructor/$',
|
||||
views.AuthorListCustomPaginator.as_view()),
|
||||
url(r'^list/books/sorted/$',
|
||||
views.BookList.as_view(ordering='name')),
|
||||
url(r'^list/books/sortedbypagesandnamedec/$',
|
||||
views.BookList.as_view(ordering=('pages', '-name'))),
|
||||
path('list/dict/', views.DictList.as_view()),
|
||||
path('list/dict/paginated/', views.DictList.as_view(paginate_by=1)),
|
||||
path('list/artists/', views.ArtistList.as_view(), name='artists_list'),
|
||||
path('list/authors/', views.AuthorList.as_view(), name='authors_list'),
|
||||
path('list/authors/paginated/', views.AuthorList.as_view(paginate_by=30)),
|
||||
path('list/authors/paginated/<int:page>/', views.AuthorList.as_view(paginate_by=30)),
|
||||
path('list/authors/paginated-orphaned/', views.AuthorList.as_view(paginate_by=30, paginate_orphans=2)),
|
||||
path('list/authors/notempty/', views.AuthorList.as_view(allow_empty=False)),
|
||||
path('list/authors/notempty/paginated/', views.AuthorList.as_view(allow_empty=False, paginate_by=2)),
|
||||
path('list/authors/template_name/', views.AuthorList.as_view(template_name='generic_views/list.html')),
|
||||
path('list/authors/template_name_suffix/', views.AuthorList.as_view(template_name_suffix='_objects')),
|
||||
path('list/authors/context_object_name/', views.AuthorList.as_view(context_object_name='author_list')),
|
||||
path('list/authors/dupe_context_object_name/', views.AuthorList.as_view(context_object_name='object_list')),
|
||||
path('list/authors/invalid/', views.AuthorList.as_view(queryset=None)),
|
||||
path(
|
||||
'list/authors/paginated/custom_class/',
|
||||
views.AuthorList.as_view(paginate_by=5, paginator_class=views.CustomPaginator),
|
||||
),
|
||||
path('list/authors/paginated/custom_page_kwarg/', views.AuthorList.as_view(paginate_by=30, page_kwarg='pagina')),
|
||||
path('list/authors/paginated/custom_constructor/', views.AuthorListCustomPaginator.as_view()),
|
||||
path('list/books/sorted/', views.BookList.as_view(ordering='name')),
|
||||
path('list/books/sortedbypagesandnamedec/', views.BookList.as_view(ordering=('pages', '-name'))),
|
||||
|
||||
# YearArchiveView
|
||||
# Mixing keyword and positional captures below is intentional; the views
|
||||
# ought to be able to accept either.
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/$',
|
||||
views.BookYearArchive.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/make_object_list/$',
|
||||
views.BookYearArchive.as_view(make_object_list=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/allow_empty/$',
|
||||
views.BookYearArchive.as_view(allow_empty=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/allow_future/$',
|
||||
views.BookYearArchive.as_view(allow_future=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/paginated/$',
|
||||
views.BookYearArchive.as_view(make_object_list=True, paginate_by=30)),
|
||||
url(r'^dates/books/(?P<year>\d{4})/sortedbyname/$',
|
||||
views.BookYearArchive.as_view(make_object_list=True, ordering='name')),
|
||||
url(r'^dates/books/(?P<year>\d{4})/sortedbypageandnamedec/$',
|
||||
views.BookYearArchive.as_view(make_object_list=True, ordering=('pages', '-name'))),
|
||||
url(r'^dates/books/no_year/$',
|
||||
views.BookYearArchive.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/reverse/$',
|
||||
views.BookYearArchive.as_view(queryset=Book.objects.order_by('pubdate'))),
|
||||
url(r'^dates/booksignings/(?P<year>[0-9]{4})/$',
|
||||
views.BookSigningYearArchive.as_view()),
|
||||
path('dates/books/<int:year>/', views.BookYearArchive.as_view()),
|
||||
path('dates/books/<int:year>/make_object_list/', views.BookYearArchive.as_view(make_object_list=True)),
|
||||
path('dates/books/<int:year>/allow_empty/', views.BookYearArchive.as_view(allow_empty=True)),
|
||||
path('dates/books/<int:year>/allow_future/', views.BookYearArchive.as_view(allow_future=True)),
|
||||
path('dates/books/<int:year>/paginated/', views.BookYearArchive.as_view(make_object_list=True, paginate_by=30)),
|
||||
path(
|
||||
'dates/books/<int:year>/sortedbyname/',
|
||||
views.BookYearArchive.as_view(make_object_list=True, ordering='name'),
|
||||
),
|
||||
path(
|
||||
'dates/books/<int:year>/sortedbypageandnamedec/',
|
||||
views.BookYearArchive.as_view(make_object_list=True, ordering=('pages', '-name')),
|
||||
),
|
||||
path('dates/books/no_year/', views.BookYearArchive.as_view()),
|
||||
path('dates/books/<int:year>/reverse/', views.BookYearArchive.as_view(queryset=Book.objects.order_by('pubdate'))),
|
||||
path('dates/booksignings/<int:year>/', views.BookSigningYearArchive.as_view()),
|
||||
|
||||
# MonthArchiveView
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/$',
|
||||
views.BookMonthArchive.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/$',
|
||||
views.BookMonthArchive.as_view(month_format='%m')),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/allow_empty/$',
|
||||
views.BookMonthArchive.as_view(allow_empty=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/allow_future/$',
|
||||
views.BookMonthArchive.as_view(allow_future=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/paginated/$',
|
||||
views.BookMonthArchive.as_view(paginate_by=30)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/no_month/$',
|
||||
views.BookMonthArchive.as_view()),
|
||||
url(r'^dates/booksignings/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/$',
|
||||
views.BookSigningMonthArchive.as_view()),
|
||||
path('dates/books/<int:year>/<int:month>/', views.BookMonthArchive.as_view(month_format='%m')),
|
||||
path('dates/books/<int:year>/<month>/', views.BookMonthArchive.as_view()),
|
||||
path('dates/books/<int:year>/<month>/allow_empty/', views.BookMonthArchive.as_view(allow_empty=True)),
|
||||
path('dates/books/<int:year>/<month>/allow_future/', views.BookMonthArchive.as_view(allow_future=True)),
|
||||
path('dates/books/<int:year>/<month>/paginated/', views.BookMonthArchive.as_view(paginate_by=30)),
|
||||
path('dates/books/<int:year>/no_month/', views.BookMonthArchive.as_view()),
|
||||
path('dates/booksignings/<int:year>/<month>/', views.BookSigningMonthArchive.as_view()),
|
||||
|
||||
# WeekArchiveView
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/week/(?P<week>[0-9]{1,2})/$',
|
||||
views.BookWeekArchive.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/week/(?P<week>[0-9]{1,2})/allow_empty/$',
|
||||
views.BookWeekArchive.as_view(allow_empty=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/week/(?P<week>[0-9]{1,2})/allow_future/$',
|
||||
views.BookWeekArchive.as_view(allow_future=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/week/(?P<week>[0-9]{1,2})/paginated/$',
|
||||
views.BookWeekArchive.as_view(paginate_by=30)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/week/no_week/$',
|
||||
views.BookWeekArchive.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/week/(?P<week>[0-9]{1,2})/monday/$',
|
||||
views.BookWeekArchive.as_view(week_format='%W')),
|
||||
url(r'^dates/booksignings/(?P<year>[0-9]{4})/week/(?P<week>[0-9]{1,2})/$',
|
||||
views.BookSigningWeekArchive.as_view()),
|
||||
path('dates/books/<int:year>/week/<int:week>/', views.BookWeekArchive.as_view()),
|
||||
path('dates/books/<int:year>/week/<int:week>/allow_empty/', views.BookWeekArchive.as_view(allow_empty=True)),
|
||||
path('dates/books/<int:year>/week/<int:week>/allow_future/', views.BookWeekArchive.as_view(allow_future=True)),
|
||||
path('dates/books/<int:year>/week/<int:week>/paginated/', views.BookWeekArchive.as_view(paginate_by=30)),
|
||||
path('dates/books/<int:year>/week/no_week/', views.BookWeekArchive.as_view()),
|
||||
path('dates/books/<int:year>/week/<int:week>/monday/', views.BookWeekArchive.as_view(week_format='%W')),
|
||||
path('dates/booksignings/<int:year>/week/<int:week>/', views.BookSigningWeekArchive.as_view()),
|
||||
|
||||
# DayArchiveView
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/$',
|
||||
views.BookDayArchive.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/$',
|
||||
views.BookDayArchive.as_view(month_format='%m')),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/allow_empty/$',
|
||||
views.BookDayArchive.as_view(allow_empty=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/allow_future/$',
|
||||
views.BookDayArchive.as_view(allow_future=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/allow_empty_and_future/$',
|
||||
views.BookDayArchive.as_view(allow_empty=True, allow_future=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/paginated/$',
|
||||
views.BookDayArchive.as_view(paginate_by=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/no_day/$',
|
||||
views.BookDayArchive.as_view()),
|
||||
url(r'^dates/booksignings/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/$',
|
||||
views.BookSigningDayArchive.as_view()),
|
||||
path('dates/books/<int:year>/<int:month>/<int:day>/', views.BookDayArchive.as_view(month_format='%m')),
|
||||
path('dates/books/<int:year>/<month>/<int:day>/', views.BookDayArchive.as_view()),
|
||||
path('dates/books/<int:year>/<month>/<int:day>/allow_empty/', views.BookDayArchive.as_view(allow_empty=True)),
|
||||
path('dates/books/<int:year>/<month>/<int:day>/allow_future/', views.BookDayArchive.as_view(allow_future=True)),
|
||||
path(
|
||||
'dates/books/<int:year>/<month>/<int:day>/allow_empty_and_future/',
|
||||
views.BookDayArchive.as_view(allow_empty=True, allow_future=True),
|
||||
),
|
||||
path('dates/books/<int:year>/<month>/<int:day>/paginated/', views.BookDayArchive.as_view(paginate_by=True)),
|
||||
path('dates/books/<int:year>/<month>/no_day/', views.BookDayArchive.as_view()),
|
||||
path('dates/booksignings/<int:year>/<month>/<int:day>/', views.BookSigningDayArchive.as_view()),
|
||||
|
||||
# TodayArchiveView
|
||||
url(r'^dates/books/today/$',
|
||||
views.BookTodayArchive.as_view()),
|
||||
url(r'^dates/books/today/allow_empty/$',
|
||||
views.BookTodayArchive.as_view(allow_empty=True)),
|
||||
url(r'^dates/booksignings/today/$',
|
||||
views.BookSigningTodayArchive.as_view()),
|
||||
path('dates/books/today/', views.BookTodayArchive.as_view()),
|
||||
path('dates/books/today/allow_empty/', views.BookTodayArchive.as_view(allow_empty=True)),
|
||||
path('dates/booksignings/today/', views.BookSigningTodayArchive.as_view()),
|
||||
|
||||
# DateDetailView
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/(?P<pk>[0-9]+)/$',
|
||||
views.BookDetail.as_view()),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/(?P<day>[0-9]{1,2})/(?P<pk>[0-9]+)/$',
|
||||
views.BookDetail.as_view(month_format='%m')),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/(?P<pk>[0-9]+)/allow_future/$',
|
||||
views.BookDetail.as_view(allow_future=True)),
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/nopk/$',
|
||||
views.BookDetail.as_view()),
|
||||
path('dates/books/<int:year>/<int:month>/<day>/<int:pk>/', views.BookDetail.as_view(month_format='%m')),
|
||||
path('dates/books/<int:year>/<month>/<day>/<int:pk>/', views.BookDetail.as_view()),
|
||||
path(
|
||||
'dates/books/<int:year>/<month>/<int:day>/<int:pk>/allow_future/',
|
||||
views.BookDetail.as_view(allow_future=True),
|
||||
),
|
||||
path('dates/books/<int:year>/<month>/<int:day>/nopk/', views.BookDetail.as_view()),
|
||||
|
||||
url(r'^dates/books/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/byslug/(?P<slug>[\w-]+)/$',
|
||||
views.BookDetail.as_view()),
|
||||
path('dates/books/<int:year>/<month>/<int:day>/byslug/<slug:slug>/', views.BookDetail.as_view()),
|
||||
|
||||
url(
|
||||
r'^dates/books/get_object_custom_queryset/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/'
|
||||
r'(?P<pk>[0-9]+)/$',
|
||||
path(
|
||||
'dates/books/get_object_custom_queryset/<int:year>/<month>/<int:day>/<int:pk>/',
|
||||
views.BookDetailGetObjectCustomQueryset.as_view(),
|
||||
),
|
||||
|
||||
url(r'^dates/booksignings/(?P<year>[0-9]{4})/(?P<month>[a-z]{3})/(?P<day>[0-9]{1,2})/(?P<pk>[0-9]+)/$',
|
||||
views.BookSigningDetail.as_view()),
|
||||
path('dates/booksignings/<int:year>/<month>/<int:day>/<int:pk>/', views.BookSigningDetail.as_view()),
|
||||
|
||||
# Useful for testing redirects
|
||||
url(r'^accounts/login/$', auth_views.LoginView.as_view())
|
||||
path('accounts/login/', auth_views.LoginView.as_view())
|
||||
]
|
||||
|
|
Binary file not shown.
|
@ -36,3 +36,7 @@ msgstr "^profiel/"
|
|||
#: urls/namespace.py:9 urls/wrong_namespace.py:10
|
||||
msgid "^register/$"
|
||||
msgstr "^registreren/$"
|
||||
|
||||
#: urls/namespace.py:12
|
||||
msgid "register-as-path/"
|
||||
msgstr "registreren-als-pad/"
|
||||
|
|
|
@ -155,6 +155,8 @@ class URLTranslationTests(URLTestCaseBase):
|
|||
self.assertEqual(translate_url('/en/users/', 'nl'), '/nl/gebruikers/')
|
||||
# Namespaced URL
|
||||
self.assertEqual(translate_url('/en/account/register/', 'nl'), '/nl/profiel/registreren/')
|
||||
# path() URL pattern
|
||||
self.assertEqual(translate_url('/en/account/register-as-path/', 'nl'), '/nl/profiel/registreren-als-pad/')
|
||||
self.assertEqual(translation.get_language(), 'en')
|
||||
|
||||
with translation.override('nl'):
|
||||
|
@ -169,9 +171,11 @@ class URLNamespaceTests(URLTestCaseBase):
|
|||
def test_account_register(self):
|
||||
with translation.override('en'):
|
||||
self.assertEqual(reverse('account:register'), '/en/account/register/')
|
||||
self.assertEqual(reverse('account:register-as-path'), '/en/account/register-as-path/')
|
||||
|
||||
with translation.override('nl'):
|
||||
self.assertEqual(reverse('account:register'), '/nl/profiel/registreren/')
|
||||
self.assertEqual(reverse('account:register-as-path'), '/nl/profiel/registreren-als-pad/')
|
||||
|
||||
|
||||
class URLRedirectTests(URLTestCaseBase):
|
||||
|
@ -322,6 +326,18 @@ class URLResponseTests(URLTestCaseBase):
|
|||
self.assertEqual(response['content-language'], 'pt-br')
|
||||
self.assertEqual(response.context['LANGUAGE_CODE'], 'pt-br')
|
||||
|
||||
def test_en_path(self):
|
||||
response = self.client.get('/en/account/register-as-path/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['content-language'], 'en')
|
||||
self.assertEqual(response.context['LANGUAGE_CODE'], 'en')
|
||||
|
||||
def test_nl_path(self):
|
||||
response = self.client.get('/nl/profiel/registreren-als-pad/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['content-language'], 'nl')
|
||||
self.assertEqual(response.context['LANGUAGE_CODE'], 'nl')
|
||||
|
||||
|
||||
class URLRedirectWithScriptAliasTests(URLTestCaseBase):
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from django.conf.urls import url
|
||||
from django.urls import path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
@ -8,4 +9,5 @@ app_name = 'account'
|
|||
urlpatterns = [
|
||||
url(_(r'^register/$'), view, name='register'),
|
||||
url(_(r'^register-without-slash$'), view, name='register-without-slash'),
|
||||
path(_('register-as-path/'), view, name='register-as-path'),
|
||||
]
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('{x}/<{x}:{x}>/'.format(x=name), views.empty_view, name=name)
|
||||
for name in ('int', 'path', 'slug', 'str', 'uuid')
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
import base64
|
||||
|
||||
|
||||
class Base64Converter:
|
||||
regex = r'[a-zA-Z0-9+/]*={0,2}'
|
||||
|
||||
def to_python(self, value):
|
||||
return base64.b64decode(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return base64.b64encode(value).decode('ascii')
|
||||
|
||||
|
||||
class DynamicConverter:
|
||||
_dynamic_to_python = None
|
||||
_dynamic_to_url = None
|
||||
|
||||
@property
|
||||
def regex(self):
|
||||
return r'[0-9a-zA-Z]+'
|
||||
|
||||
@regex.setter
|
||||
def regex(self):
|
||||
raise Exception("You can't modify the regular expression.")
|
||||
|
||||
def to_python(self, value):
|
||||
return type(self)._dynamic_to_python(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return type(self)._dynamic_to_url(value)
|
||||
|
||||
@classmethod
|
||||
def register_to_python(cls, value):
|
||||
cls._dynamic_to_python = value
|
||||
|
||||
@classmethod
|
||||
def register_to_url(cls, value):
|
||||
cls._dynamic_to_url = value
|
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path, register_converter
|
||||
|
||||
from . import converters, views
|
||||
|
||||
register_converter(converters.Base64Converter, 'base64')
|
||||
|
||||
urlpatterns = [
|
||||
path('base64/<base64:value>/', views.empty_view, name='base64'),
|
||||
]
|
|
@ -0,0 +1,9 @@
|
|||
from django.urls import path, register_converter
|
||||
|
||||
from . import converters, views
|
||||
|
||||
register_converter(converters.DynamicConverter, 'dynamic')
|
||||
|
||||
urlpatterns = [
|
||||
path('dynamic/<dynamic:value>/', views.empty_view, name='dynamic'),
|
||||
]
|
|
@ -0,0 +1,15 @@
|
|||
from django.conf.urls import include
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('articles/2003/', views.empty_view, name='articles-2003'),
|
||||
path('articles/<int:year>/', views.empty_view, name='articles-year'),
|
||||
path('articles/<int:year>/<int:month>/', views.empty_view, name='articles-year-month'),
|
||||
path('articles/<int:year>/<int:month>/<int:day>/', views.empty_view, name='articles-year-month-day'),
|
||||
path('users/', views.empty_view, name='users'),
|
||||
path('users/<id>/', views.empty_view, name='user-with-id'),
|
||||
path('included_urls/', include('urlpatterns_reverse.included_urls')),
|
||||
path('<lang>/<path:url>/', views.empty_view, name='lang-and-path'),
|
||||
]
|
|
@ -0,0 +1,165 @@
|
|||
import uuid
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import Resolver404, path, resolve, reverse
|
||||
|
||||
from .converters import DynamicConverter
|
||||
from .views import empty_view
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='urlpatterns.path_urls')
|
||||
class SimplifiedURLTests(SimpleTestCase):
|
||||
|
||||
def test_path_lookup_without_parameters(self):
|
||||
match = resolve('/articles/2003/')
|
||||
self.assertEqual(match.url_name, 'articles-2003')
|
||||
self.assertEqual(match.args, ())
|
||||
self.assertEqual(match.kwargs, {})
|
||||
|
||||
def test_path_lookup_with_typed_parameters(self):
|
||||
match = resolve('/articles/2015/')
|
||||
self.assertEqual(match.url_name, 'articles-year')
|
||||
self.assertEqual(match.args, ())
|
||||
self.assertEqual(match.kwargs, {'year': 2015})
|
||||
|
||||
def test_path_lookup_with_multiple_paramaters(self):
|
||||
match = resolve('/articles/2015/04/12/')
|
||||
self.assertEqual(match.url_name, 'articles-year-month-day')
|
||||
self.assertEqual(match.args, ())
|
||||
self.assertEqual(match.kwargs, {'year': 2015, 'month': 4, 'day': 12})
|
||||
|
||||
def test_two_variable_at_start_of_path_pattern(self):
|
||||
match = resolve('/en/foo/')
|
||||
self.assertEqual(match.url_name, 'lang-and-path')
|
||||
self.assertEqual(match.kwargs, {'lang': 'en', 'url': 'foo'})
|
||||
|
||||
def test_path_reverse_without_parameter(self):
|
||||
url = reverse('articles-2003')
|
||||
self.assertEqual(url, '/articles/2003/')
|
||||
|
||||
def test_path_reverse_with_parameter(self):
|
||||
url = reverse('articles-year-month-day', kwargs={'year': 2015, 'month': 4, 'day': 12})
|
||||
self.assertEqual(url, '/articles/2015/4/12/')
|
||||
|
||||
@override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
|
||||
def test_non_identical_converter_resolve(self):
|
||||
match = resolve('/base64/aGVsbG8=/') # base64 of 'hello'
|
||||
self.assertEqual(match.url_name, 'base64')
|
||||
self.assertEqual(match.kwargs, {'value': b'hello'})
|
||||
|
||||
@override_settings(ROOT_URLCONF='urlpatterns.path_base64_urls')
|
||||
def test_non_identical_converter_reverse(self):
|
||||
url = reverse('base64', kwargs={'value': b'hello'})
|
||||
self.assertEqual(url, '/base64/aGVsbG8=/')
|
||||
|
||||
def test_path_inclusion_is_matchable(self):
|
||||
match = resolve('/included_urls/extra/something/')
|
||||
self.assertEqual(match.url_name, 'inner-extra')
|
||||
self.assertEqual(match.kwargs, {'extra': 'something'})
|
||||
|
||||
def test_path_inclusion_is_reversable(self):
|
||||
url = reverse('inner-extra', kwargs={'extra': 'something'})
|
||||
self.assertEqual(url, '/included_urls/extra/something/')
|
||||
|
||||
def test_invalid_converter(self):
|
||||
msg = "URL route 'foo/<nonexistent:var>/' uses invalid converter 'nonexistent'."
|
||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
||||
path('foo/<nonexistent:var>/', empty_view)
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='urlpatterns.converter_urls')
|
||||
class ConverterTests(SimpleTestCase):
|
||||
|
||||
def test_matching_urls(self):
|
||||
def no_converter(x):
|
||||
return x
|
||||
|
||||
test_data = (
|
||||
('int', {'0', '1', '01', 1234567890}, int),
|
||||
('str', {'abcxyz'}, no_converter),
|
||||
('path', {'allows.ANY*characters'}, no_converter),
|
||||
('slug', {'abcxyz-ABCXYZ_01234567890'}, no_converter),
|
||||
('uuid', {'39da9369-838e-4750-91a5-f7805cd82839'}, uuid.UUID),
|
||||
)
|
||||
for url_name, url_suffixes, converter in test_data:
|
||||
for url_suffix in url_suffixes:
|
||||
url = '/%s/%s/' % (url_name, url_suffix)
|
||||
with self.subTest(url=url):
|
||||
match = resolve(url)
|
||||
self.assertEqual(match.url_name, url_name)
|
||||
self.assertEqual(match.kwargs, {url_name: converter(url_suffix)})
|
||||
# reverse() works with string parameters.
|
||||
string_kwargs = {url_name: url_suffix}
|
||||
self.assertEqual(reverse(url_name, kwargs=string_kwargs), url)
|
||||
# reverse() also works with native types (int, UUID, etc.).
|
||||
if converter is not no_converter:
|
||||
# The converted value might be different for int (a
|
||||
# leading zero is lost in the conversion).
|
||||
converted_value = match.kwargs[url_name]
|
||||
converted_url = '/%s/%s/' % (url_name, converted_value)
|
||||
self.assertEqual(reverse(url_name, kwargs={url_name: converted_value}), converted_url)
|
||||
|
||||
def test_nonmatching_urls(self):
|
||||
test_data = (
|
||||
('int', {'-1', 'letters'}),
|
||||
('str', {'', '/'}),
|
||||
('path', {''}),
|
||||
('slug', {'', 'stars*notallowed'}),
|
||||
('uuid', {
|
||||
'',
|
||||
'9da9369-838e-4750-91a5-f7805cd82839',
|
||||
'39da9369-838-4750-91a5-f7805cd82839',
|
||||
'39da9369-838e-475-91a5-f7805cd82839',
|
||||
'39da9369-838e-4750-91a-f7805cd82839',
|
||||
'39da9369-838e-4750-91a5-f7805cd8283',
|
||||
}),
|
||||
)
|
||||
for url_name, url_suffixes in test_data:
|
||||
for url_suffix in url_suffixes:
|
||||
url = '/%s/%s/' % (url_name, url_suffix)
|
||||
with self.subTest(url=url), self.assertRaises(Resolver404):
|
||||
resolve(url)
|
||||
|
||||
|
||||
class ParameterRestrictionTests(SimpleTestCase):
|
||||
def test_non_identifier_parameter_name_causes_exception(self):
|
||||
msg = (
|
||||
"URL route 'hello/<int:1>/' uses parameter name '1' which isn't "
|
||||
"a valid Python identifier."
|
||||
)
|
||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
||||
path(r'hello/<int:1>/', lambda r: None)
|
||||
|
||||
def test_allows_non_ascii_but_valid_identifiers(self):
|
||||
# \u0394 is "GREEK CAPITAL LETTER DELTA", a valid identifier.
|
||||
p = path('hello/<str:\u0394>/', lambda r: None)
|
||||
match = p.resolve('hello/1/')
|
||||
self.assertEqual(match.kwargs, {'\u0394': '1'})
|
||||
|
||||
|
||||
@override_settings(ROOT_URLCONF='urlpatterns.path_dynamic_urls')
|
||||
class ConversionExceptionTests(SimpleTestCase):
|
||||
"""How are errors in Converter.to_python() and to_url() handled?"""
|
||||
|
||||
def test_resolve_value_error_means_no_match(self):
|
||||
@DynamicConverter.register_to_python
|
||||
def raises_value_error(value):
|
||||
raise ValueError()
|
||||
with self.assertRaises(Resolver404):
|
||||
resolve('/dynamic/abc/')
|
||||
|
||||
def test_resolve_type_error_propogates(self):
|
||||
@DynamicConverter.register_to_python
|
||||
def raises_type_error(value):
|
||||
raise TypeError('This type error propagates.')
|
||||
with self.assertRaisesMessage(TypeError, 'This type error propagates.'):
|
||||
resolve('/dynamic/abc/')
|
||||
|
||||
def test_reverse_value_error_propagates(self):
|
||||
@DynamicConverter.register_to_url
|
||||
def raises_value_error(value):
|
||||
raise ValueError('This value error propagates.')
|
||||
with self.assertRaisesMessage(ValueError, 'This value error propagates.'):
|
||||
reverse('dynamic', kwargs={'value': object()})
|
|
@ -0,0 +1,5 @@
|
|||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def empty_view(request, *args, **kwargs):
|
||||
return HttpResponse('')
|
|
@ -3,15 +3,14 @@ from unittest import mock
|
|||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
from django.urls import LocaleRegexProvider
|
||||
from django.urls.resolvers import LocaleRegexDescriptor
|
||||
from django.urls.resolvers import LocaleRegexDescriptor, RegexPattern
|
||||
from django.utils import translation
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
@override_settings(LOCALE_PATHS=[os.path.join(here, 'translations', 'locale')])
|
||||
class LocaleRegexProviderTests(SimpleTestCase):
|
||||
class LocaleRegexDescriptorTests(SimpleTestCase):
|
||||
def setUp(self):
|
||||
translation.trans_real._translations = {}
|
||||
|
||||
|
@ -19,7 +18,7 @@ class LocaleRegexProviderTests(SimpleTestCase):
|
|||
translation.trans_real._translations = {}
|
||||
|
||||
def test_translated_regex_compiled_per_language(self):
|
||||
provider = LocaleRegexProvider(translation.gettext_lazy('^foo/$'))
|
||||
provider = RegexPattern(translation.gettext_lazy('^foo/$'))
|
||||
with translation.override('de'):
|
||||
de_compiled = provider.regex
|
||||
# compiled only once per language
|
||||
|
@ -33,7 +32,7 @@ class LocaleRegexProviderTests(SimpleTestCase):
|
|||
self.assertEqual(de_compiled, de_compiled_2)
|
||||
|
||||
def test_nontranslated_regex_compiled_once(self):
|
||||
provider = LocaleRegexProvider('^foo/$')
|
||||
provider = RegexPattern('^foo/$')
|
||||
with translation.override('de'):
|
||||
de_compiled = provider.regex
|
||||
with translation.override('fr'):
|
||||
|
@ -46,10 +45,10 @@ class LocaleRegexProviderTests(SimpleTestCase):
|
|||
|
||||
def test_regex_compile_error(self):
|
||||
"""Regex errors are re-raised as ImproperlyConfigured."""
|
||||
provider = LocaleRegexProvider('*')
|
||||
provider = RegexPattern('*')
|
||||
msg = '"*" is not a valid regular expression: nothing to repeat'
|
||||
with self.assertRaisesMessage(ImproperlyConfigured, msg):
|
||||
provider.regex
|
||||
|
||||
def test_access_locale_regex_descriptor(self):
|
||||
self.assertIsInstance(LocaleRegexProvider.regex, LocaleRegexDescriptor)
|
||||
self.assertIsInstance(RegexPattern.regex, LocaleRegexDescriptor)
|
|
@ -17,9 +17,10 @@ from django.shortcuts import redirect
|
|||
from django.test import SimpleTestCase, TestCase, override_settings
|
||||
from django.test.utils import override_script_prefix
|
||||
from django.urls import (
|
||||
NoReverseMatch, RegexURLPattern, RegexURLResolver, Resolver404,
|
||||
ResolverMatch, get_callable, get_resolver, resolve, reverse, reverse_lazy,
|
||||
NoReverseMatch, Resolver404, ResolverMatch, URLPattern, URLResolver,
|
||||
get_callable, get_resolver, resolve, reverse, reverse_lazy,
|
||||
)
|
||||
from django.urls.resolvers import RegexPattern
|
||||
|
||||
from . import middleware, urlconf_outer, views
|
||||
from .utils import URLObject
|
||||
|
@ -259,9 +260,9 @@ class NoURLPatternsTests(SimpleTestCase):
|
|||
|
||||
def test_no_urls_exception(self):
|
||||
"""
|
||||
RegexURLResolver should raise an exception when no urlpatterns exist.
|
||||
URLResolver should raise an exception when no urlpatterns exist.
|
||||
"""
|
||||
resolver = RegexURLResolver(r'^$', settings.ROOT_URLCONF)
|
||||
resolver = URLResolver(RegexPattern(r'^$'), settings.ROOT_URLCONF)
|
||||
|
||||
with self.assertRaisesMessage(
|
||||
ImproperlyConfigured,
|
||||
|
@ -368,13 +369,13 @@ class URLPatternReverse(SimpleTestCase):
|
|||
class ResolverTests(SimpleTestCase):
|
||||
def test_resolver_repr(self):
|
||||
"""
|
||||
Test repr of RegexURLResolver, especially when urlconf_name is a list
|
||||
Test repr of URLResolver, especially when urlconf_name is a list
|
||||
(#17892).
|
||||
"""
|
||||
# Pick a resolver from a namespaced URLconf
|
||||
resolver = get_resolver('urlpatterns_reverse.namespace_urls')
|
||||
sub_resolver = resolver.namespace_dict['test-ns1'][1]
|
||||
self.assertIn('<RegexURLPattern list>', repr(sub_resolver))
|
||||
self.assertIn('<URLPattern list>', repr(sub_resolver))
|
||||
|
||||
def test_reverse_lazy_object_coercion_by_resolve(self):
|
||||
"""
|
||||
|
@ -445,13 +446,13 @@ class ResolverTests(SimpleTestCase):
|
|||
# you try to resolve a nonexistent URL in the first level of included
|
||||
# URLs in named_urls.py (e.g., '/included/nonexistent-url')
|
||||
url_types_names = [
|
||||
[{'type': RegexURLPattern, 'name': 'named-url1'}],
|
||||
[{'type': RegexURLPattern, 'name': 'named-url2'}],
|
||||
[{'type': RegexURLPattern, 'name': None}],
|
||||
[{'type': RegexURLResolver}, {'type': RegexURLPattern, 'name': 'named-url3'}],
|
||||
[{'type': RegexURLResolver}, {'type': RegexURLPattern, 'name': 'named-url4'}],
|
||||
[{'type': RegexURLResolver}, {'type': RegexURLPattern, 'name': None}],
|
||||
[{'type': RegexURLResolver}, {'type': RegexURLResolver}],
|
||||
[{'type': URLPattern, 'name': 'named-url1'}],
|
||||
[{'type': URLPattern, 'name': 'named-url2'}],
|
||||
[{'type': URLPattern, 'name': None}],
|
||||
[{'type': URLResolver}, {'type': URLPattern, 'name': 'named-url3'}],
|
||||
[{'type': URLResolver}, {'type': URLPattern, 'name': 'named-url4'}],
|
||||
[{'type': URLResolver}, {'type': URLPattern, 'name': None}],
|
||||
[{'type': URLResolver}, {'type': URLResolver}],
|
||||
]
|
||||
with self.assertRaisesMessage(Resolver404, 'tried') as cm:
|
||||
resolve('/included/nonexistent-url', urlconf=urls)
|
||||
|
@ -494,10 +495,10 @@ class ResolverTests(SimpleTestCase):
|
|||
|
||||
def test_populate_concurrency(self):
|
||||
"""
|
||||
RegexURLResolver._populate() can be called concurrently, but not more
|
||||
URLResolver._populate() can be called concurrently, but not more
|
||||
than once per thread (#26888).
|
||||
"""
|
||||
resolver = RegexURLResolver(r'^/', 'urlpatterns_reverse.urls')
|
||||
resolver = URLResolver(RegexPattern(r'^/'), 'urlpatterns_reverse.urls')
|
||||
resolver._local.populating = True
|
||||
thread = threading.Thread(target=resolver._populate)
|
||||
thread.start()
|
||||
|
@ -1039,8 +1040,8 @@ class ErrorHandlerResolutionTests(SimpleTestCase):
|
|||
def setUp(self):
|
||||
urlconf = 'urlpatterns_reverse.urls_error_handlers'
|
||||
urlconf_callables = 'urlpatterns_reverse.urls_error_handlers_callables'
|
||||
self.resolver = RegexURLResolver(r'^$', urlconf)
|
||||
self.callable_resolver = RegexURLResolver(r'^$', urlconf_callables)
|
||||
self.resolver = URLResolver(RegexPattern(r'^$'), urlconf)
|
||||
self.callable_resolver = URLResolver(RegexPattern(r'^$'), urlconf_callables)
|
||||
|
||||
def test_named_handlers(self):
|
||||
handler = (empty_view, {})
|
||||
|
|
|
@ -112,7 +112,14 @@ class DebugViewTests(LoggingCaptureMixin, SimpleTestCase):
|
|||
def test_404_not_in_urls(self):
|
||||
response = self.client.get('/not-in-urls')
|
||||
self.assertNotContains(response, "Raised by:", status_code=404)
|
||||
self.assertContains(response, "Django tried these URL patterns", status_code=404)
|
||||
self.assertContains(response, "<code>not-in-urls</code>, didn't match", status_code=404)
|
||||
# Pattern and view name of a RegexURLPattern appear.
|
||||
self.assertContains(response, r"^regex-post/(?P<pk>[0-9]+)/$", status_code=404)
|
||||
self.assertContains(response, "[name='regex-post']", status_code=404)
|
||||
# Pattern and view name of a RoutePattern appear.
|
||||
self.assertContains(response, r"path-post/<int:pk>/", status_code=404)
|
||||
self.assertContains(response, "[name='path-post']", status_code=404)
|
||||
|
||||
@override_settings(ROOT_URLCONF=WithoutEmptyPathUrls)
|
||||
def test_404_empty_path_not_in_urls(self):
|
||||
|
|
|
@ -141,7 +141,7 @@ class StaticHelperTest(StaticTests):
|
|||
urls.urlpatterns = self._old_views_urlpatterns
|
||||
|
||||
def test_prefix(self):
|
||||
self.assertEqual(static('test')[0].regex.pattern, '^test(?P<path>.*)$')
|
||||
self.assertEqual(static('test')[0].pattern.regex.pattern, '^test(?P<path>.*)$')
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
def test_debug_off(self):
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import os
|
||||
from functools import partial
|
||||
from os import path
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.i18n import i18n_patterns
|
||||
from django.urls import path, re_path
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import defaults, i18n, static
|
||||
|
||||
from . import views
|
||||
|
||||
base_dir = path.dirname(path.abspath(__file__))
|
||||
media_dir = path.join(base_dir, 'media')
|
||||
locale_dir = path.join(base_dir, 'locale')
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
media_dir = os.path.join(base_dir, 'media')
|
||||
locale_dir = os.path.join(base_dir, 'locale')
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.index_page),
|
||||
|
@ -64,4 +65,7 @@ urlpatterns += [
|
|||
),
|
||||
url(r'^render_no_template/$', views.render_no_template, name='render_no_template'),
|
||||
url(r'^test-setlang/(?P<parameter>[^/]+)/$', views.with_parameter, name='with_parameter'),
|
||||
# Patterns to test the technical 404.
|
||||
re_path(r'^regex-post/(?P<pk>[0-9]+)/$', views.index_page, name='regex-post'),
|
||||
path('path-post/<int:pk>/', views.index_page, name='path-post'),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue