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