diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html
index 4e4bd6d1b2..77fe4c2d7f 100644
--- a/django/contrib/admin/templates/registration/password_reset_email.html
+++ b/django/contrib/admin/templates/registration/password_reset_email.html
@@ -4,7 +4,7 @@
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
-{{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %}
+{{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.username }}
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 03a6245de8..6d77eeddd7 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -14,6 +14,8 @@ from django.utils.itercompat import groupby
from django.utils.safestring import mark_safe
register = Library()
+# Regex for token keyword arguments
+kwarg_re = re.compile(r"(?:(\w+)=)?(.+)")
class AutoEscapeControlNode(Node):
"""Implements the actions of the autoescape tag."""
@@ -1063,12 +1065,9 @@ def templatetag(parser, token):
return TemplateTagNode(tag)
templatetag = register.tag(templatetag)
-# Regex for URL arguments including filters
-url_arg_re = re.compile(
- r"(?:(%(name)s)=)?(%(value)s(?:\|%(name)s(?::%(value)s)?)*)" % {
- 'name':'\w+',
- 'value':'''(?:(?:'[^']*')|(?:"[^"]*")|(?:[\w\.-]+))'''},
- re.VERBOSE)
+# Backwards compatibility check which will fail against for old comma
+# separated arguments in the url tag.
+url_backwards_re = re.compile(r'''(('[^']*'|"[^"]*"|[^,]+)=?)+$''')
def url(parser, token):
"""
@@ -1077,7 +1076,11 @@ def url(parser, token):
This is a way to define links that aren't tied to a particular URL
configuration::
- {% url path.to.some_view arg1,arg2,name1=value1 %}
+ {% url path.to.some_view arg1 arg2 %}
+
+ or
+
+ {% url path.to.some_view name1=value1 name2=value2 %}
The first argument is a path to a view. It can be an absolute python path
or just ``app_name.view_name`` without the project name if the view is
@@ -1109,27 +1112,28 @@ def url(parser, token):
args = []
kwargs = {}
asvar = None
+ bits = bits[2:]
+ if len(bits) >= 2 and bits[-2] == 'as':
+ asvar = bits[-1]
+ bits = bits[:-2]
- if len(bits) > 2:
- bits = iter(bits[2:])
+ # Backwards compatibility: {% url urlname arg1,arg2 %} or
+ # {% url urlname arg1,arg2 as foo %} cases.
+ if bits:
+ old_args = ''.join(bits)
+ if not url_backwards_re.match(old_args):
+ bits = old_args.split(",")
+
+ if len(bits):
for bit in bits:
- if bit == 'as':
- asvar = bits.next()
- break
+ match = kwarg_re.match(bit)
+ if not match:
+ raise TemplateSyntaxError("Malformed arguments to url tag")
+ name, value = match.groups()
+ if name:
+ kwargs[name] = parser.compile_filter(value)
else:
- end = 0
- for i, match in enumerate(url_arg_re.finditer(bit)):
- if (i == 0 and match.start() != 0) or \
- (i > 0 and (bit[end:match.start()] != ',')):
- raise TemplateSyntaxError("Malformed arguments to url tag")
- end = match.end()
- name, value = match.group(1), match.group(2)
- if name:
- kwargs[name] = parser.compile_filter(value)
- else:
- args.append(parser.compile_filter(value))
- if end != len(bit):
- raise TemplateSyntaxError("Malformed arguments to url tag")
+ args.append(parser.compile_filter(value))
return URLNode(viewname, args, kwargs, asvar)
url = register.tag(url)
diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
index 2d65bb22dc..291abcb8cc 100644
--- a/docs/ref/templates/builtins.txt
+++ b/docs/ref/templates/builtins.txt
@@ -904,7 +904,7 @@ Returns an absolute URL (i.e., a URL without the domain name) matching a given
view function and optional parameters. This is a way to output links without
violating the DRY principle by having to hard-code URLs in your templates::
- {% url path.to.some_view v1,v2 %}
+ {% url path.to.some_view v1 v2 %}
The first argument is a path to a view function in the format
``package.package.module.function``. Additional arguments are optional and
@@ -912,7 +912,7 @@ should be comma-separated values that will be used as arguments in the URL.
The example above shows passing positional arguments. Alternatively you may
use keyword syntax::
- {% url path.to.some_view arg1=v1,arg2=v2 %}
+ {% url path.to.some_view arg1=v1 arg2=v2 %}
Do not mix both positional and keyword syntax in a single call. All arguments
required by the URLconf should be present.
@@ -954,7 +954,7 @@ If you'd like to retrieve a URL without displaying it, you can use a slightly
different call::
- {% url path.to.view arg, arg2 as the_url %}
+ {% url path.to.view arg arg2 as the_url %}
I'm linking to {{ the_url }}
@@ -976,6 +976,20 @@ This will follow the normal :ref:`namespaced URL resolution strategy
`, including using any hints provided
by the context as to the current application.
+.. versionchanged:: 1.2
+
+For backwards compatibility, the ``{% url %}`` tag also supports the
+use of commas to separate arguments. You shouldn't use this in any new
+projects, but for the sake of the people who are still using it,
+here's what it looks like::
+
+ {% url path.to.view arg,arg2 %}
+ {% url path.to.view arg, arg2 %}
+
+This syntax doesn't support the use of literal commas, or or equals
+signs. Did we mention you shouldn't use this syntax in any new
+projects?
+
.. templatetag:: widthratio
widthratio
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 95bb45de43..2df02f69be 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -205,20 +205,20 @@ class Templates(unittest.TestCase):
def test_extends_include_missing_baseloader(self):
"""
- Tests that the correct template is identified as not existing
- when {% extends %} specifies a template that does exist, but
+ Tests that the correct template is identified as not existing
+ when {% extends %} specifies a template that does exist, but
that template has an {% include %} of something that does not
exist. See #12787.
"""
- # TEMPLATE_DEBUG must be true, otherwise the exception raised
+ # TEMPLATE_DEBUG must be true, otherwise the exception raised
# during {% include %} processing will be suppressed.
old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, True
old_loaders = loader.template_source_loaders
try:
- # Test the base loader class via the app loader. load_template
- # from base is used by all shipped loaders excepting cached,
+ # Test the base loader class via the app loader. load_template
+ # from base is used by all shipped loaders excepting cached,
# which has its own test.
loader.template_source_loaders = (app_directories.Loader(),)
@@ -237,7 +237,7 @@ class Templates(unittest.TestCase):
def test_extends_include_missing_cachedloader(self):
"""
- Same as test_extends_include_missing_baseloader, only tests
+ Same as test_extends_include_missing_baseloader, only tests
behavior of the cached loader instead of BaseLoader.
"""
@@ -1173,9 +1173,15 @@ class Templates(unittest.TestCase):
### URL TAG ########################################################
# Successes
+ 'legacyurl02': ('{% url regressiontests.templates.views.client_action id=client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
+ 'legacyurl02a': ('{% url regressiontests.templates.views.client_action client.id,"update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
+ 'legacyurl10': ('{% url regressiontests.templates.views.client_action id=client.id,action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'),
+ 'legacyurl13': ('{% url regressiontests.templates.views.client_action id=client.id, action=arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'),
+ 'legacyurl14': ('{% url regressiontests.templates.views.client_action client.id, arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'),
+
'url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
- 'url02': ('{% url regressiontests.templates.views.client_action id=client.id,action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
- 'url02a': ('{% url regressiontests.templates.views.client_action client.id,"update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
+ 'url02': ('{% url regressiontests.templates.views.client_action id=client.id action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
+ 'url02a': ('{% url regressiontests.templates.views.client_action client.id "update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'),
'url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'),
'url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'),
'url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
@@ -1183,10 +1189,12 @@ class Templates(unittest.TestCase):
'url07': (u'{% url regressiontests.templates.views.client2 tag=v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
'url08': (u'{% url метка_оператора v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
'url09': (u'{% url метка_оператора_2 tag=v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'),
- 'url10': ('{% url regressiontests.templates.views.client_action id=client.id,action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'),
- 'url11': ('{% url regressiontests.templates.views.client_action id=client.id,action="==" %}', {'client': {'id': 1}}, '/url_tag/client/1/==/'),
- 'url12': ('{% url regressiontests.templates.views.client_action id=client.id,action="," %}', {'client': {'id': 1}}, '/url_tag/client/1/,/'),
- 'url12': ('{% url regressiontests.templates.views.client_action id=client.id,action=arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'),
+ 'url10': ('{% url regressiontests.templates.views.client_action id=client.id action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'),
+ 'url11': ('{% url regressiontests.templates.views.client_action id=client.id action="==" %}', {'client': {'id': 1}}, '/url_tag/client/1/==/'),
+ 'url12': ('{% url regressiontests.templates.views.client_action id=client.id action="," %}', {'client': {'id': 1}}, '/url_tag/client/1/,/'),
+ 'url13': ('{% url regressiontests.templates.views.client_action id=client.id action=arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'),
+ 'url14': ('{% url regressiontests.templates.views.client_action client.id arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'),
+ 'url15': ('{% url regressiontests.templates.views.client_action 12 "test" %}', {}, '/url_tag/client/12/test/'),
# Failures
'url-fail01': ('{% url %}', {}, template.TemplateSyntaxError),