From 3ede006fc98f7e96ae9fb997872f78635576d5f8 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Sat, 26 Nov 2005 22:46:31 +0000 Subject: [PATCH] Fixed #911 -- Made template system scoped to the parser instead of the template module. Also changed the way tags/filters are registered and added support for multiple arguments to {% load %} tag. Thanks, rjwittams. This is a backwards-incompatible change for people who've created custom template tags or filters. See http://code.djangoproject.com/wiki/BackwardsIncompatibleChanges for upgrade instructions. git-svn-id: http://code.djangoproject.com/svn/django/trunk@1443 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../admin/templates/admin/change_form.html | 4 +- .../admin/templates/admin/change_list.html | 3 +- .../templates/admin/edit_inline_stacked.html | 1 + .../templates/admin/edit_inline_tabular.html | 1 + .../admin/templates/admin/field_line.html | 1 + .../contrib/admin/templates/admin/filter.html | 1 + .../admin/templates/admin/filters.html | 1 + .../admin/templates/admin/pagination.html | 1 + .../admin/templates/admin/search_form.html | 1 + .../admin/templates/widget/default.html | 2 +- .../contrib/admin/templates/widget/file.html | 2 +- .../admin/templates/widget/foreign.html | 3 +- .../contrib/admin/templatetags/admin_list.py | 32 +- .../admin/templatetags/admin_modify.py | 37 +-- .../admin/templatetags/adminapplist.py | 4 +- .../contrib/admin/templatetags/adminmedia.py | 5 +- django/contrib/admin/templatetags/log.py | 4 +- django/contrib/admin/views/template.py | 12 +- .../contrib/comments/templatetags/comments.py | 14 +- django/contrib/markup/templatetags/markup.py | 26 +- django/core/template/__init__.py | 294 ++++++++++++++---- django/core/template/decorators.py | 67 ---- django/core/template/defaultfilters.py | 162 +++++----- django/core/template/defaulttags.py | 178 +++++------ django/core/template/loader.py | 169 +--------- django/core/template/loader_tags.py | 172 ++++++++++ django/templatetags/i18n.py | 22 +- docs/templates.txt | 5 + docs/templates_python.txt | 90 ++++-- tests/othertests/defaultfilters.py | 12 +- tests/othertests/markup.py | 13 +- tests/othertests/templates.py | 18 +- tests/testapp/templatetags/testtags.py | 6 +- 33 files changed, 781 insertions(+), 582 deletions(-) delete mode 100644 django/core/template/decorators.py create mode 100644 django/core/template/loader_tags.py diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index b5ae024866..3378f6f1c1 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -1,7 +1,5 @@ {% extends "admin/base_site" %} -{% load i18n %} -{% load admin_modify %} -{% load adminmedia %} +{% load i18n admin_modify adminmedia %} {% block extrahead %} {% for js in bound_manipulator.javascript_imports %}{% include_admin_script js %}{% endfor %} {% endblock %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index ccc3990519..b93f9e95f9 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -1,5 +1,4 @@ -{% load admin_list %} -{% load i18n %} +{% load adminmedia admin_list i18n %} {% extends "admin/base_site" %} {% block bodyclass %}change-list{% endblock %} {% if not is_popup %}{% block breadcrumbs %}{% endblock %}{% endif %} diff --git a/django/contrib/admin/templates/admin/edit_inline_stacked.html b/django/contrib/admin/templates/admin/edit_inline_stacked.html index 5e5ea8c0fc..62549ef82d 100644 --- a/django/contrib/admin/templates/admin/edit_inline_stacked.html +++ b/django/contrib/admin/templates/admin/edit_inline_stacked.html @@ -1,3 +1,4 @@ +{% load admin_modify %}
{% for fcw in bound_related_object.form_field_collection_wrappers %}

{{ bound_related_object.relation.opts.verbose_name|capfirst }} #{{ forloop.counter }}

diff --git a/django/contrib/admin/templates/admin/edit_inline_tabular.html b/django/contrib/admin/templates/admin/edit_inline_tabular.html index c06ee05df8..e9535df02c 100644 --- a/django/contrib/admin/templates/admin/edit_inline_tabular.html +++ b/django/contrib/admin/templates/admin/edit_inline_tabular.html @@ -1,3 +1,4 @@ +{% load admin_modify %}

{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}

diff --git a/django/contrib/admin/templates/admin/field_line.html b/django/contrib/admin/templates/admin/field_line.html index 5e526e6fd6..10f37d5dc9 100644 --- a/django/contrib/admin/templates/admin/field_line.html +++ b/django/contrib/admin/templates/admin/field_line.html @@ -1,3 +1,4 @@ +{% load admin_modify %}
{% for bound_field in bound_fields %}{{ bound_field.html_error_list }}{% endfor %} {% for bound_field in bound_fields %} diff --git a/django/contrib/admin/templates/admin/filter.html b/django/contrib/admin/templates/admin/filter.html index 385b1824f2..f6f5455c01 100644 --- a/django/contrib/admin/templates/admin/filter.html +++ b/django/contrib/admin/templates/admin/filter.html @@ -1,3 +1,4 @@ +{% load i18n %}

{% blocktrans %} By {{ title }} {% endblocktrans %}

    {% for choice in choices %} diff --git a/django/contrib/admin/templates/admin/filters.html b/django/contrib/admin/templates/admin/filters.html index b3c6a25831..93c2f65b15 100644 --- a/django/contrib/admin/templates/admin/filters.html +++ b/django/contrib/admin/templates/admin/filters.html @@ -1,3 +1,4 @@ +{% load admin_list %} {% if cl.has_filters %}

    Filter

    {% for spec in cl.filter_specs %} diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html index 5d80ecdcc4..64b1b1a3dd 100644 --- a/django/contrib/admin/templates/admin/pagination.html +++ b/django/contrib/admin/templates/admin/pagination.html @@ -1,3 +1,4 @@ +{% load admin_list %}

    {% if pagination_required %} {% for i in page_range %} diff --git a/django/contrib/admin/templates/admin/search_form.html b/django/contrib/admin/templates/admin/search_form.html index 19b6b7eb95..e398028fe6 100644 --- a/django/contrib/admin/templates/admin/search_form.html +++ b/django/contrib/admin/templates/admin/search_form.html @@ -1,3 +1,4 @@ +{% load adminmedia %} {% if cl.lookup_opts.admin.search_fields %}

    diff --git a/django/contrib/admin/templates/widget/default.html b/django/contrib/admin/templates/widget/default.html index 08a82fdbfa..0af231ddcb 100644 --- a/django/contrib/admin/templates/widget/default.html +++ b/django/contrib/admin/templates/widget/default.html @@ -1 +1 @@ -{% output_all bound_field.form_fields %} +{% load admin_modify %}{% output_all bound_field.form_fields %} diff --git a/django/contrib/admin/templates/widget/file.html b/django/contrib/admin/templates/widget/file.html index f81534b474..bacc30c521 100644 --- a/django/contrib/admin/templates/widget/file.html +++ b/django/contrib/admin/templates/widget/file.html @@ -1,4 +1,4 @@ -{% if bound_field.original_value %} +{% load admin_modify %}{% if bound_field.original_value %} Currently: {{ bound_field.original_value }}
    Change: {% output_all bound_field.form_fields %} {% else %} {% output_all bound_field.form_fields %} {% endif %} diff --git a/django/contrib/admin/templates/widget/foreign.html b/django/contrib/admin/templates/widget/foreign.html index 9decb1143c..81c68c63a2 100644 --- a/django/contrib/admin/templates/widget/foreign.html +++ b/django/contrib/admin/templates/widget/foreign.html @@ -1,7 +1,8 @@ +{% load admin_modify adminmedia %} {% output_all bound_field.form_fields %} {% if bound_field.raw_id_admin %} Lookup {% else %} {% if bound_field.needs_add_label %} Add Another -{% endif %} {% endif %} +{% endif %}{% endif %} diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index 9733114872..471f1930a9 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -3,16 +3,18 @@ from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, from django.contrib.admin.views.main import IS_POPUP_VAR, EMPTY_CHANGELIST_VALUE, MONTHS from django.core import meta, template from django.core.exceptions import ObjectDoesNotExist -from django.core.template.decorators import simple_tag, inclusion_tag from django.utils import dateformat from django.utils.html import strip_tags, escape from django.utils.text import capfirst from django.utils.translation import get_date_formats from django.conf.settings import ADMIN_MEDIA_PREFIX +from django.core.template import Library + +register = Library() DOT = '.' -#@simple_tag +#@register.simple_tag def paginator_number(cl,i): if i == DOT: return '... ' @@ -20,9 +22,9 @@ def paginator_number(cl,i): return '%d ' % (i+1) else: return '%d ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1) -paginator_number = simple_tag(paginator_number) +paginator_number = register.simple_tag(paginator_number) -#@inclusion_tag('admin/pagination') +#@register.inclusion_tag('admin/pagination') def pagination(cl): paginator, page_num = cl.paginator, cl.page_num @@ -64,7 +66,7 @@ def pagination(cl): 'ALL_VAR': ALL_VAR, '1': 1, } -pagination = inclusion_tag('admin/pagination')(pagination) +pagination = register.inclusion_tag('admin/pagination')(pagination) def result_headers(cl): lookup_opts = cl.lookup_opts @@ -177,15 +179,15 @@ def results(cl): for res in cl.result_list: yield list(items_for_result(cl,res)) -#@inclusion_tag("admin/change_list_results") +#@register.inclusion_tag("admin/change_list_results") def result_list(cl): res = list(results(cl)) return {'cl': cl, 'result_headers': list(result_headers(cl)), 'results': list(results(cl))} -result_list = inclusion_tag("admin/change_list_results")(result_list) +result_list = register.inclusion_tag("admin/change_list_results")(result_list) -#@inclusion_tag("admin/date_hierarchy") +#@register.inclusion_tag("admin/date_hierarchy") def date_hierarchy(cl): lookup_opts, params, lookup_params, lookup_mod = \ cl.lookup_opts, cl.params, cl.lookup_params, cl.lookup_mod @@ -256,23 +258,23 @@ def date_hierarchy(cl): 'title': year.year } for year in years ] } -date_hierarchy = inclusion_tag('admin/date_hierarchy')(date_hierarchy) +date_hierarchy = register.inclusion_tag('admin/date_hierarchy')(date_hierarchy) -#@inclusion_tag('admin/search_form') +#@register.inclusion_tag('admin/search_form') def search_form(cl): return { 'cl': cl, 'show_result_count': cl.result_count != cl.full_result_count and not cl.opts.one_to_one_field, 'search_var': SEARCH_VAR } -search_form = inclusion_tag('admin/search_form')(search_form) +search_form = register.inclusion_tag('admin/search_form')(search_form) -#@inclusion_tag('admin/filter') +#@register.inclusion_tag('admin/filter') def filter(cl, spec): return {'title': spec.title(), 'choices' : list(spec.choices(cl))} -filter = inclusion_tag('admin/filter')(filter) +filter = register.inclusion_tag('admin/filter')(filter) -#@inclusion_tag('admin/filters') +#@register.inclusion_tag('admin/filters') def filters(cl): return {'cl': cl} -filters = inclusion_tag('admin/filters')(filters) +filters = register.inclusion_tag('admin/filters')(filters) diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 891aec1b1f..58fcef4210 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -2,24 +2,25 @@ from django.core import template, template_loader, meta from django.utils.html import escape from django.utils.text import capfirst from django.utils.functional import curry -from django.core.template.decorators import simple_tag, inclusion_tag from django.contrib.admin.views.main import AdminBoundField from django.core.meta.fields import BoundField, Field from django.core.meta import BoundRelatedObject, TABULAR, STACKED from django.conf.settings import ADMIN_MEDIA_PREFIX import re +register = template.Library() + word_re = re.compile('[A-Z][a-z]+') def class_name_to_underscored(name): return '_'.join([s.lower() for s in word_re.findall(name)[:-1]]) -#@simple_tag +#@register.simple_tag def include_admin_script(script_path): return '' % (ADMIN_MEDIA_PREFIX, script_path) -include_admin_script = simple_tag(include_admin_script) +include_admin_script = register.simple_tag(include_admin_script) -#@inclusion_tag('admin/submit_line', takes_context=True) +#@register.inclusion_tag('admin/submit_line', takes_context=True) def submit_row(context, bound_manipulator): change = context['change'] add = context['add'] @@ -36,9 +37,9 @@ def submit_row(context, bound_manipulator): 'show_save_and_continue': not is_popup, 'show_save': True } -submit_row = inclusion_tag('admin/submit_line', takes_context=True)(submit_row) +submit_row = register.inclusion_tag('admin/submit_line', takes_context=True)(submit_row) -#@simple_tag +#@register.simple_tag def field_label(bound_field): class_names = [] if isinstance(bound_field.field, meta.BooleanField): @@ -53,7 +54,7 @@ def field_label(bound_field): class_str = class_names and ' class="%s"' % ' '.join(class_names) or '' return ' ' % (bound_field.element_id, class_str, \ capfirst(bound_field.field.verbose_name), colon) -field_label = simple_tag(field_label) +field_label = register.simple_tag(field_label) class FieldWidgetNode(template.Node): nodelists = {} @@ -170,12 +171,12 @@ class EditInlineNode(template.Node): context.pop() return output -#@simple_tag +#@register.simple_tag def output_all(form_fields): return ''.join([str(f) for f in form_fields]) -output_all = simple_tag(output_all) +output_all = register.simple_tag(output_all) -#@simple_tag +#@register.simple_tag def auto_populated_field_script(auto_pop_fields, change = False): for field in auto_pop_fields: t = [] @@ -191,9 +192,9 @@ def auto_populated_field_script(auto_pop_fields, change = False): ' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % ( f, field.name, add_values, field.maxlength)) return ''.join(t) -auto_populated_field_script = simple_tag(auto_populated_field_script) +auto_populated_field_script = register.simple_tag(auto_populated_field_script) -#@simple_tag +#@register.simple_tag def filter_interface_script_maybe(bound_field): f = bound_field.field if f.rel and isinstance(f.rel, meta.ManyToMany) and f.rel.filter_interface: @@ -202,7 +203,7 @@ def filter_interface_script_maybe(bound_field): f.name, f.verbose_name, f.rel.filter_interface-1, ADMIN_MEDIA_PREFIX) else: return '' -filter_interface_script_maybe = simple_tag(filter_interface_script_maybe) +filter_interface_script_maybe = register.simple_tag(filter_interface_script_maybe) def do_one_arg_tag(node_factory, parser,token): tokens = token.contents.split() @@ -213,7 +214,7 @@ def do_one_arg_tag(node_factory, parser,token): def register_one_arg_tag(node): tag_name = class_name_to_underscored(node.__name__) parse_func = curry(do_one_arg_tag, node) - template.register_tag(tag_name, parse_func) + register.tag(tag_name, parse_func) one_arg_tag_nodes = ( FieldWidgetNode, @@ -223,7 +224,7 @@ one_arg_tag_nodes = ( for node in one_arg_tag_nodes: register_one_arg_tag(node) -#@inclusion_tag('admin/field_line', takes_context=True) +#@register.inclusion_tag('admin/field_line', takes_context=True) def admin_field_line(context, argument_val): if (isinstance(argument_val, BoundField)): bound_fields = [argument_val] @@ -249,10 +250,10 @@ def admin_field_line(context, argument_val): 'bound_fields': bound_fields, 'class_names': " ".join(class_names), } -admin_field_line = inclusion_tag('admin/field_line', takes_context=True)(admin_field_line) +admin_field_line = register.inclusion_tag('admin/field_line', takes_context=True)(admin_field_line) -#@simple_tag +#@register.simple_tag def object_pk(bound_manip, ordered_obj): return bound_manip.get_ordered_object_pk(ordered_obj) -object_pk = simple_tag(object_pk) +object_pk = register.simple_tag(object_pk) diff --git a/django/contrib/admin/templatetags/adminapplist.py b/django/contrib/admin/templatetags/adminapplist.py index 92e1bd3ccb..7a91516ebc 100644 --- a/django/contrib/admin/templatetags/adminapplist.py +++ b/django/contrib/admin/templatetags/adminapplist.py @@ -1,5 +1,7 @@ from django.core import template +register = template.Library() + class AdminApplistNode(template.Node): def __init__(self, varname): self.varname = varname @@ -54,4 +56,4 @@ def get_admin_app_list(parser, token): raise template.TemplateSyntaxError, "First argument to '%s' tag must be 'as'" % tokens[0] return AdminApplistNode(tokens[2]) -template.register_tag('get_admin_app_list', get_admin_app_list) +register.tag('get_admin_app_list', get_admin_app_list) diff --git a/django/contrib/admin/templatetags/adminmedia.py b/django/contrib/admin/templatetags/adminmedia.py index 3a822ed9b0..3238bfcfc7 100644 --- a/django/contrib/admin/templatetags/adminmedia.py +++ b/django/contrib/admin/templatetags/adminmedia.py @@ -1,4 +1,5 @@ -from django.core.template.decorators import simple_tag +from django.core.template import Library +register = Library() def admin_media_prefix(): try: @@ -6,4 +7,4 @@ def admin_media_prefix(): except ImportError: return '' return ADMIN_MEDIA_PREFIX -admin_media_prefix = simple_tag(admin_media_prefix) +admin_media_prefix = register.simple_tag(admin_media_prefix) \ No newline at end of file diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index b24f7c1dad..013e07c80f 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -1,6 +1,8 @@ from django.models.admin import log from django.core import template +register = template.Library() + class AdminLogNode(template.Node): def __init__(self, limit, varname, user): self.limit, self.varname, self.user = limit, varname, user @@ -48,4 +50,4 @@ class DoGetAdminLog: raise template.TemplateSyntaxError, "Fourth argument in '%s' must be 'for_user'" % self.tag_name return AdminLogNode(limit=tokens[1], varname=tokens[3], user=(len(tokens) > 5 and tokens[5] or None)) -template.register_tag('get_admin_log', DoGetAdminLog('get_admin_log')) +register.tag('get_admin_log', DoGetAdminLog('get_admin_log')) diff --git a/django/contrib/admin/views/template.py b/django/contrib/admin/views/template.py index fbac6d4f12..3effd57c10 100644 --- a/django/contrib/admin/views/template.py +++ b/django/contrib/admin/views/template.py @@ -50,21 +50,23 @@ class TemplateValidator(formfields.Manipulator): return # so that inheritance works in the site's context, register a new function - # for "extends" that uses the site's TEMPLATE_DIR instead + # for "extends" that uses the site's TEMPLATE_DIRS instead. def new_do_extends(parser, token): node = loader.do_extends(parser, token) node.template_dirs = settings_module.TEMPLATE_DIRS return node - template.register_tag('extends', new_do_extends) + register = template.Library() + register.tag('extends', new_do_extends) + template.builtins.append(register) - # now validate the template using the new template dirs - # making sure to reset the extends function in any case + # Now validate the template using the new template dirs + # making sure to reset the extends function in any case. error = None try: tmpl = loader.get_template_from_string(field_data) tmpl.render(template.Context({})) except template.TemplateSyntaxError, e: error = e - template.register_tag('extends', loader.do_extends) + template.builtins.remove(register) if error: raise validators.ValidationError, e.args diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 126009a5b9..c2c4ab47d5 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -6,6 +6,8 @@ from django.models.comments import comments, freecomments from django.models.core import contenttypes import re +register = template.Library() + COMMENT_FORM = ''' {% load i18n %} {% if display_form %} @@ -360,10 +362,10 @@ class DoGetCommentList: return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering) # registration comments -template.register_tag('get_comment_list', DoGetCommentList(False)) -template.register_tag('comment_form', DoCommentForm(False)) -template.register_tag('get_comment_count', DoCommentCount(False)) +register.tag('get_comment_list', DoGetCommentList(False)) +register.tag('comment_form', DoCommentForm(False)) +register.tag('get_comment_count', DoCommentCount(False)) # free comments -template.register_tag('get_free_comment_list', DoGetCommentList(True)) -template.register_tag('free_comment_form', DoCommentForm(True)) -template.register_tag('get_free_comment_count', DoCommentCount(True)) +register.tag('get_free_comment_list', DoGetCommentList(True)) +register.tag('free_comment_form', DoCommentForm(True)) +register.tag('get_free_comment_count', DoCommentCount(True)) diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 2ee6f06af7..5124889b89 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -4,35 +4,37 @@ markup syntaxes to HTML; currently there is support for: * Textile, which requires the PyTextile library available at http://dealmeida.net/projects/textile/ - + * Markdown, which requires the Python-markdown library from http://www.freewisdom.org/projects/python-markdown - + * ReStructuredText, which requires docutils from http://docutils.sf.net/ - + In each case, if the required library is not installed, the filter will silently fail and return the un-marked-up text. """ from django.core import template -def textile(value, _): +register = template.Library() + +def textile(value): try: import textile except ImportError: return value else: return textile.textile(value) - -def markdown(value, _): + +def markdown(value): try: import markdown except ImportError: return value else: return markdown.markdown(value) - -def restructuredtext(value, _): + +def restructuredtext(value): try: from docutils.core import publish_parts except ImportError: @@ -40,7 +42,7 @@ def restructuredtext(value, _): else: parts = publish_parts(source=value, writer_name="html4css1") return parts["fragment"] - -template.register_filter("textile", textile, False) -template.register_filter("markdown", markdown, False) -template.register_filter("restructuredtext", restructuredtext, False) \ No newline at end of file + +register.filter(textile) +register.filter(markdown) +register.filter(restructuredtext) diff --git a/django/core/template/__init__.py b/django/core/template/__init__.py index 5cb4e0a1c6..5a755fd15f 100644 --- a/django/core/template/__init__.py +++ b/django/core/template/__init__.py @@ -3,7 +3,7 @@ This is the Django template system. How it works: -The tokenize() function converts a template string (i.e., a string containing +The Lexer.tokenize() function converts a template string (i.e., a string containing markup with custom template tags) to tokens, which can be either plain text (TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK). @@ -55,6 +55,8 @@ times with multiple contexts) '\n\n\n\n' """ import re +from inspect import getargspec +from django.utils.functional import curry from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG __all__ = ('Template','Context','compile_string') @@ -82,11 +84,10 @@ UNKNOWN_SOURCE="" tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) -# global dict used by register_tag; maps custom tags to callback functions -registered_tags = {} - -# global dict used by register_filter; maps custom filters to callback functions -registered_filters = {} +# global dictionary of libraries that have been loaded using get_library +libraries = {} +# global list of libraries to load by default for a new parser +builtins = [] class TemplateSyntaxError(Exception): pass @@ -105,12 +106,15 @@ class SilentVariableFailure(Exception): "Any function raising this exception will be ignored by resolve_variable" pass +class InvalidTemplateLibrary(Exception): + pass + class Origin(object): def __init__(self, name): self.name = name def reload(self): - raise NotImplementedException + raise NotImplementedError def __str__(self): return self.name @@ -264,6 +268,10 @@ class DebugLexer(Lexer): class Parser(object): def __init__(self, tokens): self.tokens = tokens + self.tags = {} + self.filters = {} + for lib in builtins: + self.add_library(lib) def parse(self, parse_until=[]): nodelist = self.create_nodelist() @@ -274,7 +282,8 @@ class Parser(object): elif token.token_type == TOKEN_VAR: if not token.contents: self.empty_variable(token) - var_node = self.create_variable_node(token.contents) + filter_expression = self.compile_filter(token.contents) + var_node = self.create_variable_node(filter_expression) self.extend_nodelist(nodelist, var_node,token) elif token.token_type == TOKEN_BLOCK: if token.contents in parse_until: @@ -288,7 +297,7 @@ class Parser(object): # execute callback function for this tag and append resulting node self.enter_command(command, token) try: - compile_func = registered_tags[command] + compile_func = self.tags[command] except KeyError: self.invalid_block_tag(token, command) try: @@ -302,8 +311,8 @@ class Parser(object): self.unclosed_block_tag(parse_until) return nodelist - def create_variable_node(self, contents): - return VariableNode(contents) + def create_variable_node(self, filter_expression): + return VariableNode(filter_expression) def create_nodelist(self): return NodeList() @@ -344,6 +353,20 @@ class Parser(object): def delete_first_token(self): del self.tokens[0] + def add_library(self, lib): + self.tags.update(lib.tags) + self.filters.update(lib.filters) + + def compile_filter(self,token): + "Convenient wrapper for FilterExpression" + return FilterExpression(token, self) + + def find_filter(self, filter_name): + if self.filters.has_key(filter_name): + return self.filters[filter_name] + else: + raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name + class DebugParser(Parser): def __init__(self, lexer): super(DebugParser, self).__init__(lexer) @@ -483,7 +506,8 @@ filter_raw_string = r""" (?:%(arg_sep)s (?: %(i18n_open)s"(?P%(str)s)"%(i18n_close)s| - "(?P%(str)s)" + "(?P%(str)s)"| + (?P[%(var_chars)s]+) ) )? )""" % { @@ -498,7 +522,7 @@ filter_raw_string = r""" filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "") filter_re = re.compile(filter_raw_string) -class FilterParser(object): +class FilterExpression(object): """ Parses a variable token and its optional filters (all as a single string), and return a list of tuples of the filter name and arguments. @@ -513,7 +537,8 @@ class FilterParser(object): This class should never be instantiated outside of the get_filters_from_token helper function. """ - def __init__(self, token): + def __init__(self, token, parser): + self.token = token matches = filter_re.finditer(token) var = None filters = [] @@ -536,27 +561,69 @@ class FilterParser(object): raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var else: filter_name = match.group("filter_name") - arg, i18n_arg = match.group("arg","i18n_arg") + args = [] + constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg") if i18n_arg: - arg =_(i18n_arg.replace('\\', '')) - if arg: - arg = arg.replace('\\', '') - if not registered_filters.has_key(filter_name): - raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name - if registered_filters[filter_name][1] == True and arg is None: - raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name - if registered_filters[filter_name][1] == False and arg is not None: - raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg) - filters.append( (filter_name,arg) ) + args.append((False, _(i18n_arg.replace('\\', '')))) + elif constant_arg: + args.append((False, constant_arg.replace('\\', ''))) + elif var_arg: + args.append((True, var_arg)) + filter_func = parser.find_filter(filter_name) + self.args_check(filter_name,filter_func, args) + filters.append( (filter_func,args)) upto = match.end() if upto != len(token): raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:] self.var , self.filters = var, filters -def get_filters_from_token(token): - "Convenient wrapper for FilterParser" - p = FilterParser(token) - return (p.var, p.filters) + def resolve(self, context): + try: + obj = resolve_variable(self.var, context) + except VariableDoesNotExist: + obj = '' + for func, args in self.filters: + arg_vals = [] + for lookup, arg in args: + if not lookup: + arg_vals.append(arg) + else: + arg_vals.append(resolve_variable(arg, context)) + obj = func(obj, *arg_vals) + return obj + + def args_check(name, func, provided): + provided = list(provided) + plen = len(provided) + (args, varargs, varkw, defaults) = getargspec(func) + # First argument is filter input. + args.pop(0) + if defaults: + nondefs = args[:-len(defaults)] + else: + nondefs = args + # Args without defaults must be provided. + try: + for arg in nondefs: + provided.pop(0) + except IndexError: + # Not enough + raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen) + + # Defaults can be overridden. + defaults = defaults and list(defaults) or [] + try: + for parg in provided: + defaults.pop(0) + except IndexError: + # Too many. + raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen) + + return True + args_check = staticmethod(args_check) + + def __str__(self): + return self.token def resolve_variable(path, context): """ @@ -607,22 +674,6 @@ def resolve_variable(path, context): del bits[0] return current -def resolve_variable_with_filters(var_string, context): - """ - var_string is a full variable expression with optional filters, like: - a.b.c|lower|date:"y/m/d" - This function resolves the variable in the context, applies all filters and - returns the object. - """ - var, filters = get_filters_from_token(var_string) - try: - obj = resolve_variable(var, context) - except VariableDoesNotExist: - obj = '' - for name, arg in filters: - obj = registered_filters[name][0](obj, arg) - return obj - class Node: def render(self, context): "Return the node rendered as a string" @@ -687,11 +738,11 @@ class TextNode(Node): return self.s class VariableNode(Node): - def __init__(self, var_string): - self.var_string = var_string + def __init__(self, filter_expression): + self.filter_expression = filter_expression def __repr__(self): - return "" % self.var_string + return "" % self.filter_expression def encode_output(self, output): # Check type so that we don't run str() on a Unicode object @@ -703,30 +754,153 @@ class VariableNode(Node): return output def render(self, context): - output = resolve_variable_with_filters(self.var_string, context) + output = self.filter_expression.resolve(context) return self.encode_output(output) class DebugVariableNode(VariableNode): def render(self, context): try: - output = resolve_variable_with_filters(self.var_string, context) + output = self.filter_expression.resolve(context) except TemplateSyntaxError, e: if not hasattr(e, 'source'): e.source = self.source raise return self.encode_output(output) -def register_tag(token_command, callback_function): - registered_tags[token_command] = callback_function +def generic_tag_compiler(params, defaults, name, node_class, parser, token): + "Returns a template.Node subclass." + bits = token.contents.split()[1:] + bmax = len(params) + def_len = defaults and len(defaults) or 0 + bmin = bmax - def_len + if(len(bits) < bmin or len(bits) > bmax): + if bmin == bmax: + message = "%s takes %s arguments" % (name, bmin) + else: + message = "%s takes between %s and %s arguments" % (name, bmin, bmax) + raise TemplateSyntaxError, message + return node_class(bits) -def unregister_tag(token_command): - del registered_tags[token_command] +class Library(object): + def __init__(self): + self.filters = {} + self.tags = {} -def register_filter(filter_name, callback_function, has_arg): - registered_filters[filter_name] = (callback_function, has_arg) + def tag(self, name = None, compile_function = None): + if name == None and compile_function == None: + # @register.tag() + return self.tag_function + elif name != None and compile_function == None: + if(callable(name)): + # @register.tag + return self.tag_function(name) + else: + # @register.tag('somename') or @register.tag(name='somename') + def dec(func): + return self.tag(name, func) + return dec + elif name != None and compile_function != None: + # register.tag('somename', somefunc) + self.tags[name] = compile_function + return compile_function + else: + raise InvalidTemplateLibrary, "Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function) -def unregister_filter(filter_name): - del registered_filters[filter_name] + def tag_function(self,func): + self.tags[func.__name__] = func + return func -import defaulttags -import defaultfilters + def filter(self, name = None, filter_func = None): + if name == None and filter_func == None: + # @register.filter() + return self.filter_function + elif filter_func == None: + if(callable(name)): + # @register.filter + return self.filter_function(name) + else: + # @register.filter('somename') or @register.filter(name='somename') + def dec(func): + return self.filter(name, func) + return dec + elif name != None and filter_func != None: + # register.filter('somename', somefunc) + self.filters[name] = filter_func + else: + raise InvalidTemplateLibrary, "Unsupported arguments to Library.filter: (%r, %r, %r)", (name, compile_function, has_arg) + + def filter_function(self, func): + self.filters[func.__name__] = func + return func + + def simple_tag(self,func): + (params, xx, xxx, defaults) = getargspec(func) + + class SimpleNode(Node): + def __init__(self, vars_to_resolve): + self.vars_to_resolve = vars_to_resolve + + def render(self, context): + resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] + return func(*resolved_vars) + + compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, SimpleNode) + compile_func.__doc__ = func.__doc__ + self.tag(func.__name__, compile_func) + return func + + def inclusion_tag(self, file_name, context_class=Context, takes_context=False): + def dec(func): + (params, xx, xxx, defaults) = getargspec(func) + if takes_context: + if params[0] == 'context': + params = params[1:] + else: + raise TemplateSyntaxError, "Any tag function decorated with takes_context=True must have a first argument of 'context'" + + class InclusionNode(Node): + def __init__(self, vars_to_resolve): + self.vars_to_resolve = vars_to_resolve + + def render(self, context): + resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] + if takes_context: + args = [context] + resolved_vars + else: + args = resolved_vars + + dict = func(*args) + + if not getattr(self, 'nodelist', False): + from django.core.template_loader import get_template + t = get_template(file_name) + self.nodelist = t.nodelist + return self.nodelist.render(context_class(dict)) + + compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, InclusionNode) + compile_func.__doc__ = func.__doc__ + self.tag(func.__name__, compile_func) + return func + return dec + +def get_library(module_name): + lib = libraries.get(module_name, None) + if not lib: + try: + mod = __import__(module_name, '', '', ['']) + except ImportError, e: + raise InvalidTemplateLibrary, "Could not load template library from %s, %s" % (module_name, e) + for k, v in mod.__dict__.items(): + if isinstance(v, Library): + lib = v + libraries[module_name] = lib + break + if not lib: + raise InvalidTemplateLibrary, "Template library %s does not have a Library member" % module_name + return lib + +def add_to_builtins(module_name): + builtins.append(get_library(module_name)) + +add_to_builtins('django.core.template.defaulttags') +add_to_builtins('django.core.template.defaultfilters') diff --git a/django/core/template/decorators.py b/django/core/template/decorators.py deleted file mode 100644 index 2a61a600ec..0000000000 --- a/django/core/template/decorators.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.core.template import Context, Node, TemplateSyntaxError, register_tag, resolve_variable -from django.core.template_loader import get_template -from django.utils.functional import curry -from inspect import getargspec - -def generic_tag_compiler(params, defaults, name, node_class, parser, token): - "Returns a template.Node subclass." - bits = token.contents.split()[1:] - bmax = len(params) - def_len = defaults and len(defaults) or 0 - bmin = bmax - def_len - if(len(bits) < bmin or len(bits) > bmax): - if bmin == bmax: - message = "%s takes %s arguments" % (name, bmin) - else: - message = "%s takes between %s and %s arguments" % (name, bmin, bmax) - raise TemplateSyntaxError, message - return node_class(bits) - -def simple_tag(func): - (params, xx, xxx, defaults) = getargspec(func) - - class SimpleNode(Node): - def __init__(self, vars_to_resolve): - self.vars_to_resolve = vars_to_resolve - - def render(self, context): - resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] - return func(*resolved_vars) - - compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, SimpleNode) - compile_func.__doc__ = func.__doc__ - register_tag(func.__name__, compile_func) - return func - -def inclusion_tag(file_name, context_class=Context, takes_context=False): - def dec(func): - (params, xx, xxx, defaults) = getargspec(func) - if takes_context: - if params[0] == 'context': - params = params[1:] - else: - raise TemplateSyntaxError, "Any tag function decorated with takes_context=True must have a first argument of 'context'" - - class InclusionNode(Node): - def __init__(self, vars_to_resolve): - self.vars_to_resolve = vars_to_resolve - - def render(self, context): - resolved_vars = [resolve_variable(var, context) for var in self.vars_to_resolve] - if takes_context: - args = [context] + resolved_vars - else: - args = resolved_vars - - dict = func(*args) - - if not getattr(self, 'nodelist', False): - t = get_template(file_name) - self.nodelist = t.nodelist - return self.nodelist.render(context_class(dict)) - - compile_func = curry(generic_tag_compiler, params, defaults, func.__name__, InclusionNode) - compile_func.__doc__ = func.__doc__ - register_tag(func.__name__, compile_func) - return func - return dec diff --git a/django/core/template/defaultfilters.py b/django/core/template/defaultfilters.py index 3a25add5e1..5d87b62e57 100644 --- a/django/core/template/defaultfilters.py +++ b/django/core/template/defaultfilters.py @@ -1,28 +1,32 @@ "Default variable filters" -from django.core.template import register_filter, resolve_variable +from django.core.template import resolve_variable, Library +from django.conf.settings import DATE_FORMAT, TIME_FORMAT import re import random as random_module +register = Library() + ################### # STRINGS # ################### -def addslashes(value, _): + +def addslashes(value): "Adds slashes - useful for passing strings to JavaScript, for example." return value.replace('"', '\\"').replace("'", "\\'") -def capfirst(value, _): +def capfirst(value): "Capitalizes the first character of the value" value = str(value) return value and value[0].upper() + value[1:] -def fix_ampersands(value, _): +def fix_ampersands(value): "Replaces ampersands with ``&`` entities" from django.utils.html import fix_ampersands return fix_ampersands(value) -def floatformat(text, _): +def floatformat(text): """ Displays a floating point number as 34.2 (with one decimal place) -- but only if there's a point to be displayed @@ -37,7 +41,7 @@ def floatformat(text, _): else: return '%d' % int(f) -def linenumbers(value, _): +def linenumbers(value): "Displays text with line numbers" from django.utils.html import escape lines = value.split('\n') @@ -47,18 +51,18 @@ def linenumbers(value, _): lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) return '\n'.join(lines) -def lower(value, _): +def lower(value): "Converts a string into all lowercase" return value.lower() -def make_list(value, _): +def make_list(value): """ Returns the value turned into a list. For an integer, it's a list of digits. For a string, it's a list of characters. """ return list(str(value)) -def slugify(value, _): +def slugify(value): "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" value = re.sub('[^\w\s-]', '', value).strip().lower() return re.sub('\s+', '-', value) @@ -77,7 +81,7 @@ def stringformat(value, arg): except (ValueError, TypeError): return "" -def title(value, _): +def title(value): "Converts a string into titlecase" return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) @@ -96,16 +100,16 @@ def truncatewords(value, arg): value = str(value) return truncate_words(value, length) -def upper(value, _): +def upper(value): "Converts a string into all uppercase" return value.upper() -def urlencode(value, _): +def urlencode(value): "Escapes a value for use in a URL" import urllib return urllib.quote(value) -def urlize(value, _): +def urlize(value): "Converts URLs in plain text into clickable links" from django.utils.html import urlize return urlize(value, nofollow=True) @@ -119,7 +123,7 @@ def urlizetrunc(value, limit): from django.utils.html import urlize return urlize(value, trim_url_limit=int(limit), nofollow=True) -def wordcount(value, _): +def wordcount(value): "Returns the number of words" return len(value.split()) @@ -160,17 +164,17 @@ def cut(value, arg): # HTML STRINGS # ################### -def escape(value, _): +def escape(value): "Escapes a string's HTML" from django.utils.html import escape return escape(value) -def linebreaks(value, _): +def linebreaks(value): "Converts newlines into

    and
    s" from django.utils.html import linebreaks return linebreaks(value) -def linebreaksbr(value, _): +def linebreaksbr(value): "Converts newlines into
    s" return value.replace('\n', '
    ') @@ -184,7 +188,7 @@ def removetags(value, tags): value = endtag_re.sub('', value) return value -def striptags(value, _): +def striptags(value): "Strips all [X]HTML tags" from django.utils.html import strip_tags if not isinstance(value, basestring): @@ -214,7 +218,7 @@ def dictsortreversed(value, arg): decorated.reverse() return [item[1] for item in decorated] -def first(value, _): +def first(value): "Returns the first item in a list" try: return value[0] @@ -228,7 +232,7 @@ def join(value, arg): except AttributeError: # fail silently but nicely return value -def length(value, _): +def length(value): "Returns the length of the value - useful for lists" return len(value) @@ -236,7 +240,7 @@ def length_is(value, arg): "Returns a boolean of whether the value's length is the argument" return len(value) == int(arg) -def random(value, _): +def random(value): "Returns a random item from the list" return random_module.choice(value) @@ -253,7 +257,7 @@ def slice_(value, arg): except (ValueError, TypeError): return value # Fail silently. -def unordered_list(value, _): +def unordered_list(value): """ Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing

      tags. @@ -314,17 +318,17 @@ def get_digit(value, arg): # DATES # ################### -def date(value, arg): +def date(value, arg=DATE_FORMAT): "Formats a date according to the given format" from django.utils.dateformat import format return format(value, arg) -def time(value, arg): +def time(value, arg=TIME_FORMAT): "Formats a time according to the given format" from django.utils.dateformat import time_format return time_format(value, arg) -def timesince(value, _): +def timesince(value): 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' from django.utils.timesince import timesince return timesince(value) @@ -347,7 +351,7 @@ def divisibleby(value, arg): "Returns true if the value is devisible by the argument" return int(value) % int(arg) == 0 -def yesno(value, arg): +def yesno(value, arg=_("yes,no,maybe")): """ Given a string mapping values for true, false and (optionally) None, returns one of those strings accoding to the value: @@ -379,7 +383,7 @@ def yesno(value, arg): # MISC # ################### -def filesizeformat(bytes, _): +def filesizeformat(bytes): """ Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 bytes, etc). @@ -393,7 +397,7 @@ def filesizeformat(bytes, _): return "%.1f MB" % (bytes / (1024 * 1024)) return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) -def pluralize(value, _): +def pluralize(value): "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'" try: if int(value) != 1: @@ -408,62 +412,62 @@ def pluralize(value, _): pass return '' -def phone2numeric(value, _): +def phone2numeric(value): "Takes a phone number and converts it in to its numerical equivalent" from django.utils.text import phone2numeric return phone2numeric(value) -def pprint(value, _): +def pprint(value): "A wrapper around pprint.pprint -- for debugging, really" from pprint import pformat return pformat(value) -# Syntax: register_filter(name of filter, callback, has_argument) -register_filter('add', add, True) -register_filter('addslashes', addslashes, False) -register_filter('capfirst', capfirst, False) -register_filter('center', center, True) -register_filter('cut', cut, True) -register_filter('date', date, True) -register_filter('default', default, True) -register_filter('default_if_none', default_if_none, True) -register_filter('dictsort', dictsort, True) -register_filter('dictsortreversed', dictsortreversed, True) -register_filter('divisibleby', divisibleby, True) -register_filter('escape', escape, False) -register_filter('filesizeformat', filesizeformat, False) -register_filter('first', first, False) -register_filter('fix_ampersands', fix_ampersands, False) -register_filter('floatformat', floatformat, False) -register_filter('get_digit', get_digit, True) -register_filter('join', join, True) -register_filter('length', length, False) -register_filter('length_is', length_is, True) -register_filter('linebreaks', linebreaks, False) -register_filter('linebreaksbr', linebreaksbr, False) -register_filter('linenumbers', linenumbers, False) -register_filter('ljust', ljust, True) -register_filter('lower', lower, False) -register_filter('make_list', make_list, False) -register_filter('phone2numeric', phone2numeric, False) -register_filter('pluralize', pluralize, False) -register_filter('pprint', pprint, False) -register_filter('removetags', removetags, True) -register_filter('random', random, False) -register_filter('rjust', rjust, True) -register_filter('slice', slice_, True) -register_filter('slugify', slugify, False) -register_filter('stringformat', stringformat, True) -register_filter('striptags', striptags, False) -register_filter('time', time, True) -register_filter('timesince', timesince, False) -register_filter('title', title, False) -register_filter('truncatewords', truncatewords, True) -register_filter('unordered_list', unordered_list, False) -register_filter('upper', upper, False) -register_filter('urlencode', urlencode, False) -register_filter('urlize', urlize, False) -register_filter('urlizetrunc', urlizetrunc, True) -register_filter('wordcount', wordcount, False) -register_filter('wordwrap', wordwrap, True) -register_filter('yesno', yesno, True) +# Syntax: register.filter(name of filter, callback) +register.filter(add) +register.filter(addslashes) +register.filter(capfirst) +register.filter(center) +register.filter(cut) +register.filter(date) +register.filter(default) +register.filter(default_if_none) +register.filter(dictsort) +register.filter(dictsortreversed) +register.filter(divisibleby) +register.filter(escape) +register.filter(filesizeformat) +register.filter(first) +register.filter(fix_ampersands) +register.filter(floatformat) +register.filter(get_digit) +register.filter(join) +register.filter(length) +register.filter(length_is) +register.filter(linebreaks) +register.filter(linebreaksbr) +register.filter(linenumbers) +register.filter(ljust) +register.filter(lower) +register.filter(make_list) +register.filter(phone2numeric) +register.filter(pluralize) +register.filter(pprint) +register.filter(removetags) +register.filter(random) +register.filter(rjust) +register.filter(slice_) +register.filter(slugify) +register.filter(stringformat) +register.filter(striptags) +register.filter(time) +register.filter(timesince) +register.filter(title) +register.filter(truncatewords) +register.filter(unordered_list) +register.filter(upper) +register.filter(urlencode) +register.filter(urlize) +register.filter(urlizetrunc) +register.filter(wordcount) +register.filter(wordwrap) +register.filter(yesno) \ No newline at end of file diff --git a/django/core/template/defaulttags.py b/django/core/template/defaulttags.py index 08ae3d9852..e47c8d1d02 100644 --- a/django/core/template/defaulttags.py +++ b/django/core/template/defaulttags.py @@ -1,9 +1,12 @@ "Default tags used by the template system, available to all templates." -from django.core.template import Node, NodeList, Template, Context, resolve_variable, resolve_variable_with_filters, get_filters_from_token, registered_filters -from django.core.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END, register_tag +from django.core.template import Node, NodeList, Template, Context, resolve_variable +from django.core.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END +from django.core.template import get_library, Library, InvalidTemplateLibrary import sys +register = Library() + class CommentNode(Node): def render(self, context): return '' @@ -27,15 +30,13 @@ class DebugNode(Node): return ''.join(output) class FilterNode(Node): - def __init__(self, filters, nodelist): - self.filters, self.nodelist = filters, nodelist + def __init__(self, filter_expr, nodelist): + self.filter_expr, self.nodelist = filter_expr, nodelist def render(self, context): output = self.nodelist.render(context) # apply filters - for f in self.filters: - output = registered_filters[f[0]][0](output, f[1]) - return output + return self.filter_expr.resolve(Context({'var': output})) class FirstOfNode(Node): def __init__(self, vars): @@ -81,7 +82,7 @@ class ForNode(Node): parentloop = {} context.push() try: - values = resolve_variable_with_filters(self.sequence, context) + values = self.sequence.resolve(context) except VariableDoesNotExist: values = [] if values is None: @@ -147,8 +148,8 @@ class IfEqualNode(Node): return self.nodelist_false.render(context) class IfNode(Node): - def __init__(self, boolvars, nodelist_true, nodelist_false): - self.boolvars = boolvars + def __init__(self, bool_exprs, nodelist_true, nodelist_false): + self.bool_exprs = bool_exprs self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false def __repr__(self): @@ -169,9 +170,9 @@ class IfNode(Node): return nodes def render(self, context): - for ifnot, boolvar in self.boolvars: + for ifnot, bool_expr in self.bool_exprs: try: - value = resolve_variable_with_filters(boolvar, context) + value = bool_expr.resolve(context) except VariableDoesNotExist: value = None if (value and not ifnot) or (ifnot and not value): @@ -179,19 +180,18 @@ class IfNode(Node): return self.nodelist_false.render(context) class RegroupNode(Node): - def __init__(self, target_var, expression, var_name): - self.target_var, self.expression = target_var, expression + def __init__(self, target, expression, var_name): + self.target, self.expression = target, expression self.var_name = var_name def render(self, context): - obj_list = resolve_variable_with_filters(self.target_var, context) + obj_list = self.target.resolve(context) if obj_list == '': # target_var wasn't found in context; fail silently context[self.var_name] = [] return '' output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} for obj in obj_list: - grouper = resolve_variable_with_filters('var.%s' % self.expression, \ - Context({'var': obj})) + grouper = self.expression.resolve(Context({'var': obj})) # TODO: Is this a sensible way to determine equality? if output and repr(output[-1]['grouper']) == repr(grouper): output[-1]['list'].append(obj) @@ -236,21 +236,7 @@ class SsiNode(Node): return output class LoadNode(Node): - def __init__(self, taglib): - self.taglib = taglib - - def load_taglib(taglib): - mod = __import__("django.templatetags.%s" % taglib.split('.')[-1], '', '', ['']) - reload(mod) - return mod - load_taglib = staticmethod(load_taglib) - def render(self, context): - "Import the relevant module" - try: - self.__class__.load_taglib(self.taglib) - except ImportError: - pass # Fail silently for invalid loads. return '' class NowNode(Node): @@ -276,15 +262,15 @@ class TemplateTagNode(Node): return self.mapping.get(self.tagtype, '') class WidthRatioNode(Node): - def __init__(self, val_var, max_var, max_width): - self.val_var = val_var - self.max_var = max_var + def __init__(self, val_expr, max_expr, max_width): + self.val_expr = val_expr + self.max_expr = max_expr self.max_width = max_width def render(self, context): try: - value = resolve_variable_with_filters(self.val_var, context) - maxvalue = resolve_variable_with_filters(self.max_var, context) + value = self.val_expr.resolve(context) + maxvalue = self.max_expr.resolve(context) except VariableDoesNotExist: return '' try: @@ -295,15 +281,18 @@ class WidthRatioNode(Node): return '' return str(int(round(ratio))) -def do_comment(parser, token): +#@register.tag +def comment(parser, token): """ Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` """ nodelist = parser.parse(('endcomment',)) parser.delete_first_token() return CommentNode() +comment = register.tag(comment) -def do_cycle(parser, token): +#@register.tag +def cycle(parser, token): """ Cycle among the given strings each time this tag is encountered @@ -369,11 +358,9 @@ def do_cycle(parser, token): else: raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) +cycle = register.tag(cycle) -def do_debug(parser, token): - "Print a whole load of debugging information, including the context and imported modules" - return DebugNode() - +#@register.tag(name="filter") def do_filter(parser, token): """ Filter the contents of the blog through variable filters. @@ -388,12 +375,14 @@ def do_filter(parser, token): {% endfilter %} """ _, rest = token.contents.split(None, 1) - _, filters = get_filters_from_token('var|%s' % rest) + filter_expr = parser.compile_filter("var|%s" % (rest)) nodelist = parser.parse(('endfilter',)) parser.delete_first_token() - return FilterNode(filters, nodelist) + return FilterNode(filter_expr, nodelist) +filter = register.tag("filter", do_filter) -def do_firstof(parser, token): +#@register.tag +def firstof(parser, token): """ Outputs the first variable passed that is not False. @@ -419,8 +408,9 @@ def do_firstof(parser, token): if len(bits) < 1: raise TemplateSyntaxError, "'firstof' statement requires at least one argument" return FirstOfNode(bits) +firstof = register.tag(firstof) - +#@register.tag(name="for") def do_for(parser, token): """ Loop over each item in an array. @@ -462,11 +452,12 @@ def do_for(parser, token): if bits[2] != 'in': raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents loopvar = bits[1] - sequence = bits[3] + sequence = parser.compile_filter(bits[3]) reversed = (len(bits) == 5) nodelist_loop = parser.parse(('endfor',)) parser.delete_first_token() return ForNode(loopvar, sequence, reversed, nodelist_loop) +do_for = register.tag("for", do_for) def do_ifequal(parser, token, negate): """ @@ -497,6 +488,17 @@ def do_ifequal(parser, token, negate): nodelist_false = NodeList() return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) +#@register.tag +def ifequal(parser, token): + return do_ifequal(parser, token, False) +ifequal = register.tag(ifequal) + +#@register.tag +def ifnotequal(parser, token): + return do_ifequal(parser, token, True) +ifnotequal = register.tag(ifnotequal) + +#@register.tag(name="if") def do_if(parser, token): """ The ``{% if %}`` tag evaluates a variable, and if that variable is "true" @@ -554,9 +556,9 @@ def do_if(parser, token): not_, boolvar = boolpair.split() if not_ != 'not': raise TemplateSyntaxError, "Expected 'not' in if statement" - boolvars.append((True, boolvar)) + boolvars.append((True, parser.compile_filter(boolvar))) else: - boolvars.append((False, boolpair)) + boolvars.append((False, parser.compile_filter(boolpair))) nodelist_true = parser.parse(('else', 'endif')) token = parser.next_token() if token.contents == 'else': @@ -565,8 +567,10 @@ def do_if(parser, token): else: nodelist_false = NodeList() return IfNode(boolvars, nodelist_true, nodelist_false) +do_if = register.tag("if", do_if) -def do_ifchanged(parser, token): +#@register.tag +def ifchanged(parser, token): """ Check if a value has changed from the last iteration of a loop. @@ -587,8 +591,10 @@ def do_ifchanged(parser, token): nodelist = parser.parse(('endifchanged',)) parser.delete_first_token() return IfChangedNode(nodelist) +ifchanged = register.tag(ifchanged) -def do_ssi(parser, token): +#@register.tag +def ssi(parser, token): """ Output the contents of a given file into the page. @@ -613,8 +619,10 @@ def do_ssi(parser, token): else: raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] return SsiNode(bits[1], parsed) +ssi = register.tag(ssi) -def do_load(parser, token): +#@register.tag +def load(parser, token): """ Load a custom template tag set. @@ -623,17 +631,18 @@ def do_load(parser, token): {% load news.photos %} """ bits = token.contents.split() - if len(bits) != 2: - raise TemplateSyntaxError, "'load' statement takes one argument" - taglib = bits[1] - # check at compile time that the module can be imported - try: - LoadNode.load_taglib(taglib) - except ImportError, e: - raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e) - return LoadNode(taglib) + for taglib in bits[1:]: + # add the library to the parser + try: + lib = get_library("django.templatetags.%s" % taglib.split('.')[-1]) + 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) -def do_now(parser, token): +#@register.tag +def now(parser, token): """ Display the date, formatted according to the given string. @@ -649,8 +658,10 @@ def do_now(parser, token): raise TemplateSyntaxError, "'now' statement takes one argument" format_string = bits[1] return NowNode(format_string) +now = register.tag(now) -def do_regroup(parser, token): +#@register.tag +def regroup(parser, token): """ Regroup a list of alike objects by a common attribute. @@ -699,17 +710,21 @@ def do_regroup(parser, token): firstbits = token.contents.split(None, 3) if len(firstbits) != 4: raise TemplateSyntaxError, "'regroup' tag takes five arguments" - target_var = firstbits[1] + target = parser.compile_filter(firstbits[1]) if firstbits[2] != 'by': raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" lastbits_reversed = firstbits[3][::-1].split(None, 2) if lastbits_reversed[1][::-1] != 'as': raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" - expression = lastbits_reversed[2][::-1] - var_name = lastbits_reversed[0][::-1] - return RegroupNode(target_var, expression, var_name) -def do_templatetag(parser, token): + expression = parser.compile_filters('var.%s' % lastbits_reversed[2][::-1]) + + var_name = lastbits_reversed[0][::-1] + return RegroupNode(target, expression, var_name) +regroup = register.tag(regroup) + +#@register.tag +def templatetag(parser, token): """ Output one of the bits used to compose template tags. @@ -735,8 +750,10 @@ def do_templatetag(parser, token): raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ (tag, TemplateTagNode.mapping.keys()) return TemplateTagNode(tag) +templatetag = register.tag(templatetag) -def do_widthratio(parser, token): +@register.tag +def widthratio(parser, token): """ For creating bar charts and such, this tag calculates the ratio of a given value to a maximum value, and then applies that ratio to a constant. @@ -752,26 +769,11 @@ def do_widthratio(parser, token): bits = token.contents.split() if len(bits) != 4: raise TemplateSyntaxError("widthratio takes three arguments") - tag, this_value_var, max_value_var, max_width = bits + tag, this_value_expr, max_value_expr, max_width = bits try: max_width = int(max_width) except ValueError: raise TemplateSyntaxError("widthratio final argument must be an integer") - return WidthRatioNode(this_value_var, max_value_var, max_width) - -register_tag('comment', do_comment) -register_tag('cycle', do_cycle) -register_tag('debug', do_debug) -register_tag('filter', do_filter) -register_tag('firstof', do_firstof) -register_tag('for', do_for) -register_tag('ifequal', lambda parser, token: do_ifequal(parser, token, False)) -register_tag('ifnotequal', lambda parser, token: do_ifequal(parser, token, True)) -register_tag('if', do_if) -register_tag('ifchanged', do_ifchanged) -register_tag('regroup', do_regroup) -register_tag('ssi', do_ssi) -register_tag('load', do_load) -register_tag('now', do_now) -register_tag('templatetag', do_templatetag) -register_tag('widthratio', do_widthratio) + return WidthRatioNode(parser.compile_filter(this_value_expr), + parser.compile_filter(max_value_expr), max_width) +widthratio = register.tag(widthratio) diff --git a/django/core/template/loader.py b/django/core/template/loader.py index 6d747d560d..e1409c2bd0 100644 --- a/django/core/template/loader.py +++ b/django/core/template/loader.py @@ -21,7 +21,7 @@ # installed, because pkg_resources is necessary to read eggs. from django.core.exceptions import ImproperlyConfigured -from django.core.template import Origin, StringOrigin, Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag +from django.core.template import Origin, StringOrigin, Template, TemplateDoesNotExist, add_to_builtins from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG template_source_loaders = [] @@ -68,9 +68,6 @@ def find_template_source(name, dirs=None): def load_template_source(name, dirs=None): find_template_source(name, dirs)[0] -class ExtendsError(Exception): - pass - def get_template(template_name): """ Returns a compiled Template object for the given template name, @@ -113,166 +110,4 @@ def select_template(template_name_list): # If we get here, none of the templates could be loaded raise TemplateDoesNotExist, ', '.join(template_name_list) -class BlockNode(Node): - def __init__(self, name, nodelist, parent=None): - self.name, self.nodelist, self.parent = name, nodelist, parent - - def __repr__(self): - return "" % (self.name, self.nodelist) - - def render(self, context): - context.push() - # Save context in case of block.super(). - self.context = context - context['block'] = self - result = self.nodelist.render(context) - context.pop() - return result - - def super(self): - if self.parent: - return self.parent.render(self.context) - return '' - - def add_parent(self, nodelist): - if self.parent: - self.parent.add_parent(nodelist) - else: - self.parent = BlockNode(self.name, nodelist) - -class ExtendsNode(Node): - def __init__(self, nodelist, parent_name, parent_name_var, template_dirs=None): - self.nodelist = nodelist - self.parent_name, self.parent_name_var = parent_name, parent_name_var - self.template_dirs = template_dirs - - def get_parent(self, context): - if self.parent_name_var: - self.parent_name = resolve_variable_with_filters(self.parent_name_var, context) - parent = self.parent_name - if not parent: - error_msg = "Invalid template name in 'extends' tag: %r." % parent - if self.parent_name_var: - error_msg += " Got this from the %r variable." % self.parent_name_var - raise TemplateSyntaxError, error_msg - try: - return get_template_from_string(*find_template_source(parent, self.template_dirs)) - except TemplateDoesNotExist: - raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent - - def render(self, context): - compiled_parent = self.get_parent(context) - parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) - parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) - for block_node in self.nodelist.get_nodes_by_type(BlockNode): - # Check for a BlockNode with this node's name, and replace it if found. - try: - parent_block = parent_blocks[block_node.name] - except KeyError: - # This BlockNode wasn't found in the parent template, but the - # parent block might be defined in the parent's *parent*, so we - # add this BlockNode to the parent's ExtendsNode nodelist, so - # it'll be checked when the parent node's render() is called. - if parent_is_child: - compiled_parent.nodelist[0].nodelist.append(block_node) - else: - # Keep any existing parents and add a new one. Used by BlockNode. - parent_block.parent = block_node.parent - parent_block.add_parent(parent_block.nodelist) - parent_block.nodelist = block_node.nodelist - return compiled_parent.render(context) - -class ConstantIncludeNode(Node): - def __init__(self, template_path): - try: - t = get_template(template_path) - self.template = t - except Exception, e: - if TEMPLATE_DEBUG: - raise - self.template = None - - def render(self, context): - if self.template: - return self.template.render(context) - else: - return '' - -class IncludeNode(Node): - def __init__(self, template_name): - self.template_name = template_name - - def render(self, context): - try: - template_name = resolve_variable(self.template_name, context) - t = get_template(template_name) - return t.render(context) - except TemplateSyntaxError, e: - if TEMPLATE_DEBUG: - raise - return '' - except: - return '' # Fail silently for invalid included templates. - -def do_block(parser, token): - """ - Define a block that can be overridden by child templates. - """ - bits = token.contents.split() - if len(bits) != 2: - raise TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0] - block_name = bits[1] - # Keep track of the names of BlockNodes found in this template, so we can - # check for duplication. - try: - if block_name in parser.__loaded_blocks: - raise TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) - parser.__loaded_blocks.append(block_name) - except AttributeError: # parser._loaded_blocks isn't a list yet - parser.__loaded_blocks = [block_name] - nodelist = parser.parse(('endblock',)) - parser.delete_first_token() - return BlockNode(block_name, nodelist) - -def do_extends(parser, token): - """ - Signal that this template extends a parent template. - - This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) - uses the literal value "base" as the name of the parent template to extend, - or ``{% extends variable %}`` uses the value of ``variable`` as the name - of the parent template to extend. - """ - bits = token.contents.split() - if len(bits) != 2: - raise TemplateSyntaxError, "'%s' takes one argument" % bits[0] - parent_name, parent_name_var = None, None - if bits[1][0] in ('"', "'") and bits[1][-1] == bits[1][0]: - parent_name = bits[1][1:-1] - else: - parent_name_var = bits[1] - nodelist = parser.parse() - if nodelist.get_nodes_by_type(ExtendsNode): - raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] - return ExtendsNode(nodelist, parent_name, parent_name_var) - -def do_include(parser, token): - """ - Loads a template and renders it with the current context. - - Example:: - - {% include "foo/some_include" %} - """ - - bits = token.contents.split() - if len(bits) != 2: - raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] - path = bits[1] - if path[0] in ('"', "'") and path[-1] == path[0]: - return ConstantIncludeNode(path[1:-1]) - return IncludeNode(bits[1]) - -register_tag('block', do_block) -register_tag('extends', do_extends) -register_tag('include', do_include) +add_to_builtins('django.core.template.loader_tags') diff --git a/django/core/template/loader_tags.py b/django/core/template/loader_tags.py new file mode 100644 index 0000000000..20d794c473 --- /dev/null +++ b/django/core/template/loader_tags.py @@ -0,0 +1,172 @@ +from django.core.template import TemplateSyntaxError, TemplateDoesNotExist, resolve_variable +from django.core.template import Library, Context, Node +from django.core.template.loader import get_template, get_template_from_string, find_template_source +from django.conf.settings import TEMPLATE_DEBUG +register = Library() + +class ExtendsError(Exception): + pass + +class BlockNode(Node): + def __init__(self, name, nodelist, parent=None): + self.name, self.nodelist, self.parent = name, nodelist, parent + + def __repr__(self): + return "" % (self.name, self.nodelist) + + def render(self, context): + context.push() + # Save context in case of block.super(). + self.context = context + context['block'] = self + result = self.nodelist.render(context) + context.pop() + return result + + def super(self): + if self.parent: + return self.parent.render(self.context) + return '' + + def add_parent(self, nodelist): + if self.parent: + self.parent.add_parent(nodelist) + else: + self.parent = BlockNode(self.name, nodelist) + +class ExtendsNode(Node): + def __init__(self, nodelist, parent_name, parent_name_expr, template_dirs=None): + self.nodelist = nodelist + self.parent_name, self.parent_name_expr = parent_name, parent_name_expr + self.template_dirs = template_dirs + + def get_parent(self, context): + if self.parent_name_expr: + self.parent_name = self.parent_name_expr.resolve(context) + parent = self.parent_name + if not parent: + error_msg = "Invalid template name in 'extends' tag: %r." % parent + if self.parent_name_expr: + error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr. + raise TemplateSyntaxError, error_msg + try: + return get_template_from_string(*find_template_source(parent, self.template_dirs)) + except TemplateDoesNotExist: + raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent + + def render(self, context): + compiled_parent = self.get_parent(context) + parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) + parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) + for block_node in self.nodelist.get_nodes_by_type(BlockNode): + # Check for a BlockNode with this node's name, and replace it if found. + try: + parent_block = parent_blocks[block_node.name] + except KeyError: + # This BlockNode wasn't found in the parent template, but the + # parent block might be defined in the parent's *parent*, so we + # add this BlockNode to the parent's ExtendsNode nodelist, so + # it'll be checked when the parent node's render() is called. + if parent_is_child: + compiled_parent.nodelist[0].nodelist.append(block_node) + else: + # Keep any existing parents and add a new one. Used by BlockNode. + parent_block.parent = block_node.parent + parent_block.add_parent(parent_block.nodelist) + parent_block.nodelist = block_node.nodelist + return compiled_parent.render(context) + +class ConstantIncludeNode(Node): + def __init__(self, template_path): + try: + t = get_template(template_path) + self.template = t + except: + if TEMPLATE_DEBUG: + pass +# raise + self.template = None + + def render(self, context): + if self.template: + return self.template.render(context) + else: + return '' + +class IncludeNode(Node): + def __init__(self, template_name): + self.template_name = template_name + + def render(self, context): + try: + template_name = resolve_variable(self.template_name, context) + t = get_template(template_name) + return t.render(context) + except TemplateSyntaxError, e: + if TEMPLATE_DEBUG: + raise + return '' + except: + return '' # Fail silently for invalid included templates. + +def do_block(parser, token): + """ + Define a block that can be overridden by child templates. + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'%s' tag takes only one argument" % bits[0] + block_name = bits[1] + # Keep track of the names of BlockNodes found in this template, so we can + # check for duplication. + try: + if block_name in parser.__loaded_blocks: + raise TemplateSyntaxError, "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) + parser.__loaded_blocks.append(block_name) + except AttributeError: # parser._loaded_blocks isn't a list yet + parser.__loaded_blocks = [block_name] + nodelist = parser.parse(('endblock',)) + parser.delete_first_token() + return BlockNode(block_name, nodelist) + +def do_extends(parser, token): + """ + Signal that this template extends a parent template. + + This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) + uses the literal value "base" as the name of the parent template to extend, + or ``{% extends variable %}`` uses the value of ``variable`` as the name + of the parent template to extend. + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'%s' takes one argument" % bits[0] + parent_name, parent_name_expr = None, None + if bits[1][0] in ('"', "'") and bits[1][-1] == bits[1][0]: + parent_name = bits[1][1:-1] + else: + parent_name_expr = parser.compile_filter(bits[1]) + nodelist = parser.parse() + if nodelist.get_nodes_by_type(ExtendsNode): + raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] + return ExtendsNode(nodelist, parent_name, parent_name_expr) + +def do_include(parser, token): + """ + Loads a template and renders it with the current context. + + Example:: + + {% include "foo/some_include" %} + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] + path = bits[1] + if path[0] in ('"', "'") and path[-1] == path[0]: + return ConstantIncludeNode(path[1:-1]) + return IncludeNode(bits[1]) + +register.tag('block', do_block) +register.tag('extends', do_extends) +register.tag('include', do_include) diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index e6ef3b062e..7c2019cac0 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -1,9 +1,11 @@ -from django.core.template import Node, NodeList, Template, Context, resolve_variable, resolve_variable_with_filters, registered_filters -from django.core.template import TemplateSyntaxError, register_tag, TokenParser +from django.core.template import Node, NodeList, Template, Context, resolve_variable +from django.core.template import TemplateSyntaxError, TokenParser, Library from django.core.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR from django.utils import translation import re, sys +register = Library() + class GetAvailableLanguagesNode(Node): def __init__(self, variable): self.variable = variable @@ -53,10 +55,10 @@ class BlockTranslateNode(Node): def render(self, context): context.push() for var,val in self.extra_context.items(): - context[var] = resolve_variable_with_filters(val, context) + context[var] = val.resolve(context) singular = self.render_token_list(self.singular) if self.plural and self.countervar and self.counter: - count = resolve_variable_with_filters(self.counter, context) + count = self.counter.resolve(context) context[self.countervar] = count plural = self.render_token_list(self.plural) result = translation.ngettext(singular, plural, count) % context @@ -179,9 +181,9 @@ def do_block_translate(parser, token): value = self.value() if self.tag() != 'as': raise TemplateSyntaxError, "variable bindings in 'blocktrans' must be 'with value as variable'" - extra_context[self.tag()] = value + extra_context[self.tag()] = parser.compile_filter(value) elif tag == 'count': - counter = self.value() + counter = parser.compile_filter(self.value()) if self.tag() != 'as': raise TemplateSyntaxError, "counter specification in 'blocktrans' must be 'count value as variable'" countervar = self.tag() @@ -213,7 +215,7 @@ def do_block_translate(parser, token): return BlockTranslateNode(extra_context, singular, plural, countervar, counter) -register_tag('get_available_languages', do_get_available_languages) -register_tag('get_current_language', do_get_current_language) -register_tag('trans', do_translate) -register_tag('blocktrans', do_block_translate) +register.tag('get_available_languages', do_get_available_languages) +register.tag('get_current_language', do_get_current_language) +register.tag('trans', do_translate) +register.tag('blocktrans', do_block_translate) diff --git a/docs/templates.txt b/docs/templates.txt index 53df12e6a0..8c673e1d1c 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -278,6 +278,11 @@ In the above, the ``load`` tag loads the ``comments`` tag library, which then makes the ``comment_form`` tag available for use. Consult the documentation area in your admin to find the list of custom libraries in your installation. +**New in Django development version:** The ``{% load %}`` tag can take multiple +library names, separated by spaces. Example:: + + {% load comments i18n %} + Built-in tag and filter reference ================================= diff --git a/docs/templates_python.txt b/docs/templates_python.txt index 48c17c63ae..790b580bd6 100644 --- a/docs/templates_python.txt +++ b/docs/templates_python.txt @@ -444,6 +444,15 @@ the given Python module name, not the name of the app. Once you've created that Python module, you'll just have to write a bit of Python code, depending on whether you're writing filters or tags. +To be a valid tag library, the module contain a module-level variable that is a +``template.Library`` instance, in which all the tags and filters are +registered. So, near the top of your module, put the following:: + + from django.core import template + register = template.Library() + +Convention is to call this instance ``register``. + .. admonition:: Behind the scenes For a ton of examples, read the source code for Django's default filters @@ -453,10 +462,16 @@ Python code, depending on whether you're writing filters or tags. Writing custom template filters ------------------------------- -Custom filters are just Python functions that take two arguments: +**This section applies to the Django development version.** - * The value of the variable (input) -- not necessarily a string - * The value of the argument -- always a string +Custom filters are just Python functions that take one or two arguments: + + * The value of the variable (input) -- not necessarily a string. + * The value of the argument -- this can have a default value, or be left + out altogether. + +For example, in the filter ``{{ var|foo:"bar" }}``, the filter ``foo`` would be +passed the variable ``var`` and the argument ``"bar"``. Filter functions should always return something. They shouldn't raise exceptions. They should fail silently. In case of error, they should return @@ -468,36 +483,48 @@ Here's an example filter definition:: "Removes all values of arg from the given string" return value.replace(arg, '') -Most filters don't take arguments. For filters that don't take arguments, the -convention is to use a single underscore as the second argument to the filter -definition. Example:: +And here's an example of how that filter would be used:: - def lower(value, _): + {{ somevariable|cut:"0" }} + +Most filters don't take arguments. In this case, just leave the argument out of +your function. Example:: + + def lower(value): # Only one argument. "Converts a string into all lowercase" return value.lower() -When you've written your filter definition, you need to register it, to make it -available to Django's template language:: +When you've written your filter definition, you need to register it with +your ``Library`` instance, to make it available to Django's template language:: - from django.core import template - template.register_filter('cut', cut, True) - template.register_filter('lower', lower, False) + register.filter('cut', cut) + register.filter('lower', lower) -``register_filter`` takes three arguments: +The ``Library.filter()`` method takes two arguments: 1. The name of the filter -- a string. 2. The compilation function -- a Python function (not the name of the function as a string). - 3. A boolean, designating whether the filter requires an argument. This - tells Django's template parser whether to throw ``TemplateSyntaxError`` - when filter arguments are given (or missing). -The convention is to put all ``register_filter`` calls at the bottom of your -template-library module. +If you're using Python 2.4 or above, you can use ``register.filter()`` as a +decorator instead:: + + @register.filter(name='cut') + def cut(value, arg): + return value.replace(arg, '') + + @register.filter + def lower(value): + return value.lower() + +If you leave off the ``name`` argument, as in the second example above, Django +will use the function's name as the filter name. Writing custom template tags ---------------------------- +**This section applies to the Django development version.** + Tags are more complex than filters, because tags can do anything. A quick overview @@ -525,8 +552,6 @@ For each template tag the template parser encounters, it calls a Python function with the tag contents and the parser object itself. This function is responsible for returning a ``Node`` instance based on the contents of the tag. -By convention, the name of each compilation function should start with ``do_``. - For example, let's write a template tag, ``{% current_time %}``, that displays the current date/time, formatted according to a parameter given in the tag, in `strftime syntax`_. It's a good idea to decide the tag syntax before anything @@ -612,17 +637,32 @@ without having to be parsed multiple times. Registering the tag ~~~~~~~~~~~~~~~~~~~ -Finally, use a ``register_tag`` call, as in ``register_filter`` above. Example:: +Finally, register the tag with your module's ``Library`` instance, as explained +in "Writing custom template filters" above. Example:: - from django.core import template - template.register_tag('current_time', do_current_time) + register.tag('current_time', do_current_time) -``register_tag`` takes two arguments: +The ``tag()`` method takes two arguments: - 1. The name of the template tag -- a string. + 1. The name of the template tag -- a string. If this is left out, the + name of the compilation function will be used. 2. The compilation function -- a Python function (not the name of the function as a string). +As with filter registration, it is also possible to use this as a decorator, in +Python 2.4 and above: + + @register.tag(name="current_time") + def do_current_time(parser, token): + # ... + + @register.tag + def shout(parser, token): + # ... + +If you leave off the ``name`` argument, as in the second example above, Django +will use the function's name as the tag name. + Setting a variable in the context ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/othertests/defaultfilters.py b/tests/othertests/defaultfilters.py index d440e25dd5..539994ba84 100644 --- a/tests/othertests/defaultfilters.py +++ b/tests/othertests/defaultfilters.py @@ -1,15 +1,15 @@ """ ->>> floatformat(7.7, None) +>>> floatformat(7.7) '7.7' ->>> floatformat(7.0, None) +>>> floatformat(7.0) '7' ->>> floatformat(0.7, None) +>>> floatformat(0.7) '0.7' ->>> floatformat(0.07, None) +>>> floatformat(0.07) '0.1' ->>> floatformat(0.007, None) +>>> floatformat(0.007) '0.0' ->>> floatformat(0.0, None) +>>> floatformat(0.0) '0' """ diff --git a/tests/othertests/markup.py b/tests/othertests/markup.py index b1ffb99c8a..d2d2203b17 100644 --- a/tests/othertests/markup.py +++ b/tests/othertests/markup.py @@ -1,19 +1,20 @@ # Quick tests for the markup templatetags (django.contrib.markup) -from django.core.template import Template, Context -import django.contrib.markup.templatetags.markup # this registers the filters +from django.core.template import Template, Context, add_to_builtins + +add_to_builtins('django.contrib.markup.templatetags.markup') # find out if markup modules are installed and tailor the test appropriately try: import textile except ImportError: textile = None - + try: import markdown except ImportError: markdown = None - + try: import docutils except ImportError: @@ -36,7 +37,7 @@ if textile:

      Paragraph 2 with “quotes” and code

      """ else: assert rendered == textile_content - + ### test markdown markdown_content = """Paragraph 1 @@ -64,4 +65,4 @@ if docutils: assert rendered =="""

      Paragraph 1

      Paragraph 2 with a link

      """ else: - assert rendered == rest_content \ No newline at end of file + assert rendered == rest_content diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py index c1dbdde64f..86a67bea67 100644 --- a/tests/othertests/templates.py +++ b/tests/othertests/templates.py @@ -1,8 +1,12 @@ -import traceback +from django.conf import settings + +# Turn TEMPLATE_DEBUG off, because tests assume that. +settings.TEMPLATE_DEBUG = False from django.core import template from django.core.template import loader from django.utils.translation import activate, deactivate, install +import traceback # Helper objects for template tests class SomeClass: @@ -99,8 +103,14 @@ TEMPLATE_TESTS = { # Chained filters, with an argument to the first one 'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "Yes"}, "yes"), - #Escaped string as argument - 'basic-syntax30': (r"""{{ var|default_if_none:" endquote\" hah" }}""", {"var": None}, ' endquote" hah'), + # Escaped string as argument + 'basic-syntax30': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote" hah'), + + # Variable as argument + 'basic-syntax31': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'), + + # Default argument testing + 'basic-syntax32' : (r'{{ var|yesno:"yup,nup,mup" }} {{ var|yesno }}', {"var": True}, 'yup yes'), ### IF TAG ################################################################ 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"), @@ -289,7 +299,7 @@ TEMPLATE_TESTS = { def test_template_loader(template_name, template_dirs=None): "A custom template loader that loads the unit-test templates." try: - return ( TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name ) + return (TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name) except KeyError: raise template.TemplateDoesNotExist, template_name diff --git a/tests/testapp/templatetags/testtags.py b/tests/testapp/templatetags/testtags.py index 7b755043fa..e9177d8c24 100644 --- a/tests/testapp/templatetags/testtags.py +++ b/tests/testapp/templatetags/testtags.py @@ -2,6 +2,8 @@ from django.core import template +register = template.Library() + class EchoNode(template.Node): def __init__(self, contents): self.contents = contents @@ -11,5 +13,5 @@ class EchoNode(template.Node): def do_echo(parser, token): return EchoNode(token.contents.split()[1:]) - -template.register_tag("echo", do_echo) \ No newline at end of file + +register.tag("echo", do_echo) \ No newline at end of file