diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index c76857e4b6..12d26dd4b7 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -97,9 +97,9 @@ TEMPLATE_FILE_EXTENSION = '.html' # See the comments in django/core/template/loader.py for interface # documentation. TEMPLATE_LOADERS = ( - 'django.core.template.loaders.filesystem.load_template_source', - 'django.core.template.loaders.app_directories.load_template_source', -# 'django.core.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', ) # List of processors used by DjangoContext to populate the context. diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index 511726a453..c5cd9ce17d 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -45,9 +45,9 @@ SECRET_KEY = '' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( - 'django.core.template.loaders.filesystem.load_template_source', - 'django.core.template.loaders.app_directories.load_template_source', -# 'django.core.template.loaders.eggs.load_template_source', + 'django.template.loaders.filesystem.load_template_source', + 'django.template.loaders.app_directories.load_template_source', +# 'django.template.loaders.eggs.load_template_source', ) MIDDLEWARE_CLASSES = ( diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py index d2f7a97557..280a1922c7 100644 --- a/django/contrib/admin/templatetags/admin_list.py +++ b/django/contrib/admin/templatetags/admin_list.py @@ -1,7 +1,7 @@ from django.contrib.admin.views.changelist import MAX_SHOW_ALL_ALLOWED, DEFAULT_RESULTS_PER_PAGE, ALL_VAR from django.contrib.admin.views.changelist import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR from django.contrib.admin.views.changelist import IS_POPUP_VAR, EMPTY_CHANGELIST_VALUE, MONTHS -from django.core import template +from django import template from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils import dateformat @@ -9,7 +9,7 @@ from django.utils.html import strip_tags, escape from django.utils.text import capfirst from django.utils.translation import get_date_formats from django.conf.settings import ADMIN_MEDIA_PREFIX -from django.core.template import Library +from django.template import Library register = Library() diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py index 9afe134dc2..835e254c43 100644 --- a/django/contrib/admin/templatetags/admin_modify.py +++ b/django/contrib/admin/templatetags/admin_modify.py @@ -1,4 +1,5 @@ -from django.core import template, template_loader +from django import template +from djang.core import template_loader from django.utils.html import escape from django.utils.text import capfirst from django.utils.functional import curry diff --git a/django/contrib/admin/templatetags/adminapplist.py b/django/contrib/admin/templatetags/adminapplist.py index 97dbb1b5ae..007df5b765 100644 --- a/django/contrib/admin/templatetags/adminapplist.py +++ b/django/contrib/admin/templatetags/adminapplist.py @@ -1,4 +1,4 @@ -from django.core import template +from django import template register = template.Library() diff --git a/django/contrib/admin/templatetags/adminmedia.py b/django/contrib/admin/templatetags/adminmedia.py index 3238bfcfc7..bebed4a84f 100644 --- a/django/contrib/admin/templatetags/adminmedia.py +++ b/django/contrib/admin/templatetags/adminmedia.py @@ -1,4 +1,4 @@ -from django.core.template import Library +from django.template import Library register = Library() def admin_media_prefix(): diff --git a/django/contrib/admin/templatetags/breadcrumbs.py b/django/contrib/admin/templatetags/breadcrumbs.py index 41705bbdb4..a8a48f2ea2 100644 --- a/django/contrib/admin/templatetags/breadcrumbs.py +++ b/django/contrib/admin/templatetags/breadcrumbs.py @@ -1,4 +1,4 @@ -from django.core.template import Library +from django.template import Library register = Library() diff --git a/django/contrib/admin/templatetags/log.py b/django/contrib/admin/templatetags/log.py index dc07ea44f5..e29ba40d18 100644 --- a/django/contrib/admin/templatetags/log.py +++ b/django/contrib/admin/templatetags/log.py @@ -1,5 +1,5 @@ from django.contrib.admin.models import LogEntry -from django.core import template +from django import template register = template.Library() diff --git a/django/contrib/admin/views/doc.py b/django/contrib/admin/views/doc.py index 34a945a60e..27eeace5ee 100644 --- a/django/contrib/admin/views/doc.py +++ b/django/contrib/admin/views/doc.py @@ -1,11 +1,11 @@ -from django import templatetags +from django import template, templatetags from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required from django.db import models from django.core.extensions import DjangoContext, render_to_response from django.core.exceptions import ViewDoesNotExist from django.http import Http404 -from django.core import template, urlresolvers +from django.core import urlresolvers from django.contrib.admin import utils from django.contrib.sites.models import Site import inspect, os, re diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py index c1f12039cc..1b98b6ce42 100644 --- a/django/contrib/admin/views/main.py +++ b/django/contrib/admin/views/main.py @@ -1,8 +1,9 @@ # Generic admin views. from django.conf import settings from django.contrib.admin.views.decorators import staff_member_required -from django.core import formfields, template -from django.core.template import loader +from django.core import formfields +from django import template +from django.template import loader from django.db import models from django.http import Http404 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied diff --git a/django/contrib/admin/views/stages/add.py b/django/contrib/admin/views/stages/add.py index ffbfd67a0c..d019c117ca 100644 --- a/django/contrib/admin/views/stages/add.py +++ b/django/contrib/admin/views/stages/add.py @@ -2,7 +2,8 @@ from django.contrib.admin.models import LogEntry from django.contrib.admin.views.decorators import staff_member_required from django.contrib.admin.views.main import get_model_and_app from django.contrib.admin.views.stages.modify import render_change_form -from django.core import formfields, template +from django.core import formfields +from django import template from django.http import Http404 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied from django.core.extensions import DjangoContext as Context diff --git a/django/contrib/admin/views/stages/change.py b/django/contrib/admin/views/stages/change.py index 26d578eed5..fafc1d0315 100644 --- a/django/contrib/admin/views/stages/change.py +++ b/django/contrib/admin/views/stages/change.py @@ -1,5 +1,6 @@ from django.contrib.admin.views.main import get_model_and_app -from django.core import formfields, template +from django.core import formfields +from django import template from django.http import Http404 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied from django.core.extensions import DjangoContext as Context diff --git a/django/contrib/admin/views/template.py b/django/contrib/admin/views/template.py index 33f0e7662f..9b3824f99f 100644 --- a/django/contrib/admin/views/template.py +++ b/django/contrib/admin/views/template.py @@ -1,7 +1,7 @@ from django.contrib.admin.views.decorators import staff_member_required from django.core import formfields, validators -from django.core import template -from django.core.template import loader +from django import template +from django.template import loader from django.core.extensions import DjangoContext, render_to_response from django.contrib.sites.models import Site from django.conf import settings diff --git a/django/contrib/comments/templatetags/comments.py b/django/contrib/comments/templatetags/comments.py index 9b9d79202b..5c2b1dfae0 100644 --- a/django/contrib/comments/templatetags/comments.py +++ b/django/contrib/comments/templatetags/comments.py @@ -1,7 +1,7 @@ from django.contrib.comments.models import Comment, FreeComment from django.contrib.comments.models import PHOTOS_REQUIRED, PHOTOS_OPTIONAL, RATINGS_REQUIRED, RATINGS_OPTIONAL, IS_PUBLIC from django.contrib.comments.models import MIN_PHOTO_DIMENSION, MAX_PHOTO_DIMENSION -from django.core import template +from django import template from django.core.exceptions import ObjectDoesNotExist from django.contrib.contenttypes.models import ContentType import re diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py index 5124889b89..933d56e520 100644 --- a/django/contrib/markup/templatetags/markup.py +++ b/django/contrib/markup/templatetags/markup.py @@ -14,7 +14,7 @@ In each case, if the required library is not installed, the filter will silently fail and return the un-marked-up text. """ -from django.core import template +from django import template register = template.Library() diff --git a/django/contrib/syndication/feeds.py b/django/contrib/syndication/feeds.py index 902cde7003..79328f8ef9 100644 --- a/django/contrib/syndication/feeds.py +++ b/django/contrib/syndication/feeds.py @@ -1,5 +1,5 @@ from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from django.core.template import Context, loader, Template, TemplateDoesNotExist +from django.template import Context, loader, Template, TemplateDoesNotExist from django.contrib.sites.models import Site from django.utils import feedgenerator from django.conf.settings import LANGUAGE_CODE, SETTINGS_MODULE diff --git a/django/core/extensions.py b/django/core/extensions.py index 62c1407a62..bd33b08db5 100644 --- a/django/core/extensions.py +++ b/django/core/extensions.py @@ -3,7 +3,7 @@ # for convenience's sake. from django.core.exceptions import ImproperlyConfigured -from django.core.template import Context, loader +from django.template import Context, loader from django.conf.settings import TEMPLATE_CONTEXT_PROCESSORS from django.http import HttpResponse, Http404 diff --git a/django/core/template_loader.py b/django/core/template_loader.py index e268c390e1..ee86178cc1 100644 --- a/django/core/template_loader.py +++ b/django/core/template_loader.py @@ -1,7 +1,7 @@ # This module is DEPRECATED! # -# You should no longer be using django.core.template_loader. +# You should no longer be using django.template_loader. # -# Use django.core.template.loader instead. +# Use django.template.loader instead. -from django.core.template.loader import * +from django.template.loader import * diff --git a/django/template/__init__.py b/django/template/__init__.py new file mode 100644 index 0000000000..929cea179b --- /dev/null +++ b/django/template/__init__.py @@ -0,0 +1,914 @@ +""" +This is the Django template system. + +How it works: + +The Lexer.tokenize() function converts a template string (i.e., a string containing +markup with custom template tags) to tokens, which can be either plain text +(TOKEN_TEXT), variables (TOKEN_VAR) or block statements (TOKEN_BLOCK). + +The Parser() class takes a list of tokens in its constructor, and its parse() +method returns a compiled template -- which is, under the hood, a list of +Node objects. + +Each Node is responsible for creating some sort of output -- e.g. simple text +(TextNode), variable values in a given context (VariableNode), results of basic +logic (IfNode), results of looping (ForNode), or anything else. The core Node +types are TextNode, VariableNode, IfNode and ForNode, but plugin modules can +define their own custom node types. + +Each Node has a render() method, which takes a Context and returns a string of +the rendered node. For example, the render() method of a Variable Node returns +the variable's value as a string. The render() method of an IfNode returns the +rendered output of whatever was inside the loop, recursively. + +The Template class is a convenient wrapper that takes care of template +compilation and rendering. + +Usage: + +The only thing you should ever use directly in this file is the Template class. +Create a compiled template object with a template_string, then call render() +with a context. In the compilation stage, the TemplateSyntaxError exception +will be raised if the template doesn't have proper syntax. + +Sample code: + +>>> import template +>>> s = ''' +... +... {% if test %} +...

{{ varvalue }}

+... {% endif %} +... +... ''' +>>> t = template.Template(s) + +(t is now a compiled template, and its render() method can be called multiple +times with multiple contexts) + +>>> c = template.Context({'test':True, 'varvalue': 'Hello'}) +>>> t.render(c) +'\n\n\n

Hello

\n\n\n' +>>> c = template.Context({'test':False, 'varvalue': 'Hello'}) +>>> t.render(c) +'\n\n\n\n' +""" +import re +from inspect import getargspec +from django.utils.functional import curry +from django.conf.settings import DEFAULT_CHARSET +from django.conf import settings + +__all__ = ('Template','Context','compile_string') + +TOKEN_TEXT = 0 +TOKEN_VAR = 1 +TOKEN_BLOCK = 2 + +# template syntax constants +FILTER_SEPARATOR = '|' +FILTER_ARGUMENT_SEPARATOR = ':' +VARIABLE_ATTRIBUTE_SEPARATOR = '.' +BLOCK_TAG_START = '{%' +BLOCK_TAG_END = '%}' +VARIABLE_TAG_START = '{{' +VARIABLE_TAG_END = '}}' + +ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' + +# what to report as the origin for templates that come from non-loader sources +# (e.g. strings) +UNKNOWN_SOURCE="<unknown source>" + +# 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 dictionary of libraries that have been loaded using get_library +libraries = {} +# global list of libraries to load by default for a new parser +builtins = [] + +class TemplateSyntaxError(Exception): + pass + +class ContextPopException(Exception): + "pop() has been called more times than push()" + pass + +class TemplateDoesNotExist(Exception): + pass + +class VariableDoesNotExist(Exception): + pass + +class InvalidTemplateLibrary(Exception): + pass + +class Origin(object): + def __init__(self, name): + self.name = name + + def reload(self): + raise NotImplementedError + + def __str__(self): + return self.name + +class StringOrigin(Origin): + def __init__(self, source): + super(StringOrigin, self).__init__(UNKNOWN_SOURCE) + self.source = source + + def reload(self): + return self.source + +class Template: + def __init__(self, template_string, origin=None): + "Compilation stage" + if settings.TEMPLATE_DEBUG and origin == None: + origin = StringOrigin(template_string) + # Could do some crazy stack-frame stuff to record where this string + # came from... + self.nodelist = compile_string(template_string, origin) + + def __iter__(self): + for node in self.nodelist: + for subnode in node: + yield subnode + + def render(self, context): + "Display stage -- can be called many times" + return self.nodelist.render(context) + +def compile_string(template_string, origin): + "Compiles template_string into NodeList ready for rendering" + lexer = lexer_factory(template_string, origin) + parser = parser_factory(lexer.tokenize()) + return parser.parse() + +class Context: + "A stack container for variable context" + def __init__(self, dict=None): + dict = dict or {} + self.dicts = [dict] + + def __repr__(self): + return repr(self.dicts) + + def __iter__(self): + for d in self.dicts: + yield d + + def push(self): + self.dicts = [{}] + self.dicts + + def pop(self): + if len(self.dicts) == 1: + raise ContextPopException + del self.dicts[0] + + def __setitem__(self, key, value): + "Set a variable in the current context" + self.dicts[0][key] = value + + def __getitem__(self, key): + "Get a variable's value, starting at the current context and going upward" + for dict in self.dicts: + if dict.has_key(key): + return dict[key] + return '' + + def __delitem__(self, key): + "Delete a variable from the current context" + del self.dicts[0][key] + + def has_key(self, key): + for dict in self.dicts: + if dict.has_key(key): + return True + return False + + def get(self, key, otherwise): + for dict in self.dicts: + if dict.has_key(key): + return dict[key] + return otherwise + + def update(self, other_dict): + "Like dict.update(). Pushes an entire dictionary's keys and values onto the context." + self.dicts = [other_dict] + self.dicts + +class Token: + def __init__(self, token_type, contents): + "The token_type must be TOKEN_TEXT, TOKEN_VAR or TOKEN_BLOCK" + self.token_type, self.contents = token_type, contents + + def __str__(self): + return '<%s token: "%s...">' % ( + {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type], + self.contents[:20].replace('\n', '') + ) + + def __repr__(self): + return '<%s token: "%s">' % ( + {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type], + self.contents[:].replace('\n', '') + ) + +class Lexer(object): + def __init__(self, template_string, origin): + self.template_string = template_string + self.origin = origin + + def tokenize(self): + "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(self.template_string)) + return map(self.create_token, bits) + + def create_token(self,token_string): + "Convert the given token string into a new Token object and return it" + if token_string.startswith(VARIABLE_TAG_START): + token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) + elif token_string.startswith(BLOCK_TAG_START): + token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip()) + else: + token = Token(TOKEN_TEXT, token_string) + return token + +class DebugLexer(Lexer): + def __init__(self, template_string, origin): + super(DebugLexer, self).__init__(template_string, origin) + + def tokenize(self): + "Return a list of tokens from a given template_string" + token_tups, upto = [], 0 + for match in tag_re.finditer(self.template_string): + start, end = match.span() + if start > upto: + token_tups.append( (self.template_string[upto:start], (upto, start)) ) + upto = start + token_tups.append( (self.template_string[start:end], (start,end)) ) + upto = end + last_bit = self.template_string[upto:] + if last_bit: + token_tups.append( (last_bit, (upto, upto + len(last_bit))) ) + return [self.create_token(tok, (self.origin, loc)) for tok, loc in token_tups] + + def create_token(self, token_string, source): + token = super(DebugLexer, self).create_token(token_string) + token.source = source + return token + +class Parser(object): + def __init__(self, tokens): + self.tokens = tokens + self.tags = {} + self.filters = {} + for lib in builtins: + self.add_library(lib) + + def parse(self, parse_until=[]): + nodelist = self.create_nodelist() + while self.tokens: + token = self.next_token() + if token.token_type == TOKEN_TEXT: + self.extend_nodelist(nodelist, TextNode(token.contents), token) + elif token.token_type == TOKEN_VAR: + if not token.contents: + self.empty_variable(token) + filter_expression = self.compile_filter(token.contents) + var_node = self.create_variable_node(filter_expression) + self.extend_nodelist(nodelist, var_node,token) + elif token.token_type == TOKEN_BLOCK: + if token.contents in parse_until: + # put token back on token list so calling code knows why it terminated + self.prepend_token(token) + return nodelist + try: + command = token.contents.split()[0] + except IndexError: + self.empty_block_tag(token) + # execute callback function for this tag and append resulting node + self.enter_command(command, token) + try: + compile_func = self.tags[command] + except KeyError: + self.invalid_block_tag(token, command) + try: + compiled_result = compile_func(self, token) + except TemplateSyntaxError, e: + if not self.compile_function_error(token, e): + raise + self.extend_nodelist(nodelist, compiled_result, token) + self.exit_command() + if parse_until: + self.unclosed_block_tag(parse_until) + return nodelist + + def create_variable_node(self, filter_expression): + return VariableNode(filter_expression) + + def create_nodelist(self): + return NodeList() + + def extend_nodelist(self, nodelist, node, token): + nodelist.append(node) + + def enter_command(self, command, token): + pass + + def exit_command(self): + pass + + def error(self, token, msg ): + return TemplateSyntaxError(msg) + + def empty_variable(self, token): + raise self.error( token, "Empty variable tag") + + def empty_block_tag(self, token): + raise self.error( token, "Empty block tag") + + def invalid_block_tag(self, token, command): + raise self.error( token, "Invalid block tag: '%s'" % command) + + def unclosed_block_tag(self, parse_until): + raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until)) + + def compile_function_error(self, token, e): + pass + + def next_token(self): + return self.tokens.pop(0) + + def prepend_token(self, token): + self.tokens.insert(0, token) + + def delete_first_token(self): + del self.tokens[0] + + def add_library(self, lib): + self.tags.update(lib.tags) + self.filters.update(lib.filters) + + def compile_filter(self,token): + "Convenient wrapper for FilterExpression" + return FilterExpression(token, self) + + def find_filter(self, filter_name): + if self.filters.has_key(filter_name): + return self.filters[filter_name] + else: + raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name + +class DebugParser(Parser): + def __init__(self, lexer): + super(DebugParser, self).__init__(lexer) + self.command_stack = [] + + def enter_command(self, command, token): + self.command_stack.append( (command, token.source) ) + + def exit_command(self): + self.command_stack.pop() + + def error(self, token, msg): + return self.source_error(token.source, msg) + + def source_error(self, source,msg): + e = TemplateSyntaxError(msg) + e.source = source + return e + + def create_nodelist(self): + return DebugNodeList() + + def create_variable_node(self, contents): + return DebugVariableNode(contents) + + def extend_nodelist(self, nodelist, node, token): + node.source = token.source + super(DebugParser, self).extend_nodelist(nodelist, node, token) + + def unclosed_block_tag(self, parse_until): + (command, source) = self.command_stack.pop() + msg = "Unclosed tag '%s'. Looking for one of: %s " % (command, ', '.join(parse_until)) + raise self.source_error( source, msg) + + def compile_function_error(self, token, e): + if not hasattr(e, 'source'): + e.source = token.source + + +def lexer_factory(*args, **kwargs): + if settings.TEMPLATE_DEBUG: + return DebugLexer(*args, **kwargs) + else: + return Lexer(*args, **kwargs) + +def parser_factory(*args, **kwargs): + if settings.TEMPLATE_DEBUG: + return DebugParser(*args, **kwargs) + else: + return Parser(*args, **kwargs) + + +class TokenParser: + """ + Subclass this and implement the top() method to parse a template line. When + instantiating the parser, pass in the line from the Django template parser. + + The parser's "tagname" instance-variable stores the name of the tag that + the filter was called with. + """ + def __init__(self, subject): + self.subject = subject + self.pointer = 0 + self.backout = [] + self.tagname = self.tag() + + def top(self): + "Overload this method to do the actual parsing and return the result." + raise NotImplemented + + def more(self): + "Returns True if there is more stuff in the tag." + return self.pointer < len(self.subject) + + def back(self): + "Undoes the last microparser. Use this for lookahead and backtracking." + if not len(self.backout): + raise TemplateSyntaxError, "back called without some previous parsing" + self.pointer = self.backout.pop() + + def tag(self): + "A microparser that just returns the next tag from the line." + subject = self.subject + i = self.pointer + if i >= len(subject): + raise TemplateSyntaxError, "expected another tag, found end of string: %s" % subject + p = i + while i < len(subject) and subject[i] not in (' ', '\t'): + i += 1 + s = subject[p:i] + while i < len(subject) and subject[i] in (' ', '\t'): + i += 1 + self.backout.append(self.pointer) + self.pointer = i + return s + + def value(self): + "A microparser that parses for a value: some string constant or variable name." + subject = self.subject + i = self.pointer + if i >= len(subject): + raise TemplateSyntaxError, "Searching for value. Expected another value but found end of string: %s" % subject + if subject[i] in ('"', "'"): + p = i + i += 1 + while i < len(subject) and subject[i] != subject[p]: + i += 1 + if i >= len(subject): + raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject + i += 1 + res = subject[p:i] + while i < len(subject) and subject[i] in (' ', '\t'): + i += 1 + self.backout.append(self.pointer) + self.pointer = i + return res + else: + p = i + while i < len(subject) and subject[i] not in (' ', '\t'): + if subject[i] in ('"', "'"): + c = subject[i] + i += 1 + while i < len(subject) and subject[i] != c: + i += 1 + if i >= len(subject): + raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % subject + i += 1 + s = subject[p:i] + while i < len(subject) and subject[i] in (' ', '\t'): + i += 1 + self.backout.append(self.pointer) + self.pointer = i + return s + + + + +filter_raw_string = r""" +^%(i18n_open)s"(?P%(str)s)"%(i18n_close)s| +^"(?P%(str)s)"| +^(?P[%(var_chars)s]+)| + (?:%(filter_sep)s + (?P\w+) + (?:%(arg_sep)s + (?: + %(i18n_open)s"(?P%(str)s)"%(i18n_close)s| + "(?P%(str)s)"| + (?P[%(var_chars)s]+) + ) + )? + )""" % { + 'str': r"""[^"\\]*(?:\\.[^"\\]*)*""", + 'var_chars': "A-Za-z0-9\_\." , + 'filter_sep': re.escape(FILTER_SEPARATOR), + 'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR), + 'i18n_open' : re.escape("_("), + 'i18n_close' : re.escape(")"), + } + +filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "") +filter_re = re.compile(filter_raw_string) + +class FilterExpression(object): + """ + Parses a variable token and its optional filters (all as a single string), + and return a list of tuples of the filter name and arguments. + Sample: + >>> token = 'variable|default:"Default value"|date:"Y-m-d"' + >>> p = FilterParser(token) + >>> p.filters + [('default', 'Default value'), ('date', 'Y-m-d')] + >>> p.var + 'variable' + + This class should never be instantiated outside of the + get_filters_from_token helper function. + """ + def __init__(self, token, parser): + self.token = token + matches = filter_re.finditer(token) + var = None + filters = [] + upto = 0 + for match in matches: + start = match.start() + if upto != start: + raise TemplateSyntaxError, "Could not parse some characters: %s|%s|%s" % \ + (token[:upto], token[upto:start], token[start:]) + if var == None: + var, constant, i18n_constant = match.group("var", "constant", "i18n_constant") + if i18n_constant: + var = '"%s"' % _(i18n_constant) + elif constant: + var = '"%s"' % constant + upto = match.end() + if var == None: + raise TemplateSyntaxError, "Could not find variable at start of %s" % token + elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_': + raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var + else: + filter_name = match.group("filter_name") + args = [] + constant_arg, i18n_arg, var_arg = match.group("constant_arg", "i18n_arg", "var_arg") + if i18n_arg: + args.append((False, _(i18n_arg.replace('\\', '')))) + elif constant_arg: + args.append((False, constant_arg.replace('\\', ''))) + elif var_arg: + args.append((True, var_arg)) + filter_func = parser.find_filter(filter_name) + self.args_check(filter_name,filter_func, args) + filters.append( (filter_func,args)) + upto = match.end() + if upto != len(token): + raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:] + self.var , self.filters = var, filters + + def resolve(self, context): + try: + obj = resolve_variable(self.var, context) + except VariableDoesNotExist: + obj = '' + for func, args in self.filters: + arg_vals = [] + for lookup, arg in args: + if not lookup: + arg_vals.append(arg) + else: + arg_vals.append(resolve_variable(arg, context)) + obj = func(obj, *arg_vals) + return obj + + def args_check(name, func, provided): + provided = list(provided) + plen = len(provided) + (args, varargs, varkw, defaults) = getargspec(func) + # First argument is filter input. + args.pop(0) + if defaults: + nondefs = args[:-len(defaults)] + else: + nondefs = args + # Args without defaults must be provided. + try: + for arg in nondefs: + provided.pop(0) + except IndexError: + # Not enough + raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen) + + # Defaults can be overridden. + defaults = defaults and list(defaults) or [] + try: + for parg in provided: + defaults.pop(0) + except IndexError: + # Too many. + raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen) + + return True + args_check = staticmethod(args_check) + + def __str__(self): + return self.token + +def resolve_variable(path, context): + """ + Returns the resolved variable, which may contain attribute syntax, within + the given context. The variable may be a hard-coded string (if it begins + and ends with single or double quote marks). + + >>> c = {'article': {'section':'News'}} + >>> resolve_variable('article.section', c) + 'News' + >>> resolve_variable('article', c) + {'section': 'News'} + >>> class AClass: pass + >>> c = AClass() + >>> c.article = AClass() + >>> c.article.section = 'News' + >>> resolve_variable('article.section', c) + 'News' + + (The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.') + """ + if path[0] in ('"', "'") and path[0] == path[-1]: + current = path[1:-1] + else: + current = context + bits = path.split(VARIABLE_ATTRIBUTE_SEPARATOR) + while bits: + try: # dictionary lookup + current = current[bits[0]] + except (TypeError, AttributeError, KeyError): + try: # attribute lookup + current = getattr(current, bits[0]) + if callable(current): + if getattr(current, 'alters_data', False): + current = '' + else: + try: # method call (assuming no args required) + current = current() + except TypeError: # arguments *were* required + # GOTCHA: This will also catch any TypeError + # raised in the function itself. + current = '' # invalid method call + except Exception, e: + if getattr(e, 'silent_variable_failure', False): + current = '' + else: + raise + except (TypeError, AttributeError): + try: # list-index lookup + current = current[int(bits[0])] + except (IndexError, ValueError, KeyError): + raise VariableDoesNotExist, "Failed lookup for key [%s] in %r" % (bits[0], current) # missing attribute + del bits[0] + return current + +class Node: + def render(self, context): + "Return the node rendered as a string" + pass + + def __iter__(self): + yield self + + def get_nodes_by_type(self, nodetype): + "Return a list of all nodes (within this node and its nodelist) of the given type" + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + if hasattr(self, 'nodelist'): + nodes.extend(self.nodelist.get_nodes_by_type(nodetype)) + return nodes + +class NodeList(list): + def render(self, context): + bits = [] + for node in self: + if isinstance(node, Node): + bits.append(self.render_node(node, context)) + else: + bits.append(node) + return ''.join(bits) + + def get_nodes_by_type(self, nodetype): + "Return a list of all nodes of the given type" + nodes = [] + for node in self: + nodes.extend(node.get_nodes_by_type(nodetype)) + return nodes + + def render_node(self, node, context): + return(node.render(context)) + +class DebugNodeList(NodeList): + def render_node(self, node, context): + try: + result = node.render(context) + except TemplateSyntaxError, e: + if not hasattr(e, 'source'): + e.source = node.source + raise + except Exception: + from sys import exc_info + wrapped = TemplateSyntaxError('Caught an exception while rendering.') + wrapped.source = node.source + wrapped.exc_info = exc_info() + raise wrapped + return result + +class TextNode(Node): + def __init__(self, s): + self.s = s + + def __repr__(self): + return "" % self.s[:25] + + def render(self, context): + return self.s + +class VariableNode(Node): + def __init__(self, filter_expression): + self.filter_expression = filter_expression + + def __repr__(self): + return "" % self.filter_expression + + def encode_output(self, output): + # Check type so that we don't run str() on a Unicode object + if not isinstance(output, basestring): + return str(output) + elif isinstance(output, unicode): + return output.encode(DEFAULT_CHARSET) + else: + return output + + def render(self, context): + output = self.filter_expression.resolve(context) + return self.encode_output(output) + +class DebugVariableNode(VariableNode): + def render(self, context): + try: + output = self.filter_expression.resolve(context) + except TemplateSyntaxError, e: + if not hasattr(e, 'source'): + e.source = self.source + raise + return self.encode_output(output) + +def 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) + +class Library(object): + def __init__(self): + self.filters = {} + self.tags = {} + + def tag(self, name=None, compile_function=None): + if name == None and compile_function == None: + # @register.tag() + return self.tag_function + elif name != None and compile_function == None: + if(callable(name)): + # @register.tag + return self.tag_function(name) + else: + # @register.tag('somename') or @register.tag(name='somename') + def dec(func): + return self.tag(name, func) + return dec + elif name != None and compile_function != None: + # register.tag('somename', somefunc) + self.tags[name] = compile_function + return compile_function + else: + raise InvalidTemplateLibrary, "Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function) + + def tag_function(self,func): + self.tags[func.__name__] = func + return func + + def filter(self, name=None, filter_func=None): + if name == None and filter_func == None: + # @register.filter() + return self.filter_function + elif filter_func == None: + if(callable(name)): + # @register.filter + return self.filter_function(name) + else: + # @register.filter('somename') or @register.filter(name='somename') + def dec(func): + return self.filter(name, func) + return dec + elif name != None and filter_func != None: + # register.filter('somename', somefunc) + self.filters[name] = filter_func + return 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) + try: + lib = mod.register + libraries[module_name] = lib + except AttributeError: + raise InvalidTemplateLibrary, "Template library %s does not have a variable named 'register'" % module_name + return lib + +def add_to_builtins(module_name): + builtins.append(get_library(module_name)) + +add_to_builtins('django.template.defaulttags') +add_to_builtins('django.template.defaultfilters') diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py new file mode 100644 index 0000000000..34fffcb8a1 --- /dev/null +++ b/django/template/defaultfilters.py @@ -0,0 +1,487 @@ +"Default variable filters" + +from django.template import resolve_variable, Library +from django.conf.settings import DATE_FORMAT, TIME_FORMAT +from django.utils.translation import gettext +import re +import random as random_module + +register = Library() + +################### +# STRINGS # +################### + + +def addslashes(value): + "Adds slashes - useful for passing strings to JavaScript, for example." + return value.replace('"', '\\"').replace("'", "\\'") + +def capfirst(value): + "Capitalizes the first character of the value" + value = str(value) + return value and value[0].upper() + value[1:] + +def fix_ampersands(value): + "Replaces ampersands with ``&`` entities" + from django.utils.html import fix_ampersands + return fix_ampersands(value) + +def floatformat(text): + """ + Displays a floating point number as 34.2 (with one decimal place) -- but + only if there's a point to be displayed + """ + try: + f = float(text) + except ValueError: + return '' + m = f - int(f) + if m: + return '%.1f' % f + else: + return '%d' % int(f) + +def linenumbers(value): + "Displays text with line numbers" + from django.utils.html import escape + lines = value.split('\n') + # Find the maximum width of the line count, for use with zero padding string format command + width = str(len(str(len(lines)))) + for i, line in enumerate(lines): + lines[i] = ("%0" + width + "d. %s") % (i + 1, escape(line)) + return '\n'.join(lines) + +def lower(value): + "Converts a string into all lowercase" + return value.lower() + +def make_list(value): + """ + Returns the value turned into a list. For an integer, it's a list of + digits. For a string, it's a list of characters. + """ + return list(str(value)) + +def slugify(value): + "Converts to lowercase, removes non-alpha chars and converts spaces to hyphens" + value = re.sub('[^\w\s-]', '', value).strip().lower() + return re.sub('\s+', '-', value) + +def stringformat(value, arg): + """ + Formats the variable according to the argument, a string formatting specifier. + This specifier uses Python string formating syntax, with the exception that + the leading "%" is dropped. + + See http://docs.python.org/lib/typesseq-strings.html for documentation + of Python string formatting + """ + try: + return ("%" + arg) % value + except (ValueError, TypeError): + return "" + +def title(value): + "Converts a string into titlecase" + return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title()) + +def truncatewords(value, arg): + """ + Truncates a string after a certain number of words + + Argument: Number of words to truncate after + """ + from django.utils.text import truncate_words + try: + length = int(arg) + except ValueError: # invalid literal for int() + return value # Fail silently. + if not isinstance(value, basestring): + value = str(value) + return truncate_words(value, length) + +def upper(value): + "Converts a string into all uppercase" + return value.upper() + +def urlencode(value): + "Escapes a value for use in a URL" + import urllib + return urllib.quote(value) + +def urlize(value): + "Converts URLs in plain text into clickable links" + from django.utils.html import urlize + return urlize(value, nofollow=True) + +def urlizetrunc(value, limit): + """ + Converts URLs into clickable links, truncating URLs to the given character limit, + and adding 'rel=nofollow' attribute to discourage spamming. + + Argument: Length to truncate URLs to. + """ + from django.utils.html import urlize + return urlize(value, trim_url_limit=int(limit), nofollow=True) + +def wordcount(value): + "Returns the number of words" + return len(value.split()) + +def wordwrap(value, arg): + """ + Wraps words at specified line length + + Argument: number of words to wrap the text at. + """ + from django.utils.text import wrap + return wrap(str(value), int(arg)) + +def ljust(value, arg): + """ + Left-aligns the value in a field of a given width + + Argument: field size + """ + return str(value).ljust(int(arg)) + +def rjust(value, arg): + """ + Right-aligns the value in a field of a given width + + Argument: field size + """ + return str(value).rjust(int(arg)) + +def center(value, arg): + "Centers the value in a field of a given width" + return str(value).center(int(arg)) + +def cut(value, arg): + "Removes all values of arg from the given string" + return value.replace(arg, '') + +################### +# HTML STRINGS # +################### + +def escape(value): + "Escapes a string's HTML" + from django.utils.html import escape + return escape(value) + +def linebreaks(value): + "Converts newlines into

and
s" + from django.utils.html import linebreaks + return linebreaks(value) + +def linebreaksbr(value): + "Converts newlines into
s" + return value.replace('\n', '
') + +def removetags(value, tags): + "Removes a space separated list of [X]HTML tags from the output" + tags = [re.escape(tag) for tag in tags.split()] + tags_re = '(%s)' % '|'.join(tags) + starttag_re = re.compile(r'<%s(/?>|(\s+[^>]*>))' % tags_re) + endtag_re = re.compile('' % tags_re) + value = starttag_re.sub('', value) + value = endtag_re.sub('', value) + return value + +def striptags(value): + "Strips all [X]HTML tags" + from django.utils.html import strip_tags + if not isinstance(value, basestring): + value = str(value) + return strip_tags(value) + +################### +# LISTS # +################### + +def dictsort(value, arg): + """ + Takes a list of dicts, returns that list sorted by the property given in + the argument. + """ + decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] + decorated.sort() + return [item[1] for item in decorated] + +def dictsortreversed(value, arg): + """ + Takes a list of dicts, returns that list sorted in reverse order by the + property given in the argument. + """ + decorated = [(resolve_variable('var.' + arg, {'var' : item}), item) for item in value] + decorated.sort() + decorated.reverse() + return [item[1] for item in decorated] + +def first(value): + "Returns the first item in a list" + try: + return value[0] + except IndexError: + return '' + +def join(value, arg): + "Joins a list with a string, like Python's ``str.join(list)``" + try: + return arg.join(map(str, value)) + except AttributeError: # fail silently but nicely + return value + +def length(value): + "Returns the length of the value - useful for lists" + return len(value) + +def length_is(value, arg): + "Returns a boolean of whether the value's length is the argument" + return len(value) == int(arg) + +def random(value): + "Returns a random item from the list" + return random_module.choice(value) + +def slice_(value, arg): + """ + Returns a slice of the list. + + Uses the same syntax as Python's list slicing; see + http://diveintopython.org/native_data_types/lists.html#odbchelper.list.slice + for an introduction. + """ + try: + bits = [] + for x in arg.split(':'): + if len(x) == 0: + bits.append(None) + else: + bits.append(int(x)) + return value[slice(*bits)] + + except (ValueError, TypeError): + return value # Fail silently. + +def unordered_list(value): + """ + Recursively takes a self-nested list and returns an HTML unordered list -- + WITHOUT opening and closing

    tags. + + The list is assumed to be in the proper format. For example, if ``var`` contains + ``['States', [['Kansas', [['Lawrence', []], ['Topeka', []]]], ['Illinois', []]]]``, + then ``{{ var|unordered_list }}`` would return:: + +
  • States +
      +
    • Kansas +
        +
      • Lawrence
      • +
      • Topeka
      • +
      +
    • +
    • Illinois
    • +
    +
  • + """ + def _helper(value, tabs): + indent = '\t' * tabs + if value[1]: + return '%s
  • %s\n%s
      \n%s\n%s
    \n%s
  • ' % (indent, value[0], indent, + '\n'.join([_helper(v, tabs+1) for v in value[1]]), indent, indent) + else: + return '%s
  • %s
  • ' % (indent, value[0]) + return _helper(value, 1) + +################### +# INTEGERS # +################### + +def add(value, arg): + "Adds the arg to the value" + return int(value) + int(arg) + +def get_digit(value, arg): + """ + Given a whole number, returns the requested digit of it, where 1 is the + right-most digit, 2 is the second-right-most digit, etc. Returns the + original value for invalid input (if input or argument is not an integer, + or if argument is less than 1). Otherwise, output is always an integer. + """ + try: + arg = int(arg) + value = int(value) + except ValueError: + return value # Fail silently for an invalid argument + if arg < 1: + return value + try: + return int(str(value)[-arg]) + except IndexError: + return 0 + +################### +# DATES # +################### + +def date(value, arg=DATE_FORMAT): + "Formats a date according to the given format" + from django.utils.dateformat import format + return format(value, arg) + +def time(value, arg=TIME_FORMAT): + "Formats a time according to the given format" + from django.utils.dateformat import time_format + return time_format(value, arg) + +def timesince(value): + 'Formats a date as the time since that date (i.e. "4 days, 6 hours")' + from django.utils.timesince import timesince + return timesince(value) + +################### +# LOGIC # +################### + +def default(value, arg): + "If value is unavailable, use given default" + return value or arg + +def default_if_none(value, arg): + "If value is None, use given default" + if value is None: + return arg + return value + +def divisibleby(value, arg): + "Returns true if the value is devisible by the argument" + return int(value) % int(arg) == 0 + +def yesno(value, arg=None): + """ + Given a string mapping values for true, false and (optionally) None, + returns one of those strings accoding to the value: + + ========== ====================== ================================== + Value Argument Outputs + ========== ====================== ================================== + ``True`` ``"yeah,no,maybe"`` ``yeah`` + ``False`` ``"yeah,no,maybe"`` ``no`` + ``None`` ``"yeah,no,maybe"`` ``maybe`` + ``None`` ``"yeah,no"`` ``"no"`` (converts None to False + if no mapping for None is given. + ========== ====================== ================================== + """ + if arg is None: + arg = gettext('yes,no,maybe') + bits = arg.split(',') + if len(bits) < 2: + return value # Invalid arg. + try: + yes, no, maybe = bits + except ValueError: # unpack list of wrong size (no "maybe" value provided) + yes, no, maybe = bits[0], bits[1], bits[1] + if value is None: + return maybe + if value: + return yes + return no + +################### +# MISC # +################### + +def filesizeformat(bytes): + """ + Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102 + bytes, etc). + """ + bytes = float(bytes) + if bytes < 1024: + return "%d byte%s" % (bytes, bytes != 1 and 's' or '') + if bytes < 1024 * 1024: + return "%.1f KB" % (bytes / 1024) + if bytes < 1024 * 1024 * 1024: + return "%.1f MB" % (bytes / (1024 * 1024)) + return "%.1f GB" % (bytes / (1024 * 1024 * 1024)) + +def pluralize(value): + "Returns 's' if the value is not 1, for '1 vote' vs. '2 votes'" + try: + if int(value) != 1: + return 's' + except ValueError: # invalid string that's not a number + pass + except TypeError: # value isn't a string or a number; maybe it's a list? + try: + if len(value) != 1: + return 's' + except TypeError: # len() of unsized object + pass + return '' + +def phone2numeric(value): + "Takes a phone number and converts it in to its numerical equivalent" + from django.utils.text import phone2numeric + return phone2numeric(value) + +def pprint(value): + "A wrapper around pprint.pprint -- for debugging, really" + from pprint import pformat + try: + return pformat(value) + except Exception, e: + return "Error in formatting:%s" % e + +# Syntax: register.filter(name of filter, callback) +register.filter(add) +register.filter(addslashes) +register.filter(capfirst) +register.filter(center) +register.filter(cut) +register.filter(date) +register.filter(default) +register.filter(default_if_none) +register.filter(dictsort) +register.filter(dictsortreversed) +register.filter(divisibleby) +register.filter(escape) +register.filter(filesizeformat) +register.filter(first) +register.filter(fix_ampersands) +register.filter(floatformat) +register.filter(get_digit) +register.filter(join) +register.filter(length) +register.filter(length_is) +register.filter(linebreaks) +register.filter(linebreaksbr) +register.filter(linenumbers) +register.filter(ljust) +register.filter(lower) +register.filter(make_list) +register.filter(phone2numeric) +register.filter(pluralize) +register.filter(pprint) +register.filter(removetags) +register.filter(random) +register.filter(rjust) +register.filter('slice', slice_) +register.filter(slugify) +register.filter(stringformat) +register.filter(striptags) +register.filter(time) +register.filter(timesince) +register.filter(title) +register.filter(truncatewords) +register.filter(unordered_list) +register.filter(upper) +register.filter(urlencode) +register.filter(urlize) +register.filter(urlizetrunc) +register.filter(wordcount) +register.filter(wordwrap) +register.filter(yesno) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py new file mode 100644 index 0000000000..956d736c46 --- /dev/null +++ b/django/template/defaulttags.py @@ -0,0 +1,783 @@ +"Default tags used by the template system, available to all templates." + +from django.template import Node, NodeList, Template, Context, resolve_variable +from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG_START, BLOCK_TAG_END, VARIABLE_TAG_START, VARIABLE_TAG_END +from django.template import get_library, Library, InvalidTemplateLibrary +import sys + +register = Library() + +class CommentNode(Node): + def render(self, context): + return '' + +class CycleNode(Node): + def __init__(self, cyclevars): + self.cyclevars = cyclevars + self.cyclevars_len = len(cyclevars) + self.counter = -1 + + def render(self, context): + self.counter += 1 + return self.cyclevars[self.counter % self.cyclevars_len] + +class DebugNode(Node): + def render(self, context): + from pprint import pformat + output = [pformat(val) for val in context] + output.append('\n\n') + output.append(pformat(sys.modules)) + return ''.join(output) + +class FilterNode(Node): + def __init__(self, filter_expr, nodelist): + self.filter_expr, self.nodelist = filter_expr, nodelist + + def render(self, context): + output = self.nodelist.render(context) + # apply filters + return self.filter_expr.resolve(Context({'var': output})) + +class FirstOfNode(Node): + def __init__(self, vars): + self.vars = vars + + def render(self, context): + for var in self.vars: + value = resolve_variable(var, context) + if value: + return str(value) + return '' + +class ForNode(Node): + def __init__(self, loopvar, sequence, reversed, nodelist_loop): + self.loopvar, self.sequence = loopvar, sequence + self.reversed = reversed + self.nodelist_loop = nodelist_loop + + def __repr__(self): + if self.reversed: + reversed = ' reversed' + else: + reversed = '' + return "" % \ + (self.loopvar, self.sequence, len(self.nodelist_loop), reversed) + + def __iter__(self): + for node in self.nodelist_loop: + yield node + + def get_nodes_by_type(self, nodetype): + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + nodes.extend(self.nodelist_loop.get_nodes_by_type(nodetype)) + return nodes + + def render(self, context): + nodelist = NodeList() + if context.has_key('forloop'): + parentloop = context['forloop'] + else: + parentloop = {} + context.push() + try: + values = self.sequence.resolve(context) + except VariableDoesNotExist: + values = [] + if values is None: + values = [] + len_values = len(values) + if self.reversed: + # From http://www.python.org/doc/current/tut/node11.html + def reverse(data): + for index in range(len(data)-1, -1, -1): + yield data[index] + values = reverse(values) + for i, item in enumerate(values): + context['forloop'] = { + # shortcuts for current loop iteration number + 'counter0': i, + 'counter': i+1, + # reverse counter iteration numbers + 'revcounter': len_values - i, + 'revcounter0': len_values - i - 1, + # boolean values designating first and last times through loop + 'first': (i == 0), + 'last': (i == len_values - 1), + 'parentloop': parentloop, + } + context[self.loopvar] = item + for node in self.nodelist_loop: + nodelist.append(node.render(context)) + context.pop() + return nodelist.render(context) + +class IfChangedNode(Node): + def __init__(self, nodelist): + self.nodelist = nodelist + self._last_seen = None + + def render(self, context): + content = self.nodelist.render(context) + if content != self._last_seen: + firstloop = (self._last_seen == None) + self._last_seen = content + context.push() + context['ifchanged'] = {'firstloop': firstloop} + content = self.nodelist.render(context) + context.pop() + return content + else: + return '' + +class IfEqualNode(Node): + def __init__(self, var1, var2, nodelist_true, nodelist_false, negate): + self.var1, self.var2 = var1, var2 + self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false + self.negate = negate + + def __repr__(self): + return "" + + def render(self, context): + val1 = resolve_variable(self.var1, context) + val2 = resolve_variable(self.var2, context) + if (self.negate and val1 != val2) or (not self.negate and val1 == val2): + return self.nodelist_true.render(context) + return self.nodelist_false.render(context) + +class IfNode(Node): + def __init__(self, bool_exprs, nodelist_true, nodelist_false): + self.bool_exprs = bool_exprs + self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false + + def __repr__(self): + return "" + + def __iter__(self): + for node in self.nodelist_true: + yield node + for node in self.nodelist_false: + yield node + + def get_nodes_by_type(self, nodetype): + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype)) + nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) + return nodes + + def render(self, context): + for ifnot, bool_expr in self.bool_exprs: + try: + value = bool_expr.resolve(context) + except VariableDoesNotExist: + value = None + if (value and not ifnot) or (ifnot and not value): + return self.nodelist_true.render(context) + return self.nodelist_false.render(context) + +class RegroupNode(Node): + def __init__(self, target, expression, var_name): + self.target, self.expression = target, expression + self.var_name = var_name + + def render(self, context): + obj_list = self.target.resolve(context) + if obj_list == '': # target_var wasn't found in context; fail silently + context[self.var_name] = [] + return '' + output = [] # list of dictionaries in the format {'grouper': 'key', 'list': [list of contents]} + for obj in obj_list: + grouper = self.expression.resolve(Context({'var': obj})) + # TODO: Is this a sensible way to determine equality? + if output and repr(output[-1]['grouper']) == repr(grouper): + output[-1]['list'].append(obj) + else: + output.append({'grouper': grouper, 'list': [obj]}) + context[self.var_name] = output + return '' + +def include_is_allowed(filepath): + from django.conf.settings import ALLOWED_INCLUDE_ROOTS + for root in ALLOWED_INCLUDE_ROOTS: + if filepath.startswith(root): + return True + return False + +class SsiNode(Node): + def __init__(self, filepath, parsed): + self.filepath, self.parsed = filepath, parsed + + def render(self, context): + from django.conf.settings import DEBUG + if not include_is_allowed(self.filepath): + if DEBUG: + return "[Didn't have permission to include file]" + else: + return '' # Fail silently for invalid includes. + try: + fp = open(self.filepath, 'r') + output = fp.read() + fp.close() + except IOError: + output = '' + if self.parsed: + try: + t = Template(output) + return t.render(context) + except TemplateSyntaxError, e: + if DEBUG: + return "[Included template had syntax error: %s]" % e + else: + return '' # Fail silently for invalid included templates. + return output + +class LoadNode(Node): + def render(self, context): + return '' + +class NowNode(Node): + def __init__(self, format_string): + self.format_string = format_string + + def render(self, context): + from datetime import datetime + from django.utils.dateformat import DateFormat + df = DateFormat(datetime.now()) + return df.format(self.format_string) + +class TemplateTagNode(Node): + mapping = {'openblock': BLOCK_TAG_START, + 'closeblock': BLOCK_TAG_END, + 'openvariable': VARIABLE_TAG_START, + 'closevariable': VARIABLE_TAG_END} + + def __init__(self, tagtype): + self.tagtype = tagtype + + def render(self, context): + return self.mapping.get(self.tagtype, '') + +class WidthRatioNode(Node): + def __init__(self, val_expr, max_expr, max_width): + self.val_expr = val_expr + self.max_expr = max_expr + self.max_width = max_width + + def render(self, context): + try: + value = self.val_expr.resolve(context) + maxvalue = self.max_expr.resolve(context) + except VariableDoesNotExist: + return '' + try: + value = float(value) + maxvalue = float(maxvalue) + ratio = (value / maxvalue) * int(self.max_width) + except (ValueError, ZeroDivisionError): + return '' + return str(int(round(ratio))) + +#@register.tag +def comment(parser, token): + """ + Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` + """ + nodelist = parser.parse(('endcomment',)) + parser.delete_first_token() + return CommentNode() +comment = register.tag(comment) + +#@register.tag +def cycle(parser, token): + """ + Cycle among the given strings each time this tag is encountered + + Within a loop, cycles among the given strings each time through + the loop:: + + {% for o in some_list %} + + ... + + {% endfor %} + + Outside of a loop, give the values a unique name the first time you call + it, then use that name each sucessive time through:: + + ... + ... + ... + + You can use any number of values, seperated by commas. Make sure not to + put spaces between the values -- only commas. + """ + + # Note: This returns the exact same node on each {% cycle name %} call; that + # is, the node object returned from {% cycle a,b,c as name %} and the one + # returned from {% cycle name %} are the exact same object. This shouldn't + # cause problems (heh), but if it does, now you know. + # + # Ugly hack warning: this stuffs the named template dict into parser so + # that names are only unique within each template (as opposed to using + # a global variable, which would make cycle names have to be unique across + # *all* templates. + + args = token.contents.split() + if len(args) < 2: + raise TemplateSyntaxError("'Cycle' statement requires at least two arguments") + + elif len(args) == 2 and "," in args[1]: + # {% cycle a,b,c %} + cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks + return CycleNode(cyclevars) + # {% cycle name %} + + elif len(args) == 2: + name = args[1] + if not parser._namedCycleNodes.has_key(name): + raise TemplateSyntaxError("Named cycle '%s' does not exist" % name) + return parser._namedCycleNodes[name] + + elif len(args) == 4: + # {% cycle a,b,c as name %} + if args[2] != 'as': + raise TemplateSyntaxError("Second 'cycle' argument must be 'as'") + cyclevars = [v for v in args[1].split(",") if v] # split and kill blanks + name = args[3] + node = CycleNode(cyclevars) + + if not hasattr(parser, '_namedCycleNodes'): + parser._namedCycleNodes = {} + + parser._namedCycleNodes[name] = node + return node + + else: + raise TemplateSyntaxError("Invalid arguments to 'cycle': %s" % args) +cycle = register.tag(cycle) + +def debug(parser, token): + return DebugNode() +debug = register.tag(debug) + +#@register.tag(name="filter") +def do_filter(parser, token): + """ + Filter the contents of the blog through variable filters. + + Filters can also be piped through each other, and they can have + arguments -- just like in variable syntax. + + Sample usage:: + + {% filter escape|lower %} + This text will be HTML-escaped, and will appear in lowercase. + {% endfilter %} + """ + _, rest = token.contents.split(None, 1) + filter_expr = parser.compile_filter("var|%s" % (rest)) + nodelist = parser.parse(('endfilter',)) + parser.delete_first_token() + return FilterNode(filter_expr, nodelist) +filter = register.tag("filter", do_filter) + +#@register.tag +def firstof(parser, token): + """ + Outputs the first variable passed that is not False. + + Outputs nothing if all the passed variables are False. + + Sample usage:: + + {% firstof var1 var2 var3 %} + + This is equivalent to:: + + {% if var1 %} + {{ var1 }} + {% else %}{% if var2 %} + {{ var2 }} + {% else %}{% if var3 %} + {{ var3 }} + {% endif %}{% endif %}{% endif %} + + but obviously much cleaner! + """ + bits = token.contents.split()[1:] + if len(bits) < 1: + raise TemplateSyntaxError, "'firstof' statement requires at least one argument" + return FirstOfNode(bits) +firstof = register.tag(firstof) + +#@register.tag(name="for") +def do_for(parser, token): + """ + Loop over each item in an array. + + For example, to display a list of athletes given ``athlete_list``:: + +
      + {% for athlete in athlete_list %} +
    • {{ athlete.name }}
    • + {% endfor %} +
    + + You can also loop over a list in reverse by using + ``{% for obj in list reversed %}``. + + The for loop sets a number of variables available within the loop: + + ========================== ================================================ + Variable Description + ========================== ================================================ + ``forloop.counter`` The current iteration of the loop (1-indexed) + ``forloop.counter0`` The current iteration of the loop (0-indexed) + ``forloop.revcounter`` The number of iterations from the end of the + loop (1-indexed) + ``forloop.revcounter0`` The number of iterations from the end of the + loop (0-indexed) + ``forloop.first`` True if this is the first time through the loop + ``forloop.last`` True if this is the last time through the loop + ``forloop.parentloop`` For nested loops, this is the loop "above" the + current one + ========================== ================================================ + + """ + bits = token.contents.split() + if len(bits) == 5 and bits[4] != 'reversed': + raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents + if len(bits) not in (4, 5): + raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents + if bits[2] != 'in': + raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents + loopvar = bits[1] + sequence = parser.compile_filter(bits[3]) + reversed = (len(bits) == 5) + nodelist_loop = parser.parse(('endfor',)) + parser.delete_first_token() + return ForNode(loopvar, sequence, reversed, nodelist_loop) +do_for = register.tag("for", do_for) + +def do_ifequal(parser, token, negate): + """ + Output the contents of the block if the two arguments equal/don't equal each other. + + Examples:: + + {% ifequal user.id comment.user_id %} + ... + {% endifequal %} + + {% ifnotequal user.id comment.user_id %} + ... + {% else %} + ... + {% endifnotequal %} + """ + bits = token.contents.split() + if len(bits) != 3: + raise TemplateSyntaxError, "%r takes two arguments" % bits[0] + end_tag = 'end' + bits[0] + nodelist_true = parser.parse(('else', end_tag)) + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse((end_tag,)) + parser.delete_first_token() + else: + nodelist_false = NodeList() + return IfEqualNode(bits[1], bits[2], nodelist_true, nodelist_false, negate) + +#@register.tag +def ifequal(parser, token): + return do_ifequal(parser, token, False) +ifequal = register.tag(ifequal) + +#@register.tag +def ifnotequal(parser, token): + return do_ifequal(parser, token, True) +ifnotequal = register.tag(ifnotequal) + +#@register.tag(name="if") +def do_if(parser, token): + """ + The ``{% if %}`` tag evaluates a variable, and if that variable is "true" + (i.e. exists, is not empty, and is not a false boolean value) the contents + of the block are output: + + :: + + {% if althlete_list %} + Number of athletes: {{ althete_list|count }} + {% else %} + No athletes. + {% endif %} + + In the above, if ``athlete_list`` is not empty, the number of athletes will + be displayed by the ``{{ athlete_list|count }}`` variable. + + As you can see, the ``if`` tag can take an option ``{% else %}`` clause that + will be displayed if the test fails. + + ``if`` tags may use ``or`` or ``not`` to test a number of variables or to + negate a given variable:: + + {% if not athlete_list %} + There are no athletes. + {% endif %} + + {% if athlete_list or coach_list %} + There are some athletes or some coaches. + {% endif %} + + {% if not athlete_list or coach_list %} + There are no athletes, or there are some coaches. + {% endif %} + + For simplicity, ``if`` tags do not allow ``and`` clauses. Use nested ``if`` + tags instead:: + + {% if athlete_list %} + {% if coach_list %} + Number of athletes: {{ athlete_list|count }}. + Number of coaches: {{ coach_list|count }}. + {% endif %} + {% endif %} + """ + bits = token.contents.split() + del bits[0] + if not bits: + raise TemplateSyntaxError, "'if' statement requires at least one argument" + # bits now looks something like this: ['a', 'or', 'not', 'b', 'or', 'c.d'] + boolpairs = ' '.join(bits).split(' or ') + boolvars = [] + for boolpair in boolpairs: + if ' ' in boolpair: + not_, boolvar = boolpair.split() + if not_ != 'not': + raise TemplateSyntaxError, "Expected 'not' in if statement" + boolvars.append((True, parser.compile_filter(boolvar))) + else: + boolvars.append((False, parser.compile_filter(boolpair))) + nodelist_true = parser.parse(('else', 'endif')) + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse(('endif',)) + parser.delete_first_token() + else: + nodelist_false = NodeList() + return IfNode(boolvars, nodelist_true, nodelist_false) +do_if = register.tag("if", do_if) + +#@register.tag +def ifchanged(parser, token): + """ + Check if a value has changed from the last iteration of a loop. + + The 'ifchanged' block tag is used within a loop. It checks its own rendered + contents against its previous state and only displays its content if the + value has changed:: + +

    Archive for {{ year }}

    + + {% for date in days %} + {% ifchanged %}

    {{ date|date:"F" }}

    {% endifchanged %} + {{ date|date:"j" }} + {% endfor %} + """ + bits = token.contents.split() + if len(bits) != 1: + raise TemplateSyntaxError, "'ifchanged' tag takes no arguments" + nodelist = parser.parse(('endifchanged',)) + parser.delete_first_token() + return IfChangedNode(nodelist) +ifchanged = register.tag(ifchanged) + +#@register.tag +def ssi(parser, token): + """ + Output the contents of a given file into the page. + + Like a simple "include" tag, the ``ssi`` tag includes the contents + of another file -- which must be specified using an absolute page -- + in the current page:: + + {% ssi /home/html/ljworld.com/includes/right_generic.html %} + + If the optional "parsed" parameter is given, the contents of the included + file are evaluated as template code, with the current context:: + + {% ssi /home/html/ljworld.com/includes/right_generic.html parsed %} + """ + bits = token.contents.split() + parsed = False + if len(bits) not in (2, 3): + raise TemplateSyntaxError, "'ssi' tag takes one argument: the path to the file to be included" + if len(bits) == 3: + if bits[2] == 'parsed': + parsed = True + else: + raise TemplateSyntaxError, "Second (optional) argument to %s tag must be 'parsed'" % bits[0] + return SsiNode(bits[1], parsed) +ssi = register.tag(ssi) + +#@register.tag +def load(parser, token): + """ + Load a custom template tag set. + + For example, to load the template tags in ``django/templatetags/news/photos.py``:: + + {% load news.photos %} + """ + bits = token.contents.split() + for taglib in bits[1:]: + # add the library to the parser + try: + lib = get_library("django.templatetags.%s" % taglib.split('.')[-1]) + parser.add_library(lib) + except InvalidTemplateLibrary, e: + raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e) + return LoadNode() +load = register.tag(load) + +#@register.tag +def now(parser, token): + """ + Display the date, formatted according to the given string. + + Uses the same format as PHP's ``date()`` function; see http://php.net/date + for all the possible values. + + Sample usage:: + + It is {% now "jS F Y H:i" %} + """ + bits = token.contents.split('"') + if len(bits) != 3: + raise TemplateSyntaxError, "'now' statement takes one argument" + format_string = bits[1] + return NowNode(format_string) +now = register.tag(now) + +#@register.tag +def regroup(parser, token): + """ + Regroup a list of alike objects by a common attribute. + + This complex tag is best illustrated by use of an example: say that + ``people`` is a list of ``Person`` objects that have ``first_name``, + ``last_name``, and ``gender`` attributes, and you'd like to display a list + that looks like: + + * Male: + * George Bush + * Bill Clinton + * Female: + * Margaret Thatcher + * Colendeeza Rice + * Unknown: + * Pat Smith + + The following snippet of template code would accomplish this dubious task:: + + {% regroup people by gender as grouped %} +
      + {% for group in grouped %} +
    • {{ group.grouper }} +
        + {% for item in group.list %} +
      • {{ item }}
      • + {% endfor %} +
      + {% endfor %} +
    + + As you can see, ``{% regroup %}`` populates a variable with a list of + objects with ``grouper`` and ``list`` attributes. ``grouper`` contains the + item that was grouped by; ``list`` contains the list of objects that share + that ``grouper``. In this case, ``grouper`` would be ``Male``, ``Female`` + and ``Unknown``, and ``list`` is the list of people with those genders. + + Note that `{% regroup %}`` does not work when the list to be grouped is not + sorted by the key you are grouping by! This means that if your list of + people was not sorted by gender, you'd need to make sure it is sorted before + using it, i.e.:: + + {% regroup people|dictsort:"gender" by gender as grouped %} + + """ + firstbits = token.contents.split(None, 3) + if len(firstbits) != 4: + raise TemplateSyntaxError, "'regroup' tag takes five arguments" + target = parser.compile_filter(firstbits[1]) + if firstbits[2] != 'by': + raise TemplateSyntaxError, "second argument to 'regroup' tag must be 'by'" + lastbits_reversed = firstbits[3][::-1].split(None, 2) + if lastbits_reversed[1][::-1] != 'as': + raise TemplateSyntaxError, "next-to-last argument to 'regroup' tag must be 'as'" + + expression = parser.compile_filter('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. + + Since the template system has no concept of "escaping", to display one of + the bits used in template tags, you must use the ``{% templatetag %}`` tag. + + The argument tells which template bit to output: + + ================== ======= + Argument Outputs + ================== ======= + ``openblock`` ``{%`` + ``closeblock`` ``%}`` + ``openvariable`` ``{{`` + ``closevariable`` ``}}`` + ================== ======= + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "'templatetag' statement takes one argument" + tag = bits[1] + if not TemplateTagNode.mapping.has_key(tag): + raise TemplateSyntaxError, "Invalid templatetag argument: '%s'. Must be one of: %s" % \ + (tag, TemplateTagNode.mapping.keys()) + return TemplateTagNode(tag) +templatetag = register.tag(templatetag) + +#@register.tag +def widthratio(parser, token): + """ + For creating bar charts and such, this tag calculates the ratio of a given + value to a maximum value, and then applies that ratio to a constant. + + For example:: + + + + Above, if ``this_value`` is 175 and ``max_value`` is 200, the the image in + the above example will be 88 pixels wide (because 175/200 = .875; .875 * + 100 = 87.5 which is rounded up to 88). + """ + bits = token.contents.split() + if len(bits) != 4: + raise TemplateSyntaxError("widthratio takes three arguments") + tag, this_value_expr, max_value_expr, max_width = bits + try: + max_width = int(max_width) + except ValueError: + raise TemplateSyntaxError("widthratio final argument must be an integer") + return WidthRatioNode(parser.compile_filter(this_value_expr), + parser.compile_filter(max_value_expr), max_width) +widthratio = register.tag(widthratio) diff --git a/django/template/loader.py b/django/template/loader.py new file mode 100644 index 0000000000..1172410557 --- /dev/null +++ b/django/template/loader.py @@ -0,0 +1,111 @@ +# Wrapper for loading templates from storage of some sort (e.g. filesystem, database). +# +# This uses the TEMPLATE_LOADERS setting, which is a list of loaders to use. +# Each loader is expected to have this interface: +# +# callable(name, dirs=[]) +# +# name is the template name. +# dirs is an optional list of directories to search instead of TEMPLATE_DIRS. +# +# The loader should return a tuple of (template_source, path). The path returned +# might be shown to the user for debugging purposes, so it should identify where +# the template was loaded from. +# +# Each loader should have an "is_usable" attribute set. This is a boolean that +# specifies whether the loader can be used in this Python installation. Each +# loader is responsible for setting this when it's initialized. +# +# For example, the eggs loader (which is capable of loading templates from +# Python eggs) sets is_usable to False if the "pkg_resources" module isn't +# installed, because pkg_resources is necessary to read eggs. + +from django.core.exceptions import ImproperlyConfigured +from django.template import Origin, StringOrigin, Template, Context, TemplateDoesNotExist, add_to_builtins +from django.conf.settings import TEMPLATE_LOADERS +from django.conf import settings + +template_source_loaders = [] +for path in TEMPLATE_LOADERS: + i = path.rfind('.') + module, attr = path[:i], path[i+1:] + try: + mod = __import__(module, globals(), locals(), [attr]) + except ImportError, e: + raise ImproperlyConfigured, 'Error importing template source loader %s: "%s"' % (module, e) + try: + func = getattr(mod, attr) + except AttributeError: + raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable template source loader' % (module, attr) + if not func.is_usable: + import warnings + warnings.warn("Your TEMPLATE_LOADERS setting includes %r, but your Python installation doesn't support that type of template loading. Consider removing that line from TEMPLATE_LOADERS." % path) + else: + template_source_loaders.append(func) + +class LoaderOrigin(Origin): + def __init__(self, display_name, loader, name, dirs): + super(LoaderOrigin, self).__init__(display_name) + self.loader, self.loadname, self.dirs = loader, name, dirs + + def reload(self): + return self.loader(self.loadname, self.dirs)[0] + +def make_origin(display_name, loader, name, dirs): + if settings.TEMPLATE_DEBUG: + return LoaderOrigin(display_name, loader, name, dirs) + else: + return None + +def find_template_source(name, dirs=None): + for loader in template_source_loaders: + try: + source, display_name = loader(name, dirs) + return (source, make_origin(display_name, loader, name, dirs)) + except TemplateDoesNotExist: + pass + raise TemplateDoesNotExist, name + +def get_template(template_name): + """ + Returns a compiled Template object for the given template name, + handling template inheritance recursively. + """ + return get_template_from_string(*find_template_source(template_name)) + +def get_template_from_string(source, origin=None ): + """ + Returns a compiled Template object for the given template code, + handling template inheritance recursively. + """ + return Template(source, origin) + +def render_to_string(template_name, dictionary=None, context_instance=None): + """ + Loads the given template_name and renders it with the given dictionary as + context. The template_name may be a string to load a single template using + get_template, or it may be a tuple to use select_template to find one of + the templates in the list. Returns a string. + """ + dictionary = dictionary or {} + if isinstance(template_name, (list, tuple)): + t = select_template(template_name) + else: + t = get_template(template_name) + if context_instance: + context_instance.update(dictionary) + else: + context_instance = Context(dictionary) + return t.render(context_instance) + +def select_template(template_name_list): + "Given a list of template names, returns the first that can be loaded." + for template_name in template_name_list: + try: + return get_template(template_name) + except TemplateDoesNotExist: + continue + # If we get here, none of the templates could be loaded + raise TemplateDoesNotExist, ', '.join(template_name_list) + +add_to_builtins('django.template.loader_tags') diff --git a/django/template/loader_tags.py b/django/template/loader_tags.py new file mode 100644 index 0000000000..eafe902b82 --- /dev/null +++ b/django/template/loader_tags.py @@ -0,0 +1,172 @@ +from django.template import TemplateSyntaxError, TemplateDoesNotExist, resolve_variable +from django.template import Library, Context, Node +from django.template.loader import get_template, get_template_from_string, find_template_source + +register = Library() + +class ExtendsError(Exception): + pass + +class BlockNode(Node): + def __init__(self, name, nodelist, parent=None): + self.name, self.nodelist, self.parent = name, nodelist, parent + + def __repr__(self): + return "" % (self.name, self.nodelist) + + def render(self, context): + context.push() + # Save context in case of block.super(). + self.context = context + context['block'] = self + result = self.nodelist.render(context) + context.pop() + return result + + def super(self): + if self.parent: + return self.parent.render(self.context) + return '' + + def add_parent(self, nodelist): + if self.parent: + self.parent.add_parent(nodelist) + else: + self.parent = BlockNode(self.name, nodelist) + +class ExtendsNode(Node): + def __init__(self, nodelist, parent_name, parent_name_expr, template_dirs=None): + self.nodelist = nodelist + self.parent_name, self.parent_name_expr = parent_name, parent_name_expr + self.template_dirs = template_dirs + + def get_parent(self, context): + if self.parent_name_expr: + self.parent_name = self.parent_name_expr.resolve(context) + parent = self.parent_name + if not parent: + error_msg = "Invalid template name in 'extends' tag: %r." % parent + if self.parent_name_expr: + error_msg += " Got this from the %r variable." % self.parent_name_expr #TODO nice repr. + raise TemplateSyntaxError, error_msg + try: + return get_template_from_string(*find_template_source(parent, self.template_dirs)) + except TemplateDoesNotExist: + raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent + + def render(self, context): + compiled_parent = self.get_parent(context) + parent_is_child = isinstance(compiled_parent.nodelist[0], ExtendsNode) + parent_blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)]) + for block_node in self.nodelist.get_nodes_by_type(BlockNode): + # Check for a BlockNode with this node's name, and replace it if found. + try: + parent_block = parent_blocks[block_node.name] + except KeyError: + # This BlockNode wasn't found in the parent template, but the + # parent block might be defined in the parent's *parent*, so we + # add this BlockNode to the parent's ExtendsNode nodelist, so + # it'll be checked when the parent node's render() is called. + if parent_is_child: + compiled_parent.nodelist[0].nodelist.append(block_node) + else: + # Keep any existing parents and add a new one. Used by BlockNode. + parent_block.parent = block_node.parent + parent_block.add_parent(parent_block.nodelist) + parent_block.nodelist = block_node.nodelist + return compiled_parent.render(context) + +class ConstantIncludeNode(Node): + def __init__(self, template_path): + try: + t = get_template(template_path) + self.template = t + except: + from django.conf.settings import TEMPLATE_DEBUG + 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_expr = None, None + if bits[1][0] in ('"', "'") and bits[1][-1] == bits[1][0]: + parent_name = bits[1][1:-1] + else: + parent_name_expr = parser.compile_filter(bits[1]) + nodelist = parser.parse() + if nodelist.get_nodes_by_type(ExtendsNode): + raise TemplateSyntaxError, "'%s' cannot appear more than once in the same template" % bits[0] + return ExtendsNode(nodelist, parent_name, parent_name_expr) + +def do_include(parser, token): + """ + Loads a template and renders it with the current context. + + Example:: + + {% include "foo/some_include" %} + """ + bits = token.contents.split() + if len(bits) != 2: + raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] + path = bits[1] + if path[0] in ('"', "'") and path[-1] == path[0]: + return ConstantIncludeNode(path[1:-1]) + return IncludeNode(bits[1]) + +register.tag('block', do_block) +register.tag('extends', do_extends) +register.tag('include', do_include) diff --git a/django/template/loaders/__init__.py b/django/template/loaders/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/template/loaders/app_directories.py b/django/template/loaders/app_directories.py new file mode 100644 index 0000000000..1d4dc6bef7 --- /dev/null +++ b/django/template/loaders/app_directories.py @@ -0,0 +1,41 @@ +# Wrapper for loading templates from "template" directories in installed app packages. + +from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION +from django.core.exceptions import ImproperlyConfigured +from django.template import TemplateDoesNotExist +import os + +# At compile time, cache the directories to search. +app_template_dirs = [] +for app in INSTALLED_APPS: + i = app.rfind('.') + if i == -1: + m, a = app, None + else: + m, a = app[:i], app[i+1:] + try: + if a is None: + mod = __import__(m, '', '', []) + else: + mod = getattr(__import__(m, '', '', [a]), a) + except ImportError, e: + raise ImproperlyConfigured, 'ImportError %s: %s' % (app, e.args[0]) + template_dir = os.path.join(os.path.dirname(mod.__file__), 'templates') + if os.path.isdir(template_dir): + app_template_dirs.append(template_dir) + +# It won't change, so convert it to a tuple to save memory. +app_template_dirs = tuple(app_template_dirs) + +def get_template_sources(template_name, template_dirs=None): + for template_dir in app_template_dirs: + yield os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION + +def load_template_source(template_name, template_dirs=None): + for filepath in get_template_sources(template_name, template_dirs): + try: + return (open(filepath).read(), filepath) + except IOError: + pass + raise TemplateDoesNotExist, template_name +load_template_source.is_usable = True diff --git a/django/template/loaders/eggs.py b/django/template/loaders/eggs.py new file mode 100644 index 0000000000..62e4d4db33 --- /dev/null +++ b/django/template/loaders/eggs.py @@ -0,0 +1,25 @@ +# Wrapper for loading templates from eggs via pkg_resources.resource_string. + +try: + from pkg_resources import resource_string +except ImportError: + resource_string = None + +from django.template import TemplateDoesNotExist +from django.conf.settings import INSTALLED_APPS, TEMPLATE_FILE_EXTENSION + +def load_template_source(template_name, template_dirs=None): + """ + Loads templates from Python eggs via pkg_resource.resource_string. + + For every installed app, it tries to get the resource (app, template_name). + """ + if resource_string is not None: + pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION + for app in INSTALLED_APPS: + try: + return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name)) + except: + pass + raise TemplateDoesNotExist, template_name +load_template_source.is_usable = resource_string is not None diff --git a/django/template/loaders/filesystem.py b/django/template/loaders/filesystem.py new file mode 100644 index 0000000000..4abe50d764 --- /dev/null +++ b/django/template/loaders/filesystem.py @@ -0,0 +1,25 @@ +# Wrapper for loading templates from the filesystem. + +from django.conf.settings import TEMPLATE_DIRS, TEMPLATE_FILE_EXTENSION +from django.template import TemplateDoesNotExist +import os + +def get_template_sources(template_name, template_dirs=None): + if not template_dirs: + template_dirs = TEMPLATE_DIRS + for template_dir in template_dirs: + yield os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION + +def load_template_source(template_name, template_dirs=None): + tried = [] + for filepath in get_template_sources(template_name, template_dirs): + try: + return (open(filepath).read(), filepath) + except IOError: + tried.append(filepath) + if template_dirs: + error_msg = "Tried %s" % tried + else: + error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory." + raise TemplateDoesNotExist, error_msg +load_template_source.is_usable = True diff --git a/django/templatetags/i18n.py b/django/templatetags/i18n.py index 7c2019cac0..2e96478f46 100644 --- a/django/templatetags/i18n.py +++ b/django/templatetags/i18n.py @@ -1,6 +1,6 @@ -from django.core.template import Node, NodeList, Template, Context, resolve_variable -from django.core.template import TemplateSyntaxError, TokenParser, Library -from django.core.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR +from django.template import Node, NodeList, Template, Context, resolve_variable +from django.template import TemplateSyntaxError, TokenParser, Library +from django.template import TOKEN_BLOCK, TOKEN_TEXT, TOKEN_VAR from django.utils import translation import re, sys diff --git a/django/utils/translation.py b/django/utils/translation.py index 9a924385d6..4eb7f6c7bc 100644 --- a/django/utils/translation.py +++ b/django/utils/translation.py @@ -384,7 +384,7 @@ def templateize(src): does so by translating the Django translation tags into standard gettext function invocations. """ - from django.core.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK + from django.template import Lexer, TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK out = StringIO() intrans = False inplural = False diff --git a/django/views/debug.py b/django/views/debug.py index 70978a5128..09ae11477a 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.core.template import Template, Context, TemplateDoesNotExist +from django.template import Template, Context, TemplateDoesNotExist from django.utils.html import escape from django.http import HttpResponseServerError, HttpResponseNotFound import inspect, os, re, sys @@ -72,7 +72,7 @@ def technical_500_response(request, exc_type, exc_value, tb): template_does_not_exist = False loader_debug_info = None if issubclass(exc_type, TemplateDoesNotExist): - from django.core.template.loader import template_source_loaders + from django.template.loader import template_source_loaders template_does_not_exist = True loader_debug_info = [] for loader in template_source_loaders: diff --git a/django/views/defaults.py b/django/views/defaults.py index 5287963d0f..953cad9122 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django.core.template import Context, loader +from django.template import Context, loader from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site from django import http diff --git a/django/views/generic/create_update.py b/django/views/generic/create_update.py index dab95bbca2..c4c6084ac8 100644 --- a/django/views/generic/create_update.py +++ b/django/views/generic/create_update.py @@ -1,5 +1,6 @@ +from django import models from django.core.xheaders import populate_xheaders -from django.core.template import loader +from django.template import loader from django.core import formfields, meta from django.views.auth.login import redirect_to_login from django.core.extensions import DjangoContext @@ -7,13 +8,13 @@ from django.core.paginator import ObjectPaginator, InvalidPage from django.http import Http404, HttpResponse, HttpResponseRedirect from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured -def create_object(request, model, template_name=None, +def create_object(request, app_label, module_name, template_name=None, template_loader=loader, extra_context={}, post_save_redirect=None, login_required=False, follow=None, context_processors=None): """ Generic object-creation function. - Templates: ``/_form`` + Templates: ``/_form`` Context: form the form wrapper for the object @@ -21,12 +22,13 @@ def create_object(request, model, template_name=None, if login_required and request.user.is_anonymous(): return redirect_to_login(request.path) - manipulator = model.AddManipulator(follow=follow) + mod = models.get_module(app_label, module_name) + manipulator = mod.AddManipulator(follow=follow) if request.POST: # If data was POSTed, we're trying to create a new object new_data = request.POST.copy() - if model._meta.has_field_type(meta.FileField): + if mod.Klass._meta.has_field_type(meta.FileField): new_data.update(request.FILES) # Check for errors @@ -38,7 +40,7 @@ def create_object(request, model, template_name=None, new_object = manipulator.save(new_data) if not request.user.is_anonymous(): - request.user.add_message("The %s was created sucessfully." % model._meta.verbose_name) + request.user.add_message("The %s was created sucessfully." % mod.Klass._meta.verbose_name) # Redirect to the new object: first by trying post_save_redirect, # then by obj.get_absolute_url; fail if neither works. @@ -56,7 +58,7 @@ def create_object(request, model, template_name=None, # Create the FormWrapper, template, context, response form = formfields.FormWrapper(manipulator, new_data, errors) if not template_name: - template_name = "%s/%s_form" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_form" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'form': form, @@ -68,14 +70,14 @@ def create_object(request, model, template_name=None, c[key] = value return HttpResponse(t.render(c)) -def update_object(request, model, object_id=None, slug=None, +def update_object(request, app_label, module_name, object_id=None, slug=None, slug_field=None, template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, post_save_redirect=None, login_required=False, follow=None, context_processors=None): """ Generic object-update function. - Templates: ``/_form`` + Templates: ``/_form`` Context: form the form wrapper for the object @@ -85,21 +87,23 @@ def update_object(request, model, object_id=None, slug=None, if login_required and request.user.is_anonymous(): return redirect_to_login(request.path) + mod = models.get_module(app_label, module_name) + # Look up the object to be edited lookup_kwargs = {} if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: raise AttributeError("Generic edit view must be called with either an object_id or a slug/slug_field") lookup_kwargs.update(extra_lookup_kwargs) try: - object = model._default_manager.get_object(**lookup_kwargs) + object = mod.get_object(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs) + raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) - manipulator = model.ChangeManipulator(object.id, follow=follow) + manipulator = mod.ChangeManipulator(object.id, follow=follow) if request.POST: new_data = request.POST.copy() @@ -109,7 +113,7 @@ def update_object(request, model, object_id=None, slug=None, manipulator.save(new_data) if not request.user.is_anonymous(): - request.user.add_message("The %s was updated sucessfully." % model._meta.verbose_name) + request.user.add_message("The %s was updated sucessfully." % mod.Klass._meta.verbose_name) # Do a post-after-redirect so that reload works, etc. if post_save_redirect: @@ -125,7 +129,7 @@ def update_object(request, model, object_id=None, slug=None, form = formfields.FormWrapper(manipulator, new_data, errors) if not template_name: - template_name = "%s/%s_form" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_form" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'form': form, @@ -137,10 +141,10 @@ def update_object(request, model, object_id=None, slug=None, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, model, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) return response -def delete_object(request, model, post_delete_redirect, +def delete_object(request, app_label, module_name, post_delete_redirect, object_id=None, slug=None, slug_field=None, template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, login_required=False, context_processors=None): @@ -151,7 +155,7 @@ def delete_object(request, model, post_delete_redirect, fetched using GET; for safty, deletion will only be performed if this view is POSTed. - Templates: ``/_confirm_delete`` + Templates: ``/_confirm_delete`` Context: object the original object being deleted @@ -159,28 +163,30 @@ def delete_object(request, model, post_delete_redirect, if login_required and request.user.is_anonymous(): return redirect_to_login(request.path) + mod = models.get_module(app_label, module_name) + # Look up the object to be edited lookup_kwargs = {} if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: raise AttributeError("Generic delete view must be called with either an object_id or a slug/slug_field") lookup_kwargs.update(extra_lookup_kwargs) try: - object = model._default_manager.get_object(**lookup_kwargs) + object = mod.get_object(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404, "No %s found for %s" % (model._meta.app_label, lookup_kwargs) + raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) if request.META['REQUEST_METHOD'] == 'POST': object.delete() if not request.user.is_anonymous(): - request.user.add_message("The %s was deleted." % model._meta.verbose_name) + request.user.add_message("The %s was deleted." % mod.Klass._meta.verbose_name) return HttpResponseRedirect(post_delete_redirect) else: if not template_name: - template_name = "%s/%s_confirm_delete" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_confirm_delete" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'object': object, @@ -191,5 +197,5 @@ def delete_object(request, model, post_delete_redirect, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, model, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) return response diff --git a/django/views/generic/date_based.py b/django/views/generic/date_based.py index 71a4880c92..86857370bd 100644 --- a/django/views/generic/date_based.py +++ b/django/views/generic/date_based.py @@ -1,40 +1,42 @@ -from django.core.template import loader +from django.template import loader from django.core.exceptions import ObjectDoesNotExist from django.core.extensions import DjangoContext from django.core.xheaders import populate_xheaders +from django.models import get_module from django.http import Http404, HttpResponse import datetime, time -def archive_index(request, model, date_field, num_latest=15, +def archive_index(request, app_label, module_name, date_field, num_latest=15, template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, allow_empty=False, context_processors=None): """ Generic top-level archive of date-based objects. - Templates: ``/_archive`` + Templates: ``/_archive`` Context: date_list List of years latest Latest N (defaults to 15) objects by date """ + mod = get_module(app_label, module_name) lookup_kwargs = {'%s__lte' % date_field: datetime.datetime.now()} lookup_kwargs.update(extra_lookup_kwargs) - date_list = getattr(model._default_manager, "get_%s_list" % date_field)('year', **lookup_kwargs)[::-1] + date_list = getattr(mod, "get_%s_list" % date_field)('year', **lookup_kwargs)[::-1] if not date_list and not allow_empty: - raise Http404, "No %s available" % model._meta.verbose_name + raise Http404("No %s.%s available" % (app_label, module_name)) if date_list and num_latest: lookup_kwargs.update({ 'limit': num_latest, 'order_by': ('-' + date_field,), }) - latest = model._default_manager.get_list(**lookup_kwargs) + latest = mod.get_list(**lookup_kwargs) else: latest = None if not template_name: - template_name = "%s/%s_archive" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_archive" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'date_list' : date_list, @@ -47,30 +49,31 @@ def archive_index(request, model, date_field, num_latest=15, c[key] = value return HttpResponse(t.render(c)) -def archive_year(request, year, model, date_field, +def archive_year(request, year, app_label, module_name, date_field, template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, context_processors=None): """ Generic yearly archive view. - Templates: ``/_archive_year`` + Templates: ``/_archive_year`` Context: date_list List of months in this year with objects year This year """ + mod = get_module(app_label, module_name) now = datetime.datetime.now() lookup_kwargs = {'%s__year' % date_field: year} # Only bother to check current date if the year isn't in the past. if int(year) >= now.year: lookup_kwargs['%s__lte' % date_field] = now lookup_kwargs.update(extra_lookup_kwargs) - date_list = getattr(model._default_manager, "get_%s_list" % date_field)('month', **lookup_kwargs) + date_list = getattr(mod, "get_%s_list" % date_field)('month', **lookup_kwargs) if not date_list: raise Http404 if not template_name: - template_name = "%s/%s_archive_year" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_archive_year" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'date_list': date_list, @@ -83,13 +86,13 @@ def archive_year(request, year, model, date_field, c[key] = value return HttpResponse(t.render(c)) -def archive_month(request, year, month, model, date_field, +def archive_month(request, year, month, app_label, module_name, date_field, month_format='%b', template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, context_processors=None): """ Generic monthly archive view. - Templates: ``/_archive_month`` + Templates: ``/_archive_month`` Context: month: this month @@ -101,6 +104,7 @@ def archive_month(request, year, month, model, date_field, except ValueError: raise Http404 + mod = get_module(app_label, module_name) now = datetime.datetime.now() # Calculate first and last day of month, for use in a date-range lookup. first_day = date.replace(day=1) @@ -113,11 +117,11 @@ def archive_month(request, year, month, model, date_field, if last_day >= now.date(): lookup_kwargs['%s__lte' % date_field] = now lookup_kwargs.update(extra_lookup_kwargs) - object_list = model._default_manager.get_list(**lookup_kwargs) + object_list = mod.get_list(**lookup_kwargs) if not object_list: raise Http404 if not template_name: - template_name = "%s/%s_archive_month" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_archive_month" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'object_list': object_list, @@ -130,14 +134,14 @@ def archive_month(request, year, month, model, date_field, c[key] = value return HttpResponse(t.render(c)) -def archive_day(request, year, month, day, model, date_field, +def archive_day(request, year, month, day, app_label, module_name, date_field, month_format='%b', day_format='%d', template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, allow_empty=False, context_processors=None): """ Generic daily archive view. - Templates: ``/_archive_day`` + Templates: ``/_archive_day`` Context: object_list: list of objects published that day @@ -153,6 +157,7 @@ def archive_day(request, year, month, day, model, date_field, except ValueError: raise Http404 + mod = get_module(app_label, module_name) now = datetime.datetime.now() lookup_kwargs = { '%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max)), @@ -161,11 +166,11 @@ def archive_day(request, year, month, day, model, date_field, if date >= now.date(): lookup_kwargs['%s__lte' % date_field] = now lookup_kwargs.update(extra_lookup_kwargs) - object_list = model._default_manager.get_list(**lookup_kwargs) + object_list = mod.get_list(**lookup_kwargs) if not allow_empty and not object_list: raise Http404 if not template_name: - template_name = "%s/%s_archive_day" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_archive_day" % (app_label, module_name) t = template_loader.get_template(template_name) c = DjangoContext(request, { 'object_list': object_list, @@ -192,7 +197,7 @@ def archive_today(request, **kwargs): }) return archive_day(request, **kwargs) -def object_detail(request, year, month, day, model, date_field, +def object_detail(request, year, month, day, app_label, module_name, date_field, month_format='%b', day_format='%d', object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, @@ -200,7 +205,7 @@ def object_detail(request, year, month, day, model, date_field, """ Generic detail view from year/month/day/slug or year/month/day/id structure. - Templates: ``/_detail`` + Templates: ``/_detail`` Context: object: the object to be detailed @@ -210,6 +215,7 @@ def object_detail(request, year, month, day, model, date_field, except ValueError: raise Http404 + mod = get_module(app_label, module_name) now = datetime.datetime.now() lookup_kwargs = { '%s__range' % date_field: (datetime.datetime.combine(date, datetime.time.min), datetime.datetime.combine(date, datetime.time.max)), @@ -218,18 +224,18 @@ def object_detail(request, year, month, day, model, date_field, if date >= now.date(): lookup_kwargs['%s__lte' % date_field] = now if object_id: - lookup_kwargs['%s__exact' % model._meta.pk.name] = object_id + lookup_kwargs['%s__exact' % mod.Klass._meta.pk.name] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: - raise AttributeError, "Generic detail view must be called with either an object_id or a slug/slugfield" + raise AttributeError("Generic detail view must be called with either an object_id or a slug/slugfield") lookup_kwargs.update(extra_lookup_kwargs) try: - object = model._default_manager.get_object(**lookup_kwargs) + object = mod.get_object(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs) + raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) if not template_name: - template_name = "%s/%s_detail" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_detail" % (app_label, module_name) if template_name_field: template_name_list = [getattr(object, template_name_field), template_name] t = template_loader.select_template(template_name_list) @@ -244,5 +250,5 @@ def object_detail(request, year, month, day, model, date_field, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, model, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) return response diff --git a/django/views/generic/list_detail.py b/django/views/generic/list_detail.py index 50dc3b5cfc..3d4fc32f14 100644 --- a/django/views/generic/list_detail.py +++ b/django/views/generic/list_detail.py @@ -1,17 +1,18 @@ -from django.core.template import loader +from django import models +from django.template import loader from django.http import Http404, HttpResponse from django.core.xheaders import populate_xheaders from django.core.extensions import DjangoContext from django.core.paginator import ObjectPaginator, InvalidPage from django.core.exceptions import ObjectDoesNotExist -def object_list(request, model, paginate_by=None, allow_empty=False, +def object_list(request, app_label, module_name, paginate_by=None, allow_empty=False, template_name=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, context_processors=None): """ Generic list of objects. - Templates: ``/_list`` + Templates: ``/_list`` Context: object_list list of objects @@ -34,9 +35,10 @@ def object_list(request, model, paginate_by=None, allow_empty=False, hits number of objects, total """ + mod = models.get_module(app_label, module_name) lookup_kwargs = extra_lookup_kwargs.copy() if paginate_by: - paginator = ObjectPaginator(model, lookup_kwargs, paginate_by) + paginator = ObjectPaginator(mod, lookup_kwargs, paginate_by) page = request.GET.get('page', 0) try: object_list = paginator.get_page(page) @@ -59,7 +61,7 @@ def object_list(request, model, paginate_by=None, allow_empty=False, 'hits' : paginator.hits, }, context_processors) else: - object_list = model._default_manager.get_list(**lookup_kwargs) + object_list = mod.get_list(**lookup_kwargs) c = DjangoContext(request, { 'object_list': object_list, 'is_paginated': False @@ -72,36 +74,37 @@ def object_list(request, model, paginate_by=None, allow_empty=False, else: c[key] = value if not template_name: - template_name = "%s/%s_list" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_list" % (app_label, module_name) t = template_loader.get_template(template_name) return HttpResponse(t.render(c)) -def object_detail(request, model, object_id=None, slug=None, +def object_detail(request, app_label, module_name, object_id=None, slug=None, slug_field=None, template_name=None, template_name_field=None, template_loader=loader, extra_lookup_kwargs={}, extra_context={}, context_processors=None): """ Generic list of objects. - Templates: ``/_detail`` + Templates: ``/_detail`` Context: object the object """ + mod = models.get_module(app_label, module_name) lookup_kwargs = {} if object_id: lookup_kwargs['pk'] = object_id elif slug and slug_field: lookup_kwargs['%s__exact' % slug_field] = slug else: - raise AttributeError, "Generic detail view must be called with either an object_id or a slug/slug_field." + raise AttributeError("Generic detail view must be called with either an object_id or a slug/slug_field") lookup_kwargs.update(extra_lookup_kwargs) try: - object = model._default_manager.get_object(**lookup_kwargs) + object = mod.get_object(**lookup_kwargs) except ObjectDoesNotExist: - raise Http404, "No %s found for %s" % (model._meta.verbose_name, lookup_kwargs) + raise Http404("%s.%s does not exist for %s" % (app_label, module_name, lookup_kwargs)) if not template_name: - template_name = "%s/%s_detail" % (model._meta.app_label, model._meta.object_name.lower()) + template_name = "%s/%s_detail" % (app_label, module_name) if template_name_field: template_name_list = [getattr(object, template_name_field), template_name] t = template_loader.select_template(template_name_list) @@ -116,5 +119,5 @@ def object_detail(request, model, object_id=None, slug=None, else: c[key] = value response = HttpResponse(t.render(c)) - populate_xheaders(request, response, model, getattr(object, object._meta.pk.name)) + populate_xheaders(request, response, app_label, module_name, getattr(object, object._meta.pk.name)) return response diff --git a/django/views/registration/passwords.py b/django/views/registration/passwords.py index 162f64fc3a..ebdc06d1a9 100644 --- a/django/views/registration/passwords.py +++ b/django/views/registration/passwords.py @@ -1,6 +1,6 @@ from django.core import formfields, validators from django.core.extensions import DjangoContext, render_to_response -from django.core.template import Context, loader +from django.template import Context, loader from django.models.auth import User from django.models.core import Site from django.views.decorators.auth import login_required diff --git a/django/views/static.py b/django/views/static.py index cde95f4578..d0b24a2f42 100644 --- a/django/views/static.py +++ b/django/views/static.py @@ -5,7 +5,7 @@ import mimetypes from django.core import template_loader from django.core.exceptions import ImproperlyConfigured from django.http import Http404, HttpResponse, HttpResponseRedirect -from django.core.template import Template, Context, TemplateDoesNotExist +from django.template import Template, Context, TemplateDoesNotExist def serve(request, path, document_root=None, show_indexes=False): """