summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--django/contrib/admin/media/js/urlify.js2
-rw-r--r--django/contrib/localflavor/ca/forms.py4
-rw-r--r--django/contrib/sessions/backends/db.py16
-rw-r--r--django/contrib/sessions/backends/file.py20
-rw-r--r--django/core/mail.py50
-rw-r--r--django/core/management/sql.py2
-rw-r--r--django/db/backends/oracle/base.py23
-rw-r--r--django/template/__init__.py171
-rw-r--r--django/template/context.py6
-rw-r--r--django/template/debug.py97
-rw-r--r--django/template/defaultfilters.py11
-rw-r--r--django/template/defaulttags.py44
-rw-r--r--django/views/debug.py2
-rw-r--r--docs/django-admin.txt4
-rw-r--r--docs/localflavor.txt2
-rw-r--r--docs/middleware.txt11
-rw-r--r--docs/sites.txt6
-rw-r--r--docs/syndication_feeds.txt2
-rw-r--r--docs/templates.txt7
-rw-r--r--tests/regressiontests/backends/models.py28
-rw-r--r--tests/regressiontests/forms/localflavor/ca.py6
-rw-r--r--tests/regressiontests/middleware/models.py1
-rw-r--r--tests/regressiontests/middleware/tests.py28
-rw-r--r--tests/regressiontests/templates/filters.py3
-rw-r--r--tests/regressiontests/templates/tests.py2
25 files changed, 301 insertions, 247 deletions
diff --git a/django/contrib/admin/media/js/urlify.js b/django/contrib/admin/media/js/urlify.js
index 63e626d884..d8f2549e27 100644
--- a/django/contrib/admin/media/js/urlify.js
+++ b/django/contrib/admin/media/js/urlify.js
@@ -5,7 +5,7 @@ var LATIN_MAP = {
'O', 'Ő': 'O', 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U',
'Ý': 'Y', 'Þ': 'TH', 'ß': 'ss', 'à':'a', 'á':'a', 'â': 'a', 'ã': 'a', 'ä':
'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',
'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', 'ÿ': 'y'
}
diff --git a/django/contrib/localflavor/ca/forms.py b/django/contrib/localflavor/ca/forms.py
index daeff58acf..b40dba8335 100644
--- a/django/contrib/localflavor/ca/forms.py
+++ b/django/contrib/localflavor/ca/forms.py
@@ -80,12 +80,12 @@ class CASocialInsuranceNumberField(Field):
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"
See: http://en.wikipedia.org/wiki/Social_Insurance_Number
"""
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):
diff --git a/django/contrib/sessions/backends/db.py b/django/contrib/sessions/backends/db.py
index d1496d63bf..0f79d9ee1a 100644
--- a/django/contrib/sessions/backends/db.py
+++ b/django/contrib/sessions/backends/db.py
@@ -10,40 +10,42 @@ class SessionStore(SessionBase):
"""
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
-
+
def load(self):
try:
s = Session.objects.get(
- session_key = self.session_key,
+ session_key = self.session_key,
expire_date__gt=datetime.datetime.now()
)
return self.decode(s.session_data)
except (Session.DoesNotExist, SuspiciousOperation):
-
+
# Create a new session_key for extra security.
self.session_key = self._get_new_session_key()
self._session_cache = {}
# Save immediately to minimize collision
self.save()
+ # Ensure the user is notified via a new cookie.
+ self.modified = True
return {}
-
+
def exists(self, session_key):
try:
Session.objects.get(session_key=session_key)
except Session.DoesNotExist:
return False
return True
-
+
def save(self):
Session.objects.create(
session_key = self.session_key,
session_data = self.encode(self._session),
expire_date = datetime.datetime.now() + datetime.timedelta(seconds=settings.SESSION_COOKIE_AGE)
)
-
+
def delete(self, session_key):
try:
Session.objects.get(session_key=session_key).delete()
except Session.DoesNotExist:
- pass \ No newline at end of file
+ pass
diff --git a/django/contrib/sessions/backends/file.py b/django/contrib/sessions/backends/file.py
index a8c3c69b10..cd3e3d9c75 100644
--- a/django/contrib/sessions/backends/file.py
+++ b/django/contrib/sessions/backends/file.py
@@ -10,31 +10,31 @@ class SessionStore(SessionBase):
"""
def __init__(self, session_key=None):
self.storage_path = getattr(settings, "SESSION_FILE_PATH", tempfile.gettempdir())
-
+
# Make sure the storage path is valid.
if not os.path.isdir(self.storage_path):
raise ImproperlyConfigured("The session storage path %r doesn't exist. "\
"Please set your SESSION_FILE_PATH setting "\
"to an existing directory in which Django "\
"can store session data." % self.storage_path)
-
- self.file_prefix = settings.SESSION_COOKIE_NAME
+
+ self.file_prefix = settings.SESSION_COOKIE_NAME
super(SessionStore, self).__init__(session_key)
-
+
def _key_to_file(self, session_key=None):
"""
Get the file associated with this session key.
"""
if session_key is None:
session_key = self.session_key
-
+
# Make sure we're not vulnerable to directory traversal. Session keys
# should always be md5s, so they should never contain directory components.
if os.path.sep in session_key:
raise SuspiciousOperation("Invalid characters (directory components) in session key")
-
+
return os.path.join(self.storage_path, self.file_prefix + session_key)
-
+
def load(self):
session_data = {}
try:
@@ -46,6 +46,8 @@ class SessionStore(SessionBase):
self._session_key = self._get_new_session_key()
self._session_cache = {}
self.save()
+ # Ensure the user is notified via a new cookie.
+ self.modified = True
finally:
session_file.close()
except(IOError):
@@ -66,12 +68,12 @@ class SessionStore(SessionBase):
if os.path.exists(self._key_to_file(session_key)):
return True
return False
-
+
def delete(self, session_key):
try:
os.unlink(self._key_to_file(session_key))
except OSError:
pass
-
+
def clean(self):
pass
diff --git a/django/core/mail.py b/django/core/mail.py
index e408393d96..153dcb6e63 100644
--- a/django/core/mail.py
+++ b/django/core/mail.py
@@ -67,42 +67,32 @@ def make_msgid(idstring=None):
class BadHeaderError(ValueError):
pass
+def forbid_multi_line_headers(name, val):
+ "Forbids multi-line headers, to prevent header injection."
+ 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)
+ return name, val
+
class SafeMIMEText(MIMEText):
def __setitem__(self, name, val):
- "Forbids multi-line headers, to prevent header injection."
- 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)
+ name, val = forbid_multi_line_headers(name, val)
MIMEText.__setitem__(self, name, val)
class SafeMIMEMultipart(MIMEMultipart):
def __setitem__(self, name, val):
- "Forbids multi-line headers, to prevent header injection."
- 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)
+ name, val = forbid_multi_line_headers(name, val)
MIMEMultipart.__setitem__(self, name, val)
class SMTPConnection(object):
diff --git a/django/core/management/sql.py b/django/core/management/sql.py
index 767a2dbbfb..15bffce26b 100644
--- a/django/core/management/sql.py
+++ b/django/core/management/sql.py
@@ -293,7 +293,7 @@ def sql_model_create(model, style, known_models=set()):
style.SQL_KEYWORD('NULL'))
for field_constraints in opts.unique_together:
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)) + ' (']
for i, line in enumerate(table_output): # Combine and add commas.
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index cd830413fc..70f5688d1a 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -413,6 +413,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
return self.connection is not None
def _cursor(self, settings):
+ cursor = None
if not self._valid_connection():
if len(settings.DATABASE_HOST.strip()) == 0:
settings.DATABASE_HOST = 'localhost'
@@ -422,16 +423,25 @@ class DatabaseWrapper(BaseDatabaseWrapper):
else:
conn_string = "%s/%s@%s" % (settings.DATABASE_USER, settings.DATABASE_PASSWORD, settings.DATABASE_NAME)
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:
self.oracle_version = int(self.connection.version.split('.')[0])
except ValueError:
pass
- cursor = FormatStylePlaceholderCursor(self.connection)
+ 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)
# Default arraysize of 1 is highly sub-optimal.
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
class FormatStylePlaceholderCursor(Database.Cursor):
@@ -506,7 +516,10 @@ class FormatStylePlaceholderCursor(Database.Cursor):
return Database.Cursor.executemany(self, query, new_param_list)
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):
if size is None:
diff --git a/django/template/__init__.py b/django/template/__init__.py
index 9b1868cfce..750c48988d 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -154,15 +154,12 @@ class StringOrigin(Origin):
class Template(object):
def __init__(self, template_string, origin=None, name='<Unknown Template>'):
- "Compilation stage"
try:
template_string = smart_unicode(template_string)
except UnicodeDecodeError:
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)
- # Could do some crazy stack-frame stuff to record where this string
- # came from...
self.nodelist = compile_string(template_string, origin)
self.name = name
@@ -177,13 +174,18 @@ class Template(object):
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())
+ if settings.TEMPLATE_DEBUG:
+ 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()
class Token(object):
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
def __str__(self):
@@ -200,7 +202,7 @@ class Lexer(object):
self.origin = origin
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
result = []
for bit in tag_re.split(self.template_string):
@@ -226,30 +228,6 @@ class Lexer(object):
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"
- 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):
def __init__(self, tokens):
self.tokens = tokens
@@ -319,17 +297,17 @@ class Parser(object):
def exit_command(self):
pass
- def error(self, token, msg ):
+ def error(self, token, msg):
return TemplateSyntaxError(msg)
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):
- raise self.error( token, "Empty block tag")
+ raise self.error(token, "Empty block tag")
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):
raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
@@ -358,57 +336,7 @@ class Parser(object):
if filter_name in self.filters:
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)
+ raise TemplateSyntaxError("Invalid filter: '%s'" % filter_name)
class TokenParser(object):
"""
@@ -426,7 +354,7 @@ class TokenParser(object):
def top(self):
"Overload this method to do the actual parsing and return the result."
- raise NotImplemented
+ raise NotImplementedError()
def more(self):
"Returns True if there is more stuff in the tag."
@@ -435,7 +363,7 @@ class TokenParser(object):
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"
+ raise TemplateSyntaxError("back called without some previous parsing")
self.pointer = self.backout.pop()
def tag(self):
@@ -443,7 +371,7 @@ class TokenParser(object):
subject = self.subject
i = self.pointer
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
while i < len(subject) and subject[i] not in (' ', '\t'):
i += 1
@@ -459,14 +387,14 @@ class TokenParser(object):
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
+ 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" % (i, subject)
+ raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
i += 1
res = subject[p:i]
while i < len(subject) and subject[i] in (' ', '\t'):
@@ -483,7 +411,7 @@ class TokenParser(object):
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" % (i, subject)
+ raise TemplateSyntaxError("Searching for value. Unexpected end of string in column %d: %s" % (i, subject))
i += 1
s = subject[p:i]
while i < len(subject) and subject[i] in (' ', '\t'):
@@ -542,8 +470,8 @@ class FilterExpression(object):
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:])
+ 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:
@@ -552,9 +480,9 @@ class FilterExpression(object):
var = '"%s"' % constant.replace(r'\"', '"')
upto = match.end()
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] == '_':
- 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:
filter_name = match.group("filter_name")
args = []
@@ -570,7 +498,7 @@ class FilterExpression(object):
filters.append( (filter_func,args))
upto = match.end()
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.var = Variable(var)
@@ -627,7 +555,7 @@ class FilterExpression(object):
provided.pop(0)
except IndexError:
# 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 = defaults and list(defaults) or []
@@ -636,7 +564,7 @@ class FilterExpression(object):
defaults.pop(0)
except IndexError:
# 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
args_check = staticmethod(args_check)
@@ -816,22 +744,6 @@ class NodeList(list):
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, 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):
def __init__(self, s):
self.s = s
@@ -861,21 +773,6 @@ class VariableNode(Node):
else:
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):
"Returns a template.Node subclass."
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)
else:
message = "%s takes between %s and %s arguments" % (name, bmin, bmax)
- raise TemplateSyntaxError, message
+ raise TemplateSyntaxError(message)
return node_class(bits)
class Library(object):
@@ -913,7 +810,7 @@ class Library(object):
self.tags[name] = compile_function
return compile_function
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):
self.tags[getattr(func, "_decorated_function", func).__name__] = func
@@ -937,7 +834,7 @@ class Library(object):
self.filters[name] = filter_func
return filter_func
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):
self.filters[getattr(func, "_decorated_function", func).__name__] = func
@@ -966,7 +863,7 @@ class Library(object):
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'"
+ 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):
@@ -1003,12 +900,12 @@ def get_library(module_name):
try:
mod = __import__(module_name, {}, {}, [''])
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:
lib = mod.register
libraries[module_name] = lib
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
def add_to_builtins(module_name):
diff --git a/django/template/context.py b/django/template/context.py
index 0e41a26618..6ba53f340b 100644
--- a/django/template/context.py
+++ b/django/template/context.py
@@ -9,7 +9,6 @@ class ContextPopException(Exception):
class Context(object):
"A stack container for variable context"
-
def __init__(self, dict_=None, autoescape=True):
dict_ = dict_ or {}
self.dicts = [dict_]
@@ -78,11 +77,11 @@ def get_standard_processors():
try:
mod = __import__(module, {}, {}, [attr])
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:
func = getattr(mod, attr)
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)
_standard_context_processors = tuple(processors)
return _standard_context_processors
@@ -102,4 +101,3 @@ class RequestContext(Context):
processors = tuple(processors)
for processor in get_standard_processors() + processors:
self.update(processor(request))
-
diff --git a/django/template/debug.py b/django/template/debug.py
new file mode 100644
index 0000000000..008059c28f
--- /dev/null
+++ b/django/template/debug.py
@@ -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
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index 5cee0ab244..ddd48146f2 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -433,7 +433,7 @@ def first(value):
return value[0]
except IndexError:
return u''
-first.is_safe = True
+first.is_safe = False
def join(value, arg):
"""Joins a list with a string, like Python's ``str.join(list)``."""
@@ -449,6 +449,14 @@ def join(value, arg):
return data
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):
"""Returns the length of the value - useful for lists."""
return len(value)
@@ -800,6 +808,7 @@ register.filter(force_escape)
register.filter(get_digit)
register.filter(iriencode)
register.filter(join)
+register.filter(last)
register.filter(length)
register.filter(length_is)
register.filter(linebreaks)
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index 813e5dbed6..e5a8e6620b 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -84,19 +84,16 @@ class FirstOfNode(Node):
return u''
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.reversed = reversed
+ self.is_reversed = is_reversed
self.nodelist_loop = nodelist_loop
def __repr__(self):
- if self.reversed:
- reversed = ' reversed'
- else:
- reversed = ''
+ reversed_text = self.is_reversed and ' reversed' or ''
return "<For Node: for %s in %s, tail_len: %d%s>" % \
(', '.join(self.loopvars), self.sequence, len(self.nodelist_loop),
- reversed)
+ reversed_text)
def __iter__(self):
for node in self.nodelist_loop:
@@ -125,22 +122,23 @@ class ForNode(Node):
if not hasattr(values, '__len__'):
values = list(values)
len_values = len(values)
- if self.reversed:
+ if self.is_reversed:
values = reversed(values)
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):
- 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,
- }
+ # Shortcuts for current loop iteration number.
+ loop_dict['counter0'] = i
+ loop_dict['counter'] = i+1
+ # Reverse counter iteration numbers.
+ loop_dict['revcounter'] = len_values - i
+ loop_dict['revcounter0'] = len_values - i - 1
+ # Boolean values designating first and last times through loop.
+ loop_dict['first'] = (i == 0)
+ loop_dict['last'] = (i == len_values - 1)
+
if unpack:
# If there are multiple loop variables, unpack the item into
# them.
@@ -619,8 +617,8 @@ def do_for(parser, token):
raise TemplateSyntaxError("'for' statements should have at least four"
" words: %s" % token.contents)
- reversed = bits[-1] == 'reversed'
- in_index = reversed and -3 or -2
+ is_reversed = bits[-1] == 'reversed'
+ in_index = is_reversed and -3 or -2
if bits[in_index] != 'in':
raise TemplateSyntaxError("'for' statements should use the format"
" 'for x in y': %s" % token.contents)
@@ -634,7 +632,7 @@ def do_for(parser, token):
sequence = parser.compile_filter(bits[in_index+1])
nodelist_loop = parser.parse(('endfor',))
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)
def do_ifequal(parser, token, negate):
diff --git a/django/views/debug.py b/django/views/debug.py
index 18a396d3a6..e3be2a7dcd 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -507,7 +507,7 @@ Exception Type: {{ exception_type|escape }} at {{ request.path|escape }}
Exception Value: {{ exception_value|escape }}
</textarea>
<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>
</form>
</div>
diff --git a/docs/django-admin.txt b/docs/django-admin.txt
index fe43f9586c..21821ab2e9 100644
--- a/docs/django-admin.txt
+++ b/docs/django-admin.txt
@@ -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
variable.
-Note that this option is unnecessary in ``manage.py``, because it takes care of
-setting ``DJANGO_SETTINGS_MODULE`` for you.
+Note that this option is unnecessary in ``manage.py``, because it uses
+``settings.py`` from the current project by default.
Extra niceties
==============
diff --git a/docs/localflavor.txt b/docs/localflavor.txt
index 154922ff45..fc227fee28 100644
--- a/docs/localflavor.txt
+++ b/docs/localflavor.txt
@@ -181,7 +181,7 @@ CASocialInsuranceNumberField
----------------------------
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`_.
.. _Luhn mod-10 checksum: http://en.wikipedia.org/wiki/Luhn_algorithm
diff --git a/docs/middleware.txt b/docs/middleware.txt
index f2cf18dbdf..a2853e2965 100644
--- a/docs/middleware.txt
+++ b/docs/middleware.txt
@@ -154,6 +154,17 @@ every incoming ``HttpRequest`` object. See `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
---------------------------------------------------
diff --git a/docs/sites.txt b/docs/sites.txt
index 5896afcf41..9516b43995 100644
--- a/docs/sites.txt
+++ b/docs/sites.txt
@@ -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
-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::
from django.conf import settings
@@ -330,13 +330,13 @@ Here's how Django uses the sites framework:
retrieving flatpages to display.
* 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
hook for providing item URLs will use the ``domain`` from the current
``Site`` object if you don't specify a fully-qualified domain.
* 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 current ``Site`` object when calculating an object's URL.
diff --git a/docs/syndication_feeds.txt b/docs/syndication_feeds.txt
index b3edf4c008..ebd6af26f8 100644
--- a/docs/syndication_feeds.txt
+++ b/docs/syndication_feeds.txt
@@ -201,7 +201,7 @@ the feed.
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):
def get_object(self, bits):
diff --git a/docs/templates.txt b/docs/templates.txt
index ffda321512..5b8eb13db0 100644
--- a/docs/templates.txt
+++ b/docs/templates.txt
@@ -1230,7 +1230,7 @@ addslashes
Adds slashes before quotes. Useful for escaping strings in CSV, for example.
**New in Django development version**: for escaping data in JavaScript strings,
-use the `escapejs` filter instead.
+use the `escapejs`_ filter instead.
capfirst
~~~~~~~~
@@ -1403,6 +1403,11 @@ join
Joins a list with a string, like Python's ``str.join(list)``.
+last
+~~~~
+
+Returns the last item in a list.
+
length
~~~~~~
diff --git a/tests/regressiontests/backends/models.py b/tests/regressiontests/backends/models.py
index db16d2c198..a041ab6b12 100644
--- a/tests/regressiontests/backends/models.py
+++ b/tests/regressiontests/backends/models.py
@@ -8,6 +8,13 @@ class Square(models.Model):
def __unicode__(self):
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:
t_convert = lambda x: x.upper()
else:
@@ -32,4 +39,25 @@ __test__ = {'API_TESTS': """
>>> Square.objects.count()
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')]
+
"""}
diff --git a/tests/regressiontests/forms/localflavor/ca.py b/tests/regressiontests/forms/localflavor/ca.py
index a13a6de65f..48171b0558 100644
--- a/tests/regressiontests/forms/localflavor/ca.py
+++ b/tests/regressiontests/forms/localflavor/ca.py
@@ -213,13 +213,13 @@ u'046-454-286'
>>> f.clean('046-454-287')
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')
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')
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.']
"""
diff --git a/tests/regressiontests/middleware/models.py b/tests/regressiontests/middleware/models.py
new file mode 100644
index 0000000000..71abcc5198
--- /dev/null
+++ b/tests/regressiontests/middleware/models.py
@@ -0,0 +1 @@
+# models.py file for tests to run.
diff --git a/tests/regressiontests/middleware/tests.py b/tests/regressiontests/middleware/tests.py
index cb5c29abe1..5e7fd320d6 100644
--- a/tests/regressiontests/middleware/tests.py
+++ b/tests/regressiontests/middleware/tests.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
-import unittest
-
-from django.test import TestCase
+from django.test import TestCase
from django.http import HttpRequest
from django.middleware.common import CommonMiddleware
from django.conf import settings
@@ -19,7 +17,7 @@ class CommonMiddlewareTest(TestCase):
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
request = self._get_request('slash/')
@@ -27,7 +25,7 @@ class CommonMiddlewareTest(TestCase):
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
request = self._get_request('noslash')
@@ -35,7 +33,7 @@ class CommonMiddlewareTest(TestCase):
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
request = self._get_request('unknown')
@@ -43,7 +41,7 @@ class CommonMiddlewareTest(TestCase):
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
request = self._get_request('slash')
@@ -53,9 +51,9 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_no_redirect_on_POST_in_DEBUG(self):
"""
- 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
- redirected to a slashed version
+ 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
+ redirected to a slashed version.
"""
settings.APPEND_SLASH = True
settings.DEBUG = True
@@ -68,11 +66,12 @@ class CommonMiddlewareTest(TestCase):
try:
CommonMiddleware().process_request(request)
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):
"""
- tests disabling append slash functionality
+ Tests disabling append slash functionality.
"""
settings.APPEND_SLASH = False
request = self._get_request('slash')
@@ -80,8 +79,8 @@ class CommonMiddlewareTest(TestCase):
def test_append_slash_quoted(self):
"""
- tests that urls which require quoting are redirected to their slash
- version ok
+ Tests that URLs which require quoting are redirected to their slash
+ version ok.
"""
settings.APPEND_SLASH = True
request = self._get_request('needsquoting#')
@@ -90,4 +89,3 @@ class CommonMiddlewareTest(TestCase):
self.assertEquals(
r['Location'],
'http://testserver/middleware/needsquoting%23/')
-
diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py
index f38b2cdef1..fd73dc57ba 100644
--- a/tests/regressiontests/templates/filters.py
+++ b/tests/regressiontests/templates/filters.py
@@ -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-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-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"),
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 846023afc9..9e9033f975 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -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-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-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-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/"),