Browse Source

Fixed #27367 -- Doc'd and tested reversing of URLs with the same name.

Thanks Reinout van Rees for contributing to the patch.
Robert Roskam 8 years ago
parent
commit
98bcc5d81b

+ 20 - 7
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
 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.
 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
 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:
 .. _topics-http-defining-url-namespaces:
 
 

+ 17 - 0
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<first>\w+)/$', empty_view, name='name-conflict'),
+    url(r'^conflict-cannot-go-here/(?P<middle>\w+)/$', empty_view, name='name-conflict'),
+    url(r'^conflict-middle/(?P<middle>\w+)/$', empty_view, name='name-conflict'),
+    url(r'^conflict-last/(?P<last>\w+)/$', empty_view, name='name-conflict'),
+    # Two kwargs
+    url(r'^conflict/(?P<another>\w+)/(?P<extra>\w+)/cannot-go-here/$', empty_view, name='name-conflict'),
+    url(r'^conflict/(?P<extra>\w+)/(?P<another>\w+)/$', empty_view, name='name-conflict'),
+]

+ 17 - 0
tests/urlpatterns_reverse/tests.py

@@ -386,6 +386,23 @@ class ResolverTests(SimpleTestCase):
         self.assertEqual(resolver.reverse('named-url2', 'arg'), 'extra/arg/')
         self.assertEqual(resolver.reverse('named-url2', 'arg'), 'extra/arg/')
         self.assertEqual(resolver.reverse('named-url2', extra='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):
     def test_non_regex(self):
         """
         """
         A Resolver404 is raised if resolving doesn't meet the basic
         A Resolver404 is raised if resolving doesn't meet the basic