From c7d7e4cb78aec43e23603993d7a119fb99598ab9 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Fri, 7 Oct 2005 01:13:32 +0000 Subject: [PATCH] Template fixes for inline tabular to let errors show up properly Typo fix for extends tag doc Hugely improved messages for template syntax errors. Tokens and nodes are tagged with file and line numbers. These are used in error messages. Example: TemplateSyntaxError: Unclosed tag 'if' starting at /usr/lib/python2.4/site-packages/django/conf/admin_templates/admin_submit_line.html, line 6. Looking for one of: else, endif git-svn-id: http://code.djangoproject.com/svn/django/branches/new-admin@796 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- .../admin_edit_inline_tabular.html | 2 +- .../admin_templates/admin_submit_line.html | 10 +-- django/core/defaulttags.py | 13 ++- django/core/template.py | 84 ++++++++++++++----- django/core/template_file.py | 10 ++- django/core/template_loader.py | 12 +-- django/views/admin/main.py | 3 +- 7 files changed, 95 insertions(+), 39 deletions(-) diff --git a/django/conf/admin_templates/admin_edit_inline_tabular.html b/django/conf/admin_templates/admin_edit_inline_tabular.html index e8882fa627..a7f25cbcbe 100644 --- a/django/conf/admin_templates/admin_edit_inline_tabular.html +++ b/django/conf/admin_templates/admin_edit_inline_tabular.html @@ -21,7 +21,7 @@ {% for bound_field in fcw.bound_fields %} {% if not bound_field.not_in_table %} - + {% field_widget bound_field %} {% endif %} diff --git a/django/conf/admin_templates/admin_submit_line.html b/django/conf/admin_templates/admin_submit_line.html index 0182d53601..8f495d53ab 100644 --- a/django/conf/admin_templates/admin_submit_line.html +++ b/django/conf/admin_templates/admin_submit_line.html @@ -1,7 +1,7 @@
- {% if show_delete_link %}

Delete

{% endif %} - {% if show_save_as_new %}{%endif%} - {% if show_save_and_add_another %}{% endif %} - {% if show_save_and_continue %}{% endif %} - {% if show_save %}{% endif %} +{% if show_delete_link %}

Delete

{% endif %} +{% if show_save_as_new %}{%endif%} +{% if show_save_and_add_another %}{% endif %} +{% if show_save_and_continue %}{% endif %} +{% if show_save %}
diff --git a/django/core/defaulttags.py b/django/core/defaulttags.py index 41595fcee0..3ff05a8ae1 100644 --- a/django/core/defaulttags.py +++ b/django/core/defaulttags.py @@ -230,11 +230,17 @@ class SsiNode(template.Node): class ConstantIncludeNode(template.Node): def __init__(self, template_path): - t = template_loader.get_template(template_path) - self.nodelist = t.nodelist + try: + t = template_loader.get_template(template_path) + self.nodelist = t.nodelist + except Exception, e: + self.nodelist = None def render(self, context): - return self.nodelist.render(context) + if self.nodelist: + return self.nodelist.render(context) + else: + return '' class IncludeNode(template.Node): def __init__(self, template_path_var): @@ -247,7 +253,6 @@ class IncludeNode(template.Node): t = template_loader.get_template(template_path) return t.render(context) except Exception, e: - print e return '' # Fail silently for invalid included templates. diff --git a/django/core/template.py b/django/core/template.py index 71a8e621c8..6f1da2a858 100644 --- a/django/core/template.py +++ b/django/core/template.py @@ -74,10 +74,18 @@ VARIABLE_TAG_END = '}}' ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' +#What to report as the origin of templates that come from non file sources (eg strings) +UNKNOWN_SOURCE="" + + +#match starts of lines +newline_re = re.compile("^", re.M); + # match a variable or block tag and capture the entire tag, including start/end delimiters tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END), re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END))) + # global dict used by register_tag; maps custom tags to callback functions registered_tags = {} @@ -102,9 +110,9 @@ class SilentVariableFailure(Exception): pass class Template: - def __init__(self, template_string): + def __init__(self, template_string, filename=UNKNOWN_SOURCE): "Compilation stage" - self.nodelist = compile_string(template_string) + self.nodelist = compile_string(template_string, filename) def __iter__(self): for node in self.nodelist: @@ -115,9 +123,9 @@ class Template: "Display stage -- can be called many times" return self.nodelist.render(context) -def compile_string(template_string): +def compile_string(template_string, filename): "Compiles template_string into NodeList ready for rendering" - tokens = tokenize(template_string) + tokens = tokenize(template_string, filename) parser = Parser(tokens) return parser.parse() @@ -168,45 +176,70 @@ class Context: self.dicts = [other_dict] + self.dicts class Token: - def __init__(self, token_type, contents): + def __init__(self, token_type, contents, source): "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK" self.token_type, self.contents = token_type, contents + self.source = source def __str__(self): - return '<%s token: "%s...">' % ( + return '<%s token: "%s..." from %s, line %d>' % ( {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type], - self.contents[:20].replace('\n', '') + self.contents[:20].replace('\n', ''), + self.source[0], self.source[1] ) -def tokenize(template_string): + +def tokenize(template_string, filename): "Return a list of tokens from a given template_string" # remove all empty strings, because the regex has a tendency to add them - bits = filter(None, tag_re.split(template_string)) - return map(create_token, bits) + linebreaks = [match.start() for match in newline_re.finditer(template_string)] + lastline = len(linebreaks) + token_tups = [] + upto = 0 + line = 1 + + + for match in tag_re.finditer(template_string): + start, end = match.span() + if start > upto: + token_tups.append( (template_string[upto:start], line) ) + upto = start + while linebreaks and line != lastline and linebreaks[line] <= upto: + line += 1 + + token_tups.append( (template_string[start:end], line) ) + upto = end + + while linebreaks and line != lastline and linebreaks[line] <= upto: + line += 1 + + return [ create_token(tok, (filename, line)) for tok, line in token_tups] -def create_token(token_string): +def create_token(token_string, source): "Convert the given token string into a new Token object and return it" if token_string.startswith(VARIABLE_TAG_START): - return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) + return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip(), source) elif token_string.startswith(BLOCK_TAG_START): - return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) + return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip(), source) else: - return Token(TOKEN_TEXT, token_string) + return Token(TOKEN_TEXT, token_string, source) + class Parser: def __init__(self, tokens): self.tokens = tokens + self.command_stack = [] def parse(self, parse_until=[]): nodelist = NodeList() while self.tokens: token = self.next_token() if token.token_type == TOKEN_TEXT: - nodelist.append(TextNode(token.contents)) + nodelist.append(TextNode(token.contents), token) elif token.token_type == TOKEN_VAR: if not token.contents: - raise TemplateSyntaxError, "Empty variable tag" - nodelist.append(VariableNode(token.contents)) + raise TemplateSyntaxError, "Empty variable tag at %s, line %d" % (token.source[0], token.source[1]) + nodelist.append(VariableNode(token.contents), token) elif token.token_type == TOKEN_BLOCK: if token.contents in parse_until: # put token back on token list so calling code knows why it terminated @@ -218,11 +251,16 @@ class Parser: raise TemplateSyntaxError, "Empty block tag" try: # execute callback function for this tag and append resulting node - nodelist.append(registered_tags[command](self, token)) + self.command_stack.append( (command, token.source) ) + nodelist.append(registered_tags[command](self, token), token) + self.command_stack.pop() except KeyError: - raise TemplateSyntaxError, "Invalid block tag: '%s'" % command + raise TemplateSyntaxError, "Invalid block tag: '%s' at %s, line %d" % (command, token.source[0], token.source[1]) if parse_until: - raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until) + (command, (file,line)) = self.command_stack.pop() + msg = "Unclosed tag '%s' starting at %s, line %d. Looking for one of: %s " % \ + (command, file, line, ', '.join(parse_until) ) + raise TemplateSyntaxError, msg return nodelist def next_token(self): @@ -434,6 +472,7 @@ class Node: if hasattr(self, 'nodelist'): nodes.extend(self.nodelist.get_nodes_by_type(nodetype)) return nodes + class NodeList(list): def render(self, context): @@ -451,6 +490,11 @@ class NodeList(list): for node in self: nodes.extend(node.get_nodes_by_type(nodetype)) return nodes + + def append(self, node, token = None): + if token: + node.source = token.source + super(NodeList, self).append(node) class TextNode(Node): def __init__(self, s): diff --git a/django/core/template_file.py b/django/core/template_file.py index 6f5a324d61..ae2d017703 100644 --- a/django/core/template_file.py +++ b/django/core/template_file.py @@ -4,14 +4,16 @@ from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION from django.core.template import TemplateDoesNotExist import os -def load_template_source(template_name, template_dirs=None): + +def find_template_source(template_name, template_dirs=None): + "Returns a tuple of (template_string, filepath)." if not template_dirs: template_dirs = TEMPLATE_DIRS tried = [] for template_dir in template_dirs: filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION try: - return open(filepath).read() + return (open(filepath).read(), filepath) except IOError: tried.append(filepath) if template_dirs: @@ -19,3 +21,7 @@ def load_template_source(template_name, template_dirs=None): else: error_msg = "Your TEMPLATE_DIRS settings is empty. Change it to point to at least one template directory." raise TemplateDoesNotExist, error_msg + + +def load_template_source(template_name, template_dirs=None): + return find_template_source(template_name, template_dirs)[0] diff --git a/django/core/template_loader.py b/django/core/template_loader.py index 7c26cf0faa..b4b69a328e 100644 --- a/django/core/template_loader.py +++ b/django/core/template_loader.py @@ -1,6 +1,6 @@ "Wrapper for loading templates from storage of some sort (e.g. files or db)" import template -from template_file import load_template_source +from template_file import find_template_source class ExtendsError(Exception): pass @@ -10,14 +10,14 @@ def get_template(template_name): Returns a compiled template.Template object for the given template name, handling template inheritance recursively. """ - return get_template_from_string(load_template_source(template_name)) + return get_template_from_string(*find_template_source(template_name)) -def get_template_from_string(source): +def get_template_from_string(source, filename=template.UNKNOWN_SOURCE): """ Returns a compiled template.Template object for the given template code, handling template inheritance recursively. """ - return template.Template(source) + return template.Template(source, filename) def render_to_string(template_name, dictionary=None, context_instance=None): """ @@ -90,7 +90,7 @@ class ExtendsNode(template.Node): error_msg += " Got this from the %r variable." % self.parent_name_var raise template.TemplateSyntaxError, error_msg try: - return get_template_from_string(load_template_source(parent, self.template_dirs)) + return get_template_from_string(*find_template_source(parent, self.template_dirs)) except template.TemplateDoesNotExist: raise template.TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent @@ -142,7 +142,7 @@ def do_extends(parser, token): 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 ``{% entends variable %}`` uses the value of ``variable`` as the name + or ``{% extends variable %}`` uses the value of ``variable`` as the name of the parent template to extend. """ bits = token.contents.split() diff --git a/django/views/admin/main.py b/django/views/admin/main.py index 911448e11e..1e451413a1 100644 --- a/django/views/admin/main.py +++ b/django/views/admin/main.py @@ -573,7 +573,8 @@ class AdminBoundField(BoundField): classes.append('nowrap') if max([bool(f.errors()) for f in self.form_fields]): classes.append('error') - self.cell_class_attribute = ' '.join(classes) + if classes: + self.cell_class_attribute = ' class="%s" ' % ' '.join(classes) self._repr_filled = False