From c0916144502b2b9d75b75ce6b01148aaa3dcaf68 Mon Sep 17 00:00:00 2001 From: Robert Roskam Date: Sat, 5 Nov 2016 10:48:31 -0400 Subject: [PATCH] [1.11.x] Fixed #27367 -- Doc'd and tested reversing of URLs with the same name. Thanks Reinout van Rees for contributing to the patch. Backport of 98bcc5d81bca578f3a5b4d47907ba4ac40446887 from master --- docs/topics/http/urls.txt | 27 ++++++++++++++----- .../named_urls_conflict.py | 17 ++++++++++++ tests/urlpatterns_reverse/tests.py | 17 ++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 tests/urlpatterns_reverse/named_urls_conflict.py diff --git a/docs/topics/http/urls.txt b/docs/topics/http/urls.txt index 59b3b698f1..6e891e72f3 100644 --- a/docs/topics/http/urls.txt +++ b/docs/topics/http/urls.txt @@ -597,15 +597,28 @@ In order to perform URL reversing, you'll need to use **named URL patterns** as done in the examples above. The string used for the URL name can contain any characters you like. You are not restricted to valid Python names. -When you name your URL patterns, make sure you use names that are unlikely -to clash with any other application's choice of names. If you call your URL -pattern ``comment``, and another application does the same thing, there's -no guarantee which URL will be inserted into your template when you use -this name. +When naming URL patterns, choose names that are unlikely to clash with other +applications' choice of names. If you call your URL pattern ``comment`` +and another application does the same thing, the URL that +:func:`~django.urls.reverse()` finds depends on whichever pattern is last in +your project's ``urlpatterns`` list. Putting a prefix on your URL names, perhaps derived from the application -name, will decrease the chances of collision. We recommend something like -``myapp-comment`` instead of ``comment``. +name (such as ``myapp-comment`` instead of ``comment``), decreases the chance +of collision. + +You can deliberately choose the *same URL name* as another application if you +want to override a view. For example, a common use case is to override the +:class:`~django.contrib.auth.views.LoginView`. Parts of Django and most +third-party apps assume that this view has a URL pattern with the name +``login``. If you have a custom login view and give its URL the name ``login``, +:func:`~django.urls.reverse()` will find your custom view as long as it's in +``urlpatterns`` after ``django.contrib.auth.urls`` is included (if that's +included at all). + +You may also use the same name for multiple URL patterns if they differ in +their arguments. In addition to the URL name, :func:`~django.urls.reverse()` +matches the number of arguments and the names of the keyword arguments. .. _topics-http-defining-url-namespaces: diff --git a/tests/urlpatterns_reverse/named_urls_conflict.py b/tests/urlpatterns_reverse/named_urls_conflict.py new file mode 100644 index 0000000000..0c0c6eb47e --- /dev/null +++ b/tests/urlpatterns_reverse/named_urls_conflict.py @@ -0,0 +1,17 @@ +from django.conf.urls import url + +from .views import empty_view + +urlpatterns = [ + # No kwargs + url(r'^conflict/cannot-go-here/$', empty_view, name='name-conflict'), + url(r'^conflict/$', empty_view, name='name-conflict'), + # One kwarg + url(r'^conflict-first/(?P\w+)/$', empty_view, name='name-conflict'), + url(r'^conflict-cannot-go-here/(?P\w+)/$', empty_view, name='name-conflict'), + url(r'^conflict-middle/(?P\w+)/$', empty_view, name='name-conflict'), + url(r'^conflict-last/(?P\w+)/$', empty_view, name='name-conflict'), + # Two kwargs + url(r'^conflict/(?P\w+)/(?P\w+)/cannot-go-here/$', empty_view, name='name-conflict'), + url(r'^conflict/(?P\w+)/(?P\w+)/$', empty_view, name='name-conflict'), +] diff --git a/tests/urlpatterns_reverse/tests.py b/tests/urlpatterns_reverse/tests.py index 8dbd57a27d..2cb933df37 100644 --- a/tests/urlpatterns_reverse/tests.py +++ b/tests/urlpatterns_reverse/tests.py @@ -382,6 +382,23 @@ class ResolverTests(SimpleTestCase): self.assertEqual(resolver.reverse('named-url2', 'arg'), 'extra/arg/') self.assertEqual(resolver.reverse('named-url2', extra='arg'), 'extra/arg/') + def test_resolver_reverse_conflict(self): + """ + url() name arguments don't need to be unique. The last registered + pattern takes precedence for conflicting names. + """ + resolver = get_resolver('urlpatterns_reverse.named_urls_conflict') + # Without arguments, the last URL in urlpatterns has precedence. + self.assertEqual(resolver.reverse('name-conflict'), 'conflict/') + # With an arg, the last URL in urlpatterns has precedence. + self.assertEqual(resolver.reverse('name-conflict', 'arg'), 'conflict-last/arg/') + # With a kwarg, other url()s can be reversed. + self.assertEqual(resolver.reverse('name-conflict', first='arg'), 'conflict-first/arg/') + self.assertEqual(resolver.reverse('name-conflict', middle='arg'), 'conflict-middle/arg/') + self.assertEqual(resolver.reverse('name-conflict', last='arg'), 'conflict-last/arg/') + # The number and order of the arguments don't interfere with reversing. + self.assertEqual(resolver.reverse('name-conflict', 'arg', 'arg'), 'conflict/arg/arg/') + def test_non_regex(self): """ A Resolver404 is raised if resolving doesn't meet the basic