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
This commit is contained in:
Adrian Holovaty 2005-11-26 22:46:31 +00:00
parent 5676d5b068
commit 3ede006fc9
33 changed files with 781 additions and 582 deletions

View File

@ -1,7 +1,5 @@
{% extends "admin/base_site" %} {% extends "admin/base_site" %}
{% load i18n %} {% load i18n admin_modify adminmedia %}
{% load admin_modify %}
{% load adminmedia %}
{% block extrahead %} {% block extrahead %}
{% for js in bound_manipulator.javascript_imports %}{% include_admin_script js %}{% endfor %} {% for js in bound_manipulator.javascript_imports %}{% include_admin_script js %}{% endfor %}
{% endblock %} {% endblock %}

View File

@ -1,5 +1,4 @@
{% load admin_list %} {% load adminmedia admin_list i18n %}
{% load i18n %}
{% extends "admin/base_site" %} {% extends "admin/base_site" %}
{% block bodyclass %}change-list{% endblock %} {% block bodyclass %}change-list{% endblock %}
{% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> &rsaquo; {{ cl.opts.verbose_name_plural|capfirst }} </div>{% endblock %}{% endif %} {% if not is_popup %}{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans "Home" %}</a> &rsaquo; {{ cl.opts.verbose_name_plural|capfirst }} </div>{% endblock %}{% endif %}

View File

@ -1,3 +1,4 @@
{% load admin_modify %}
<fieldset class="module aligned"> <fieldset class="module aligned">
{% for fcw in bound_related_object.form_field_collection_wrappers %} {% for fcw in bound_related_object.form_field_collection_wrappers %}
<h2>{{ bound_related_object.relation.opts.verbose_name|capfirst }}&nbsp;#{{ forloop.counter }}</h2> <h2>{{ bound_related_object.relation.opts.verbose_name|capfirst }}&nbsp;#{{ forloop.counter }}</h2>

View File

@ -1,3 +1,4 @@
{% load admin_modify %}
<fieldset class="module"> <fieldset class="module">
<h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2><table> <h2>{{ bound_related_object.relation.opts.verbose_name_plural|capfirst }}</h2><table>
<thead><tr> <thead><tr>

View File

@ -1,3 +1,4 @@
{% load admin_modify %}
<div class="{{ class_names }}" > <div class="{{ class_names }}" >
{% for bound_field in bound_fields %}{{ bound_field.html_error_list }}{% endfor %} {% for bound_field in bound_fields %}{{ bound_field.html_error_list }}{% endfor %}
{% for bound_field in bound_fields %} {% for bound_field in bound_fields %}

View File

@ -1,3 +1,4 @@
{% load i18n %}
<h3>{% blocktrans %} By {{ title }} {% endblocktrans %}</h3> <h3>{% blocktrans %} By {{ title }} {% endblocktrans %}</h3>
<ul> <ul>
{% for choice in choices %} {% for choice in choices %}

View File

@ -1,3 +1,4 @@
{% load admin_list %}
{% if cl.has_filters %}<div id="changelist-filter"> {% if cl.has_filters %}<div id="changelist-filter">
<h2>Filter</h2> <h2>Filter</h2>
{% for spec in cl.filter_specs %} {% for spec in cl.filter_specs %}

View File

@ -1,3 +1,4 @@
{% load admin_list %}
<p class="paginator"> <p class="paginator">
{% if pagination_required %} {% if pagination_required %}
{% for i in page_range %} {% for i in page_range %}

View File

@ -1,3 +1,4 @@
{% load adminmedia %}
{% if cl.lookup_opts.admin.search_fields %} {% if cl.lookup_opts.admin.search_fields %}
<div id="toolbar"><form id="changelist-search" action="" method="get"> <div id="toolbar"><form id="changelist-search" action="" method="get">
<label><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" /></label> <label><img src="{% admin_media_prefix %}img/admin/icon_searchbox.png" /></label>

View File

@ -1 +1 @@
{% output_all bound_field.form_fields %} {% load admin_modify %}{% output_all bound_field.form_fields %}

View File

@ -1,4 +1,4 @@
{% if bound_field.original_value %} {% load admin_modify %}{% if bound_field.original_value %}
Currently: <a href="{{ bound_field.original_url }}" > {{ bound_field.original_value }} </a><br /> Currently: <a href="{{ bound_field.original_url }}" > {{ bound_field.original_value }} </a><br />
Change: {% output_all bound_field.form_fields %} Change: {% output_all bound_field.form_fields %}
{% else %} {% output_all bound_field.form_fields %} {% endif %} {% else %} {% output_all bound_field.form_fields %} {% endif %}

View File

@ -1,3 +1,4 @@
{% load admin_modify adminmedia %}
{% output_all bound_field.form_fields %} {% output_all bound_field.form_fields %}
{% if bound_field.raw_id_admin %} {% if bound_field.raw_id_admin %}
<a href="../../../{{ bound_field.field.rel.to.app_label }}/{{ bound_field.field.rel.to.module_name }}/" class="related-lookup" id="lookup_{{bound_field.element_id}}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a> <a href="../../../{{ bound_field.field.rel.to.app_label }}/{{ bound_field.field.rel.to.module_name }}/" class="related-lookup" id="lookup_{{bound_field.element_id}}" onclick="return showRelatedObjectLookupPopup(this);"> <img src="{% admin_media_prefix %}img/admin/selector-search.gif" width="16" height="16" alt="Lookup"></a>

View File

@ -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.contrib.admin.views.main import IS_POPUP_VAR, EMPTY_CHANGELIST_VALUE, MONTHS
from django.core import meta, template from django.core import meta, template
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.template.decorators import simple_tag, inclusion_tag
from django.utils import dateformat from django.utils import dateformat
from django.utils.html import strip_tags, escape from django.utils.html import strip_tags, escape
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import get_date_formats from django.utils.translation import get_date_formats
from django.conf.settings import ADMIN_MEDIA_PREFIX from django.conf.settings import ADMIN_MEDIA_PREFIX
from django.core.template import Library
register = Library()
DOT = '.' DOT = '.'
#@simple_tag #@register.simple_tag
def paginator_number(cl,i): def paginator_number(cl,i):
if i == DOT: if i == DOT:
return '... ' return '... '
@ -20,9 +22,9 @@ def paginator_number(cl,i):
return '<span class="this-page">%d</span> ' % (i+1) return '<span class="this-page">%d</span> ' % (i+1)
else: else:
return '<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1) return '<a href="%s"%s>%d</a> ' % (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): def pagination(cl):
paginator, page_num = cl.paginator, cl.page_num paginator, page_num = cl.paginator, cl.page_num
@ -64,7 +66,7 @@ def pagination(cl):
'ALL_VAR': ALL_VAR, 'ALL_VAR': ALL_VAR,
'1': 1, '1': 1,
} }
pagination = inclusion_tag('admin/pagination')(pagination) pagination = register.inclusion_tag('admin/pagination')(pagination)
def result_headers(cl): def result_headers(cl):
lookup_opts = cl.lookup_opts lookup_opts = cl.lookup_opts
@ -177,15 +179,15 @@ def results(cl):
for res in cl.result_list: for res in cl.result_list:
yield list(items_for_result(cl,res)) yield list(items_for_result(cl,res))
#@inclusion_tag("admin/change_list_results") #@register.inclusion_tag("admin/change_list_results")
def result_list(cl): def result_list(cl):
res = list(results(cl)) res = list(results(cl))
return {'cl': cl, return {'cl': cl,
'result_headers': list(result_headers(cl)), 'result_headers': list(result_headers(cl)),
'results': list(results(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): def date_hierarchy(cl):
lookup_opts, params, lookup_params, lookup_mod = \ lookup_opts, params, lookup_params, lookup_mod = \
cl.lookup_opts, cl.params, cl.lookup_params, cl.lookup_mod cl.lookup_opts, cl.params, cl.lookup_params, cl.lookup_mod
@ -256,23 +258,23 @@ def date_hierarchy(cl):
'title': year.year 'title': year.year
} for year in years ] } 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): def search_form(cl):
return { return {
'cl': cl, 'cl': cl,
'show_result_count': cl.result_count != cl.full_result_count and not cl.opts.one_to_one_field, 'show_result_count': cl.result_count != cl.full_result_count and not cl.opts.one_to_one_field,
'search_var': SEARCH_VAR '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): def filter(cl, spec):
return {'title': spec.title(), 'choices' : list(spec.choices(cl))} 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): def filters(cl):
return {'cl': cl} return {'cl': cl}
filters = inclusion_tag('admin/filters')(filters) filters = register.inclusion_tag('admin/filters')(filters)

View File

@ -2,24 +2,25 @@ from django.core import template, template_loader, meta
from django.utils.html import escape from django.utils.html import escape
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.functional import curry 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.contrib.admin.views.main import AdminBoundField
from django.core.meta.fields import BoundField, Field from django.core.meta.fields import BoundField, Field
from django.core.meta import BoundRelatedObject, TABULAR, STACKED from django.core.meta import BoundRelatedObject, TABULAR, STACKED
from django.conf.settings import ADMIN_MEDIA_PREFIX from django.conf.settings import ADMIN_MEDIA_PREFIX
import re import re
register = template.Library()
word_re = re.compile('[A-Z][a-z]+') word_re = re.compile('[A-Z][a-z]+')
def class_name_to_underscored(name): def class_name_to_underscored(name):
return '_'.join([s.lower() for s in word_re.findall(name)[:-1]]) return '_'.join([s.lower() for s in word_re.findall(name)[:-1]])
#@simple_tag #@register.simple_tag
def include_admin_script(script_path): def include_admin_script(script_path):
return '<script type="text/javascript" src="%s%s"></script>' % (ADMIN_MEDIA_PREFIX, script_path) return '<script type="text/javascript" src="%s%s"></script>' % (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): def submit_row(context, bound_manipulator):
change = context['change'] change = context['change']
add = context['add'] add = context['add']
@ -36,9 +37,9 @@ def submit_row(context, bound_manipulator):
'show_save_and_continue': not is_popup, 'show_save_and_continue': not is_popup,
'show_save': True '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): def field_label(bound_field):
class_names = [] class_names = []
if isinstance(bound_field.field, meta.BooleanField): 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 '' class_str = class_names and ' class="%s"' % ' '.join(class_names) or ''
return '<label for="%s"%s>%s%s</label> ' % (bound_field.element_id, class_str, \ return '<label for="%s"%s>%s%s</label> ' % (bound_field.element_id, class_str, \
capfirst(bound_field.field.verbose_name), colon) capfirst(bound_field.field.verbose_name), colon)
field_label = simple_tag(field_label) field_label = register.simple_tag(field_label)
class FieldWidgetNode(template.Node): class FieldWidgetNode(template.Node):
nodelists = {} nodelists = {}
@ -170,12 +171,12 @@ class EditInlineNode(template.Node):
context.pop() context.pop()
return output return output
#@simple_tag #@register.simple_tag
def output_all(form_fields): def output_all(form_fields):
return ''.join([str(f) for f in 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): def auto_populated_field_script(auto_pop_fields, change = False):
for field in auto_pop_fields: for field in auto_pop_fields:
t = [] t = []
@ -191,9 +192,9 @@ def auto_populated_field_script(auto_pop_fields, change = False):
' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % ( ' if(!e._changed) { e.value = URLify(%s, %s);} }; ' % (
f, field.name, add_values, field.maxlength)) f, field.name, add_values, field.maxlength))
return ''.join(t) 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): def filter_interface_script_maybe(bound_field):
f = bound_field.field f = bound_field.field
if f.rel and isinstance(f.rel, meta.ManyToMany) and f.rel.filter_interface: 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) f.name, f.verbose_name, f.rel.filter_interface-1, ADMIN_MEDIA_PREFIX)
else: else:
return '' 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): def do_one_arg_tag(node_factory, parser,token):
tokens = token.contents.split() tokens = token.contents.split()
@ -213,7 +214,7 @@ def do_one_arg_tag(node_factory, parser,token):
def register_one_arg_tag(node): def register_one_arg_tag(node):
tag_name = class_name_to_underscored(node.__name__) tag_name = class_name_to_underscored(node.__name__)
parse_func = curry(do_one_arg_tag, node) 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 = ( one_arg_tag_nodes = (
FieldWidgetNode, FieldWidgetNode,
@ -223,7 +224,7 @@ one_arg_tag_nodes = (
for node in one_arg_tag_nodes: for node in one_arg_tag_nodes:
register_one_arg_tag(node) 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): def admin_field_line(context, argument_val):
if (isinstance(argument_val, BoundField)): if (isinstance(argument_val, BoundField)):
bound_fields = [argument_val] bound_fields = [argument_val]
@ -249,10 +250,10 @@ def admin_field_line(context, argument_val):
'bound_fields': bound_fields, 'bound_fields': bound_fields,
'class_names': " ".join(class_names), '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): def object_pk(bound_manip, ordered_obj):
return bound_manip.get_ordered_object_pk(ordered_obj) return bound_manip.get_ordered_object_pk(ordered_obj)
object_pk = simple_tag(object_pk) object_pk = register.simple_tag(object_pk)

View File

@ -1,5 +1,7 @@
from django.core import template from django.core import template
register = template.Library()
class AdminApplistNode(template.Node): class AdminApplistNode(template.Node):
def __init__(self, varname): def __init__(self, varname):
self.varname = 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] raise template.TemplateSyntaxError, "First argument to '%s' tag must be 'as'" % tokens[0]
return AdminApplistNode(tokens[2]) 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)

View File

@ -1,4 +1,5 @@
from django.core.template.decorators import simple_tag from django.core.template import Library
register = Library()
def admin_media_prefix(): def admin_media_prefix():
try: try:
@ -6,4 +7,4 @@ def admin_media_prefix():
except ImportError: except ImportError:
return '' return ''
return ADMIN_MEDIA_PREFIX return ADMIN_MEDIA_PREFIX
admin_media_prefix = simple_tag(admin_media_prefix) admin_media_prefix = register.simple_tag(admin_media_prefix)

View File

@ -1,6 +1,8 @@
from django.models.admin import log from django.models.admin import log
from django.core import template from django.core import template
register = template.Library()
class AdminLogNode(template.Node): class AdminLogNode(template.Node):
def __init__(self, limit, varname, user): def __init__(self, limit, varname, user):
self.limit, self.varname, self.user = 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 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)) 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'))

View File

@ -50,21 +50,23 @@ class TemplateValidator(formfields.Manipulator):
return return
# so that inheritance works in the site's context, register a new function # 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): def new_do_extends(parser, token):
node = loader.do_extends(parser, token) node = loader.do_extends(parser, token)
node.template_dirs = settings_module.TEMPLATE_DIRS node.template_dirs = settings_module.TEMPLATE_DIRS
return node 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 # Now validate the template using the new template dirs
# making sure to reset the extends function in any case # making sure to reset the extends function in any case.
error = None error = None
try: try:
tmpl = loader.get_template_from_string(field_data) tmpl = loader.get_template_from_string(field_data)
tmpl.render(template.Context({})) tmpl.render(template.Context({}))
except template.TemplateSyntaxError, e: except template.TemplateSyntaxError, e:
error = e error = e
template.register_tag('extends', loader.do_extends) template.builtins.remove(register)
if error: if error:
raise validators.ValidationError, e.args raise validators.ValidationError, e.args

View File

@ -6,6 +6,8 @@ from django.models.comments import comments, freecomments
from django.models.core import contenttypes from django.models.core import contenttypes
import re import re
register = template.Library()
COMMENT_FORM = ''' COMMENT_FORM = '''
{% load i18n %} {% load i18n %}
{% if display_form %} {% if display_form %}
@ -360,10 +362,10 @@ class DoGetCommentList:
return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering) return CommentListNode(package, module, var_name, obj_id, tokens[5], self.free, ordering)
# registration comments # registration comments
template.register_tag('get_comment_list', DoGetCommentList(False)) register.tag('get_comment_list', DoGetCommentList(False))
template.register_tag('comment_form', DoCommentForm(False)) register.tag('comment_form', DoCommentForm(False))
template.register_tag('get_comment_count', DoCommentCount(False)) register.tag('get_comment_count', DoCommentCount(False))
# free comments # free comments
template.register_tag('get_free_comment_list', DoGetCommentList(True)) register.tag('get_free_comment_list', DoGetCommentList(True))
template.register_tag('free_comment_form', DoCommentForm(True)) register.tag('free_comment_form', DoCommentForm(True))
template.register_tag('get_free_comment_count', DoCommentCount(True)) register.tag('get_free_comment_count', DoCommentCount(True))

View File

@ -16,7 +16,9 @@ silently fail and return the un-marked-up text.
from django.core import template from django.core import template
def textile(value, _): register = template.Library()
def textile(value):
try: try:
import textile import textile
except ImportError: except ImportError:
@ -24,7 +26,7 @@ def textile(value, _):
else: else:
return textile.textile(value) return textile.textile(value)
def markdown(value, _): def markdown(value):
try: try:
import markdown import markdown
except ImportError: except ImportError:
@ -32,7 +34,7 @@ def markdown(value, _):
else: else:
return markdown.markdown(value) return markdown.markdown(value)
def restructuredtext(value, _): def restructuredtext(value):
try: try:
from docutils.core import publish_parts from docutils.core import publish_parts
except ImportError: except ImportError:
@ -41,6 +43,6 @@ def restructuredtext(value, _):
parts = publish_parts(source=value, writer_name="html4css1") parts = publish_parts(source=value, writer_name="html4css1")
return parts["fragment"] return parts["fragment"]
template.register_filter("textile", textile, False) register.filter(textile)
template.register_filter("markdown", markdown, False) register.filter(markdown)
template.register_filter("restructuredtext", restructuredtext, False) register.filter(restructuredtext)

View File

@ -3,7 +3,7 @@ This is the Django template system.
How it works: 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 markup with custom template tags) to tokens, which can be either plain text
(TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK). (TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK).
@ -55,6 +55,8 @@ times with multiple contexts)
'\n<html>\n\n</html>\n' '\n<html>\n\n</html>\n'
""" """
import re import re
from inspect import getargspec
from django.utils.functional import curry
from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG
__all__ = ('Template','Context','compile_string') __all__ = ('Template','Context','compile_string')
@ -82,11 +84,10 @@ UNKNOWN_SOURCE="<unknown source>"
tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), 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))) re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))
# global dict used by register_tag; maps custom tags to callback functions # global dictionary of libraries that have been loaded using get_library
registered_tags = {} libraries = {}
# global list of libraries to load by default for a new parser
# global dict used by register_filter; maps custom filters to callback functions builtins = []
registered_filters = {}
class TemplateSyntaxError(Exception): class TemplateSyntaxError(Exception):
pass pass
@ -105,12 +106,15 @@ class SilentVariableFailure(Exception):
"Any function raising this exception will be ignored by resolve_variable" "Any function raising this exception will be ignored by resolve_variable"
pass pass
class InvalidTemplateLibrary(Exception):
pass
class Origin(object): class Origin(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
def reload(self): def reload(self):
raise NotImplementedException raise NotImplementedError
def __str__(self): def __str__(self):
return self.name return self.name
@ -264,6 +268,10 @@ class DebugLexer(Lexer):
class Parser(object): class Parser(object):
def __init__(self, tokens): def __init__(self, tokens):
self.tokens = tokens self.tokens = tokens
self.tags = {}
self.filters = {}
for lib in builtins:
self.add_library(lib)
def parse(self, parse_until=[]): def parse(self, parse_until=[]):
nodelist = self.create_nodelist() nodelist = self.create_nodelist()
@ -274,7 +282,8 @@ class Parser(object):
elif token.token_type == TOKEN_VAR: elif token.token_type == TOKEN_VAR:
if not token.contents: if not token.contents:
self.empty_variable(token) 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) self.extend_nodelist(nodelist, var_node,token)
elif token.token_type == TOKEN_BLOCK: elif token.token_type == TOKEN_BLOCK:
if token.contents in parse_until: if token.contents in parse_until:
@ -288,7 +297,7 @@ class Parser(object):
# execute callback function for this tag and append resulting node # execute callback function for this tag and append resulting node
self.enter_command(command, token) self.enter_command(command, token)
try: try:
compile_func = registered_tags[command] compile_func = self.tags[command]
except KeyError: except KeyError:
self.invalid_block_tag(token, command) self.invalid_block_tag(token, command)
try: try:
@ -302,8 +311,8 @@ class Parser(object):
self.unclosed_block_tag(parse_until) self.unclosed_block_tag(parse_until)
return nodelist return nodelist
def create_variable_node(self, contents): def create_variable_node(self, filter_expression):
return VariableNode(contents) return VariableNode(filter_expression)
def create_nodelist(self): def create_nodelist(self):
return NodeList() return NodeList()
@ -344,6 +353,20 @@ class Parser(object):
def delete_first_token(self): def delete_first_token(self):
del self.tokens[0] 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): class DebugParser(Parser):
def __init__(self, lexer): def __init__(self, lexer):
super(DebugParser, self).__init__(lexer) super(DebugParser, self).__init__(lexer)
@ -483,7 +506,8 @@ filter_raw_string = r"""
(?:%(arg_sep)s (?:%(arg_sep)s
(?: (?:
%(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s| %(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s|
"(?P<arg>%(str)s)" "(?P<constant_arg>%(str)s)"|
(?P<var_arg>[%(var_chars)s]+)
) )
)? )?
)""" % { )""" % {
@ -498,7 +522,7 @@ filter_raw_string = r"""
filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "") filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
filter_re = re.compile(filter_raw_string) 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), 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. 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 This class should never be instantiated outside of the
get_filters_from_token helper function. get_filters_from_token helper function.
""" """
def __init__(self, token): def __init__(self, token, parser):
self.token = token
matches = filter_re.finditer(token) matches = filter_re.finditer(token)
var = None var = None
filters = [] filters = []
@ -536,27 +561,69 @@ class FilterParser(object):
raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var
else: else:
filter_name = match.group("filter_name") 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: if i18n_arg:
arg =_(i18n_arg.replace('\\', '')) args.append((False, _(i18n_arg.replace('\\', ''))))
if arg: elif constant_arg:
arg = arg.replace('\\', '') args.append((False, constant_arg.replace('\\', '')))
if not registered_filters.has_key(filter_name): elif var_arg:
raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name args.append((True, var_arg))
if registered_filters[filter_name][1] == True and arg is None: filter_func = parser.find_filter(filter_name)
raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name self.args_check(filter_name,filter_func, args)
if registered_filters[filter_name][1] == False and arg is not None: filters.append( (filter_func,args))
raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg)
filters.append( (filter_name,arg) )
upto = match.end() upto = match.end()
if upto != len(token): if upto != len(token):
raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:] raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:]
self.var , self.filters = var, filters self.var , self.filters = var, filters
def get_filters_from_token(token): def resolve(self, context):
"Convenient wrapper for FilterParser" try:
p = FilterParser(token) obj = resolve_variable(self.var, context)
return (p.var, p.filters) 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): def resolve_variable(path, context):
""" """
@ -607,22 +674,6 @@ def resolve_variable(path, context):
del bits[0] del bits[0]
return current 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: class Node:
def render(self, context): def render(self, context):
"Return the node rendered as a string" "Return the node rendered as a string"
@ -687,11 +738,11 @@ class TextNode(Node):
return self.s return self.s
class VariableNode(Node): class VariableNode(Node):
def __init__(self, var_string): def __init__(self, filter_expression):
self.var_string = var_string self.filter_expression = filter_expression
def __repr__(self): def __repr__(self):
return "<Variable Node: %s>" % self.var_string return "<Variable Node: %s>" % self.filter_expression
def encode_output(self, output): def encode_output(self, output):
# Check type so that we don't run str() on a Unicode object # Check type so that we don't run str() on a Unicode object
@ -703,30 +754,153 @@ class VariableNode(Node):
return output return output
def render(self, context): def render(self, context):
output = resolve_variable_with_filters(self.var_string, context) output = self.filter_expression.resolve(context)
return self.encode_output(output) return self.encode_output(output)
class DebugVariableNode(VariableNode): class DebugVariableNode(VariableNode):
def render(self, context): def render(self, context):
try: try:
output = resolve_variable_with_filters(self.var_string, context) output = self.filter_expression.resolve(context)
except TemplateSyntaxError, e: except TemplateSyntaxError, e:
if not hasattr(e, 'source'): if not hasattr(e, 'source'):
e.source = self.source e.source = self.source
raise raise
return self.encode_output(output) return self.encode_output(output)
def register_tag(token_command, callback_function): def generic_tag_compiler(params, defaults, name, node_class, parser, token):
registered_tags[token_command] = callback_function "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): class Library(object):
del registered_tags[token_command] def __init__(self):
self.filters = {}
self.tags = {}
def register_filter(filter_name, callback_function, has_arg): def tag(self, name = None, compile_function = None):
registered_filters[filter_name] = (callback_function, has_arg) 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): def tag_function(self,func):
del registered_filters[filter_name] self.tags[func.__name__] = func
return func
import defaulttags def filter(self, name = None, filter_func = None):
import defaultfilters 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')

View File

@ -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

View File

@ -1,28 +1,32 @@
"Default variable filters" "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 re
import random as random_module import random as random_module
register = Library()
################### ###################
# STRINGS # # STRINGS #
################### ###################
def addslashes(value, _):
def addslashes(value):
"Adds slashes - useful for passing strings to JavaScript, for example." "Adds slashes - useful for passing strings to JavaScript, for example."
return value.replace('"', '\\"').replace("'", "\\'") return value.replace('"', '\\"').replace("'", "\\'")
def capfirst(value, _): def capfirst(value):
"Capitalizes the first character of the value" "Capitalizes the first character of the value"
value = str(value) value = str(value)
return value and value[0].upper() + value[1:] return value and value[0].upper() + value[1:]
def fix_ampersands(value, _): def fix_ampersands(value):
"Replaces ampersands with ``&amp;`` entities" "Replaces ampersands with ``&amp;`` entities"
from django.utils.html import fix_ampersands from django.utils.html import fix_ampersands
return fix_ampersands(value) return fix_ampersands(value)
def floatformat(text, _): def floatformat(text):
""" """
Displays a floating point number as 34.2 (with one decimal place) -- but Displays a floating point number as 34.2 (with one decimal place) -- but
only if there's a point to be displayed only if there's a point to be displayed
@ -37,7 +41,7 @@ def floatformat(text, _):
else: else:
return '%d' % int(f) return '%d' % int(f)
def linenumbers(value, _): def linenumbers(value):
"Displays text with line numbers" "Displays text with line numbers"
from django.utils.html import escape from django.utils.html import escape
lines = value.split('\n') lines = value.split('\n')
@ -47,18 +51,18 @@ def linenumbers(value, _):
lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line))
return '\n'.join(lines) return '\n'.join(lines)
def lower(value, _): def lower(value):
"Converts a string into all lowercase" "Converts a string into all lowercase"
return value.lower() 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 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. digits. For a string, it's a list of characters.
""" """
return list(str(value)) return list(str(value))
def slugify(value, _): def slugify(value):
"Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens"
value = re.sub('[^\w\s-]', '', value).strip().lower() value = re.sub('[^\w\s-]', '', value).strip().lower()
return re.sub('\s+', '-', value) return re.sub('\s+', '-', value)
@ -77,7 +81,7 @@ def stringformat(value, arg):
except (ValueError, TypeError): except (ValueError, TypeError):
return "" return ""
def title(value, _): def title(value):
"Converts a string into titlecase" "Converts a string into titlecase"
return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) 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) value = str(value)
return truncate_words(value, length) return truncate_words(value, length)
def upper(value, _): def upper(value):
"Converts a string into all uppercase" "Converts a string into all uppercase"
return value.upper() return value.upper()
def urlencode(value, _): def urlencode(value):
"Escapes a value for use in a URL" "Escapes a value for use in a URL"
import urllib import urllib
return urllib.quote(value) return urllib.quote(value)
def urlize(value, _): def urlize(value):
"Converts URLs in plain text into clickable links" "Converts URLs in plain text into clickable links"
from django.utils.html import urlize from django.utils.html import urlize
return urlize(value, nofollow=True) return urlize(value, nofollow=True)
@ -119,7 +123,7 @@ def urlizetrunc(value, limit):
from django.utils.html import urlize from django.utils.html import urlize
return urlize(value, trim_url_limit=int(limit), nofollow=True) return urlize(value, trim_url_limit=int(limit), nofollow=True)
def wordcount(value, _): def wordcount(value):
"Returns the number of words" "Returns the number of words"
return len(value.split()) return len(value.split())
@ -160,17 +164,17 @@ def cut(value, arg):
# HTML STRINGS # # HTML STRINGS #
################### ###################
def escape(value, _): def escape(value):
"Escapes a string's HTML" "Escapes a string's HTML"
from django.utils.html import escape from django.utils.html import escape
return escape(value) return escape(value)
def linebreaks(value, _): def linebreaks(value):
"Converts newlines into <p> and <br />s" "Converts newlines into <p> and <br />s"
from django.utils.html import linebreaks from django.utils.html import linebreaks
return linebreaks(value) return linebreaks(value)
def linebreaksbr(value, _): def linebreaksbr(value):
"Converts newlines into <br />s" "Converts newlines into <br />s"
return value.replace('\n', '<br />') return value.replace('\n', '<br />')
@ -184,7 +188,7 @@ def removetags(value, tags):
value = endtag_re.sub('', value) value = endtag_re.sub('', value)
return value return value
def striptags(value, _): def striptags(value):
"Strips all [X]HTML tags" "Strips all [X]HTML tags"
from django.utils.html import strip_tags from django.utils.html import strip_tags
if not isinstance(value, basestring): if not isinstance(value, basestring):
@ -214,7 +218,7 @@ def dictsortreversed(value, arg):
decorated.reverse() decorated.reverse()
return [item[1] for item in decorated] return [item[1] for item in decorated]
def first(value, _): def first(value):
"Returns the first item in a list" "Returns the first item in a list"
try: try:
return value[0] return value[0]
@ -228,7 +232,7 @@ def join(value, arg):
except AttributeError: # fail silently but nicely except AttributeError: # fail silently but nicely
return value return value
def length(value, _): def length(value):
"Returns the length of the value - useful for lists" "Returns the length of the value - useful for lists"
return len(value) return len(value)
@ -236,7 +240,7 @@ def length_is(value, arg):
"Returns a boolean of whether the value's length is the argument" "Returns a boolean of whether the value's length is the argument"
return len(value) == int(arg) return len(value) == int(arg)
def random(value, _): def random(value):
"Returns a random item from the list" "Returns a random item from the list"
return random_module.choice(value) return random_module.choice(value)
@ -253,7 +257,7 @@ def slice_(value, arg):
except (ValueError, TypeError): except (ValueError, TypeError):
return value # Fail silently. return value # Fail silently.
def unordered_list(value, _): def unordered_list(value):
""" """
Recursively takes a self-nested list and returns an HTML unordered list -- Recursively takes a self-nested list and returns an HTML unordered list --
WITHOUT opening and closing <ul> tags. WITHOUT opening and closing <ul> tags.
@ -314,17 +318,17 @@ def get_digit(value, arg):
# DATES # # DATES #
################### ###################
def date(value, arg): def date(value, arg=DATE_FORMAT):
"Formats a date according to the given format" "Formats a date according to the given format"
from django.utils.dateformat import format from django.utils.dateformat import format
return format(value, arg) return format(value, arg)
def time(value, arg): def time(value, arg=TIME_FORMAT):
"Formats a time according to the given format" "Formats a time according to the given format"
from django.utils.dateformat import time_format from django.utils.dateformat import time_format
return time_format(value, arg) 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")' 'Formats a date as the time since that date (i.e. "4 days, 6 hours")'
from django.utils.timesince import timesince from django.utils.timesince import timesince
return timesince(value) return timesince(value)
@ -347,7 +351,7 @@ def divisibleby(value, arg):
"Returns true if the value is devisible by the argument" "Returns true if the value is devisible by the argument"
return int(value) % int(arg) == 0 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, Given a string mapping values for true, false and (optionally) None,
returns one of those strings accoding to the value: returns one of those strings accoding to the value:
@ -379,7 +383,7 @@ def yesno(value, arg):
# MISC # # MISC #
################### ###################
def filesizeformat(bytes, _): def filesizeformat(bytes):
""" """
Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102
bytes, etc). bytes, etc).
@ -393,7 +397,7 @@ def filesizeformat(bytes, _):
return "%.1f MB" % (bytes / (1024 * 1024)) return "%.1f MB" % (bytes / (1024 * 1024))
return "%.1f GB" % (bytes / (1024 * 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'" "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'"
try: try:
if int(value) != 1: if int(value) != 1:
@ -408,62 +412,62 @@ def pluralize(value, _):
pass pass
return '' return ''
def phone2numeric(value, _): def phone2numeric(value):
"Takes a phone number and converts it in to its numerical equivalent" "Takes a phone number and converts it in to its numerical equivalent"
from django.utils.text import phone2numeric from django.utils.text import phone2numeric
return phone2numeric(value) return phone2numeric(value)
def pprint(value, _): def pprint(value):
"A wrapper around pprint.pprint -- for debugging, really" "A wrapper around pprint.pprint -- for debugging, really"
from pprint import pformat from pprint import pformat
return pformat(value) return pformat(value)
# Syntax: register_filter(name of filter, callback, has_argument) # Syntax: register.filter(name of filter, callback)
register_filter('add', add, True) register.filter(add)
register_filter('addslashes', addslashes, False) register.filter(addslashes)
register_filter('capfirst', capfirst, False) register.filter(capfirst)
register_filter('center', center, True) register.filter(center)
register_filter('cut', cut, True) register.filter(cut)
register_filter('date', date, True) register.filter(date)
register_filter('default', default, True) register.filter(default)
register_filter('default_if_none', default_if_none, True) register.filter(default_if_none)
register_filter('dictsort', dictsort, True) register.filter(dictsort)
register_filter('dictsortreversed', dictsortreversed, True) register.filter(dictsortreversed)
register_filter('divisibleby', divisibleby, True) register.filter(divisibleby)
register_filter('escape', escape, False) register.filter(escape)
register_filter('filesizeformat', filesizeformat, False) register.filter(filesizeformat)
register_filter('first', first, False) register.filter(first)
register_filter('fix_ampersands', fix_ampersands, False) register.filter(fix_ampersands)
register_filter('floatformat', floatformat, False) register.filter(floatformat)
register_filter('get_digit', get_digit, True) register.filter(get_digit)
register_filter('join', join, True) register.filter(join)
register_filter('length', length, False) register.filter(length)
register_filter('length_is', length_is, True) register.filter(length_is)
register_filter('linebreaks', linebreaks, False) register.filter(linebreaks)
register_filter('linebreaksbr', linebreaksbr, False) register.filter(linebreaksbr)
register_filter('linenumbers', linenumbers, False) register.filter(linenumbers)
register_filter('ljust', ljust, True) register.filter(ljust)
register_filter('lower', lower, False) register.filter(lower)
register_filter('make_list', make_list, False) register.filter(make_list)
register_filter('phone2numeric', phone2numeric, False) register.filter(phone2numeric)
register_filter('pluralize', pluralize, False) register.filter(pluralize)
register_filter('pprint', pprint, False) register.filter(pprint)
register_filter('removetags', removetags, True) register.filter(removetags)
register_filter('random', random, False) register.filter(random)
register_filter('rjust', rjust, True) register.filter(rjust)
register_filter('slice', slice_, True) register.filter(slice_)
register_filter('slugify', slugify, False) register.filter(slugify)
register_filter('stringformat', stringformat, True) register.filter(stringformat)
register_filter('striptags', striptags, False) register.filter(striptags)
register_filter('time', time, True) register.filter(time)
register_filter('timesince', timesince, False) register.filter(timesince)
register_filter('title', title, False) register.filter(title)
register_filter('truncatewords', truncatewords, True) register.filter(truncatewords)
register_filter('unordered_list', unordered_list, False) register.filter(unordered_list)
register_filter('upper', upper, False) register.filter(upper)
register_filter('urlencode', urlencode, False) register.filter(urlencode)
register_filter('urlize', urlize, False) register.filter(urlize)
register_filter('urlizetrunc', urlizetrunc, True) register.filter(urlizetrunc)
register_filter('wordcount', wordcount, False) register.filter(wordcount)
register_filter('wordwrap', wordwrap, True) register.filter(wordwrap)
register_filter('yesno', yesno, True) register.filter(yesno)

View File

@ -1,9 +1,12 @@
"Default tags used by the template system, available to all templates." "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 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, register_tag 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 import sys
register = Library()
class CommentNode(Node): class CommentNode(Node):
def render(self, context): def render(self, context):
return '' return ''
@ -27,15 +30,13 @@ class DebugNode(Node):
return ''.join(output) return ''.join(output)
class FilterNode(Node): class FilterNode(Node):
def __init__(self, filters, nodelist): def __init__(self, filter_expr, nodelist):
self.filters, self.nodelist = filters, nodelist self.filter_expr, self.nodelist = filter_expr, nodelist
def render(self, context): def render(self, context):
output = self.nodelist.render(context) output = self.nodelist.render(context)
# apply filters # apply filters
for f in self.filters: return self.filter_expr.resolve(Context({'var': output}))
output = registered_filters[f[0]][0](output, f[1])
return output
class FirstOfNode(Node): class FirstOfNode(Node):
def __init__(self, vars): def __init__(self, vars):
@ -81,7 +82,7 @@ class ForNode(Node):
parentloop = {} parentloop = {}
context.push() context.push()
try: try:
values = resolve_variable_with_filters(self.sequence, context) values = self.sequence.resolve(context)
except VariableDoesNotExist: except VariableDoesNotExist:
values = [] values = []
if values is None: if values is None:
@ -147,8 +148,8 @@ class IfEqualNode(Node):
return self.nodelist_false.render(context) return self.nodelist_false.render(context)
class IfNode(Node): class IfNode(Node):
def __init__(self, boolvars, nodelist_true, nodelist_false): def __init__(self, bool_exprs, nodelist_true, nodelist_false):
self.boolvars = boolvars self.bool_exprs = bool_exprs
self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false
def __repr__(self): def __repr__(self):
@ -169,9 +170,9 @@ class IfNode(Node):
return nodes return nodes
def render(self, context): def render(self, context):
for ifnot, boolvar in self.boolvars: for ifnot, bool_expr in self.bool_exprs:
try: try:
value = resolve_variable_with_filters(boolvar, context) value = bool_expr.resolve(context)
except VariableDoesNotExist: except VariableDoesNotExist:
value = None value = None
if (value and not ifnot) or (ifnot and not value): if (value and not ifnot) or (ifnot and not value):
@ -179,19 +180,18 @@ class IfNode(Node):
return self.nodelist_false.render(context) return self.nodelist_false.render(context)
class RegroupNode(Node): class RegroupNode(Node):
def __init__(self, target_var, expression, var_name): def __init__(self, target, expression, var_name):
self.target_var, self.expression = target_var, expression self.target, self.expression = target, expression
self.var_name = var_name self.var_name = var_name
def render(self, context): 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 if obj_list == '': # target_var wasn't found in context; fail silently
context[self.var_name] = [] context[self.var_name] = []
return '' return ''
output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]}
for obj in obj_list: for obj in obj_list:
grouper = resolve_variable_with_filters('var.%s' % self.expression, \ grouper = self.expression.resolve(Context({'var': obj}))
Context({'var': obj}))
# TODO: Is this a sensible way to determine equality? # TODO: Is this a sensible way to determine equality?
if output and repr(output[-1]['grouper']) == repr(grouper): if output and repr(output[-1]['grouper']) == repr(grouper):
output[-1]['list'].append(obj) output[-1]['list'].append(obj)
@ -236,21 +236,7 @@ class SsiNode(Node):
return output return output
class LoadNode(Node): 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): def render(self, context):
"Import the relevant module"
try:
self.__class__.load_taglib(self.taglib)
except ImportError:
pass # Fail silently for invalid loads.
return '' return ''
class NowNode(Node): class NowNode(Node):
@ -276,15 +262,15 @@ class TemplateTagNode(Node):
return self.mapping.get(self.tagtype, '') return self.mapping.get(self.tagtype, '')
class WidthRatioNode(Node): class WidthRatioNode(Node):
def __init__(self, val_var, max_var, max_width): def __init__(self, val_expr, max_expr, max_width):
self.val_var = val_var self.val_expr = val_expr
self.max_var = max_var self.max_expr = max_expr
self.max_width = max_width self.max_width = max_width
def render(self, context): def render(self, context):
try: try:
value = resolve_variable_with_filters(self.val_var, context) value = self.val_expr.resolve(context)
maxvalue = resolve_variable_with_filters(self.max_var, context) maxvalue = self.max_expr.resolve(context)
except VariableDoesNotExist: except VariableDoesNotExist:
return '' return ''
try: try:
@ -295,15 +281,18 @@ class WidthRatioNode(Node):
return '' return ''
return str(int(round(ratio))) return str(int(round(ratio)))
def do_comment(parser, token): #@register.tag
def comment(parser, token):
""" """
Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` Ignore everything between ``{% comment %}`` and ``{% endcomment %}``
""" """
nodelist = parser.parse(('endcomment',)) nodelist = parser.parse(('endcomment',))
parser.delete_first_token() parser.delete_first_token()
return CommentNode() 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 Cycle among the given strings each time this tag is encountered
@ -369,11 +358,9 @@ def do_cycle(parser, token):
else: else:
raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args)
cycle = register.tag(cycle)
def do_debug(parser, token): #@register.tag(name="filter")
"Print a whole load of debugging information, including the context and imported modules"
return DebugNode()
def do_filter(parser, token): def do_filter(parser, token):
""" """
Filter the contents of the blog through variable filters. Filter the contents of the blog through variable filters.
@ -388,12 +375,14 @@ def do_filter(parser, token):
{% endfilter %} {% endfilter %}
""" """
_, rest = token.contents.split(None, 1) _, 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',)) nodelist = parser.parse(('endfilter',))
parser.delete_first_token() 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. Outputs the first variable passed that is not False.
@ -419,8 +408,9 @@ def do_firstof(parser, token):
if len(bits) < 1: if len(bits) < 1:
raise TemplateSyntaxError, "'firstof' statement requires at least one argument" raise TemplateSyntaxError, "'firstof' statement requires at least one argument"
return FirstOfNode(bits) return FirstOfNode(bits)
firstof = register.tag(firstof)
#@register.tag(name="for")
def do_for(parser, token): def do_for(parser, token):
""" """
Loop over each item in an array. Loop over each item in an array.
@ -462,11 +452,12 @@ def do_for(parser, token):
if bits[2] != 'in': if bits[2] != 'in':
raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents
loopvar = bits[1] loopvar = bits[1]
sequence = bits[3] sequence = parser.compile_filter(bits[3])
reversed = (len(bits) == 5) reversed = (len(bits) == 5)
nodelist_loop = parser.parse(('endfor',)) nodelist_loop = parser.parse(('endfor',))
parser.delete_first_token() parser.delete_first_token()
return ForNode(loopvar, sequence, reversed, nodelist_loop) return ForNode(loopvar, sequence, reversed, nodelist_loop)
do_for = register.tag("for", do_for)
def do_ifequal(parser, token, negate): def do_ifequal(parser, token, negate):
""" """
@ -497,6 +488,17 @@ def do_ifequal(parser, token, negate):
nodelist_false = NodeList() nodelist_false = NodeList()
return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) 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): def do_if(parser, token):
""" """
The ``{% if %}`` tag evaluates a variable, and if that variable is "true" 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() not_, boolvar = boolpair.split()
if not_ != 'not': if not_ != 'not':
raise TemplateSyntaxError, "Expected 'not' in if statement" raise TemplateSyntaxError, "Expected 'not' in if statement"
boolvars.append((True, boolvar)) boolvars.append((True, parser.compile_filter(boolvar)))
else: else:
boolvars.append((False, boolpair)) boolvars.append((False, parser.compile_filter(boolpair)))
nodelist_true = parser.parse(('else', 'endif')) nodelist_true = parser.parse(('else', 'endif'))
token = parser.next_token() token = parser.next_token()
if token.contents == 'else': if token.contents == 'else':
@ -565,8 +567,10 @@ def do_if(parser, token):
else: else:
nodelist_false = NodeList() nodelist_false = NodeList()
return IfNode(boolvars, nodelist_true, nodelist_false) 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. 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',)) nodelist = parser.parse(('endifchanged',))
parser.delete_first_token() parser.delete_first_token()
return IfChangedNode(nodelist) 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. Output the contents of a given file into the page.
@ -613,8 +619,10 @@ def do_ssi(parser, token):
else: else:
raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0]
return SsiNode(bits[1], parsed) 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. Load a custom template tag set.
@ -623,17 +631,18 @@ def do_load(parser, token):
{% load news.photos %} {% load news.photos %}
""" """
bits = token.contents.split() bits = token.contents.split()
if len(bits) != 2: for taglib in bits[1:]:
raise TemplateSyntaxError, "'load' statement takes one argument" # add the library to the parser
taglib = bits[1]
# check at compile time that the module can be imported
try: try:
LoadNode.load_taglib(taglib) lib = get_library("django.templatetags.%s" % taglib.split('.')[-1])
except ImportError, e: parser.add_library(lib)
except InvalidTemplateLibrary, e:
raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e) raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e)
return LoadNode(taglib) 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. 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" raise TemplateSyntaxError, "'now' statement takes one argument"
format_string = bits[1] format_string = bits[1]
return NowNode(format_string) 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. 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) firstbits = token.contents.split(None, 3)
if len(firstbits) != 4: if len(firstbits) != 4:
raise TemplateSyntaxError, "'regroup' tag takes five arguments" raise TemplateSyntaxError, "'regroup' tag takes five arguments"
target_var = firstbits[1] target = parser.compile_filter(firstbits[1])
if firstbits[2] != 'by': if firstbits[2] != 'by':
raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'"
lastbits_reversed = firstbits[3][::-1].split(None, 2) lastbits_reversed = firstbits[3][::-1].split(None, 2)
if lastbits_reversed[1][::-1] != 'as': if lastbits_reversed[1][::-1] != 'as':
raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be '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. 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" % \ raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \
(tag, TemplateTagNode.mapping.keys()) (tag, TemplateTagNode.mapping.keys())
return TemplateTagNode(tag) 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 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. 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() bits = token.contents.split()
if len(bits) != 4: if len(bits) != 4:
raise TemplateSyntaxError("widthratio takes three arguments") 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: try:
max_width = int(max_width) max_width = int(max_width)
except ValueError: except ValueError:
raise TemplateSyntaxError("widthratio final argument must be an integer") raise TemplateSyntaxError("widthratio final argument must be an integer")
return WidthRatioNode(this_value_var, max_value_var, max_width) return WidthRatioNode(parser.compile_filter(this_value_expr),
parser.compile_filter(max_value_expr), max_width)
register_tag('comment', do_comment) widthratio = register.tag(widthratio)
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)

View File

@ -21,7 +21,7 @@
# installed, because pkg_resources is necessary to read eggs. # installed, because pkg_resources is necessary to read eggs.
from django.core.exceptions import ImproperlyConfigured 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 from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG
template_source_loaders = [] template_source_loaders = []
@ -68,9 +68,6 @@ def find_template_source(name, dirs=None):
def load_template_source(name, dirs=None): def load_template_source(name, dirs=None):
find_template_source(name, dirs)[0] find_template_source(name, dirs)[0]
class ExtendsError(Exception):
pass
def get_template(template_name): def get_template(template_name):
""" """
Returns a compiled Template object for the given 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 # If we get here, none of the templates could be loaded
raise TemplateDoesNotExist, ', '.join(template_name_list) raise TemplateDoesNotExist, ', '.join(template_name_list)
class BlockNode(Node): add_to_builtins('django.core.template.loader_tags')
def __init__(self, name, nodelist, parent=None):
self.name, self.nodelist, self.parent = name, nodelist, parent
def __repr__(self):
return "<Block Node: %s. Contents: %r>" % (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)

View File

@ -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 "<Block Node: %s. Contents: %r>" % (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)

View File

@ -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 Node, NodeList, Template, Context, resolve_variable
from django.core.template import TemplateSyntaxError, register_tag, TokenParser from django.core.template import TemplateSyntaxError, TokenParser, Library
from django.core.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR from django.core.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR
from django.utils import translation from django.utils import translation
import re, sys import re, sys
register = Library()
class GetAvailableLanguagesNode(Node): class GetAvailableLanguagesNode(Node):
def __init__(self, variable): def __init__(self, variable):
self.variable = variable self.variable = variable
@ -53,10 +55,10 @@ class BlockTranslateNode(Node):
def render(self, context): def render(self, context):
context.push() context.push()
for var,val in self.extra_context.items(): 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) singular = self.render_token_list(self.singular)
if self.plural and self.countervar and self.counter: 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 context[self.countervar] = count
plural = self.render_token_list(self.plural) plural = self.render_token_list(self.plural)
result = translation.ngettext(singular, plural, count) % context result = translation.ngettext(singular, plural, count) % context
@ -179,9 +181,9 @@ def do_block_translate(parser, token):
value = self.value() value = self.value()
if self.tag() != 'as': if self.tag() != 'as':
raise TemplateSyntaxError, "variable bindings in 'blocktrans' must be 'with value as variable'" 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': elif tag == 'count':
counter = self.value() counter = parser.compile_filter(self.value())
if self.tag() != 'as': if self.tag() != 'as':
raise TemplateSyntaxError, "counter specification in 'blocktrans' must be 'count value as variable'" raise TemplateSyntaxError, "counter specification in 'blocktrans' must be 'count value as variable'"
countervar = self.tag() countervar = self.tag()
@ -213,7 +215,7 @@ def do_block_translate(parser, token):
return BlockTranslateNode(extra_context, singular, plural, countervar, counter) return BlockTranslateNode(extra_context, singular, plural, countervar, counter)
register_tag('get_available_languages', do_get_available_languages) register.tag('get_available_languages', do_get_available_languages)
register_tag('get_current_language', do_get_current_language) register.tag('get_current_language', do_get_current_language)
register_tag('trans', do_translate) register.tag('trans', do_translate)
register_tag('blocktrans', do_block_translate) register.tag('blocktrans', do_block_translate)

View File

@ -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 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. 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 Built-in tag and filter reference
================================= =================================

View File

@ -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 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. 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 .. admonition:: Behind the scenes
For a ton of examples, read the source code for Django's default filters 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 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 Custom filters are just Python functions that take one or two arguments:
* The value of the argument -- always a string
* 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 Filter functions should always return something. They shouldn't raise
exceptions. They should fail silently. In case of error, they should return 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" "Removes all values of arg from the given string"
return value.replace(arg, '') return value.replace(arg, '')
Most filters don't take arguments. For filters that don't take arguments, the And here's an example of how that filter would be used::
convention is to use a single underscore as the second argument to the filter
definition. Example::
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" "Converts a string into all lowercase"
return value.lower() return value.lower()
When you've written your filter definition, you need to register it, to make it When you've written your filter definition, you need to register it with
available to Django's template language:: your ``Library`` instance, to make it available to Django's template language::
from django.core import template register.filter('cut', cut)
template.register_filter('cut', cut, True) register.filter('lower', lower)
template.register_filter('lower', lower, False)
``register_filter`` takes three arguments: The ``Library.filter()`` method takes two arguments:
1. The name of the filter -- a string. 1. The name of the filter -- a string.
2. The compilation function -- a Python function (not the name of the 2. The compilation function -- a Python function (not the name of the
function as a string). 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 If you're using Python 2.4 or above, you can use ``register.filter()`` as a
template-library module. 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 Writing custom template tags
---------------------------- ----------------------------
**This section applies to the Django development version.**
Tags are more complex than filters, because tags can do anything. Tags are more complex than filters, because tags can do anything.
A quick overview 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 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. 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 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 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 `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 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 register.tag('current_time', do_current_time)
template.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 2. The compilation function -- a Python function (not the name of the
function as a string). 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 Setting a variable in the context
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -1,15 +1,15 @@
""" """
>>> floatformat(7.7, None) >>> floatformat(7.7)
'7.7' '7.7'
>>> floatformat(7.0, None) >>> floatformat(7.0)
'7' '7'
>>> floatformat(0.7, None) >>> floatformat(0.7)
'0.7' '0.7'
>>> floatformat(0.07, None) >>> floatformat(0.07)
'0.1' '0.1'
>>> floatformat(0.007, None) >>> floatformat(0.007)
'0.0' '0.0'
>>> floatformat(0.0, None) >>> floatformat(0.0)
'0' '0'
""" """

View File

@ -1,7 +1,8 @@
# Quick tests for the markup templatetags (django.contrib.markup) # Quick tests for the markup templatetags (django.contrib.markup)
from django.core.template import Template, Context from django.core.template import Template, Context, add_to_builtins
import django.contrib.markup.templatetags.markup # this registers the filters
add_to_builtins('django.contrib.markup.templatetags.markup')
# find out if markup modules are installed and tailor the test appropriately # find out if markup modules are installed and tailor the test appropriately
try: try:

View File

@ -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 import template
from django.core.template import loader from django.core.template import loader
from django.utils.translation import activate, deactivate, install from django.utils.translation import activate, deactivate, install
import traceback
# Helper objects for template tests # Helper objects for template tests
class SomeClass: class SomeClass:
@ -100,7 +104,13 @@ TEMPLATE_TESTS = {
'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"), 'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"),
# Escaped string as argument # Escaped string as argument
'basic-syntax30': (r"""{{ var|default_if_none:" endquote\" hah" }}""", {"var": None}, ' endquote" hah'), '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 TAG ################################################################
'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"), 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"),

View File

@ -2,6 +2,8 @@
from django.core import template from django.core import template
register = template.Library()
class EchoNode(template.Node): class EchoNode(template.Node):
def __init__(self, contents): def __init__(self, contents):
self.contents = contents self.contents = contents
@ -12,4 +14,4 @@ class EchoNode(template.Node):
def do_echo(parser, token): def do_echo(parser, token):
return EchoNode(token.contents.split()[1:]) return EchoNode(token.contents.split()[1:])
template.register_tag("echo", do_echo) register.tag("echo", do_echo)