diff options
Diffstat (limited to 'tests')
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 Binary files differindex e3109784c7..5eac50466c 100644 --- a/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo +++ b/tests/i18n/patterns/locale/nl/LC_MESSAGES/django.mo 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<pk>[0-9]+)/$", status_code=404) + self.assertContains(response, "[name='regex-post']", status_code=404) + # Pattern and view name of a RoutePattern appear. + self.assertContains(response, r"path-post/<int:pk>/", status_code=404) + self.assertContains(response, "[name='path-post']", status_code=404) @override_settings(ROOT_URLCONF=WithoutEmptyPathUrls) def test_404_empty_path_not_in_urls(self): 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'), ] |
