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:
Sjoerd Job Postmus 2016-10-20 19:29:04 +02:00 committed by Tim Graham
parent c4c128d67c
commit df41b5a05d
77 changed files with 1663 additions and 1105 deletions

View File

@ -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

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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:

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {}

View File

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

View File

@ -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)

70
django/urls/converters.py Normal file
View File

@ -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]

View File

@ -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]

View File

@ -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:

View File

@ -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>

View File

@ -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)

View File

@ -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
================

View File

@ -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.

View File

@ -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

View File

@ -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:

View File

@ -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
-----------

View File

@ -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

View File

@ -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**

View File

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

View File

@ -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**:

View File

@ -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

View File

@ -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::

View File

@ -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

View File

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

View File

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

View File

@ -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:

View File

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

View File

@ -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

View File

@ -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()),
# ...
]

View File

@ -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

View File

@ -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::

View File

@ -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``
==============

View File

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

View File

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

View File

@ -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`.

View File

@ -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

View File

@ -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``
---------------------------------

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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::

View File

@ -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::

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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/``.)

View File

@ -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]

View File

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

View File

@ -0,0 +1,5 @@
from django.urls import include, path
urlpatterns = [
path('', include([(r'^tuple/$', lambda x: x)])),
]

View File

@ -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())
]

View File

@ -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/"

View File

@ -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):
"""

View File

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

View File

View File

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

View File

@ -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

View File

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

View File

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

View File

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

165
tests/urlpatterns/tests.py Normal file
View File

@ -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()})

View File

@ -0,0 +1,5 @@
from django.http import HttpResponse
def empty_view(request, *args, **kwargs):
return HttpResponse('')

View File

@ -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)

View File

@ -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, {})

View File

@ -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&lt;pk&gt;[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/&lt;int:pk&gt;/", 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):

View File

@ -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):

View File

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