summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/check_framework/test_urls.py43
-rw-r--r--tests/check_framework/urls/beginning_with_slash.py4
-rw-r--r--tests/check_framework/urls/include_contains_tuple.py5
-rw-r--r--tests/generic_views/urls.py428
-rw-r--r--tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mobin697 -> 752 bytes
-rw-r--r--tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po4
-rw-r--r--tests/i18n/patterns/tests.py16
-rw-r--r--tests/i18n/patterns/urls/namespace.py2
-rw-r--r--tests/urlpatterns/__init__.py0
-rw-r--r--tests/urlpatterns/converter_urls.py8
-rw-r--r--tests/urlpatterns/converters.py38
-rw-r--r--tests/urlpatterns/path_base64_urls.py9
-rw-r--r--tests/urlpatterns/path_dynamic_urls.py9
-rw-r--r--tests/urlpatterns/path_urls.py15
-rw-r--r--tests/urlpatterns/tests.py165
-rw-r--r--tests/urlpatterns/views.py5
-rw-r--r--tests/urlpatterns_reverse/test_localeregexdescriptor.py (renamed from tests/urlpatterns_reverse/test_localeregexprovider.py)13
-rw-r--r--tests/urlpatterns_reverse/tests.py35
-rw-r--r--tests/view_tests/tests/test_debug.py7
-rw-r--r--tests/view_tests/tests/test_static.py2
-rw-r--r--tests/view_tests/urls.py12
21 files changed, 523 insertions, 297 deletions
diff --git a/tests/check_framework/test_urls.py b/tests/check_framework/test_urls.py
index 07acab599b..5a0d811433 100644
--- a/tests/check_framework/test_urls.py
+++ b/tests/check_framework/test_urls.py
@@ -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.beginning_with_slash')
- def test_beginning_with_slash(self):
+ @override_settings(ROOT_URLCONF='check_framework.urls.include_contains_tuple')
+ def test_contains_included_tuple(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 '/'."
- )
+ 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\.$"
+ ))
- self.assertIn(expected_msg, warning.msg)
+ @override_settings(ROOT_URLCONF='check_framework.urls.beginning_with_slash')
+ def test_beginning_with_slash(self):
+ 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 '/'."
+ )
+ 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]
diff --git a/tests/check_framework/urls/beginning_with_slash.py b/tests/check_framework/urls/beginning_with_slash.py
index 736e91820e..8dac96745c 100644
--- a/tests/check_framework/urls/beginning_with_slash.py
+++ b/tests/check_framework/urls/beginning_with_slash.py
@@ -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),
]
diff --git a/tests/check_framework/urls/include_contains_tuple.py b/tests/check_framework/urls/include_contains_tuple.py
new file mode 100644
index 0000000000..02717a743c
--- /dev/null
+++ b/tests/check_framework/urls/include_contains_tuple.py
@@ -0,0 +1,5 @@
+from django.urls import include, path
+
+urlpatterns = [
+ path('', include([(r'^tuple/$', lambda x: x)])),
+]
diff --git a/tests/generic_views/urls.py b/tests/generic_views/urls.py
index 209ce2285f..b8ba5762c4 100644
--- a/tests/generic_views/urls.py
+++ b/tests/generic_views/urls.py
@@ -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())
]
diff --git a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
index e3109784c7..5eac50466c 100644
--- a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
+++ b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
index 5c31aa7f8a..a938e3371d 100644
--- a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
+++ b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.po
@@ -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/"
diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py
index de154c2343..2423614f08 100644
--- a/tests/i18n/patterns/tests.py
+++ b/tests/i18n/patterns/tests.py
@@ -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):
"""
diff --git a/tests/i18n/patterns/urls/namespace.py b/tests/i18n/patterns/urls/namespace.py
index 2f19640d37..9858c8cd5e 100644
--- a/tests/i18n/patterns/urls/namespace.py
+++ b/tests/i18n/patterns/urls/namespace.py
@@ -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'),
]
diff --git a/tests/urlpatterns/__init__.py b/tests/urlpatterns/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/urlpatterns/__init__.py
diff --git a/tests/urlpatterns/converter_urls.py b/tests/urlpatterns/converter_urls.py
new file mode 100644
index 0000000000..0cc57bd46e
--- /dev/null
+++ b/tests/urlpatterns/converter_urls.py
@@ -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')
+]
diff --git a/tests/urlpatterns/converters.py b/tests/urlpatterns/converters.py
new file mode 100644
index 0000000000..14a65b0483
--- /dev/null
+++ b/tests/urlpatterns/converters.py
@@ -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
diff --git a/tests/urlpatterns/path_base64_urls.py b/tests/urlpatterns/path_base64_urls.py
new file mode 100644
index 0000000000..872636f06c
--- /dev/null
+++ b/tests/urlpatterns/path_base64_urls.py
@@ -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'),
+]
diff --git a/tests/urlpatterns/path_dynamic_urls.py b/tests/urlpatterns/path_dynamic_urls.py
new file mode 100644
index 0000000000..0e5afe1df0
--- /dev/null
+++ b/tests/urlpatterns/path_dynamic_urls.py
@@ -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'),
+]
diff --git a/tests/urlpatterns/path_urls.py b/tests/urlpatterns/path_urls.py
new file mode 100644
index 0000000000..097f3cb166
--- /dev/null
+++ b/tests/urlpatterns/path_urls.py
@@ -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'),
+]
diff --git a/tests/urlpatterns/tests.py b/tests/urlpatterns/tests.py
new file mode 100644
index 0000000000..b200aed06d
--- /dev/null
+++ b/tests/urlpatterns/tests.py
@@ -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()})
diff --git a/tests/urlpatterns/views.py b/tests/urlpatterns/views.py
new file mode 100644
index 0000000000..50e23d1782
--- /dev/null
+++ b/tests/urlpatterns/views.py
@@ -0,0 +1,5 @@
+from django.http import HttpResponse
+
+
+def empty_view(request, *args, **kwargs):
+ return HttpResponse('')
diff --git a/tests/urlpatterns_reverse/test_localeregexprovider.py b/tests/urlpatterns_reverse/test_localeregexdescriptor.py
index 1fbba849c6..25e6cd962a 100644
--- a/tests/urlpatterns_reverse/test_localeregexprovider.py
+++ b/tests/urlpatterns_reverse/test_localeregexdescriptor.py
@@ -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)
diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py
index e3b522f55f..86f7ed5667 100644
--- a/tests/urlpatterns_reverse/tests.py
+++ b/tests/urlpatterns_reverse/tests.py
@@ -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, {})
diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py
index 71c60210e4..b677fd98c9 100644
--- a/tests/view_tests/tests/test_debug.py
+++ b/tests/view_tests/tests/test_debug.py
@@ -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):
diff --git a/tests/view_tests/tests/test_static.py b/tests/view_tests/tests/test_static.py
index ab376ba386..03cb19ea02 100644
--- a/tests/view_tests/tests/test_static.py
+++ b/tests/view_tests/tests/test_static.py
@@ -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):
diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py
index 1c1f0d0e12..c487dd7cb9 100644
--- a/tests/view_tests/urls.py
+++ b/tests/view_tests/urls.py
@@ -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'),
]