Fixed #13525 -- Added tests and docs for nested parameters in URL patterns.

When reversing, only outer parameters are used if captured parameters are
nested. Added tests to check the edge cases and documentation for the
behavior with an example to avoid it.
This commit is contained in:
Bas Peschier 2015-03-19 08:56:38 +01:00 committed by Tim Graham
parent f2e4d39a71
commit 23a5d64f40
3 changed files with 59 additions and 0 deletions

View File

@ -368,6 +368,42 @@ the following example is valid::
In the above example, the captured ``"username"`` variable is passed to the In the above example, the captured ``"username"`` variable is passed to the
included URLconf, as expected. included URLconf, as expected.
Nested arguments
================
Regular expressions allow nested arguments, and Django will resolve them and
pass them to the view. When reversing, Django will try to fill in all outer
captured arguments, ignoring any nested captured arguments. Consider the
following URL patterns which optionally take a page argument::
from django.conf.urls import url
urlpatterns = [
url(r'blog/(page-(\d+)/)?$', blog_articles), # bad
url(r'comments/(?:page-(?P<page_number>\d+)/)?$', comments), # good
]
Both patterns use nested arguments and will resolve: for example,
``blog/page-2/`` will result in a match to ``blog_articles`` with two
positional arguments: ``page-2/`` and ``2``. The second pattern for
``comments`` will match ``comments/page-2/`` with keyword argument
``page_number`` set to 2. The outer argument in this case is a non-capturing
argument ``(?:...)``.
The ``blog_articles`` view needs the outermost captured argument to be reversed,
``page-2/`` or no arguments in this case, while ``comments`` can be reversed
with either no arguments or a value for ``page_number``.
Nested captured arguments create a strong coupling between the view arguments
and the URL as illustrated by ``blog_articles``: the view receives part of the
URL (``page-2/``) instead of only the value the view is interested in. This
coupling is even more pronounced when reversing, since to reverse the view we
need to pass the piece of URL instead of the page number.
As a rule of thumb, only capture the values the view needs to work with and
use non-capturing arguments when the regular expression needs an argument but
the view ignores it.
.. _views-extra-options: .. _views-extra-options:
Passing extra options to view functions Passing extra options to view functions

View File

@ -97,6 +97,12 @@ test_data = (
('people_backref', '/people/nate-nate/', [], {'name': 'nate'}), ('people_backref', '/people/nate-nate/', [], {'name': 'nate'}),
('optional', '/optional/fred/', [], {'name': 'fred'}), ('optional', '/optional/fred/', [], {'name': 'fred'}),
('optional', '/optional/fred/', ['fred'], {}), ('optional', '/optional/fred/', ['fred'], {}),
('named_optional', '/optional/1/', [1], {}),
('named_optional', '/optional/1/', [], {'arg1': 1}),
('named_optional', '/optional/1/2/', [1, 2], {}),
('named_optional', '/optional/1/2/', [], {'arg1': 1, 'arg2': 2}),
('named_optional_terminated', '/optional/1/2/', [1, 2], {}),
('named_optional_terminated', '/optional/1/2/', [], {'arg1': 1, 'arg2': 2}),
('hardcoded', '/hardcoded/', [], {}), ('hardcoded', '/hardcoded/', [], {}),
('hardcoded2', '/hardcoded/doc.pdf', [], {}), ('hardcoded2', '/hardcoded/doc.pdf', [], {}),
('people3', '/people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}), ('people3', '/people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}),
@ -145,6 +151,17 @@ test_data = (
('part2', '/prefix/xx/part2/one/', [], {'value': 'one', 'prefix': 'xx'}), ('part2', '/prefix/xx/part2/one/', [], {'value': 'one', 'prefix': 'xx'}),
('part2', '/prefix/xx/part2/', [], {'prefix': 'xx'}), ('part2', '/prefix/xx/part2/', [], {'prefix': 'xx'}),
# Tests for nested groups. Nested capturing groups will only work if you
# *only* supply the correct outer group.
('nested-noncapture', '/nested/noncapture/opt', [], {'p': 'opt'}),
('nested-capture', '/nested/capture/opt/', ['opt/'], {}),
('nested-capture', NoReverseMatch, [], {'p': 'opt'}),
('nested-mixedcapture', '/nested/capture/mixed/opt', ['opt'], {}),
('nested-mixedcapture', NoReverseMatch, [], {'p': 'opt'}),
('nested-namedcapture', '/nested/capture/named/opt/', [], {'outer': 'opt/'}),
('nested-namedcapture', NoReverseMatch, [], {'outer': 'opt/', 'inner': 'opt'}),
('nested-namedcapture', NoReverseMatch, [], {'inner': 'opt'}),
# Regression for #9038 # Regression for #9038
# These views are resolved by method name. Each method is deployed twice - # These views are resolved by method name. Each method is deployed twice -
# once with an explicit argument, and once using the default value on # once with an explicit argument, and once using the default value on

View File

@ -32,6 +32,12 @@ with warnings.catch_warnings():
url(r'^people/(?:name/(\w+)/)?', empty_view, name="people2a"), url(r'^people/(?:name/(\w+)/)?', empty_view, name="people2a"),
url(r'^people/(?P<name>\w+)-(?P=name)/$', empty_view, name="people_backref"), url(r'^people/(?P<name>\w+)-(?P=name)/$', empty_view, name="people_backref"),
url(r'^optional/(?P<name>.*)/(?:.+/)?', empty_view, name="optional"), url(r'^optional/(?P<name>.*)/(?:.+/)?', empty_view, name="optional"),
url(r'^optional/(?P<arg1>\d+)/(?:(?P<arg2>\d+)/)?', absolute_kwargs_view, name="named_optional"),
url(r'^optional/(?P<arg1>\d+)/(?:(?P<arg2>\d+)/)?$', absolute_kwargs_view, name="named_optional_terminated"),
url(r'^nested/noncapture/(?:(?P<p>\w+))$', empty_view, name='nested-noncapture'),
url(r'^nested/capture/((\w+)/)?$', empty_view, name='nested-capture'),
url(r'^nested/capture/mixed/((?P<p>\w+))$', empty_view, name='nested-mixedcapture'),
url(r'^nested/capture/named/(?P<outer>(?P<inner>\w+)/)?$', empty_view, name='nested-namedcapture'),
url(r'^hardcoded/$', empty_view, name="hardcoded"), url(r'^hardcoded/$', empty_view, name="hardcoded"),
url(r'^hardcoded/doc\.pdf$', empty_view, name="hardcoded2"), url(r'^hardcoded/doc\.pdf$', empty_view, name="hardcoded2"),
url(r'^people/(?P<state>\w\w)/(?P<name>\w+)/$', empty_view, name="people3"), url(r'^people/(?P<state>\w\w)/(?P<name>\w+)/$', empty_view, name="people3"),