1
0
mirror of https://github.com/django/django.git synced 2025-07-06 18:59:13 +00:00

queryset-refactor: Merged from trunk up to [7002].

git-svn-id: http://code.djangoproject.com/svn/django/branches/queryset-refactor@7004 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
Malcolm Tredinnick 2008-01-09 06:08:40 +00:00
parent b824d803ce
commit 83cb2218bc
25 changed files with 301 additions and 247 deletions

View File

@ -5,7 +5,7 @@ var LATIN_MAP = {
'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', 'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U',
'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä': 'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä':
'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'o', 'ñ': 'n', 'ò': 'o', 'ó': 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó':
'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u', 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', 'ø': 'o', 'ù': 'u', 'ú': 'u',
'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y' 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
} }

View File

@ -80,12 +80,12 @@ class CASocialInsuranceNumberField(Field):
Checks the following rules to determine whether the number is valid: Checks the following rules to determine whether the number is valid:
* Conforms to the XXX-XXX-XXXX format. * Conforms to the XXX-XXX-XXX format.
* Passes the check digit process "Luhn Algorithm" * Passes the check digit process "Luhn Algorithm"
See: http://en.wikipedia.org/wiki/Social_Insurance_Number See: http://en.wikipedia.org/wiki/Social_Insurance_Number
""" """
default_error_messages = { default_error_messages = {
'invalid': ugettext('Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'), 'invalid': ugettext('Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format.'),
} }
def clean(self, value): def clean(self, value):

View File

@ -26,6 +26,8 @@ class SessionStore(SessionBase):
# Save immediately to minimize collision # Save immediately to minimize collision
self.save() self.save()
# Ensure the user is notified via a new cookie.
self.modified = True
return {} return {}
def exists(self, session_key): def exists(self, session_key):

View File

@ -46,6 +46,8 @@ class SessionStore(SessionBase):
self._session_key = self._get_new_session_key() self._session_key = self._get_new_session_key()
self._session_cache = {} self._session_cache = {}
self.save() self.save()
# Ensure the user is notified via a new cookie.
self.modified = True
finally: finally:
session_file.close() session_file.close()
except(IOError): except(IOError):

View File

@ -67,11 +67,10 @@ def make_msgid(idstring=None):
class BadHeaderError(ValueError): class BadHeaderError(ValueError):
pass pass
class SafeMIMEText(MIMEText): def forbid_multi_line_headers(name, val):
def __setitem__(self, name, val):
"Forbids multi-line headers, to prevent header injection." "Forbids multi-line headers, to prevent header injection."
if '\n' in val or '\r' in val: if '\n' in val or '\r' in val:
raise BadHeaderError, "Header values can't contain newlines (got %r for header %r)" % (val, name) raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
try: try:
val = force_unicode(val).encode('ascii') val = force_unicode(val).encode('ascii')
except UnicodeEncodeError: except UnicodeEncodeError:
@ -84,25 +83,16 @@ class SafeMIMEText(MIMEText):
val = ', '.join(result) val = ', '.join(result)
else: else:
val = Header(force_unicode(val), settings.DEFAULT_CHARSET) val = Header(force_unicode(val), settings.DEFAULT_CHARSET)
return name, val
class SafeMIMEText(MIMEText):
def __setitem__(self, name, val):
name, val = forbid_multi_line_headers(name, val)
MIMEText.__setitem__(self, name, val) MIMEText.__setitem__(self, name, val)
class SafeMIMEMultipart(MIMEMultipart): class SafeMIMEMultipart(MIMEMultipart):
def __setitem__(self, name, val): def __setitem__(self, name, val):
"Forbids multi-line headers, to prevent header injection." name, val = forbid_multi_line_headers(name, val)
if '\n' in val or '\r' in val:
raise BadHeaderError, "Header values can't contain newlines (got %r for header %r)" % (val, name)
try:
val = force_unicode(val).encode('ascii')
except UnicodeEncodeError:
if name.lower() in ('to', 'from', 'cc'):
result = []
for item in val.split(', '):
nm, addr = parseaddr(item)
nm = str(Header(nm, settings.DEFAULT_CHARSET))
result.append(formataddr((nm, str(addr))))
val = ', '.join(result)
else:
val = Header(force_unicode(val), settings.DEFAULT_CHARSET)
MIMEMultipart.__setitem__(self, name, val) MIMEMultipart.__setitem__(self, name, val)
class SMTPConnection(object): class SMTPConnection(object):

View File

@ -293,7 +293,7 @@ def sql_model_create(model, style, known_models=set()):
style.SQL_KEYWORD('NULL')) style.SQL_KEYWORD('NULL'))
for field_constraints in opts.unique_together: for field_constraints in opts.unique_together:
table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \ table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \
", ".join([qn(style.SQL_FIELD(opts.get_field(f).column)) for f in field_constraints])) ", ".join([style.SQL_FIELD(qn(opts.get_field(f).column)) for f in field_constraints]))
full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' (']
for i, line in enumerate(table_output): # Combine and add commas. for i, line in enumerate(table_output): # Combine and add commas.

View File

@ -413,6 +413,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
return self.connection is not None return self.connection is not None
def _cursor(self, settings): def _cursor(self, settings):
cursor = None
if not self._valid_connection(): if not self._valid_connection():
if len(settings.DATABASE_HOST.strip()) == 0: if len(settings.DATABASE_HOST.strip()) == 0:
settings.DATABASE_HOST = 'localhost' settings.DATABASE_HOST = 'localhost'
@ -422,16 +423,25 @@ class DatabaseWrapper(BaseDatabaseWrapper):
else: else:
conn_string = "%s/%s@%s" % (settings.DATABASE_USER, settings.DATABASE_PASSWORD, settings.DATABASE_NAME) conn_string = "%s/%s@%s" % (settings.DATABASE_USER, settings.DATABASE_PASSWORD, settings.DATABASE_NAME)
self.connection = Database.connect(conn_string, **self.options) self.connection = Database.connect(conn_string, **self.options)
cursor = FormatStylePlaceholderCursor(self.connection)
# Set oracle date to ansi date format. This only needs to execute
# once when we create a new connection.
cursor.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD' "
"NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'")
try: try:
self.oracle_version = int(self.connection.version.split('.')[0]) self.oracle_version = int(self.connection.version.split('.')[0])
except ValueError: except ValueError:
pass pass
try:
self.connection.stmtcachesize = 20
except:
# Django docs specify cx_Oracle version 4.3.1 or higher, but
# stmtcachesize is available only in 4.3.2 and up.
pass
if not cursor:
cursor = FormatStylePlaceholderCursor(self.connection) cursor = FormatStylePlaceholderCursor(self.connection)
# Default arraysize of 1 is highly sub-optimal. # Default arraysize of 1 is highly sub-optimal.
cursor.arraysize = 100 cursor.arraysize = 100
# Set oracle date to ansi date format.
cursor.execute("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'")
cursor.execute("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF'")
return cursor return cursor
class FormatStylePlaceholderCursor(Database.Cursor): class FormatStylePlaceholderCursor(Database.Cursor):
@ -506,7 +516,10 @@ class FormatStylePlaceholderCursor(Database.Cursor):
return Database.Cursor.executemany(self, query, new_param_list) return Database.Cursor.executemany(self, query, new_param_list)
def fetchone(self): def fetchone(self):
return to_unicode(Database.Cursor.fetchone(self)) row = Database.Cursor.fetchone(self)
if row is None:
return row
return tuple([to_unicode(e) for e in row])
def fetchmany(self, size=None): def fetchmany(self, size=None):
if size is None: if size is None:

View File

@ -154,15 +154,12 @@ class StringOrigin(Origin):
class Template(object): class Template(object):
def __init__(self, template_string, origin=None, name='<Unknown Template>'): def __init__(self, template_string, origin=None, name='<Unknown Template>'):
"Compilation stage"
try: try:
template_string = smart_unicode(template_string) template_string = smart_unicode(template_string)
except UnicodeDecodeError: except UnicodeDecodeError:
raise TemplateEncodingError("Templates can only be constructed from unicode or UTF-8 strings.") raise TemplateEncodingError("Templates can only be constructed from unicode or UTF-8 strings.")
if settings.TEMPLATE_DEBUG and origin == None: if settings.TEMPLATE_DEBUG and origin is None:
origin = StringOrigin(template_string) 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) self.nodelist = compile_string(template_string, origin)
self.name = name self.name = name
@ -177,13 +174,18 @@ class Template(object):
def compile_string(template_string, origin): def compile_string(template_string, origin):
"Compiles template_string into NodeList ready for rendering" "Compiles template_string into NodeList ready for rendering"
lexer = lexer_factory(template_string, origin) if settings.TEMPLATE_DEBUG:
parser = parser_factory(lexer.tokenize()) from debug import DebugLexer, DebugParser
lexer_class, parser_class = DebugLexer, DebugParser
else:
lexer_class, parser_class = Lexer, Parser
lexer = lexer_class(template_string, origin)
parser = parser_class(lexer.tokenize())
return parser.parse() return parser.parse()
class Token(object): class Token(object):
def __init__(self, token_type, contents): def __init__(self, token_type, contents):
"The token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT" # token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or TOKEN_COMMENT.
self.token_type, self.contents = token_type, contents self.token_type, self.contents = token_type, contents
def __str__(self): def __str__(self):
@ -200,7 +202,7 @@ class Lexer(object):
self.origin = origin self.origin = origin
def tokenize(self): def tokenize(self):
"Return a list of tokens from a given template_string" "Return a list of tokens from a given template_string."
in_tag = False in_tag = False
result = [] result = []
for bit in tag_re.split(self.template_string): for bit in tag_re.split(self.template_string):
@ -226,30 +228,6 @@ class Lexer(object):
token = Token(TOKEN_TEXT, token_string) token = Token(TOKEN_TEXT, token_string)
return token 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"
result, upto = [], 0
for match in tag_re.finditer(self.template_string):
start, end = match.span()
if start > upto:
result.append(self.create_token(self.template_string[upto:start], (upto, start), False))
upto = start
result.append(self.create_token(self.template_string[start:end], (start, end), True))
upto = end
last_bit = self.template_string[upto:]
if last_bit:
result.append(self.create_token(last_bit, (upto, upto + len(last_bit)), False))
return result
def create_token(self, token_string, source, in_tag):
token = super(DebugLexer, self).create_token(token_string, in_tag)
token.source = self.origin, source
return token
class Parser(object): class Parser(object):
def __init__(self, tokens): def __init__(self, tokens):
self.tokens = tokens self.tokens = tokens
@ -319,17 +297,17 @@ class Parser(object):
def exit_command(self): def exit_command(self):
pass pass
def error(self, token, msg ): def error(self, token, msg):
return TemplateSyntaxError(msg) return TemplateSyntaxError(msg)
def empty_variable(self, token): def empty_variable(self, token):
raise self.error( token, "Empty variable tag") raise self.error(token, "Empty variable tag")
def empty_block_tag(self, token): def empty_block_tag(self, token):
raise self.error( token, "Empty block tag") raise self.error(token, "Empty block tag")
def invalid_block_tag(self, token, command): def invalid_block_tag(self, token, command):
raise self.error( token, "Invalid block tag: '%s'" % command) raise self.error(token, "Invalid block tag: '%s'" % command)
def unclosed_block_tag(self, parse_until): def unclosed_block_tag(self, parse_until):
raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until)) raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
@ -358,57 +336,7 @@ class Parser(object):
if filter_name in self.filters: if filter_name in self.filters:
return self.filters[filter_name] return self.filters[filter_name]
else: else:
raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name 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(object): class TokenParser(object):
""" """
@ -426,7 +354,7 @@ class TokenParser(object):
def top(self): def top(self):
"Overload this method to do the actual parsing and return the result." "Overload this method to do the actual parsing and return the result."
raise NotImplemented raise NotImplementedError()
def more(self): def more(self):
"Returns True if there is more stuff in the tag." "Returns True if there is more stuff in the tag."
@ -435,7 +363,7 @@ class TokenParser(object):
def back(self): def back(self):
"Undoes the last microparser. Use this for lookahead and backtracking." "Undoes the last microparser. Use this for lookahead and backtracking."
if not len(self.backout): if not len(self.backout):
raise TemplateSyntaxError, "back called without some previous parsing" raise TemplateSyntaxError("back called without some previous parsing")
self.pointer = self.backout.pop() self.pointer = self.backout.pop()
def tag(self): def tag(self):
@ -443,7 +371,7 @@ class TokenParser(object):
subject = self.subject subject = self.subject
i = self.pointer i = self.pointer
if i >= len(subject): if i >= len(subject):
raise TemplateSyntaxError, "expected another tag, found end of string: %s" % subject raise TemplateSyntaxError("expected another tag, found end of string: %s" % subject)
p = i p = i
while i < len(subject) and subject[i] not in (' ', '\t'): while i < len(subject) and subject[i] not in (' ', '\t'):
i += 1 i += 1
@ -459,14 +387,14 @@ class TokenParser(object):
subject = self.subject subject = self.subject
i = self.pointer i = self.pointer
if i >= len(subject): if i >= len(subject):
raise TemplateSyntaxError, "Searching for value. Expected another value but found end of string: %s" % subject raise TemplateSyntaxError("Searching for value. Expected another value but found end of string: %s" % subject)
if subject[i] in ('"', "'"): if subject[i] in ('"', "'"):
p = i p = i
i += 1 i += 1
while i < len(subject) and subject[i] != subject[p]: while i < len(subject) and subject[i] != subject[p]:
i += 1 i += 1
if i >= len(subject): if i >= len(subject):
raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % (i, subject) raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
i += 1 i += 1
res = subject[p:i] res = subject[p:i]
while i < len(subject) and subject[i] in (' ', '\t'): while i < len(subject) and subject[i] in (' ', '\t'):
@ -483,7 +411,7 @@ class TokenParser(object):
while i < len(subject) and subject[i] != c: while i < len(subject) and subject[i] != c:
i += 1 i += 1
if i >= len(subject): if i >= len(subject):
raise TemplateSyntaxError, "Searching for value. Unexpected end of string in column %d: %s" % (i, subject) raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
i += 1 i += 1
s = subject[p:i] s = subject[p:i]
while i < len(subject) and subject[i] in (' ', '\t'): while i < len(subject) and subject[i] in (' ', '\t'):
@ -542,8 +470,8 @@ class FilterExpression(object):
for match in matches: for match in matches:
start = match.start() start = match.start()
if upto != start: if upto != start:
raise TemplateSyntaxError, "Could not parse some characters: %s|%s|%s" % \ raise TemplateSyntaxError("Could not parse some characters: %s|%s|%s" % \
(token[:upto], token[upto:start], token[start:]) (token[:upto], token[upto:start], token[start:]))
if var == None: if var == None:
var, constant, i18n_constant = match.group("var", "constant", "i18n_constant") var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
if i18n_constant: if i18n_constant:
@ -552,9 +480,9 @@ class FilterExpression(object):
var = '"%s"' % constant.replace(r'\"', '"') var = '"%s"' % constant.replace(r'\"', '"')
upto = match.end() upto = match.end()
if var == None: if var == None:
raise TemplateSyntaxError, "Could not find variable at start of %s" % token raise TemplateSyntaxError("Could not find variable at start of %s" % token)
elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_': elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var raise TemplateSyntaxError("Variables and attributes may not begin with underscores: '%s'" % var)
else: else:
filter_name = match.group("filter_name") filter_name = match.group("filter_name")
args = [] args = []
@ -570,7 +498,7 @@ class FilterExpression(object):
filters.append( (filter_func,args)) filters.append( (filter_func,args))
upto = match.end() upto = match.end()
if upto != len(token): if upto != len(token):
raise TemplateSyntaxError, "Could not parse the remainder: '%s' from '%s'" % (token[upto:], token) raise TemplateSyntaxError("Could not parse the remainder: '%s' from '%s'" % (token[upto:], token))
self.filters = filters self.filters = filters
self.var = Variable(var) self.var = Variable(var)
@ -627,7 +555,7 @@ class FilterExpression(object):
provided.pop(0) provided.pop(0)
except IndexError: except IndexError:
# Not enough # Not enough
raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen) raise TemplateSyntaxError("%s requires %d arguments, %d provided" % (name, len(nondefs), plen))
# Defaults can be overridden. # Defaults can be overridden.
defaults = defaults and list(defaults) or [] defaults = defaults and list(defaults) or []
@ -636,7 +564,7 @@ class FilterExpression(object):
defaults.pop(0) defaults.pop(0)
except IndexError: except IndexError:
# Too many. # Too many.
raise TemplateSyntaxError, "%s requires %d arguments, %d provided" % (name, len(nondefs), plen) raise TemplateSyntaxError("%s requires %d arguments, %d provided" % (name, len(nondefs), plen))
return True return True
args_check = staticmethod(args_check) args_check = staticmethod(args_check)
@ -816,22 +744,6 @@ class NodeList(list):
def render_node(self, node, context): def render_node(self, node, context):
return node.render(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, e:
from sys import exc_info
wrapped = TemplateSyntaxError('Caught an exception while rendering: %s' % e)
wrapped.source = node.source
wrapped.exc_info = exc_info()
raise wrapped
return result
class TextNode(Node): class TextNode(Node):
def __init__(self, s): def __init__(self, s):
self.s = s self.s = s
@ -861,21 +773,6 @@ class VariableNode(Node):
else: else:
return force_unicode(output) return force_unicode(output)
class DebugVariableNode(VariableNode):
def render(self, context):
try:
output = force_unicode(self.filter_expression.resolve(context))
except TemplateSyntaxError, e:
if not hasattr(e, 'source'):
e.source = self.source
raise
except UnicodeDecodeError:
return ''
if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData):
return escape(output)
else:
return output
def generic_tag_compiler(params, defaults, name, node_class, parser, token): def generic_tag_compiler(params, defaults, name, node_class, parser, token):
"Returns a template.Node subclass." "Returns a template.Node subclass."
bits = token.split_contents()[1:] bits = token.split_contents()[1:]
@ -887,7 +784,7 @@ def generic_tag_compiler(params, defaults, name, node_class, parser, token):
message = "%s takes %s arguments" % (name, bmin) message = "%s takes %s arguments" % (name, bmin)
else: else:
message = "%s takes between %s and %s arguments" % (name, bmin, bmax) message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
raise TemplateSyntaxError, message raise TemplateSyntaxError(message)
return node_class(bits) return node_class(bits)
class Library(object): class Library(object):
@ -913,7 +810,7 @@ class Library(object):
self.tags[name] = compile_function self.tags[name] = compile_function
return compile_function return compile_function
else: else:
raise InvalidTemplateLibrary, "Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function) raise InvalidTemplateLibrary("Unsupported arguments to Library.tag: (%r, %r)", (name, compile_function))
def tag_function(self,func): def tag_function(self,func):
self.tags[getattr(func, "_decorated_function", func).__name__] = func self.tags[getattr(func, "_decorated_function", func).__name__] = func
@ -937,7 +834,7 @@ class Library(object):
self.filters[name] = filter_func self.filters[name] = filter_func
return filter_func return filter_func
else: else:
raise InvalidTemplateLibrary, "Unsupported arguments to Library.filter: (%r, %r)", (name, filter_func) raise InvalidTemplateLibrary("Unsupported arguments to Library.filter: (%r, %r)", (name, filter_func))
def filter_function(self, func): def filter_function(self, func):
self.filters[getattr(func, "_decorated_function", func).__name__] = func self.filters[getattr(func, "_decorated_function", func).__name__] = func
@ -966,7 +863,7 @@ class Library(object):
if params[0] == 'context': if params[0] == 'context':
params = params[1:] params = params[1:]
else: else:
raise TemplateSyntaxError, "Any tag function decorated with takes_context=True must have a first argument of 'context'" raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
class InclusionNode(Node): class InclusionNode(Node):
def __init__(self, vars_to_resolve): def __init__(self, vars_to_resolve):
@ -1003,12 +900,12 @@ def get_library(module_name):
try: try:
mod = __import__(module_name, {}, {}, ['']) mod = __import__(module_name, {}, {}, [''])
except ImportError, e: except ImportError, e:
raise InvalidTemplateLibrary, "Could not load template library from %s, %s" % (module_name, e) raise InvalidTemplateLibrary("Could not load template library from %s, %s" % (module_name, e))
try: try:
lib = mod.register lib = mod.register
libraries[module_name] = lib libraries[module_name] = lib
except AttributeError: except AttributeError:
raise InvalidTemplateLibrary, "Template library %s does not have a variable named 'register'" % module_name raise InvalidTemplateLibrary("Template library %s does not have a variable named 'register'" % module_name)
return lib return lib
def add_to_builtins(module_name): def add_to_builtins(module_name):

View File

@ -9,7 +9,6 @@ class ContextPopException(Exception):
class Context(object): class Context(object):
"A stack container for variable context" "A stack container for variable context"
def __init__(self, dict_=None, autoescape=True): def __init__(self, dict_=None, autoescape=True):
dict_ = dict_ or {} dict_ = dict_ or {}
self.dicts = [dict_] self.dicts = [dict_]
@ -78,11 +77,11 @@ def get_standard_processors():
try: try:
mod = __import__(module, {}, {}, [attr]) mod = __import__(module, {}, {}, [attr])
except ImportError, e: except ImportError, e:
raise ImproperlyConfigured, 'Error importing request processor module %s: "%s"' % (module, e) raise ImproperlyConfigured('Error importing request processor module %s: "%s"' % (module, e))
try: try:
func = getattr(mod, attr) func = getattr(mod, attr)
except AttributeError: except AttributeError:
raise ImproperlyConfigured, 'Module "%s" does not define a "%s" callable request processor' % (module, attr) raise ImproperlyConfigured('Module "%s" does not define a "%s" callable request processor' % (module, attr))
processors.append(func) processors.append(func)
_standard_context_processors = tuple(processors) _standard_context_processors = tuple(processors)
return _standard_context_processors return _standard_context_processors
@ -102,4 +101,3 @@ class RequestContext(Context):
processors = tuple(processors) processors = tuple(processors)
for processor in get_standard_processors() + processors: for processor in get_standard_processors() + processors:
self.update(processor(request)) self.update(processor(request))

97
django/template/debug.py Normal file
View File

@ -0,0 +1,97 @@
from django.template import Lexer, Parser, tag_re, NodeList, VariableNode, TemplateSyntaxError
from django.utils.encoding import force_unicode
from django.utils.html import escape
from django.utils.safestring import SafeData, EscapeData
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"
result, upto = [], 0
for match in tag_re.finditer(self.template_string):
start, end = match.span()
if start > upto:
result.append(self.create_token(self.template_string[upto:start], (upto, start), False))
upto = start
result.append(self.create_token(self.template_string[start:end], (start, end), True))
upto = end
last_bit = self.template_string[upto:]
if last_bit:
result.append(self.create_token(last_bit, (upto, upto + len(last_bit)), False))
return result
def create_token(self, token_string, source, in_tag):
token = super(DebugLexer, self).create_token(token_string, in_tag)
token.source = self.origin, source
return token
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
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, e:
from sys import exc_info
wrapped = TemplateSyntaxError('Caught an exception while rendering: %s' % e)
wrapped.source = node.source
wrapped.exc_info = exc_info()
raise wrapped
return result
class DebugVariableNode(VariableNode):
def render(self, context):
try:
output = force_unicode(self.filter_expression.resolve(context))
except TemplateSyntaxError, e:
if not hasattr(e, 'source'):
e.source = self.source
raise
except UnicodeDecodeError:
return ''
if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData):
return escape(output)
else:
return output

View File

@ -433,7 +433,7 @@ def first(value):
return value[0] return value[0]
except IndexError: except IndexError:
return u'' return u''
first.is_safe = True first.is_safe = False
def join(value, arg): def join(value, arg):
"""Joins a list with a string, like Python's ``str.join(list)``.""" """Joins a list with a string, like Python's ``str.join(list)``."""
@ -449,6 +449,14 @@ def join(value, arg):
return data return data
join.is_safe = True join.is_safe = True
def last(value):
"Returns the last item in a list"
try:
return value[-1]
except IndexError:
return u''
last.is_safe = True
def length(value): def length(value):
"""Returns the length of the value - useful for lists.""" """Returns the length of the value - useful for lists."""
return len(value) return len(value)
@ -800,6 +808,7 @@ register.filter(force_escape)
register.filter(get_digit) register.filter(get_digit)
register.filter(iriencode) register.filter(iriencode)
register.filter(join) register.filter(join)
register.filter(last)
register.filter(length) register.filter(length)
register.filter(length_is) register.filter(length_is)
register.filter(linebreaks) register.filter(linebreaks)

View File

@ -84,19 +84,16 @@ class FirstOfNode(Node):
return u'' return u''
class ForNode(Node): class ForNode(Node):
def __init__(self, loopvars, sequence, reversed, nodelist_loop): def __init__(self, loopvars, sequence, is_reversed, nodelist_loop):
self.loopvars, self.sequence = loopvars, sequence self.loopvars, self.sequence = loopvars, sequence
self.reversed = reversed self.is_reversed = is_reversed
self.nodelist_loop = nodelist_loop self.nodelist_loop = nodelist_loop
def __repr__(self): def __repr__(self):
if self.reversed: reversed_text = self.is_reversed and ' reversed' or ''
reversed = ' reversed'
else:
reversed = ''
return "<For Node: for %s in %s, tail_len: %d%s>" % \ return "<For Node: for %s in %s, tail_len: %d%s>" % \
(', '.join(self.loopvars), self.sequence, len(self.nodelist_loop), (', '.join(self.loopvars), self.sequence, len(self.nodelist_loop),
reversed) reversed_text)
def __iter__(self): def __iter__(self):
for node in self.nodelist_loop: for node in self.nodelist_loop:
@ -125,22 +122,23 @@ class ForNode(Node):
if not hasattr(values, '__len__'): if not hasattr(values, '__len__'):
values = list(values) values = list(values)
len_values = len(values) len_values = len(values)
if self.reversed: if self.is_reversed:
values = reversed(values) values = reversed(values)
unpack = len(self.loopvars) > 1 unpack = len(self.loopvars) > 1
# Create a forloop value in the context. We'll update counters on each
# iteration just below.
loop_dict = context['forloop'] = {'parentloop': parentloop}
for i, item in enumerate(values): for i, item in enumerate(values):
context['forloop'] = {
# Shortcuts for current loop iteration number. # Shortcuts for current loop iteration number.
'counter0': i, loop_dict['counter0'] = i
'counter': i+1, loop_dict['counter'] = i+1
# Reverse counter iteration numbers. # Reverse counter iteration numbers.
'revcounter': len_values - i, loop_dict['revcounter'] = len_values - i
'revcounter0': len_values - i - 1, loop_dict['revcounter0'] = len_values - i - 1
# Boolean values designating first and last times through loop. # Boolean values designating first and last times through loop.
'first': (i == 0), loop_dict['first'] = (i == 0)
'last': (i == len_values - 1), loop_dict['last'] = (i == len_values - 1)
'parentloop': parentloop,
}
if unpack: if unpack:
# If there are multiple loop variables, unpack the item into # If there are multiple loop variables, unpack the item into
# them. # them.
@ -619,8 +617,8 @@ def do_for(parser, token):
raise TemplateSyntaxError("'for' statements should have at least four" raise TemplateSyntaxError("'for' statements should have at least four"
" words: %s" % token.contents) " words: %s" % token.contents)
reversed = bits[-1] == 'reversed' is_reversed = bits[-1] == 'reversed'
in_index = reversed and -3 or -2 in_index = is_reversed and -3 or -2
if bits[in_index] != 'in': if bits[in_index] != 'in':
raise TemplateSyntaxError("'for' statements should use the format" raise TemplateSyntaxError("'for' statements should use the format"
" 'for x in y': %s" % token.contents) " 'for x in y': %s" % token.contents)
@ -634,7 +632,7 @@ def do_for(parser, token):
sequence = parser.compile_filter(bits[in_index+1]) sequence = parser.compile_filter(bits[in_index+1])
nodelist_loop = parser.parse(('endfor',)) nodelist_loop = parser.parse(('endfor',))
parser.delete_first_token() parser.delete_first_token()
return ForNode(loopvars, sequence, reversed, nodelist_loop) return ForNode(loopvars, sequence, is_reversed, nodelist_loop)
do_for = register.tag("for", do_for) do_for = register.tag("for", do_for)
def do_ifequal(parser, token, negate): def do_ifequal(parser, token, negate):

View File

@ -507,7 +507,7 @@ Exception Type: {{ exception_type|escape }} at {{ request.path|escape }}
Exception Value: {{ exception_value|escape }} Exception Value: {{ exception_value|escape }}
</textarea> </textarea>
<br><br> <br><br>
<input type="submit" value="Share this traceback on public Web site"> <input type="submit" value="Share this traceback on a public Web site">
</div> </div>
</form> </form>
</div> </div>

View File

@ -716,8 +716,8 @@ in Python package syntax, e.g. ``mysite.settings``. If this isn't provided,
``django-admin.py`` will use the ``DJANGO_SETTINGS_MODULE`` environment ``django-admin.py`` will use the ``DJANGO_SETTINGS_MODULE`` environment
variable. variable.
Note that this option is unnecessary in ``manage.py``, because it takes care of Note that this option is unnecessary in ``manage.py``, because it uses
setting ``DJANGO_SETTINGS_MODULE`` for you. ``settings.py`` from the current project by default.
Extra niceties Extra niceties
============== ==============

View File

@ -181,7 +181,7 @@ CASocialInsuranceNumberField
---------------------------- ----------------------------
A form field that validates input as a Canadian Social Insurance Number (SIN). A form field that validates input as a Canadian Social Insurance Number (SIN).
A valid number must have the format XXX-XXX-XXXX and pass a `Luhn mod-10 A valid number must have the format XXX-XXX-XXX and pass a `Luhn mod-10
checksum`_. checksum`_.
.. _Luhn mod-10 checksum: http://en.wikipedia.org/wiki/Luhn_algorithm .. _Luhn mod-10 checksum: http://en.wikipedia.org/wiki/Luhn_algorithm

View File

@ -154,6 +154,17 @@ every incoming ``HttpRequest`` object. See `Authentication in Web requests`_.
.. _Authentication in Web requests: ../authentication/#authentication-in-web-requests .. _Authentication in Web requests: ../authentication/#authentication-in-web-requests
django.contrib.csrf.middleware.CsrfMiddleware
---------------------------------------------
**New in Django development version**
Adds protection against Cross Site Request Forgeries by adding hidden form
fields to POST forms and checking requests for the correct value. See the
`Cross Site Request Forgery protection documentation`_.
.. _`Cross Site Request Forgery protection documentation`: ../csrf/
django.middleware.transaction.TransactionMiddleware django.middleware.transaction.TransactionMiddleware
--------------------------------------------------- ---------------------------------------------------

View File

@ -97,7 +97,7 @@ Hooking into the current site from views
---------------------------------------- ----------------------------------------
On a lower level, you can use the sites framework in your Django views to do On a lower level, you can use the sites framework in your Django views to do
particular things based on what site in which the view is being called. particular things based on the site in which the view is being called.
For example:: For example::
from django.conf import settings from django.conf import settings
@ -330,13 +330,13 @@ Here's how Django uses the sites framework:
retrieving flatpages to display. retrieving flatpages to display.
* In the `syndication framework`_, the templates for ``title`` and * In the `syndication framework`_, the templates for ``title`` and
``description`` automatically have access to a variable ``{{{ site }}}``, ``description`` automatically have access to a variable ``{{ site }}``,
which is the ``Site`` object representing the current site. Also, the which is the ``Site`` object representing the current site. Also, the
hook for providing item URLs will use the ``domain`` from the current hook for providing item URLs will use the ``domain`` from the current
``Site`` object if you don't specify a fully-qualified domain. ``Site`` object if you don't specify a fully-qualified domain.
* In the `authentication framework`_, the ``django.contrib.auth.views.login`` * In the `authentication framework`_, the ``django.contrib.auth.views.login``
view passes the current ``Site`` name to the template as ``{{{ site_name }}}``. view passes the current ``Site`` name to the template as ``{{ site_name }}``.
* The shortcut view (``django.views.defaults.shortcut``) uses the domain of * The shortcut view (``django.views.defaults.shortcut``) uses the domain of
the current ``Site`` object when calculating an object's URL. the current ``Site`` object when calculating an object's URL.

View File

@ -201,7 +201,7 @@ the feed.
An example makes this clear. Here's the code for these beat-specific feeds:: An example makes this clear. Here's the code for these beat-specific feeds::
from django.contrib.syndication import FeedDoesNotExist from django.contrib.syndication.feeds import FeedDoesNotExist
class BeatFeed(Feed): class BeatFeed(Feed):
def get_object(self, bits): def get_object(self, bits):

View File

@ -1230,7 +1230,7 @@ addslashes
Adds slashes before quotes. Useful for escaping strings in CSV, for example. Adds slashes before quotes. Useful for escaping strings in CSV, for example.
**New in Django development version**: for escaping data in JavaScript strings, **New in Django development version**: for escaping data in JavaScript strings,
use the `escapejs` filter instead. use the `escapejs`_ filter instead.
capfirst capfirst
~~~~~~~~ ~~~~~~~~
@ -1403,6 +1403,11 @@ join
Joins a list with a string, like Python's ``str.join(list)``. Joins a list with a string, like Python's ``str.join(list)``.
last
~~~~
Returns the last item in a list.
length length
~~~~~~ ~~~~~~

View File

@ -8,6 +8,13 @@ class Square(models.Model):
def __unicode__(self): def __unicode__(self):
return "%s ** 2 == %s" % (self.root, self.square) return "%s ** 2 == %s" % (self.root, self.square)
class Person(models.Model):
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=20)
def __unicode__(self):
return u'%s %s' % (self.first_name, self.last_name)
if connection.features.uses_case_insensitive_names: if connection.features.uses_case_insensitive_names:
t_convert = lambda x: x.upper() t_convert = lambda x: x.upper()
else: else:
@ -32,4 +39,25 @@ __test__ = {'API_TESTS': """
>>> Square.objects.count() >>> Square.objects.count()
11 11
#6254: fetchone, fetchmany, fetchall return strings as unicode objects
>>> Person(first_name="John", last_name="Doe").save()
>>> Person(first_name="Jane", last_name="Doe").save()
>>> Person(first_name="Mary", last_name="Agnelline").save()
>>> Person(first_name="Peter", last_name="Parker").save()
>>> Person(first_name="Clark", last_name="Kent").save()
>>> opts2 = Person._meta
>>> f3, f4 = opts2.get_field('first_name'), opts2.get_field('last_name')
>>> query2 = ('SELECT %s, %s FROM %s ORDER BY %s'
... % (qn(f3.column), qn(f4.column), t_convert(opts2.db_table),
... qn(f3.column)))
>>> cursor.execute(query2) and None or None
>>> cursor.fetchone()
(u'Clark', u'Kent')
>>> list(cursor.fetchmany(2))
[(u'Jane', u'Doe'), (u'John', u'Doe')]
>>> list(cursor.fetchall())
[(u'Mary', u'Agnelline'), (u'Peter', u'Parker')]
"""} """}

View File

@ -213,13 +213,13 @@ u'046-454-286'
>>> f.clean('046-454-287') >>> f.clean('046-454-287')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'] ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format.']
>>> f.clean('046 454 286') >>> f.clean('046 454 286')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'] ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format.']
>>> f.clean('046-44-286') >>> f.clean('046-44-286')
Traceback (most recent call last): Traceback (most recent call last):
... ...
ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXXX format.'] ValidationError: [u'Enter a valid Canadian Social Insurance number in XXX-XXX-XXX format.']
""" """

View File

@ -0,0 +1 @@
# models.py file for tests to run.

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import unittest
from django.test import TestCase from django.test import TestCase
from django.http import HttpRequest from django.http import HttpRequest
from django.middleware.common import CommonMiddleware from django.middleware.common import CommonMiddleware
@ -19,7 +17,7 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_have_slash(self): def test_append_slash_have_slash(self):
""" """
tests that urls with slashes go unmolested Tests that URLs with slashes go unmolested.
""" """
settings.APPEND_SLASH = True settings.APPEND_SLASH = True
request = self._get_request('slash/') request = self._get_request('slash/')
@ -27,7 +25,7 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_slashless_resource(self): def test_append_slash_slashless_resource(self):
""" """
tests that matches to explicit slashless urls go unmolested Tests that matches to explicit slashless URLs go unmolested.
""" """
settings.APPEND_SLASH = True settings.APPEND_SLASH = True
request = self._get_request('noslash') request = self._get_request('noslash')
@ -35,7 +33,7 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_slashless_unknown(self): def test_append_slash_slashless_unknown(self):
""" """
tests that APPEND_SLASH doesn't redirect to unknown resources Tests that APPEND_SLASH doesn't redirect to unknown resources.
""" """
settings.APPEND_SLASH = True settings.APPEND_SLASH = True
request = self._get_request('unknown') request = self._get_request('unknown')
@ -43,7 +41,7 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_redirect(self): def test_append_slash_redirect(self):
""" """
tests that APPEND_SLASH redirects slashless urls to a valid pattern Tests that APPEND_SLASH redirects slashless URLs to a valid pattern.
""" """
settings.APPEND_SLASH = True settings.APPEND_SLASH = True
request = self._get_request('slash') request = self._get_request('slash')
@ -53,9 +51,9 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_no_redirect_on_POST_in_DEBUG(self): def test_append_slash_no_redirect_on_POST_in_DEBUG(self):
""" """
tests that while in debug mode, an exception is raised with a warning Tests that while in debug mode, an exception is raised with a warning
when a failed attempt is made to POST to an url which would normally be when a failed attempt is made to POST to an URL which would normally be
redirected to a slashed version redirected to a slashed version.
""" """
settings.APPEND_SLASH = True settings.APPEND_SLASH = True
settings.DEBUG = True settings.DEBUG = True
@ -68,11 +66,12 @@ class CommonMiddlewareTest(TestCase):
try: try:
CommonMiddleware().process_request(request) CommonMiddleware().process_request(request)
except RuntimeError, e: except RuntimeError, e:
self.assertTrue('end in a slash' in str(e)) self.failUnless('end in a slash' in str(e))
settings.DEBUG = False
def test_append_slash_disabled(self): def test_append_slash_disabled(self):
""" """
tests disabling append slash functionality Tests disabling append slash functionality.
""" """
settings.APPEND_SLASH = False settings.APPEND_SLASH = False
request = self._get_request('slash') request = self._get_request('slash')
@ -80,8 +79,8 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_quoted(self): def test_append_slash_quoted(self):
""" """
tests that urls which require quoting are redirected to their slash Tests that URLs which require quoting are redirected to their slash
version ok version ok.
""" """
settings.APPEND_SLASH = True settings.APPEND_SLASH = True
request = self._get_request('needsquoting#') request = self._get_request('needsquoting#')
@ -90,4 +89,3 @@ class CommonMiddlewareTest(TestCase):
self.assertEquals( self.assertEquals(
r['Location'], r['Location'],
'http://testserver/middleware/needsquoting%23/') 'http://testserver/middleware/needsquoting%23/')

View File

@ -179,6 +179,9 @@ def get_filter_tests():
'filter-first01': ('{{ a|first }} {{ b|first }}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&amp;b a&b"), 'filter-first01': ('{{ a|first }} {{ b|first }}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&amp;b a&b"),
'filter-first02': ('{% autoescape off %}{{ a|first }} {{ b|first }}{% endautoescape %}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"), 'filter-first02': ('{% autoescape off %}{{ a|first }} {{ b|first }}{% endautoescape %}', {"a": ["a&b", "x"], "b": [mark_safe("a&b"), "x"]}, "a&b a&b"),
'filter-last01': ('{{ a|last }} {{ b|last }}', {"a": ["x", "a&b"], "b": ["x", mark_safe("a&b")]}, "a&amp;b a&b"),
'filter-last02': ('{% autoescape off %}{{ a|last }} {{ b|last }}{% endautoescape %}', {"a": ["x", "a&b"], "b": ["x", mark_safe("a&b")]}, "a&b a&b"),
'filter-random01': ('{{ a|random }} {{ b|random }}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&amp;b a&b"), 'filter-random01': ('{{ a|random }} {{ b|random }}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&amp;b a&b"),
'filter-random02': ('{% autoescape off %}{{ a|random }} {{ b|random }}{% endautoescape %}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"), 'filter-random02': ('{% autoescape off %}{{ a|random }} {{ b|random }}{% endautoescape %}', {"a": ["a&b", "a&b"], "b": [mark_safe("a&b"), mark_safe("a&b")]}, "a&b a&b"),

View File

@ -441,6 +441,8 @@ class Templates(unittest.TestCase):
'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"), 'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"),
'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"), 'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"),
'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"), 'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"),
'for-tag-vars05': ("{% for val in values %}{% if forloop.first %}f{% else %}x{% endif %}{% endfor %}", {"values": [6, 6, 6]}, "fxx"),
'for-tag-vars06': ("{% for val in values %}{% if forloop.last %}l{% else %}x{% endif %}{% endfor %}", {"values": [6, 6, 6]}, "xxl"),
'for-tag-unpack01': ("{% for key,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), 'for-tag-unpack01': ("{% for key,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
'for-tag-unpack03': ("{% for key, value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), 'for-tag-unpack03': ("{% for key, value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),
'for-tag-unpack04': ("{% for key , value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), 'for-tag-unpack04': ("{% for key , value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"),