From 7ff5580d95f6bb40f12a0b90f51570e23b014a2d Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Sat, 20 Nov 2010 06:22:28 +0000 Subject: [PATCH] Fixed #14389, #9666 -- Started the migration path to make the first argument to url and ssi template tags syntactically consistent with other tags. Thanks to Sean Brant for the draft patch. git-svn-id: http://code.djangoproject.com/svn/django/trunk@14643 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../admin/auth/user/change_password.html | 3 +- .../contrib/admin/templates/admin/base.html | 8 +- .../admin/templates/admin/change_list.html | 10 +- .../registration/password_change_done.html | 3 +- .../registration/password_change_form.html | 3 +- .../registration/password_reset_email.html | 4 +- django/template/defaulttags.py | 80 +++++++-- django/templatetags/future.py | 99 +++++++++++ docs/internals/deprecation.txt | 5 + docs/ref/templates/builtins.txt | 64 ++++++- docs/releases/1.3.txt | 41 +++++ .../context_processors/auth_attrs_user.html | 4 +- .../templates/templates/ssi_include.html | 1 + tests/regressiontests/templates/tests.py | 159 ++++++++++++++---- .../urlpatterns_reverse/urlconf_inner.py | 5 +- 15 files changed, 424 insertions(+), 65 deletions(-) create mode 100644 django/templatetags/future.py create mode 100644 tests/regressiontests/templates/templates/ssi_include.html diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index ab47385fb4..5a2036d1dc 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -1,7 +1,8 @@ {% extends "admin/base_site.html" %} {% load i18n admin_modify adminmedia %} +{% load url from future %} {% block extrahead %}{{ block.super }} -{% url admin:jsi18n as jsi18nurl %} +{% url 'admin:jsi18n' as jsi18nurl %} {% endblock %} {% block extrastyle %}{{ block.super }}{% endblock %} diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html index 30a4e494fa..87f6c8738d 100644 --- a/django/contrib/admin/templates/admin/base.html +++ b/django/contrib/admin/templates/admin/base.html @@ -1,4 +1,4 @@ - +{% load url from future %} {% block title %}{% endblock %} @@ -28,18 +28,18 @@ {% trans 'Welcome,' %} {% filter force_escape %}{% firstof user.first_name user.username %}{% endfilter %}. {% block userlinks %} - {% url django-admindocs-docroot as docsroot %} + {% url 'django-admindocs-docroot' as docsroot %} {% if docsroot %} {% trans 'Documentation' %} / {% endif %} - {% url admin:password_change as password_change_url %} + {% url 'admin:password_change' as password_change_url %} {% if password_change_url %} {% else %} {% endif %} {% trans 'Change password' %} / - {% url admin:logout as logout_url %} + {% url 'admin:logout' as logout_url %} {% if logout_url %} {% else %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 3b037e54f9..cb4bae3da8 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -1,6 +1,6 @@ {% extends "admin/base_site.html" %} {% load adminmedia admin_list i18n %} - +{% load url from future %} {% block extrastyle %} {{ block.super }} @@ -8,7 +8,7 @@ {% endif %} {% if cl.formset or action_form %} - {% url admin:jsi18n as jsi18nurl %} + {% url 'admin:jsi18n' as jsi18nurl %} {% endif %} {{ media.css }} @@ -41,11 +41,11 @@ {% trans "Home" %} - › + › {{ app_label|capfirst }} - › + › {{ cl.opts.verbose_name_plural|capfirst }} {% endblock %} @@ -84,7 +84,7 @@ {% endif %} {% endblock %} - +
{% csrf_token %} {% if cl.formset %} {{ cl.formset.management_form }} diff --git a/django/contrib/admin/templates/registration/password_change_done.html b/django/contrib/admin/templates/registration/password_change_done.html index f7c2ceb2ec..0c0690d5e2 100644 --- a/django/contrib/admin/templates/registration/password_change_done.html +++ b/django/contrib/admin/templates/registration/password_change_done.html @@ -1,6 +1,7 @@ {% extends "admin/base_site.html" %} {% load i18n %} -{% block userlinks %}{% url django-admindocs-docroot as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} +{% load url from future %} +{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %}{% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} {% block breadcrumbs %}{% endblock %} {% block title %}{% trans 'Password change successful' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index f5c1ca5208..23d6c1d8af 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -1,7 +1,8 @@ {% extends "admin/base_site.html" %} {% load i18n adminmedia %} +{% load url from future %} {% block extrastyle %}{{ block.super }}{% endblock %} -{% block userlinks %}{% url django-admindocs-docroot as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} +{% block userlinks %}{% url 'django-admindocs-docroot' as docsroot %}{% if docsroot %}{% trans 'Documentation' %} / {% endif %} {% trans 'Change password' %} / {% trans 'Log out' %}{% endblock %} {% block breadcrumbs %}{% endblock %} {% block title %}{% trans 'Password change' %}{% endblock %} diff --git a/django/contrib/admin/templates/registration/password_reset_email.html b/django/contrib/admin/templates/registration/password_reset_email.html index 77fe4c2d7f..506a1a83dc 100644 --- a/django/contrib/admin/templates/registration/password_reset_email.html +++ b/django/contrib/admin/templates/registration/password_reset_email.html @@ -1,10 +1,10 @@ -{% load i18n %}{% autoescape off %} +{% load i18n %}{% load url from future %}{% autoescape off %} {% trans "You're receiving this e-mail because you requested a password reset" %} {% blocktrans %}for your user account at {{ site_name }}{% endblocktrans %}. {% 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 687a74477f..2dc95f4b8c 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -290,24 +290,30 @@ def include_is_allowed(filepath): return False class SsiNode(Node): - def __init__(self, filepath, parsed): - self.filepath, self.parsed = filepath, parsed + def __init__(self, filepath, parsed, legacy_filepath=True): + self.filepath = filepath + self.parsed = parsed + self.legacy_filepath = legacy_filepath def render(self, context): - if not include_is_allowed(self.filepath): + filepath = self.filepath + if not self.legacy_filepath: + filepath = filepath.resolve(context) + + if not include_is_allowed(filepath): if settings.DEBUG: return "[Didn't have permission to include file]" else: return '' # Fail silently for invalid includes. try: - fp = open(self.filepath, 'r') + fp = open(filepath, 'r') output = fp.read() fp.close() except IOError: output = '' if self.parsed: try: - t = Template(output, name=self.filepath) + t = Template(output, name=filepath) return t.render(context) except TemplateSyntaxError, e: if settings.DEBUG: @@ -356,8 +362,9 @@ class TemplateTagNode(Node): return self.mapping.get(self.tagtype, '') class URLNode(Node): - def __init__(self, view_name, args, kwargs, asvar): + def __init__(self, view_name, args, kwargs, asvar, legacy_view_name=True): self.view_name = view_name + self.legacy_view_name = legacy_view_name self.args = args self.kwargs = kwargs self.asvar = asvar @@ -365,22 +372,27 @@ class URLNode(Node): def render(self, context): from django.core.urlresolvers import reverse, NoReverseMatch args = [arg.resolve(context) for arg in self.args] - kwargs = dict([(smart_str(k,'ascii'), v.resolve(context)) + kwargs = dict([(smart_str(k, 'ascii'), v.resolve(context)) for k, v in self.kwargs.items()]) + view_name = self.view_name + if not self.legacy_view_name: + view_name = view_name.resolve(context) + # Try to look up the URL twice: once given the view name, and again # relative to what we guess is the "main" app. If they both fail, # re-raise the NoReverseMatch unless we're using the # {% url ... as var %} construct in which cause return nothing. url = '' try: - url = reverse(self.view_name, args=args, kwargs=kwargs, current_app=context.current_app) + url = reverse(view_name, args=args, kwargs=kwargs, current_app=context.current_app) except NoReverseMatch, e: if settings.SETTINGS_MODULE: project_name = settings.SETTINGS_MODULE.split('.')[0] try: - url = reverse(project_name + '.' + self.view_name, - args=args, kwargs=kwargs, current_app=context.current_app) + url = reverse(project_name + '.' + view_name, + args=args, kwargs=kwargs, + current_app=context.current_app) except NoReverseMatch: if self.asvar is None: # Re-raise the original exception, not the one with @@ -922,6 +934,11 @@ def ssi(parser, token): {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} """ + + import warnings + warnings.warn('The syntax for the ssi template tag is changing. Load the `ssi` tag from the `future` tag library to start using the new behavior.', + category=PendingDeprecationWarning) + bits = token.contents.split() parsed = False if len(bits) not in (2, 3): @@ -933,7 +950,7 @@ def ssi(parser, token): else: raise TemplateSyntaxError("Second (optional) argument to %s tag" " must be 'parsed'" % bits[0]) - return SsiNode(bits[1], parsed) + return SsiNode(bits[1], parsed, legacy_filepath=True) ssi = register.tag(ssi) #@register.tag @@ -945,16 +962,44 @@ def load(parser, token): ``django/templatetags/news/photos.py``:: {% load news.photos %} + + Can also be used to load an individual tag/filter from + a library:: + + {% load byline from news %} + """ bits = token.contents.split() - for taglib in bits[1:]: - # add the library to the parser + if len(bits) >= 4 and bits[-2] == "from": try: + taglib = bits[-1] lib = get_library(taglib) - parser.add_library(lib) except InvalidTemplateLibrary, e: raise TemplateSyntaxError("'%s' is not a valid tag library: %s" % (taglib, e)) + else: + temp_lib = Library() + for name in bits[1:-2]: + if name in lib.tags: + temp_lib.tags[name] = lib.tags[name] + # a name could be a tag *and* a filter, so check for both + if name in lib.filters: + temp_lib.filters[name] = lib.filters[name] + elif name in lib.filters: + temp_lib.filters[name] = lib.filters[name] + else: + raise TemplateSyntaxError("'%s' is not a valid tag or filter in tag library '%s'" % + (name, taglib)) + parser.add_library(temp_lib) + else: + for taglib in bits[1:]: + # add the library to the parser + try: + lib = get_library(taglib) + parser.add_library(lib) + except InvalidTemplateLibrary, e: + raise TemplateSyntaxError("'%s' is not a valid tag library: %s" % + (taglib, e)) return LoadNode() load = register.tag(load) @@ -1140,6 +1185,11 @@ def url(parser, token): The URL will look like ``/clients/client/123/``. """ + + import warnings + warnings.warn('The syntax for the url template tag is changing. Load the `url` tag from the `future` tag library to start using the new behavior.', + category=PendingDeprecationWarning) + bits = token.split_contents() if len(bits) < 2: raise TemplateSyntaxError("'%s' takes at least one argument" @@ -1196,7 +1246,7 @@ def url(parser, token): else: args.append(parser.compile_filter(value)) - return URLNode(viewname, args, kwargs, asvar) + return URLNode(viewname, args, kwargs, asvar, legacy_view_name=True) url = register.tag(url) #@register.tag diff --git a/django/templatetags/future.py b/django/templatetags/future.py new file mode 100644 index 0000000000..cd29dc276a --- /dev/null +++ b/django/templatetags/future.py @@ -0,0 +1,99 @@ +from django.conf import settings +from django.template import Library, Node, Template, TemplateSyntaxError +from django.template.defaulttags import kwarg_re, include_is_allowed, SsiNode, URLNode +from django.utils.encoding import smart_str + + +register = Library() + +@register.tag +def ssi(parser, token): + """ + Outputs the contents of a given file into the page. + + Like a simple "include" tag, the ``ssi`` tag includes the contents + of another file -- which must be specified using an absolute path -- + in the current page:: + + {% ssi "/home/html/ljworld.com/includes/right_generic.html" %} + + If the optional "parsed" parameter is given, the contents of the included + file are evaluated as template code, with the current context:: + + {% ssi "/home/html/ljworld.com/includes/right_generic.html" parsed %} + """ + bits = token.contents.split() + parsed = False + if len(bits) not in (2, 3): + raise TemplateSyntaxError("'ssi' tag takes one argument: the path to" + " the file to be included") + if len(bits) == 3: + if bits[2] == 'parsed': + parsed = True + else: + raise TemplateSyntaxError("Second (optional) argument to %s tag" + " must be 'parsed'" % bits[0]) + filepath = parser.compile_filter(bits[1]) + return SsiNode(filepath, parsed, legacy_filepath=False) + +@register.tag +def url(parser, token): + """ + Returns an absolute URL matching given view with its parameters. + + This is a way to define links that aren't tied to a particular URL + configuration:: + + {% 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 + located inside the project. Other arguments are comma-separated values + that will be filled in place of positional and keyword arguments in the + URL. All arguments for the URL should be present. + + For example if you have a view ``app_name.client`` taking client's id and + the corresponding line in a URLconf looks like this:: + + ('^client/(\d+)/$', 'app_name.client') + + and this app's URLconf is included into the project's URLconf under some + path:: + + ('^clients/', include('project_name.app_name.urls')) + + then in a template you can create a link for a certain client like this:: + + {% url "app_name.client" client.id %} + + The URL will look like ``/clients/client/123/``. + """ + bits = token.split_contents() + if len(bits) < 2: + raise TemplateSyntaxError("'%s' takes at least one argument" + " (path to a view)" % bits[0]) + viewname = parser.compile_filter(bits[1]) + args = [] + kwargs = {} + asvar = None + bits = bits[2:] + if len(bits) >= 2 and bits[-2] == 'as': + asvar = bits[-1] + bits = bits[:-2] + + if len(bits): + for bit in bits: + 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: + args.append(parser.compile_filter(value)) + + return URLNode(viewname, args, kwargs, asvar, legacy_view_name=False) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 1065f8e97d..1f5758c877 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -131,6 +131,11 @@ their deprecation, as per the :ref:`Django deprecation policy been deprecated in favor of the :class:`~django.contrib.staticfiles.handlers.StaticFilesHandler`. + * The :ttag:`url` and :ttag:`ssi` template tags will be + modified so that the first argument to each tag is a + template variable, not an implied string. The new-style + behavior is provided in the ``future`` template tag library. + * 2.0 * ``django.views.defaults.shortcut()``. This function has been moved to ``django.contrib.contenttypes.views.shortcut()`` as part of the diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 0d66b5115b..85050cf5ee 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -657,6 +657,17 @@ load Load a custom template tag set. +For example, the following template would load all the tags and filters registered +in ``somelibrary`` and ``otherlibrary``:: + + {% load somelibrary otherlibrary %} + +You can also selectively load individual templates or tags from a library, using +the ``from`` argument. In this example, the template tags/filters named ``foo`` +and ``bar`` will be loaded from ``somelibrary``:: + + {% load foo bar from somelibrary %} + See :doc:`Custom tag and filter libraries ` for more information. .. templatetag:: now @@ -838,6 +849,30 @@ Note that if you use ``{% ssi %}``, you'll need to define See also: ``{% include %}``. +.. admonition:: Forwards compatibility + + .. versionchanged:: 1.3 + + In Django 1.5, the behavior of the :ttag:`ssi` template tag will + change, with the the first argument being made into a context + variable, rather than being a special case unquoted constant. This + will allow the :ttag:`ssi` tag to use a context variable as the + value of the page to be included. + + In order to provide a forwards compatibility path, Django 1.3 + provides a future compatibility library -- ``future`` -- that + implements the new behavior. To use this library, add a + :ttag:`load` call at the top of any template using the :ttag:`ssi` + tag, and wrap the first argument to the :ttag:`ssi` tag in quotes. + For example:: + + {% load ssi from future %} + {% ssi '/home/html/ljworld.com/includes/right_generic.html' %} + + In Django 1.5, the unquoted constant behavior will be replaced + with the behavior provided by the ``future`` tag library. + Existing templates be migrated to use the new syntax. + .. templatetag:: templatetag templatetag @@ -955,10 +990,37 @@ 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 +This syntax doesn't support the use of literal commas, or equals signs. Did we mention you shouldn't use this syntax in any new projects? +.. admonition:: Forwards compatibility + + .. versionchanged:: 1.3 + + In Django 1.5, the behavior of the :ttag:`url` template tag will + change, with the the first argument being made into a context + variable, rather than being a special case unquoted constant. This + will allow the :ttag:`url` tag to use a context variable as the + value of the URL name to be reversed. + + In order to provide a forwards compatibility path, Django 1.3 + provides a future compatibility library -- ``future`` -- that + implements the new behavior. To use this library, add a + :ttag:`load` call at the top of any template using the :ttag:`url` + tag, and wrap the first argument to the :ttag:`url` tag in quotes. + For example:: + + {% load url from future %} + {% url 'myapp:view-name' %} + + The new library also drops support for the comma syntax for + separating arguments to the :ttag:`url` template tag. + + In Django 1.5, the old behavior will be replaced with the behavior + provided by the ``future`` tag library. Existing templates be + migrated to use the new syntax. + .. templatetag:: widthratio widthratio diff --git a/docs/releases/1.3.txt b/docs/releases/1.3.txt index ed72b94ab2..d1c42496ce 100644 --- a/docs/releases/1.3.txt +++ b/docs/releases/1.3.txt @@ -344,3 +344,44 @@ and Ctrl-C test termination) have been made redundant. In view of this redundancy, :class:`~django.test.simple.DjangoTestRunner` has been turned into an empty placeholder class, and will be removed entirely in Django 1.5. + +Changes to :ttag:`url` and :ttag:`ssi` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Most template tags will allow you to pass in either constants or +variables as arguments -- for example:: + + {% extends "base.html" %} + +allows you to specify a base template as a constant, but if you have a +context variable ``templ`` that contains the value ``base.html``:: + + {% extends templ %} + +is also legal. + +However, due to an accident of history, the :ttag:`url` and +:ttag:`ssi` are different. These tags use the second, quoteless +syntax, but interpret the argument as a constant. This means it isn't +possible to use a context variable as the target of a :ttag:`url` and +:ttag:`ssi` tag. + +Django 1.3 marks the start of the process to correct this historical +accident. Django 1.3 adds a new template library -- ``future`` -- that +provides alternate implementations of the :ttag:`url` and :ttag:`ssi` +template tags. This ``future`` library implement behavior that makes +the handling of the first argument consistent with the handling of all +other variables. So, an existing template that contains:: + + {% url sample %} + +should be replaced with:: + + {% load url from future %} + {% url 'sample' %} + +The tags implementing the old behavior have been deprecated, and in +Django 1.5, the old behavior will be replaced with the new behavior. +To ensure compatibility with future versions of Django, existing +templates should be modified to use the new ``future`` libraries and +syntax. diff --git a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html index 7ed16d7867..5df2a344cc 100644 --- a/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html +++ b/tests/regressiontests/context_processors/templates/context_processors/auth_attrs_user.html @@ -1,4 +1,4 @@ -unicode: {{ user }} +{% load url from future %}unicode: {{ user }} id: {{ user.id }} username: {{ user.username }} -url: {% url userpage user %} +url: {% url 'userpage' user %} diff --git a/tests/regressiontests/templates/templates/ssi_include.html b/tests/regressiontests/templates/templates/ssi_include.html new file mode 100644 index 0000000000..58d5926fbb --- /dev/null +++ b/tests/regressiontests/templates/templates/ssi_include.html @@ -0,0 +1 @@ +This is for testing an ssi include. {{ test }} diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index ea93beb69e..4e1ae9bf84 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -51,7 +51,12 @@ class EchoNode(template.Node): def do_echo(parser, token): return EchoNode(token.contents.split()[1:]) +def do_upper(value): + return value.upper() + register.tag("echo", do_echo) +register.tag("other_echo", do_echo) +register.filter("upper", do_upper) template.libraries['testtags'] = register @@ -354,6 +359,10 @@ class Templates(unittest.TestCase): old_invalid = settings.TEMPLATE_STRING_IF_INVALID expected_invalid_str = 'INVALID' + #Set ALLOWED_INCLUDE_ROOTS so that ssi works. + old_allowed_include_roots = settings.ALLOWED_INCLUDE_ROOTS + settings.ALLOWED_INCLUDE_ROOTS = os.path.dirname(os.path.abspath(__file__)) + # Warm the URL reversing cache. This ensures we don't pay the cost # warming the cache during one of the tests. urlresolvers.reverse('regressiontests.templates.views.client_action', @@ -416,6 +425,7 @@ class Templates(unittest.TestCase): deactivate() settings.TEMPLATE_DEBUG = old_td settings.TEMPLATE_STRING_IF_INVALID = old_invalid + settings.ALLOWED_INCLUDE_ROOTS = old_allowed_include_roots self.assertEqual(failures, [], "Tests failed:\n%s\n%s" % ('-'*70, ("\n%s\n" % ('-'*70)).join(failures))) @@ -1056,6 +1066,19 @@ class Templates(unittest.TestCase): 'inheritance40': ("{% extends 'inheritance33' %}{% block opt %}new{{ block.super }}{% endblock %}", {'optional': 1}, '1new23'), 'inheritance41': ("{% extends 'inheritance36' %}{% block opt %}new{{ block.super }}{% endblock %}", {'numbers': '123'}, '_new1_new2_new3_'), + ### LOADING TAG LIBRARIES ################################################# + + # {% load %} tag, importing individual tags + 'load1': ("{% load echo from testtags %}{% echo this that theother %}", {}, 'this that theother'), + 'load2': ("{% load echo other_echo from testtags %}{% echo this that theother %} {% other_echo and another thing %}", {}, 'this that theother and another thing'), + 'load3': ("{% load echo upper from testtags %}{% echo this that theother %} {{ statement|upper }}", {'statement': 'not shouting'}, 'this that theother NOT SHOUTING'), + + # {% load %} tag errors + 'load4': ("{% load echo other_echo bad_tag from testtags %}", {}, template.TemplateSyntaxError), + 'load5': ("{% load echo other_echo bad_tag from %}", {}, template.TemplateSyntaxError), + 'load6': ("{% load from testtags %}", {}, template.TemplateSyntaxError), + 'load7': ("{% load echo from bad_library %}", {}, template.TemplateSyntaxError), + ### I18N ################################################################## # {% spaceless %} tag @@ -1179,6 +1202,30 @@ class Templates(unittest.TestCase): '{% endfor %},' + \ '{% endfor %}', {}, ''), + ### SSI TAG ######################################################## + + # Test normal behavior + 'old-ssi01': ('{%% ssi %s %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates', 'ssi_include.html'), {}, 'This is for testing an ssi include. {{ test }}\n'), + 'old-ssi02': ('{%% ssi %s %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'not_here'), {}, ''), + + # Test parsed output + 'old-ssi06': ('{%% ssi %s parsed %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates', 'ssi_include.html'), {'test': 'Look ma! It parsed!'}, 'This is for testing an ssi include. Look ma! It parsed!\n'), + 'old-ssi07': ('{%% ssi %s parsed %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'not_here'), {'test': 'Look ma! It parsed!'}, ''), + + # Future compatibility + # Test normal behavior + 'ssi01': ('{%% load ssi from future %%}{%% ssi "%s" %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates', 'ssi_include.html'), {}, 'This is for testing an ssi include. {{ test }}\n'), + 'ssi02': ('{%% load ssi from future %%}{%% ssi "%s" %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'not_here'), {}, ''), + 'ssi03': ("{%% load ssi from future %%}{%% ssi '%s' %%}" % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'not_here'), {}, ''), + + # Test passing as a variable + 'ssi04': ('{% load ssi from future %}{% ssi ssi_file %}', {'ssi_file': os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates', 'ssi_include.html')}, 'This is for testing an ssi include. {{ test }}\n'), + 'ssi05': ('{% load ssi from future %}{% ssi ssi_file %}', {'ssi_file': 'no_file'}, ''), + + # Test parsed output + 'ssi06': ('{%% load ssi from future %%}{%% ssi "%s" parsed %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'templates', 'ssi_include.html'), {'test': 'Look ma! It parsed!'}, 'This is for testing an ssi include. Look ma! It parsed!\n'), + 'ssi07': ('{%% load ssi from future %%}{%% ssi "%s" parsed %%}' % os.path.join(os.path.dirname(os.path.abspath(__file__)), 'not_here'), {'test': 'Look ma! It parsed!'}, ''), + ### TEMPLATETAG TAG ####################################################### 'templatetag01': ('{% templatetag openblock %}', {}, '{%'), @@ -1244,41 +1291,91 @@ class Templates(unittest.TestCase): 'legacyurl16a': ("{% url regressiontests.templates.views.client_action action='update',id='1' %}", {}, '/url_tag/client/1/update/'), 'legacyurl17': ('{% url regressiontests.templates.views.client_action client_id=client.my_id,action=action %}', {'client': {'my_id': 1}, 'action': 'update'}, '/url_tag/client/1/update/'), - '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/'), - 'url02b': ("{% url regressiontests.templates.views.client_action id=client.id action='update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), - 'url02c': ("{% 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/'), - 'url06': (u'{% url метка_оператора_2 tag=v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), - '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/,/'), - '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/'), - 'url18': ('{% url regressiontests.templates.views.client "1,2" %}', {}, '/url_tag/client/1,2/'), + 'old-url01': ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), + 'old-url02': ('{% url regressiontests.templates.views.client_action id=client.id action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'old-url02a': ('{% url regressiontests.templates.views.client_action client.id "update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'old-url02b': ("{% url regressiontests.templates.views.client_action id=client.id action='update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'old-url02c': ("{% url regressiontests.templates.views.client_action client.id 'update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'old-url03': ('{% url regressiontests.templates.views.index %}', {}, '/url_tag/'), + 'old-url04': ('{% url named.client client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'), + 'old-url05': (u'{% url метка_оператора v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'old-url06': (u'{% url метка_оператора_2 tag=v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'old-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/'), + 'old-url08': (u'{% url метка_оператора v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'old-url09': (u'{% url метка_оператора_2 tag=v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'old-url10': ('{% url regressiontests.templates.views.client_action id=client.id action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'), + 'old-url11': ('{% url regressiontests.templates.views.client_action id=client.id action="==" %}', {'client': {'id': 1}}, '/url_tag/client/1/==/'), + 'old-url12': ('{% url regressiontests.templates.views.client_action id=client.id action="," %}', {'client': {'id': 1}}, '/url_tag/client/1/,/'), + 'old-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/'), + 'old-url14': ('{% url regressiontests.templates.views.client_action client.id arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'), + 'old-url15': ('{% url regressiontests.templates.views.client_action 12 "test" %}', {}, '/url_tag/client/12/test/'), + 'old-url18': ('{% url regressiontests.templates.views.client "1,2" %}', {}, '/url_tag/client/1,2/'), # Failures - 'url-fail01': ('{% url %}', {}, template.TemplateSyntaxError), - 'url-fail02': ('{% url no_such_view %}', {}, urlresolvers.NoReverseMatch), - 'url-fail03': ('{% url regressiontests.templates.views.client %}', {}, urlresolvers.NoReverseMatch), - 'url-fail04': ('{% url view id, %}', {}, template.TemplateSyntaxError), - 'url-fail05': ('{% url view id= %}', {}, template.TemplateSyntaxError), - 'url-fail06': ('{% url view a.id=id %}', {}, template.TemplateSyntaxError), - 'url-fail07': ('{% url view a.id!id %}', {}, template.TemplateSyntaxError), - 'url-fail08': ('{% url view id="unterminatedstring %}', {}, template.TemplateSyntaxError), - 'url-fail09': ('{% url view id=", %}', {}, template.TemplateSyntaxError), + 'old-url-fail01': ('{% url %}', {}, template.TemplateSyntaxError), + 'old-url-fail02': ('{% url no_such_view %}', {}, urlresolvers.NoReverseMatch), + 'old-url-fail03': ('{% url regressiontests.templates.views.client %}', {}, urlresolvers.NoReverseMatch), + 'old-url-fail04': ('{% url view id, %}', {}, template.TemplateSyntaxError), + 'old-url-fail05': ('{% url view id= %}', {}, template.TemplateSyntaxError), + 'old-url-fail06': ('{% url view a.id=id %}', {}, template.TemplateSyntaxError), + 'old-url-fail07': ('{% url view a.id!id %}', {}, template.TemplateSyntaxError), + 'old-url-fail08': ('{% url view id="unterminatedstring %}', {}, template.TemplateSyntaxError), + 'old-url-fail09': ('{% url view id=", %}', {}, template.TemplateSyntaxError), # {% url ... as var %} - 'url-asvar01': ('{% url regressiontests.templates.views.index as url %}', {}, ''), - 'url-asvar02': ('{% url regressiontests.templates.views.index as url %}{{ url }}', {}, '/url_tag/'), - 'url-asvar03': ('{% url no_such_view as url %}{{ url }}', {}, ''), + 'old-url-asvar01': ('{% url regressiontests.templates.views.index as url %}', {}, ''), + 'old-url-asvar02': ('{% url regressiontests.templates.views.index as url %}{{ url }}', {}, '/url_tag/'), + 'old-url-asvar03': ('{% url no_such_view as url %}{{ url }}', {}, ''), + + # forward compatibility + # Successes + 'url01': ('{% load url from future %}{% url "regressiontests.templates.views.client" client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'), + 'url02': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" id=client.id action="update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url02a': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" client.id "update" %}', {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url02b': ("{% load url from future %}{% url 'regressiontests.templates.views.client_action' id=client.id action='update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url02c': ("{% load url from future %}{% url 'regressiontests.templates.views.client_action' client.id 'update' %}", {'client': {'id': 1}}, '/url_tag/client/1/update/'), + 'url03': ('{% load url from future %}{% url "regressiontests.templates.views.index" %}', {}, '/url_tag/'), + 'url04': ('{% load url from future %}{% url "named.client" client.id %}', {'client': {'id': 1}}, '/url_tag/named-client/1/'), + 'url05': (u'{% load url from future %}{% url "метка_оператора" v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url06': (u'{% load url from future %}{% url "метка_оператора_2" tag=v %}', {'v': u'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url07': (u'{% load url from future %}{% 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'{% load url from future %}{% url "метка_оператора" v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url09': (u'{% load url from future %}{% url "метка_оператора_2" tag=v %}', {'v': 'Ω'}, '/url_tag/%D0%AE%D0%BD%D0%B8%D0%BA%D0%BE%D0%B4/%CE%A9/'), + 'url10': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" id=client.id action="two words" %}', {'client': {'id': 1}}, '/url_tag/client/1/two%20words/'), + 'url11': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" id=client.id action="==" %}', {'client': {'id': 1}}, '/url_tag/client/1/==/'), + 'url12': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" id=client.id action="," %}', {'client': {'id': 1}}, '/url_tag/client/1/,/'), + 'url13': ('{% load url from future %}{% 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': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" client.id arg|join:"-" %}', {'client': {'id': 1}, 'arg':['a','b']}, '/url_tag/client/1/a-b/'), + 'url15': ('{% load url from future %}{% url "regressiontests.templates.views.client_action" 12 "test" %}', {}, '/url_tag/client/12/test/'), + 'url18': ('{% load url from future %}{% url "regressiontests.templates.views.client" "1,2" %}', {}, '/url_tag/client/1,2/'), + + 'url19': ('{% load url from future %}{% url named_url client.id %}', {'named_url': 'regressiontests.templates.views.client', 'client': {'id': 1}}, '/url_tag/client/1/'), + + # Failures + 'url-fail01': ('{% load url from future %}{% url %}', {}, template.TemplateSyntaxError), + 'url-fail02': ('{% load url from future %}{% url "no_such_view" %}', {}, urlresolvers.NoReverseMatch), + 'url-fail03': ('{% load url from future %}{% url "regressiontests.templates.views.client" %}', {}, urlresolvers.NoReverseMatch), + 'url-fail04': ('{% load url from future %}{% url "view" id, %}', {}, template.TemplateSyntaxError), + 'url-fail05': ('{% load url from future %}{% url "view" id= %}', {}, template.TemplateSyntaxError), + 'url-fail06': ('{% load url from future %}{% url "view" a.id=id %}', {}, template.TemplateSyntaxError), + 'url-fail07': ('{% load url from future %}{% url "view" a.id!id %}', {}, template.TemplateSyntaxError), + 'url-fail08': ('{% load url from future %}{% url "view" id="unterminatedstring %}', {}, template.TemplateSyntaxError), + 'url-fail09': ('{% load url from future %}{% url "view" id=", %}', {}, template.TemplateSyntaxError), + + 'url-fail11': ('{% load url from future %}{% url named_url %}', {}, urlresolvers.NoReverseMatch), + 'url-fail12': ('{% load url from future %}{% url named_url %}', {'named_url': 'no_such_view'}, urlresolvers.NoReverseMatch), + 'url-fail13': ('{% load url from future %}{% url named_url %}', {'named_url': 'regressiontests.templates.views.client'}, urlresolvers.NoReverseMatch), + 'url-fail14': ('{% load url from future %}{% url named_url id, %}', {'named_url': 'view'}, template.TemplateSyntaxError), + 'url-fail15': ('{% load url from future %}{% url named_url id= %}', {'named_url': 'view'}, template.TemplateSyntaxError), + 'url-fail16': ('{% load url from future %}{% url named_url a.id=id %}', {'named_url': 'view'}, template.TemplateSyntaxError), + 'url-fail17': ('{% load url from future %}{% url named_url a.id!id %}', {'named_url': 'view'}, template.TemplateSyntaxError), + 'url-fail18': ('{% load url from future %}{% url named_url id="unterminatedstring %}', {'named_url': 'view'}, template.TemplateSyntaxError), + 'url-fail19': ('{% load url from future %}{% url named_url id=", %}', {'named_url': 'view'}, template.TemplateSyntaxError), + + # {% url ... as var %} + 'url-asvar01': ('{% load url from future %}{% url "regressiontests.templates.views.index" as url %}', {}, ''), + 'url-asvar02': ('{% load url from future %}{% url "regressiontests.templates.views.index" as url %}{{ url }}', {}, '/url_tag/'), + 'url-asvar03': ('{% load url from future %}{% url "no_such_view" as url %}{{ url }}', {}, ''), ### CACHE TAG ###################################################### 'cache01': ('{% load cache %}{% cache -1 test %}cache01{% endcache %}', {}, 'cache01'), diff --git a/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py b/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py index d188e06421..e9819623a3 100644 --- a/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py +++ b/tests/regressiontests/urlpatterns_reverse/urlconf_inner.py @@ -3,8 +3,9 @@ from django.template import Template, Context from django.http import HttpResponse def inner_view(request): - content = Template('{% url outer as outer_url %}outer:{{ outer_url }},' - '{% url inner as inner_url %}inner:{{ inner_url }}').render(Context()) + content = Template('{% load url from future %}' + '{% url "outer" as outer_url %}outer:{{ outer_url }},' + '{% url "inner" as inner_url %}inner:{{ inner_url }}').render(Context()) return HttpResponse(content) urlpatterns = patterns('',