summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoseph Kocherhans <joseph@jkocherhans.com>2007-11-30 05:50:44 +0000
committerJoseph Kocherhans <joseph@jkocherhans.com>2007-11-30 05:50:44 +0000
commitf88babafc58eafece72d3f2f7444336c69196808 (patch)
tree0f52526851e157511d3c94140b979c0c2a8af079
parentc82b2becf2bffe8ad18827b80a6c12433f799660 (diff)
newforms-admin: Merged trunk revision [6671].
git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@6776 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/contrib/admin/filterspecs.py6
-rw-r--r--django/contrib/admin/models.py3
-rw-r--r--django/contrib/admin/options.py5
-rw-r--r--django/contrib/admin/sites.py3
-rw-r--r--django/contrib/admin/templates/admin/base_site.html2
-rw-r--r--django/contrib/admin/templates/admin/change_form.html4
-rw-r--r--django/contrib/admin/templates/admin/date_hierarchy.html4
-rw-r--r--django/contrib/admin/templates/admin/delete_confirmation.html4
-rw-r--r--django/contrib/admin/templates/admin/index.html6
-rw-r--r--django/contrib/admin/templates/admin/invalid_setup.html2
-rw-r--r--django/contrib/admin/templates/admin/object_history.html6
-rw-r--r--django/contrib/admin/templates/admin/pagination.html2
-rw-r--r--django/contrib/admin/templates/admin_doc/model_detail.html8
-rw-r--r--django/contrib/admin/templatetags/admin_list.py17
-rw-r--r--django/contrib/admin/util.py19
-rw-r--r--django/contrib/admin/views/decorators.py3
-rw-r--r--django/contrib/admin/views/main.py2
-rw-r--r--django/contrib/admindocs/utils.py3
-rw-r--r--django/contrib/admindocs/views.py3
-rw-r--r--django/contrib/csrf/middleware.py7
-rw-r--r--django/contrib/databrowse/datastructures.py13
-rw-r--r--django/contrib/databrowse/plugins/calendars.py12
-rw-r--r--django/contrib/databrowse/plugins/fieldchoices.py10
-rw-r--r--django/contrib/databrowse/sites.py3
-rw-r--r--django/contrib/flatpages/views.py8
-rw-r--r--django/contrib/humanize/templatetags/humanize.py4
-rw-r--r--django/contrib/markup/templatetags/markup.py10
-rw-r--r--django/contrib/markup/tests.py6
-rw-r--r--django/contrib/sitemaps/templates/sitemap.xml3
-rw-r--r--django/contrib/sitemaps/templates/sitemap_index.xml3
-rw-r--r--django/newforms/forms.py15
-rw-r--r--django/newforms/util.py8
-rw-r--r--django/newforms/widgets.py40
-rw-r--r--django/oldforms/__init__.py43
-rw-r--r--django/template/__init__.py70
-rw-r--r--django/template/context.py5
-rw-r--r--django/template/defaultfilters.py173
-rw-r--r--django/template/defaulttags.py37
-rw-r--r--django/utils/encoding.py6
-rw-r--r--django/utils/html.py33
-rw-r--r--django/utils/safestring.py124
-rw-r--r--django/views/debug.py30
-rw-r--r--docs/templates.txt149
-rw-r--r--docs/templates_python.txt142
-rw-r--r--tests/regressiontests/defaultfilters/tests.py4
-rw-r--r--tests/regressiontests/forms/forms.py2
-rw-r--r--tests/regressiontests/forms/tests.py2
-rw-r--r--tests/regressiontests/humanize/tests.py3
-rw-r--r--tests/regressiontests/templates/filters.py220
-rw-r--r--tests/regressiontests/templates/tests.py207
50 files changed, 1184 insertions, 310 deletions
diff --git a/django/contrib/admin/filterspecs.py b/django/contrib/admin/filterspecs.py
index 70dc29d24f..d6a4a0bc48 100644
--- a/django/contrib/admin/filterspecs.py
+++ b/django/contrib/admin/filterspecs.py
@@ -9,6 +9,8 @@ certain test -- e.g. being a DateField or ForeignKey.
from django.db import models
from django.utils.encoding import smart_unicode, iri_to_uri
from django.utils.translation import ugettext as _
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
import datetime
class FilterSpec(object):
@@ -39,7 +41,7 @@ class FilterSpec(object):
def output(self, cl):
t = []
if self.has_output():
- t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % self.title())
+ t.append(_(u'<h3>By %s:</h3>\n<ul>\n') % escape(self.title()))
for choice in self.choices(cl):
t.append(u'<li%s><a href="%s">%s</a></li>\n' % \
@@ -47,7 +49,7 @@ class FilterSpec(object):
iri_to_uri(choice['query_string']),
choice['display']))
t.append('</ul>\n\n')
- return "".join(t)
+ return mark_safe("".join(t))
class RelatedFilterSpec(FilterSpec):
def __init__(self, f, request, params, model, model_admin):
diff --git a/django/contrib/admin/models.py b/django/contrib/admin/models.py
index ffa2740d5e..23c8661336 100644
--- a/django/contrib/admin/models.py
+++ b/django/contrib/admin/models.py
@@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
+from django.utils.safestring import mark_safe
ADDITION = 1
CHANGE = 2
@@ -49,4 +50,4 @@ class LogEntry(models.Model):
Returns the admin URL to edit the object represented by this log entry.
This is relative to the Django admin index page.
"""
- return u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id)
+ return mark_safe(u"%s/%s/%s/" % (self.content_type.app_label, self.content_type.model, self.object_id))
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index cafa054d07..de4f1206a4 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -9,6 +9,7 @@ from django.db import models
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render_to_response
from django.utils.html import escape
+from django.utils.safestring import mark_safe
from django.utils.text import capfirst, get_text_list
from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode
@@ -462,7 +463,7 @@ class ModelAdmin(BaseModelAdmin):
'has_file_field': True, # FIXME - this should check if form or formsets have a FileField,
'has_absolute_url': hasattr(model, 'get_absolute_url'),
'ordered_objects': ordered_objects,
- 'form_url': form_url,
+ 'form_url': mark_safe(form_url),
'opts': opts,
'content_type_id': ContentType.objects.get_for_model(model).id,
'save_on_top': self.save_on_top,
@@ -652,7 +653,7 @@ class ModelAdmin(BaseModelAdmin):
# Populate deleted_objects, a data structure of all related objects that
# will also be deleted.
- deleted_objects = [u'%s: <a href="../../%s/">%s</a>' % (force_unicode(capfirst(opts.verbose_name)), object_id, escape(str(obj))), []]
+ deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), force_unicode(object_id), escape(obj))), []]
perms_needed = sets.Set()
get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
index 8d7ec139d6..06d1a040d6 100644
--- a/django/contrib/admin/sites.py
+++ b/django/contrib/admin/sites.py
@@ -3,6 +3,7 @@ from django.contrib.admin import ModelAdmin
from django.contrib.auth import authenticate, login
from django.db.models.base import ModelBase
from django.shortcuts import render_to_response
+from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy, ugettext as _
import base64
@@ -272,7 +273,7 @@ class AdminSite(object):
if True in perms.values():
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
- 'admin_url': '%s/%s/' % (app_label, model.__name__.lower()),
+ 'admin_url': mark_safe('%s/%s/' % (app_label, model.__name__.lower())),
'perms': perms,
}
if app_label in app_dict:
diff --git a/django/contrib/admin/templates/admin/base_site.html b/django/contrib/admin/templates/admin/base_site.html
index 2bc7310873..b867bd29bd 100644
--- a/django/contrib/admin/templates/admin/base_site.html
+++ b/django/contrib/admin/templates/admin/base_site.html
@@ -1,7 +1,7 @@
{% extends "admin/base.html" %}
{% load i18n %}
-{% block title %}{{ title|escape }} | {% trans 'Django site admin' %}{% endblock %}
+{% block title %}{{ title }} | {% trans 'Django site admin' %}{% endblock %}
{% block branding %}
<h1 id="site-name">{% trans 'Django administration' %}</h1>
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index fab4d816e9..b0a957468c 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -17,8 +17,8 @@
{% block breadcrumbs %}{% if not is_popup %}
<div class="breadcrumbs">
<a href="../../../">{% trans "Home" %}</a> &rsaquo;
- <a href="../">{{ opts.verbose_name_plural|capfirst|escape }}</a> &rsaquo;
- {% if add %}{% trans "Add" %} {{ opts.verbose_name|escape }}{% else %}{{ original|truncatewords:"18"|escape }}{% endif %}
+ <a href="../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
+ {% if add %}{% trans "Add" %} {{ opts.verbose_name }}{% else %}{{ original|truncatewords:"18" }}{% endif %}
</div>
{% endif %}{% endblock %}
diff --git a/django/contrib/admin/templates/admin/date_hierarchy.html b/django/contrib/admin/templates/admin/date_hierarchy.html
index d2d69616c7..005851051c 100644
--- a/django/contrib/admin/templates/admin/date_hierarchy.html
+++ b/django/contrib/admin/templates/admin/date_hierarchy.html
@@ -1,9 +1,9 @@
{% if show %}
<div class="xfull">
<ul class="toplinks">
-{% if back %}<li class="date-back"><a href="{{ back.link }}">&lsaquo; {{ back.title|escape }}</a></li>{% endif %}
+{% if back %}<li class="date-back"><a href="{{ back.link }}">&lsaquo; {{ back.title }}</a></li>{% endif %}
{% for choice in choices %}
-<li> {% if choice.link %}<a href="{{ choice.link }}">{% endif %}{{ choice.title|escape }}{% if choice.link %}</a>{% endif %}</li>
+<li> {% if choice.link %}<a href="{{ choice.link }}">{% endif %}{{ choice.title }}{% if choice.link %}</a>{% endif %}</li>
{% endfor %}
</ul><br class="clear" />
</div>
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index a11e7b3e84..368332e0dc 100644
--- a/django/contrib/admin/templates/admin/delete_confirmation.html
+++ b/django/contrib/admin/templates/admin/delete_confirmation.html
@@ -6,7 +6,7 @@
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="../../../../">{% trans "Home" %}</a> &rsaquo;
- <a href="../../">{{ opts.verbose_name_plural|capfirst|escape }}</a> &rsaquo;
+ <a href="../../">{{ opts.verbose_name_plural|capfirst }}</a> &rsaquo;
<a href="../">{{ object|escape|truncatewords:"18" }}</a> &rsaquo;
{% trans 'Delete' %}
</div>
@@ -17,7 +17,7 @@
<p>{% blocktrans with object|escape as escaped_object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p>
<ul>
{% for obj in perms_lacking %}
- <li>{{ obj|escape }}</li>
+ <li>{{ obj }}</li>
{% endfor %}
</ul>
{% else %}
diff --git a/django/contrib/admin/templates/admin/index.html b/django/contrib/admin/templates/admin/index.html
index 76b3bcc838..074be03208 100644
--- a/django/contrib/admin/templates/admin/index.html
+++ b/django/contrib/admin/templates/admin/index.html
@@ -20,9 +20,9 @@
{% for model in app.models %}
<tr>
{% if model.perms.change %}
- <th scope="row"><a href="{{ model.admin_url }}">{{ model.name|escape }}</a></th>
+ <th scope="row"><a href="{{ model.admin_url }}">{{ model.name }}</a></th>
{% else %}
- <th scope="row">{{ model.name|escape }}</th>
+ <th scope="row">{{ model.name }}</th>
{% endif %}
{% if model.perms.add %}
@@ -59,7 +59,7 @@
{% else %}
<ul class="actionlist">
{% for entry in admin_log %}
- <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">{% if not entry.is_deletion %}<a href="{{ entry.get_admin_url }}">{% endif %}{{ entry.object_repr|escape }}{% if not entry.is_deletion %}</a>{% endif %}<br /><span class="mini quiet">{% filter capfirst|escape %}{% trans entry.content_type.name %}{% endfilter %}</span></li>
+ <li class="{% if entry.is_addition %}addlink{% endif %}{% if entry.is_change %}changelink{% endif %}{% if entry.is_deletion %}deletelink{% endif %}">{% if not entry.is_deletion %}<a href="{{ entry.get_admin_url }}">{% endif %}{{ entry.object_repr|escape }}{% if not entry.is_deletion %}</a>{% endif %}<br /><span class="mini quiet">{% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %}</span></li>
{% endfor %}
</ul>
{% endif %}
diff --git a/django/contrib/admin/templates/admin/invalid_setup.html b/django/contrib/admin/templates/admin/invalid_setup.html
index 4fa41bbaa0..f09b316b06 100644
--- a/django/contrib/admin/templates/admin/invalid_setup.html
+++ b/django/contrib/admin/templates/admin/invalid_setup.html
@@ -1,7 +1,7 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
-{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {{ title|escape }}</div>{% endblock %}
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../">{% trans 'Home' %}</a> &rsaquo; {{ title }}</div>{% endblock %}
{% block content %}
<p>{% trans "Something's wrong with your database installation. Make sure the appropriate database tables have been created, and make sure the database is readable by the appropriate user." %}</p>
diff --git a/django/contrib/admin/templates/admin/object_history.html b/django/contrib/admin/templates/admin/object_history.html
index 4b1b5564bf..047268b162 100644
--- a/django/contrib/admin/templates/admin/object_history.html
+++ b/django/contrib/admin/templates/admin/object_history.html
@@ -4,7 +4,7 @@
{% block userlinks %}<a href="../../../../doc/">{% trans 'Documentation' %}</a> / <a href="../../../../password_change/">{% trans 'Change password' %}</a> / <a href="../../../../logout/">{% trans 'Log out' %}</a>{% endblock %}
{% block breadcrumbs %}
-<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> &rsaquo; <a href="../../">{{ module_name|escape }}</a> &rsaquo; <a href="../">{{ object|escape|truncatewords:"18" }}</a> &rsaquo; {% trans 'History' %}</div>
+<div class="breadcrumbs"><a href="../../../../">{% trans 'Home' %}</a> &rsaquo; <a href="../../">{{ module_name }}</a> &rsaquo; <a href="../">{{ object|truncatewords:"18" }}</a> &rsaquo; {% trans 'History' %}</div>
{% endblock %}
{% block content %}
@@ -24,8 +24,8 @@
{% for action in action_list %}
<tr>
<th scope="row">{{ action.action_time|date:_("DATE_WITH_TIME_FULL") }}</th>
- <td>{{ action.user.username }}{% if action.user.first_name %} ({{ action.user.first_name|escape }} {{ action.user.last_name|escape }}){% endif %}</td>
- <td>{{ action.change_message|escape }}</td>
+ <td>{{ action.user.username }}{% if action.user.first_name %} ({{ action.user.first_name }} {{ action.user.last_name }}){% endif %}</td>
+ <td>{{ action.change_message }}</td>
</tr>
{% endfor %}
</tbody>
diff --git a/django/contrib/admin/templates/admin/pagination.html b/django/contrib/admin/templates/admin/pagination.html
index e1c09b2932..0640a4690b 100644
--- a/django/contrib/admin/templates/admin/pagination.html
+++ b/django/contrib/admin/templates/admin/pagination.html
@@ -6,6 +6,6 @@
{% paginator_number cl i %}
{% endfor %}
{% endif %}
-{{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name|escape }}{% else %}{{ cl.opts.verbose_name_plural|escape }}{% endifequal %}
+{{ cl.result_count }} {% ifequal cl.result_count 1 %}{{ cl.opts.verbose_name|escape }}{% else %}{{ cl.opts.verbose_name_plural }}{% endifequal %}
{% if show_all_url %}&nbsp;&nbsp;<a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %}
</p>
diff --git a/django/contrib/admin/templates/admin_doc/model_detail.html b/django/contrib/admin/templates/admin_doc/model_detail.html
index 70133e26dd..5cb9a943fc 100644
--- a/django/contrib/admin/templates/admin_doc/model_detail.html
+++ b/django/contrib/admin/templates/admin_doc/model_detail.html
@@ -9,16 +9,16 @@
</style>
{% endblock %}
-{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../../">Documentation</a> &rsaquo; <a href="../">Models</a> &rsaquo; {{ name|escape }}</div>{% endblock %}
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="../../../">Home</a> &rsaquo; <a href="../../">Documentation</a> &rsaquo; <a href="../">Models</a> &rsaquo; {{ name }}</div>{% endblock %}
-{% block title %}Model: {{ name|escape }}{% endblock %}
+{% block title %}Model: {{ name }}{% endblock %}
{% block content %}
<div id="content-main">
-<h1>{{ summary|escape }}</h1>
+<h1>{{ summary }}</h1>
{% if description %}
- <p>{% filter escape|linebreaksbr %}{% trans description %}{% endfilter %}</p>
+ <p>{% filter linebreaksbr %}{% trans description %}{% endfilter %}</p>
{% endif %}
<div class="module">
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 4a4c053971..9b63af6dce 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -4,8 +4,9 @@ from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR,
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import dateformat
-from django.utils.html import escape
+from django.utils.html import escape, conditional_escape
from django.utils.text import capfirst
+from django.utils.safestring import mark_safe
from django.utils.translation import get_date_formats, get_partial_date_formats, ugettext as _
from django.utils.encoding import smart_unicode, smart_str, force_unicode
from django.template import Library
@@ -19,9 +20,9 @@ def paginator_number(cl,i):
if i == DOT:
return u'... '
elif i == cl.page_num:
- return u'<span class="this-page">%d</span> ' % (i+1)
+ return mark_safe(u'<span class="this-page">%d</span> ' % (i+1))
else:
- return u'<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1)
+ return mark_safe(u'<a href="%s"%s>%d</a> ' % (cl.get_query_string({PAGE_VAR: i}), (i == cl.paginator.pages-1 and ' class="end"' or ''), i+1))
paginator_number = register.simple_tag(paginator_number)
def pagination(cl):
@@ -117,7 +118,7 @@ def result_headers(cl):
def _boolean_icon(field_val):
BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
- return u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val)
+ return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val))
def items_for_result(cl, result):
first = True
@@ -193,10 +194,10 @@ def items_for_result(cl, result):
# Convert the pk to something that can be used in Javascript.
# Problem cases are long ints (23L) and non-ASCII strings.
result_id = repr(force_unicode(getattr(result, pk)))[1:]
- yield (u'<%s%s><a href="%s"%s>%s</a></%s>' % \
- (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), result_repr, table_tag))
+ yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
+ (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag))
else:
- yield (u'<td%s>%s</td>' % (row_class, result_repr))
+ yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
def results(cl):
for res in cl.result_list:
@@ -220,7 +221,7 @@ def date_hierarchy(cl):
day_lookup = cl.params.get(day_field)
year_month_format, month_day_format = get_partial_date_formats()
- link = lambda d: cl.get_query_string(d, [field_generic])
+ link = lambda d: mark_safe(cl.get_query_string(d, [field_generic]))
if year_lookup and month_lookup and day_lookup:
day = datetime.date(int(year_lookup), int(month_lookup), int(day_lookup))
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
index b1038f711b..b478e002f5 100644
--- a/django/contrib/admin/util.py
+++ b/django/contrib/admin/util.py
@@ -1,6 +1,7 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils.html import escape
+from django.utils.safestring import mark_safe
from django.utils.text import capfirst
from django.utils.encoding import force_unicode
@@ -37,12 +38,14 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_
if related.field.rel.edit_inline or not has_admin:
# Don't display link to edit, because it either has no
# admin or is edited inline.
- nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj), []])
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), sub_obj)), []])
else:
# Display a link to the admin page.
- nh(deleted_objects, current_depth, [u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
- (force_unicode(capfirst(related.opts.verbose_name)), related.opts.app_label, related.opts.object_name.lower(),
- sub_obj._get_pk_val(), sub_obj), []])
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' %
+ (escape(force_unicode(capfirst(related.opts.verbose_name))),
+ related.opts.app_label,
+ related.opts.object_name.lower(),
+ sub_obj._get_pk_val(), sub_obj)), []])
get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
else:
has_related_objs = False
@@ -54,8 +57,8 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_
nh(deleted_objects, current_depth, [u'%s: %s' % (force_unicode(capfirst(related.opts.verbose_name)), escape(sub_obj)), []])
else:
# Display a link to the admin page.
- nh(deleted_objects, current_depth, [u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
- (force_unicode(capfirst(related.opts.verbose_name)), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj)), []])
+ nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="../../../../%s/%s/%s/">%s</a>' % \
+ (escape(force_unicode(capfirst(related.opts.verbose_name))), related.opts.app_label, related.opts.object_name.lower(), sub_obj._get_pk_val(), escape(sub_obj))), []])
get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
# If there were related objects, and the user doesn't have
# permission to delete them, add the missing perm to perms_needed.
@@ -87,9 +90,9 @@ def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_
else:
# Display a link to the admin page.
nh(deleted_objects, current_depth, [
- (_('One or more %(fieldname)s in %(name)s:') % {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name)}) + \
+ mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
(u' <a href="../../../../%s/%s/%s/">%s</a>' % \
- (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj))), []])
+ (related.opts.app_label, related.opts.module_name, sub_obj._get_pk_val(), escape(sub_obj)))), []])
# If there were related objects, and the user doesn't have
# permission to change them, add the missing perm to perms_needed.
if has_admin and has_related_objs:
diff --git a/django/contrib/admin/views/decorators.py b/django/contrib/admin/views/decorators.py
index 9ae0170b54..4f6662c970 100644
--- a/django/contrib/admin/views/decorators.py
+++ b/django/contrib/admin/views/decorators.py
@@ -4,6 +4,7 @@ from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login
from django.shortcuts import render_to_response
from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.safestring import mark_safe
import base64, datetime, md5
import cPickle as pickle
@@ -22,7 +23,7 @@ def _display_login_form(request, error_message=''):
post_data = _encode_post_data({})
return render_to_response('admin/login.html', {
'title': _('Log in'),
- 'app_path': request.path,
+ 'app_path': mark_safe(request.path),
'post_data': post_data,
'error_message': error_message
}, context_instance=template.RequestContext(request))
diff --git a/django/contrib/admin/views/main.py b/django/contrib/admin/views/main.py
index 01e659ac06..e4eb40d609 100644
--- a/django/contrib/admin/views/main.py
+++ b/django/contrib/admin/views/main.py
@@ -173,7 +173,7 @@ class ChangeList(object):
del p[k]
elif v is not None:
p[k] = v
- return '?' + '&amp;'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20')
+ return mark_safe('?' + '&amp;'.join([u'%s=%s' % (k, v) for k, v in p.items()]).replace(' ', '%20'))
def get_results(self, request):
paginator = ObjectPaginator(self.query_set, self.list_per_page)
diff --git a/django/contrib/admindocs/utils.py b/django/contrib/admindocs/utils.py
index 9adf09b6a5..4a45a622b2 100644
--- a/django/contrib/admindocs/utils.py
+++ b/django/contrib/admindocs/utils.py
@@ -3,6 +3,7 @@
import re
from email.Parser import HeaderParser
from email.Errors import HeaderParseError
+from django.utils.safestring import mark_safe
try:
import docutils.core
import docutils.nodes
@@ -66,7 +67,7 @@ def parse_rst(text, default_reference_context, thing_being_parsed=None, link_bas
parts = docutils.core.publish_parts(text, source_path=thing_being_parsed,
destination_path=None, writer_name='html',
settings_overrides=overrides)
- return parts['fragment']
+ return mark_safe(parts['fragment'])
#
# reST roles
diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
index 8e49fdaf06..47342920d7 100644
--- a/django/contrib/admindocs/views.py
+++ b/django/contrib/admindocs/views.py
@@ -10,6 +10,7 @@ from django.core import urlresolvers
from django.contrib.admindocs import utils
from django.contrib.sites.models import Site
from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
import inspect, os, re
# Exclude methods starting with these strings from documentation
@@ -29,7 +30,7 @@ def bookmarklets(request):
# Hack! This couples this view to the URL it lives at.
admin_root = request.path[:-len('doc/bookmarklets/')]
return render_to_response('admin_doc/bookmarklets.html', {
- 'admin_url': "%s://%s%s" % (request.is_secure() and 'https' or 'http', get_host(request), admin_root),
+ 'admin_url': mark_safe("%s://%s%s" % (request.is_secure() and 'https' or 'http', request.get_host(), admin_root)),
}, context_instance=RequestContext(request))
bookmarklets = staff_member_required(bookmarklets)
diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py
index 1ef0e6cefd..1a75a5d6ab 100644
--- a/django/contrib/csrf/middleware.py
+++ b/django/contrib/csrf/middleware.py
@@ -7,11 +7,12 @@ against request forgeries from other sites.
"""
from django.conf import settings
from django.http import HttpResponseForbidden
+from django.utils.safestring import mark_safe
import md5
import re
import itertools
-_ERROR_MSG = '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>'
+_ERROR_MSG = mark_safe('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>')
_POST_FORM_RE = \
re.compile(r'(<form\W[^>]*\bmethod=(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
@@ -82,10 +83,10 @@ class CsrfMiddleware(object):
itertools.repeat(''))
def add_csrf_field(match):
"""Returns the matched <form> tag plus the added <input> element"""
- return match.group() + "<div style='display:none;'>" + \
+ return mark_safe(match.group() + "<div style='display:none;'>" + \
"<input type='hidden' " + idattributes.next() + \
" name='csrfmiddlewaretoken' value='" + csrf_token + \
- "' /></div>"
+ "' /></div>")
# Modify any POST forms
response.content = _POST_FORM_RE.sub(add_csrf_field, response.content)
diff --git a/django/contrib/databrowse/datastructures.py b/django/contrib/databrowse/datastructures.py
index 0a26db18b1..4b0947eb34 100644
--- a/django/contrib/databrowse/datastructures.py
+++ b/django/contrib/databrowse/datastructures.py
@@ -8,6 +8,7 @@ from django.utils import dateformat
from django.utils.text import capfirst
from django.utils.translation import get_date_formats
from django.utils.encoding import smart_unicode, smart_str, iri_to_uri
+from django.utils.safestring import mark_safe
from django.db.models.query import QuerySet
EMPTY_VALUE = '(None)'
@@ -28,7 +29,7 @@ class EasyModel(object):
return self.site.registry[self.model]
def url(self):
- return '%s%s/%s/' % (self.site.root_url, self.model._meta.app_label, self.model._meta.module_name)
+ return mark_safe('%s%s/%s/' % (self.site.root_url, self.model._meta.app_label, self.model._meta.module_name))
def objects(self, **kwargs):
return self.get_query_set().filter(**kwargs)
@@ -68,9 +69,9 @@ class EasyField(object):
def url(self):
if self.field.choices:
- return '%s%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name)
+ return mark_safe('%s%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name))
elif self.field.rel:
- return '%s%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name)
+ return mark_safe('%s%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name))
class EasyChoice(object):
def __init__(self, easy_model, field, value, label):
@@ -81,7 +82,7 @@ class EasyChoice(object):
return smart_str(u'<EasyChoice for %s.%s>' % (self.model.model._meta.object_name, self.field.name))
def url(self):
- return '%s%s/%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.field.name, iri_to_uri(self.value))
+ return mark_safe('%s%s/%s/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.field.name, iri_to_uri(self.value)))
class EasyInstance(object):
def __init__(self, easy_model, instance):
@@ -184,14 +185,14 @@ class EasyInstanceField(object):
if self.field.rel.to in self.model.model_list:
lst = []
for value in self.values():
- url = '%s%s/%s/objects/%s/' % (self.model.site.root_url, m.model._meta.app_label, m.model._meta.module_name, iri_to_uri(value._get_pk_val()))
+ url = mark_safe('%s%s/%s/objects/%s/' % (self.model.site.root_url, m.model._meta.app_label, m.model._meta.module_name, iri_to_uri(value._get_pk_val())))
lst.append((smart_unicode(value), url))
else:
lst = [(value, None) for value in self.values()]
elif self.field.choices:
lst = []
for value in self.values():
- url = '%s%s/%s/fields/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name, iri_to_uri(self.raw_value))
+ url = mark_safe('%s%s/%s/fields/%s/%s/' % (self.model.site.root_url, self.model.model._meta.app_label, self.model.model._meta.module_name, self.field.name, iri_to_uri(self.raw_value)))
lst.append((value, url))
elif isinstance(self.field, models.URLField):
val = self.values()[0]
diff --git a/django/contrib/databrowse/plugins/calendars.py b/django/contrib/databrowse/plugins/calendars.py
index 1b1a197290..5d5a020e6d 100644
--- a/django/contrib/databrowse/plugins/calendars.py
+++ b/django/contrib/databrowse/plugins/calendars.py
@@ -5,8 +5,9 @@ from django.contrib.databrowse.sites import DatabrowsePlugin
from django.shortcuts import render_to_response
from django.utils.text import capfirst
from django.utils.translation import get_date_formats
-from django.views.generic import date_based
from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
+from django.views.generic import date_based
import datetime
import time
@@ -29,16 +30,17 @@ class CalendarPlugin(DatabrowsePlugin):
fields = self.field_dict(model)
if not fields:
return u''
- return u'<p class="filter"><strong>View calendar by:</strong> %s</p>' % \
- u', '.join(['<a href="calendars/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()])
+ return mark_safe(u'<p class="filter"><strong>View calendar by:</strong> %s</p>' % \
+ u', '.join(['<a href="calendars/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()]))
def urls(self, plugin_name, easy_instance_field):
if isinstance(easy_instance_field.field, models.DateField):
- return [u'%s%s/%s/%s/%s/%s/' % (easy_instance_field.model.url(),
+ return [mark_safe(u'%s%s/%s/%s/%s/%s/' % (
+ easy_instance_field.model.url(),
plugin_name, easy_instance_field.field.name,
easy_instance_field.raw_value.year,
easy_instance_field.raw_value.strftime('%b').lower(),
- easy_instance_field.raw_value.day)]
+ easy_instance_field.raw_value.day))]
def model_view(self, request, model_databrowse, url):
self.model, self.site = model_databrowse.model, model_databrowse.site
diff --git a/django/contrib/databrowse/plugins/fieldchoices.py b/django/contrib/databrowse/plugins/fieldchoices.py
index a5dd07c639..dea6bac700 100644
--- a/django/contrib/databrowse/plugins/fieldchoices.py
+++ b/django/contrib/databrowse/plugins/fieldchoices.py
@@ -5,6 +5,7 @@ from django.contrib.databrowse.sites import DatabrowsePlugin
from django.shortcuts import render_to_response
from django.utils.text import capfirst
from django.utils.encoding import smart_str, force_unicode
+from django.utils.safestring import mark_safe
from django.views.generic import date_based
import datetime
import time
@@ -32,15 +33,16 @@ class FieldChoicePlugin(DatabrowsePlugin):
fields = self.field_dict(model)
if not fields:
return u''
- return u'<p class="filter"><strong>View by:</strong> %s</p>' % \
- u', '.join(['<a href="fields/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()])
+ return mark_safe(u'<p class="filter"><strong>View by:</strong> %s</p>' % \
+ u', '.join(['<a href="fields/%s/">%s</a>' % (f.name, force_unicode(capfirst(f.verbose_name))) for f in fields.values()]))
def urls(self, plugin_name, easy_instance_field):
if easy_instance_field.field in self.field_dict(easy_instance_field.model.model).values():
field_value = smart_str(easy_instance_field.raw_value)
- return [u'%s%s/%s/%s/' % (easy_instance_field.model.url(),
+ return [mark_safe(u'%s%s/%s/%s/' % (
+ easy_instance_field.model.url(),
plugin_name, easy_instance_field.field.name,
- urllib.quote(field_value, safe=''))]
+ urllib.quote(field_value, safe='')))]
def model_view(self, request, model_databrowse, url):
self.model, self.site = model_databrowse.model, model_databrowse.site
diff --git a/django/contrib/databrowse/sites.py b/django/contrib/databrowse/sites.py
index c07ec2129b..5d8c1c8b49 100644
--- a/django/contrib/databrowse/sites.py
+++ b/django/contrib/databrowse/sites.py
@@ -2,6 +2,7 @@ from django import http
from django.db import models
from django.contrib.databrowse.datastructures import EasyModel, EasyChoice
from django.shortcuts import render_to_response
+from django.utils.safestring import mark_safe
class AlreadyRegistered(Exception):
pass
@@ -60,7 +61,7 @@ class ModelDatabrowse(object):
def main_view(self, request):
easy_model = EasyModel(self.site, self.model)
- html_snippets = u'\n'.join([p.model_index_html(request, self.model, self.site) for p in self.plugins.values()])
+ html_snippets = mark_safe(u'\n'.join([p.model_index_html(request, self.model, self.site) for p in self.plugins.values()]))
return render_to_response('databrowse/model_detail.html', {
'model': easy_model,
'root_url': self.site.root_url,
diff --git a/django/contrib/flatpages/views.py b/django/contrib/flatpages/views.py
index f386a52101..1e451bdecc 100644
--- a/django/contrib/flatpages/views.py
+++ b/django/contrib/flatpages/views.py
@@ -4,6 +4,7 @@ from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.conf import settings
from django.core.xheaders import populate_xheaders
+from django.utils.safestring import mark_safe
DEFAULT_TEMPLATE = 'flatpages/default.html'
@@ -30,6 +31,13 @@ def flatpage(request, url):
t = loader.select_template((f.template_name, DEFAULT_TEMPLATE))
else:
t = loader.get_template(DEFAULT_TEMPLATE)
+
+ # To avoid having to always use the "|safe" filter in flatpage templates,
+ # mark the title and content as already safe (since they are raw HTML
+ # content in the first place).
+ f.title = mark_safe(f.title)
+ f.content = mark_safe(f.content)
+
c = RequestContext(request, {
'flatpage': f,
})
diff --git a/django/contrib/humanize/templatetags/humanize.py b/django/contrib/humanize/templatetags/humanize.py
index 19591606f9..4aa5a32e3f 100644
--- a/django/contrib/humanize/templatetags/humanize.py
+++ b/django/contrib/humanize/templatetags/humanize.py
@@ -21,6 +21,7 @@ def ordinal(value):
if value % 100 in (11, 12, 13): # special case
return u"%d%s" % (value, t[0])
return u'%d%s' % (value, t[value % 10])
+ordinal.is_safe = True
register.filter(ordinal)
def intcomma(value):
@@ -34,6 +35,7 @@ def intcomma(value):
return new
else:
return intcomma(new)
+intcomma.is_safe = True
register.filter(intcomma)
def intword(value):
@@ -55,6 +57,7 @@ def intword(value):
new_value = value / 1000000000000.0
return ungettext('%(value).1f trillion', '%(value).1f trillion', new_value) % {'value': new_value}
return value
+intword.is_safe = False
register.filter(intword)
def apnumber(value):
@@ -69,6 +72,7 @@ def apnumber(value):
if not 0 < value < 10:
return value
return (_('one'), _('two'), _('three'), _('four'), _('five'), _('six'), _('seven'), _('eight'), _('nine'))[value-1]
+apnumber.is_safe = True
register.filter(apnumber)
def naturalday(value, arg=None):
diff --git a/django/contrib/markup/templatetags/markup.py b/django/contrib/markup/templatetags/markup.py
index 5d1f0ff1fb..13708fd26d 100644
--- a/django/contrib/markup/templatetags/markup.py
+++ b/django/contrib/markup/templatetags/markup.py
@@ -17,6 +17,7 @@ silently fail and return the un-marked-up text.
from django import template
from django.conf import settings
from django.utils.encoding import smart_str, force_unicode
+from django.utils.safestring import mark_safe
register = template.Library()
@@ -28,7 +29,8 @@ def textile(value):
raise template.TemplateSyntaxError, "Error in {% textile %} filter: The Python textile library isn't installed."
return force_unicode(value)
else:
- return force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8'))
+ return mark_safe(force_unicode(textile.textile(smart_str(value), encoding='utf-8', output='utf-8')))
+textile.is_safe = True
def markdown(value):
try:
@@ -38,7 +40,8 @@ def markdown(value):
raise template.TemplateSyntaxError, "Error in {% markdown %} filter: The Python markdown library isn't installed."
return force_unicode(value)
else:
- return force_unicode(markdown.markdown(smart_str(value)))
+ return mark_safe(force_unicode(markdown.markdown(smart_str(value))))
+markdown.is_safe = True
def restructuredtext(value):
try:
@@ -50,7 +53,8 @@ def restructuredtext(value):
else:
docutils_settings = getattr(settings, "RESTRUCTUREDTEXT_FILTER_SETTINGS", {})
parts = publish_parts(source=smart_str(value), writer_name="html4css1", settings_overrides=docutils_settings)
- return force_unicode(parts["fragment"])
+ return mark_safe(force_unicode(parts["fragment"]))
+restructuredtext.is_safe = True
register.filter(textile)
register.filter(markdown)
diff --git a/django/contrib/markup/tests.py b/django/contrib/markup/tests.py
index bd3f52b9dd..6e7beaeb2f 100644
--- a/django/contrib/markup/tests.py
+++ b/django/contrib/markup/tests.py
@@ -1,9 +1,11 @@
# Quick tests for the markup templatetags (django.contrib.markup)
-from django.template import Template, Context, add_to_builtins
import re
import unittest
+from django.template import Template, Context, add_to_builtins
+from django.utils.html import escape
+
add_to_builtins('django.contrib.markup.templatetags.markup')
class Templates(unittest.TestCase):
@@ -24,7 +26,7 @@ Paragraph 2 with "quotes" and @code@"""
<p>Paragraph 2 with &#8220;quotes&#8221; and <code>code</code></p>""")
else:
- self.assertEqual(rendered, textile_content)
+ self.assertEqual(rendered, escape(textile_content))
def test_markdown(self):
try:
diff --git a/django/contrib/sitemaps/templates/sitemap.xml b/django/contrib/sitemaps/templates/sitemap.xml
index 16d9a0bbe0..c0d529d62e 100644
--- a/django/contrib/sitemaps/templates/sitemap.xml
+++ b/django/contrib/sitemaps/templates/sitemap.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
+{% autoescape off %}<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% spaceless %}
{% for url in urlset %}
@@ -11,3 +11,4 @@
{% endfor %}
{% endspaceless %}
</urlset>
+{% endautoescape %}
diff --git a/django/contrib/sitemaps/templates/sitemap_index.xml b/django/contrib/sitemaps/templates/sitemap_index.xml
index a2bcce85dc..497ae8bb81 100644
--- a/django/contrib/sitemaps/templates/sitemap_index.xml
+++ b/django/contrib/sitemaps/templates/sitemap_index.xml
@@ -1,4 +1,5 @@
-<?xml version="1.0" encoding="UTF-8"?>
+{% autoescape off %}<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
{% for location in sitemaps %}<sitemap><loc>{{ location|escape }}</loc></sitemap>{% endfor %}
</sitemapindex>
+{% endautoescape %}
diff --git a/django/newforms/forms.py b/django/newforms/forms.py
index 3e7ab957b0..fdc41f7dee 100644
--- a/django/newforms/forms.py
+++ b/django/newforms/forms.py
@@ -7,6 +7,7 @@ from copy import deepcopy
from django.utils.datastructures import SortedDict
from django.utils.html import escape
from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode
+from django.utils.safestring import mark_safe
from fields import Field
from widgets import Media, media_property, TextInput, Textarea
@@ -123,7 +124,8 @@ class BaseForm(StrAndUnicode):
output.append(error_row % force_unicode(bf_errors))
if bf.label:
label = escape(force_unicode(bf.label))
- # Only add the suffix if the label does not end in punctuation.
+ # Only add the suffix if the label does not end in
+ # punctuation.
if self.label_suffix:
if label[-1] not in ':?.!':
label += self.label_suffix
@@ -141,11 +143,14 @@ class BaseForm(StrAndUnicode):
str_hidden = u''.join(hidden_fields)
if output:
last_row = output[-1]
- # Chop off the trailing row_ender (e.g. '</td></tr>') and insert the hidden fields.
+ # Chop off the trailing row_ender (e.g. '</td></tr>') and
+ # insert the hidden fields.
output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender
- else: # If there aren't any rows in the output, just append the hidden fields.
+ else:
+ # If there aren't any rows in the output, just append the
+ # hidden fields.
output.append(str_hidden)
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
def as_table(self):
"Returns this form rendered as HTML <tr>s -- excluding the <table></table>."
@@ -343,7 +348,7 @@ class BoundField(StrAndUnicode):
if id_:
attrs = attrs and flatatt(attrs) or ''
contents = '<label for="%s"%s>%s</label>' % (widget.id_for_label(id_), attrs, contents)
- return contents
+ return mark_safe(contents)
def _is_hidden(self):
"Returns True if this BoundField's widget is hidden."
diff --git a/django/newforms/util.py b/django/newforms/util.py
index 490fbcaed0..b3edf41adf 100644
--- a/django/newforms/util.py
+++ b/django/newforms/util.py
@@ -1,6 +1,7 @@
from django.utils.html import escape
from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode
from django.utils.functional import Promise
+from django.utils.safestring import mark_safe
def flatatt(attrs):
"""
@@ -22,7 +23,9 @@ class ErrorDict(dict, StrAndUnicode):
def as_ul(self):
if not self: return u''
- return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s%s</li>' % (k, force_unicode(v)) for k, v in self.items()])
+ return mark_safe(u'<ul class="errorlist">%s</ul>'
+ % ''.join([u'<li>%s%s</li>' % (k, force_unicode(v))
+ for k, v in self.items()]))
def as_text(self):
return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u' * %s' % force_unicode(i) for i in v])) for k, v in self.items()])
@@ -36,7 +39,8 @@ class ErrorList(list, StrAndUnicode):
def as_ul(self):
if not self: return u''
- return u'<ul class="errorlist">%s</ul>' % ''.join([u'<li>%s</li>' % force_unicode(e) for e in self])
+ return mark_safe(u'<ul class="errorlist">%s</ul>'
+ % ''.join([u'<li>%s</li>' % force_unicode(e) for e in self]))
def as_text(self):
if not self: return u''
diff --git a/django/newforms/widgets.py b/django/newforms/widgets.py
index 3d4440bdaa..d782c9d2ec 100644
--- a/django/newforms/widgets.py
+++ b/django/newforms/widgets.py
@@ -14,6 +14,7 @@ from django.utils.datastructures import MultiValueDict
from django.utils.html import escape
from django.utils.translation import ugettext
from django.utils.encoding import StrAndUnicode, force_unicode
+from django.utils.safestring import mark_safe
from util import flatatt
from urlparse import urljoin
@@ -188,8 +189,10 @@ class Input(Widget):
def render(self, name, value, attrs=None):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
- if value != '': final_attrs['value'] = force_unicode(value) # Only add the 'value' attribute if a value is non-empty.
- return u'<input%s />' % flatatt(final_attrs)
+ if value != '':
+ # Only add the 'value' attribute if a value is non-empty.
+ final_attrs['value'] = force_unicode(value)
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
class TextInput(Input):
input_type = 'text'
@@ -222,7 +225,9 @@ class MultipleHiddenInput(HiddenInput):
def render(self, name, value, attrs=None, choices=()):
if value is None: value = []
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
- return u'\n'.join([(u'<input%s />' % flatatt(dict(value=force_unicode(v), **final_attrs))) for v in value])
+ return mark_safe(u'\n'.join([(u'<input%s />' %
+ flatatt(dict(value=force_unicode(v), **final_attrs)))
+ for v in value]))
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
@@ -251,7 +256,8 @@ class Textarea(Widget):
if value is None: value = ''
value = force_unicode(value)
final_attrs = self.build_attrs(attrs, name=name)
- return u'<textarea%s>%s</textarea>' % (flatatt(final_attrs), escape(value))
+ return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
+ escape(value)))
class DateTimeInput(Input):
input_type = 'text'
@@ -285,8 +291,9 @@ class CheckboxInput(Widget):
if result:
final_attrs['checked'] = 'checked'
if value not in ('', True, False, None):
- final_attrs['value'] = force_unicode(value) # Only add the 'value' attribute if a value is non-empty.
- return u'<input%s />' % flatatt(final_attrs)
+ # Only add the 'value' attribute if a value is non-empty.
+ final_attrs['value'] = force_unicode(value)
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
def value_from_datadict(self, data, files, name):
if name not in data:
@@ -307,13 +314,14 @@ class Select(Widget):
if value is None: value = ''
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<select%s>' % flatatt(final_attrs)]
- str_value = force_unicode(value) # Normalize to string.
+ # Normalize to string.
+ str_value = force_unicode(value)
for option_value, option_label in chain(self.choices, choices):
option_value = force_unicode(option_value)
selected_html = (option_value == str_value) and u' selected="selected"' or ''
output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(force_unicode(option_label))))
output.append(u'</select>')
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
class NullBooleanSelect(Select):
"""
@@ -350,7 +358,7 @@ class SelectMultiple(Widget):
selected_html = (option_value in str_values) and ' selected="selected"' or ''
output.append(u'<option value="%s"%s>%s</option>' % (escape(option_value), selected_html, escape(force_unicode(option_label))))
output.append(u'</select>')
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
def value_from_datadict(self, data, files, name):
if isinstance(data, MultiValueDict):
@@ -371,7 +379,8 @@ class RadioInput(StrAndUnicode):
self.index = index
def __unicode__(self):
- return u'<label>%s %s</label>' % (self.tag(), self.choice_label)
+ return mark_safe(u'<label>%s %s</label>' % (self.tag(),
+ self.choice_label))
def is_checked(self):
return self.value == self.choice_value
@@ -382,7 +391,7 @@ class RadioInput(StrAndUnicode):
final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value)
if self.is_checked():
final_attrs['checked'] = 'checked'
- return u'<input%s />' % flatatt(final_attrs)
+ return mark_safe(u'<input%s />' % flatatt(final_attrs))
class RadioFieldRenderer(StrAndUnicode):
"""
@@ -406,7 +415,8 @@ class RadioFieldRenderer(StrAndUnicode):
def render(self):
"""Outputs a <ul> for this set of radio fields."""
- return u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>' % force_unicode(w) for w in self])
+ return mark_safe(u'<ul>\n%s\n</ul>' % u'\n'.join([u'<li>%s</li>'
+ % force_unicode(w) for w in self]))
class RadioSelect(Select):
@@ -443,7 +453,8 @@ class CheckboxSelectMultiple(SelectMultiple):
has_id = attrs and 'id' in attrs
final_attrs = self.build_attrs(attrs, name=name)
output = [u'<ul>']
- str_values = set([force_unicode(v) for v in value]) # Normalize to strings.
+ # Normalize to strings
+ str_values = set([force_unicode(v) for v in value])
for i, (option_value, option_label) in enumerate(chain(self.choices, choices)):
# If an ID attribute was given, add a numeric index as a suffix,
# so that the checkboxes don't all have the same ID attribute.
@@ -454,7 +465,7 @@ class CheckboxSelectMultiple(SelectMultiple):
rendered_cb = cb.render(name, option_value)
output.append(u'<li><label>%s %s</label></li>' % (rendered_cb, escape(force_unicode(option_label))))
output.append(u'</ul>')
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
def id_for_label(self, id_):
# See the comment for RadioSelect.id_for_label()
@@ -560,3 +571,4 @@ class SplitDateTimeWidget(MultiWidget):
if value:
return [value.date(), value.time().replace(microsecond=0)]
return [None, None]
+
diff --git a/django/oldforms/__init__.py b/django/oldforms/__init__.py
index 5f7b9fc2ad..fc8727185f 100644
--- a/django/oldforms/__init__.py
+++ b/django/oldforms/__init__.py
@@ -1,6 +1,7 @@
from django.core import validators
from django.core.exceptions import PermissionDenied
from django.utils.html import escape
+from django.utils.safestring import mark_safe
from django.conf import settings
from django.utils.translation import ugettext, ungettext
from django.utils.encoding import smart_unicode, force_unicode
@@ -189,9 +190,9 @@ class FormFieldWrapper(object):
def html_error_list(self):
if self.errors():
- return '<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()])
+ return mark_safe('<ul class="errorlist"><li>%s</li></ul>' % '</li><li>'.join([escape(e) for e in self.errors()]))
else:
- return ''
+ return mark_safe('')
def get_id(self):
return self.formfield.get_id()
@@ -226,7 +227,7 @@ class FormFieldCollection(FormFieldWrapper):
return bool(len(self.errors()))
def html_combined_error_list(self):
- return ''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')])
+ return mark_safe(''.join([field.html_error_list() for field in self.formfield_dict.values() if hasattr(field, 'errors')]))
class InlineObjectCollection(object):
"An object that acts like a sparse list of form field collections."
@@ -418,9 +419,9 @@ class TextField(FormField):
max_length = u''
if self.max_length:
max_length = u'maxlength="%s" ' % self.max_length
- return u'<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
+ return mark_safe(u'<input type="%s" id="%s" class="v%s%s" name="%s" size="%s" value="%s" %s/>' % \
(self.input_type, self.get_id(), self.__class__.__name__, self.is_required and u' required' or '',
- self.field_name, self.length, escape(data), max_length)
+ self.field_name, self.length, escape(data), max_length))
def html2python(data):
return data
@@ -442,9 +443,9 @@ class LargeTextField(TextField):
def render(self, data):
if data is None:
data = ''
- return u'<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \
+ return mark_safe(u'<textarea id="%s" class="v%s%s" name="%s" rows="%s" cols="%s">%s</textarea>' % \
(self.get_id(), self.__class__.__name__, self.is_required and u' required' or u'',
- self.field_name, self.rows, self.cols, escape(data))
+ self.field_name, self.rows, self.cols, escape(data)))
class HiddenField(FormField):
def __init__(self, field_name, is_required=False, validator_list=None, max_length=None):
@@ -453,8 +454,8 @@ class HiddenField(FormField):
self.validator_list = validator_list[:]
def render(self, data):
- return u'<input type="hidden" id="%s" name="%s" value="%s" />' % \
- (self.get_id(), self.field_name, escape(data))
+ return mark_safe(u'<input type="hidden" id="%s" name="%s" value="%s" />' % \
+ (self.get_id(), self.field_name, escape(data)))
class CheckboxField(FormField):
def __init__(self, field_name, checked_by_default=False, validator_list=None, is_required=False):
@@ -468,9 +469,9 @@ class CheckboxField(FormField):
checked_html = ''
if data or (data is '' and self.checked_by_default):
checked_html = ' checked="checked"'
- return u'<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \
+ return mark_safe(u'<input type="checkbox" id="%s" class="v%s" name="%s"%s />' % \
(self.get_id(), self.__class__.__name__,
- self.field_name, checked_html)
+ self.field_name, checked_html))
def html2python(data):
"Convert value from browser ('on' or '') to a Python boolean"
@@ -502,7 +503,7 @@ class SelectField(FormField):
selected_html = u' selected="selected"'
output.append(u' <option value="%s"%s>%s</option>' % (escape(value), selected_html, force_unicode(escape(display_name))))
output.append(u' </select>')
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
def isValidChoice(self, data, form):
str_data = smart_unicode(data)
@@ -556,7 +557,7 @@ class RadioSelectField(FormField):
output = [u'<ul%s>' % (self.ul_class and u' class="%s"' % self.ul_class or u'')]
output.extend([u'<li>%s %s</li>' % (d['field'], d['label']) for d in self.datalist])
output.append(u'</ul>')
- return u''.join(output)
+ return mark_safe(u''.join(output))
def __iter__(self):
for d in self.datalist:
yield d
@@ -571,11 +572,11 @@ class RadioSelectField(FormField):
datalist.append({
'value': value,
'name': display_name,
- 'field': u'<input type="radio" id="%s" name="%s" value="%s"%s/>' % \
- (self.get_id() + u'_' + unicode(i), self.field_name, value, selected_html),
- 'label': u'<label for="%s">%s</label>' % \
+ 'field': mark_safe(u'<input type="radio" id="%s" name="%s" value="%s"%s/>' % \
+ (self.get_id() + u'_' + unicode(i), self.field_name, value, selected_html)),
+ 'label': mark_safe(u'<label for="%s">%s</label>' % \
(self.get_id() + u'_' + unicode(i), display_name),
- })
+ )})
return RadioFieldRenderer(datalist, self.ul_class)
def isValidChoice(self, data, form):
@@ -614,7 +615,7 @@ class SelectMultipleField(SelectField):
selected_html = u' selected="selected"'
output.append(u' <option value="%s"%s>%s</option>' % (escape(value), selected_html, force_unicode(escape(choice))))
output.append(u' </select>')
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
def isValidChoice(self, field_data, all_data):
# data is something like ['1', '2', '3']
@@ -667,7 +668,7 @@ class CheckboxSelectMultipleField(SelectMultipleField):
(self.get_id() + escape(value), self.__class__.__name__, field_name, checked_html,
self.get_id() + escape(value), choice))
output.append(u'</ul>')
- return u'\n'.join(output)
+ return mark_safe(u'\n'.join(output))
####################
# FILE UPLOADS #
@@ -688,8 +689,8 @@ class FileUploadField(FormField):
raise validators.CriticalValidationError, ugettext("The submitted file is empty.")
def render(self, data):
- return u'<input type="file" id="%s" class="v%s" name="%s" />' % \
- (self.get_id(), self.__class__.__name__, self.field_name)
+ return mark_safe(u'<input type="file" id="%s" class="v%s" name="%s" />' % \
+ (self.get_id(), self.__class__.__name__, self.field_name))
def html2python(data):
if data is None:
diff --git a/django/template/__init__.py b/django/template/__init__.py
index 1cfd85be06..1fd3171c12 100644
--- a/django/template/__init__.py
+++ b/django/template/__init__.py
@@ -57,6 +57,8 @@ from django.utils.functional import curry, Promise
from django.utils.text import smart_split
from django.utils.encoding import smart_unicode, force_unicode
from django.utils.translation import ugettext as _
+from django.utils.safestring import SafeData, EscapeData, mark_safe, mark_for_escaping
+from django.utils.html import escape
__all__ = ('Template', 'Context', 'RequestContext', 'compile_string')
@@ -595,7 +597,16 @@ class FilterExpression(object):
arg_vals.append(arg)
else:
arg_vals.append(arg.resolve(context))
- obj = func(obj, *arg_vals)
+ if getattr(func, 'needs_autoescape', False):
+ new_obj = func(obj, autoescape=context.autoescape, *arg_vals)
+ else:
+ new_obj = func(obj, *arg_vals)
+ if getattr(func, 'is_safe', False) and isinstance(obj, SafeData):
+ obj = mark_safe(new_obj)
+ elif isinstance(obj, EscapeData):
+ obj = mark_for_escaping(new_obj)
+ else:
+ obj = new_obj
return obj
def args_check(name, func, provided):
@@ -637,7 +648,7 @@ def resolve_variable(path, context):
"""
Returns the resolved variable, which may contain attribute syntax, within
the given context.
-
+
Deprecated; use the Variable class instead.
"""
return Variable(path).resolve(context)
@@ -647,7 +658,7 @@ class Variable(object):
A template variable, resolvable against a given context. The variable may be
a hard-coded string (if it begins and ends with single or double quote
marks)::
-
+
>>> c = {'article': {'section':'News'}}
>>> Variable('article.section').resolve(c)
u'News'
@@ -662,25 +673,25 @@ class Variable(object):
(The example assumes VARIABLE_ATTRIBUTE_SEPARATOR is '.')
"""
-
+
def __init__(self, var):
self.var = var
self.literal = None
self.lookups = None
-
+
try:
# First try to treat this variable as a number.
#
- # Note that this could cause an OverflowError here that we're not
+ # Note that this could cause an OverflowError here that we're not
# catching. Since this should only happen at compile time, that's
# probably OK.
self.literal = float(var)
-
+
# So it's a float... is it an int? If the original value contained a
# dot or an "e" then it was a float, not an int.
if '.' not in var and 'e' not in var.lower():
self.literal = int(self.literal)
-
+
# "2." is invalid
if var.endswith('.'):
raise ValueError
@@ -691,12 +702,12 @@ class Variable(object):
# we're also dealing with a literal.
if var[0] in "\"'" and var[0] == var[-1]:
self.literal = var[1:-1]
-
+
else:
# Otherwise we'll set self.lookups so that resolve() knows we're
# dealing with a bonafide variable
self.lookups = tuple(var.split(VARIABLE_ATTRIBUTE_SEPARATOR))
-
+
def resolve(self, context):
"""Resolve this variable against a given context."""
if self.lookups is not None:
@@ -705,18 +716,18 @@ class Variable(object):
else:
# We're dealing with a literal, so it's already been "resolved"
return self.literal
-
+
def __repr__(self):
return "<%s: %r>" % (self.__class__.__name__, self.var)
-
+
def __str__(self):
return self.var
def _resolve_lookup(self, context):
"""
Performs resolution of a real variable (i.e. not a literal) against the
- given context.
-
+ given context.
+
As indicated by the method's name, this method is an implementation
detail and shouldn't be called by external code. Use Variable.resolve()
instead.
@@ -757,14 +768,7 @@ class Variable(object):
current = settings.TEMPLATE_STRING_IF_INVALID
else:
raise
-
- if isinstance(current, (basestring, Promise)):
- try:
- current = force_unicode(current)
- except UnicodeDecodeError:
- # Failing to convert to unicode can happen sometimes (e.g. debug
- # tracebacks). So we allow it in this particular instance.
- pass
+
return current
class Node(object):
@@ -838,16 +842,31 @@ class VariableNode(Node):
return "<Variable Node: %s>" % self.filter_expression
def render(self, context):
- return self.filter_expression.resolve(context)
+ try:
+ output = force_unicode(self.filter_expression.resolve(context))
+ except UnicodeDecodeError:
+ # Unicode conversion can fail sometimes for reasons out of our
+ # control (e.g. exception rendering). In that case, we fail quietly.
+ return ''
+ if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData):
+ return force_unicode(escape(output))
+ else:
+ return force_unicode(output)
class DebugVariableNode(VariableNode):
def render(self, context):
try:
- return self.filter_expression.resolve(context)
+ 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."
@@ -961,7 +980,8 @@ class Library(object):
else:
t = get_template(file_name)
self.nodelist = t.nodelist
- return self.nodelist.render(context_class(dict))
+ return self.nodelist.render(context_class(dict,
+ autoescape=context.autoescape))
compile_func = curry(generic_tag_compiler, params, defaults, getattr(func, "_decorated_function", func).__name__, InclusionNode)
compile_func.__doc__ = func.__doc__
diff --git a/django/template/context.py b/django/template/context.py
index 51cd88b7e9..017d2d84b1 100644
--- a/django/template/context.py
+++ b/django/template/context.py
@@ -9,9 +9,11 @@ class ContextPopException(Exception):
class Context(object):
"A stack container for variable context"
- def __init__(self, dict_=None):
+
+ def __init__(self, dict_=None, autoescape=True):
dict_ = dict_ or {}
self.dicts = [dict_]
+ self.autoescape = autoescape
def __repr__(self):
return repr(self.dicts)
@@ -97,3 +99,4 @@ class RequestContext(Context):
processors = tuple(processors)
for processor in get_standard_processors() + processors:
self.update(processor(request))
+
diff --git a/django/template/defaultfilters.py b/django/template/defaultfilters.py
index d2d4b9e508..7d4a72efb3 100644
--- a/django/template/defaultfilters.py
+++ b/django/template/defaultfilters.py
@@ -7,6 +7,7 @@ from django.template import Variable, Library
from django.conf import settings
from django.utils.translation import ugettext, ungettext
from django.utils.encoding import force_unicode, iri_to_uri
+from django.utils.safestring import mark_safe, SafeData
register = Library()
@@ -29,6 +30,9 @@ def stringfilter(func):
# Include a reference to the real function (used to check original
# arguments by the template parser).
_dec._decorated_function = getattr(func, '_decorated_function', func)
+ for attr in ('is_safe', 'needs_autoescape'):
+ if hasattr(func, attr):
+ setattr(_dec, attr, getattr(func, attr))
return _dec
###################
@@ -39,17 +43,20 @@ def stringfilter(func):
def addslashes(value):
"""Adds slashes - useful for passing strings to JavaScript, for example."""
return value.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'")
+addslashes.is_safe = True
addslashes = stringfilter(addslashes)
def capfirst(value):
"""Capitalizes the first character of the value."""
return value and value[0].upper() + value[1:]
+capfirst.is_safe=True
capfirst = stringfilter(capfirst)
def fix_ampersands(value):
"""Replaces ampersands with ``&amp;`` entities."""
from django.utils.html import fix_ampersands
return fix_ampersands(value)
+fix_ampersands.is_safe=True
fix_ampersands = stringfilter(fix_ampersands)
def floatformat(text, arg=-1):
@@ -90,31 +97,39 @@ def floatformat(text, arg=-1):
return force_unicode(f)
m = f - int(f)
if not m and d < 0:
- return u'%d' % int(f)
+ return mark_safe(u'%d' % int(f))
else:
formatstr = u'%%.%df' % abs(d)
- return formatstr % f
+ return mark_safe(formatstr % f)
+floatformat.is_safe = True
def iriencode(value):
"""Escapes an IRI value for use in a URL."""
return force_unicode(iri_to_uri(value))
iriencode = stringfilter(iriencode)
-def linenumbers(value):
+def linenumbers(value, autoescape=None):
"""Displays text with line numbers."""
from django.utils.html import escape
lines = value.split(u'\n')
# Find the maximum width of the line count, for use with zero padding
- # string format command.
+ # string format command
width = unicode(len(unicode(len(lines))))
- for i, line in enumerate(lines):
- lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line))
- return u'\n'.join(lines)
+ if not autoescape or isinstance(value, SafeData):
+ for i, line in enumerate(lines):
+ lines[i] = (u"%0" + width + u"d. %s") % (i + 1, line)
+ else:
+ for i, line in enumerate(lines):
+ lines[i] = (u"%0" + width + u"d. %s") % (i + 1, escape(line))
+ return mark_safe(u'\n'.join(lines))
+linenumbers.is_safe = True
+linenumbers.needs_autoescape = True
linenumbers = stringfilter(linenumbers)
def lower(value):
"""Converts a string into all lowercase."""
return value.lower()
+lower.is_safe = True
lower = stringfilter(lower)
def make_list(value):
@@ -125,6 +140,7 @@ def make_list(value):
For a string, it's a list of characters.
"""
return list(value)
+make_list.is_safe = False
make_list = stringfilter(make_list)
def slugify(value):
@@ -135,7 +151,8 @@ def slugify(value):
import unicodedata
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
- return re.sub('[-\s]+', '-', value)
+ return mark_safe(re.sub('[-\s]+', '-', value))
+slugify.is_safe = True
slugify = stringfilter(slugify)
def stringformat(value, arg):
@@ -152,10 +169,12 @@ def stringformat(value, arg):
return (u"%" + unicode(arg)) % value
except (ValueError, TypeError):
return u""
+stringformat.is_safe = True
def title(value):
"""Converts a string into titlecase."""
return re.sub("([a-z])'([A-Z])", lambda m: m.group(0).lower(), value.title())
+title.is_safe = True
title = stringfilter(title)
def truncatewords(value, arg):
@@ -170,6 +189,7 @@ def truncatewords(value, arg):
except ValueError: # Invalid literal for int().
return value # Fail silently.
return truncate_words(value, length)
+truncatewords.is_safe = True
truncatewords = stringfilter(truncatewords)
def truncatewords_html(value, arg):
@@ -184,23 +204,28 @@ def truncatewords_html(value, arg):
except ValueError: # invalid literal for int()
return value # Fail silently.
return truncate_html_words(value, length)
+truncatewords_html.is_safe = True
truncatewords_html = stringfilter(truncatewords_html)
def upper(value):
"""Converts a string into all uppercase."""
return value.upper()
+upper.is_safe = False
upper = stringfilter(upper)
def urlencode(value):
"""Escapes a value for use in a URL."""
from django.utils.http import urlquote
return urlquote(value)
+urlencode.is_safe = False
urlencode = stringfilter(urlencode)
-def urlize(value):
+def urlize(value, autoescape=None):
"""Converts URLs in plain text into clickable links."""
from django.utils.html import urlize
- return urlize(value, nofollow=True)
+ return mark_safe(urlize(value, nofollow=True, autoescape=autoescape))
+urlize.is_safe=True
+urlize.needs_autoescape = True
urlize = stringfilter(urlize)
def urlizetrunc(value, limit):
@@ -211,12 +236,14 @@ def urlizetrunc(value, limit):
Argument: Length to truncate URLs to.
"""
from django.utils.html import urlize
- return urlize(value, trim_url_limit=int(limit), nofollow=True)
+ return mark_safe(urlize(value, trim_url_limit=int(limit), nofollow=True))
+urlizetrunc.is_safe = True
urlizetrunc = stringfilter(urlizetrunc)
def wordcount(value):
"""Returns the number of words."""
return len(value.split())
+wordcount.is_safe = False
wordcount = stringfilter(wordcount)
def wordwrap(value, arg):
@@ -227,6 +254,7 @@ def wordwrap(value, arg):
"""
from django.utils.text import wrap
return wrap(value, int(arg))
+wordwrap.is_safe = True
wordwrap = stringfilter(wordwrap)
def ljust(value, arg):
@@ -236,6 +264,7 @@ def ljust(value, arg):
Argument: field size.
"""
return value.ljust(int(arg))
+ljust.is_safe = True
ljust = stringfilter(ljust)
def rjust(value, arg):
@@ -245,16 +274,24 @@ def rjust(value, arg):
Argument: field size.
"""
return value.rjust(int(arg))
+rjust.is_safe = True
rjust = stringfilter(rjust)
def center(value, arg):
"""Centers the value in a field of a given width."""
return value.center(int(arg))
+center.is_safe = True
center = stringfilter(center)
def cut(value, arg):
- """Removes all values of arg from the given string."""
- return value.replace(arg, u'')
+ """
+ Removes all values of arg from the given string.
+ """
+ safe = isinstance(value, SafeData)
+ value = value.replace(arg, u'')
+ if safe and arg != ';':
+ return mark_safe(value)
+ return value
cut = stringfilter(cut)
###################
@@ -262,29 +299,60 @@ cut = stringfilter(cut)
###################
def escape(value):
- "Escapes a string's HTML"
+ """
+ Marks the value as a string that should not be auto-escaped.
+ """
+ from django.utils.safestring import mark_for_escaping
+ return mark_for_escaping(value)
+escape.is_safe = True
+escape = stringfilter(escape)
+
+def force_escape(value):
+ """
+ Escapes a string's HTML. This returns a new string containing the escaped
+ characters (as opposed to "escape", which marks the content for later
+ possible escaping).
+ """
from django.utils.html import escape
- return escape(value)
+ return mark_safe(escape(value))
escape = stringfilter(escape)
+force_escape.is_safe = True
-def linebreaks(value):
+def linebreaks(value, autoescape=None):
"""
Replaces line breaks in plain text with appropriate HTML; a single
newline becomes an HTML line break (``<br />``) and a new line
followed by a blank line becomes a paragraph break (``</p>``).
"""
from django.utils.html import linebreaks
- return linebreaks(value)
+ autoescape = autoescape and not isinstance(value, SafeData)
+ return mark_safe(linebreaks(value, autoescape))
+linebreaks.is_safe = True
+linebreaks.needs_autoescape = True
linebreaks = stringfilter(linebreaks)
-def linebreaksbr(value):
+def linebreaksbr(value, autoescape=None):
"""
Converts all newlines in a piece of plain text to HTML line breaks
(``<br />``).
"""
- return value.replace('\n', '<br />')
+ if autoescape and not isinstance(value, SafeData):
+ from django.utils.html import escape
+ value = escape(value)
+ return mark_safe(value.replace('\n', '<br />'))
+linebreaksbr.is_safe = True
+linebreaksbr.needs_autoescape = True
linebreaksbr = stringfilter(linebreaksbr)
+def safe(value):
+ """
+ Marks the value as a string that should not be auto-escaped.
+ """
+ from django.utils.safestring import mark_safe
+ return mark_safe(value)
+safe.is_safe = True
+safe = stringfilter(safe)
+
def removetags(value, tags):
"""Removes a space separated list of [X]HTML tags from the output."""
tags = [re.escape(tag) for tag in tags.split()]
@@ -294,12 +362,14 @@ def removetags(value, tags):
value = starttag_re.sub(u'', value)
value = endtag_re.sub(u'', value)
return value
+removetags.is_safe = True
removetags = stringfilter(removetags)
def striptags(value):
"""Strips all [X]HTML tags."""
from django.utils.html import strip_tags
return strip_tags(value)
+striptags.is_safe = True
striptags = stringfilter(striptags)
###################
@@ -315,6 +385,7 @@ def dictsort(value, arg):
decorated = [(var_resolve(item), item) for item in value]
decorated.sort()
return [item[1] for item in decorated]
+dictsort.is_safe = False
def dictsortreversed(value, arg):
"""
@@ -326,6 +397,7 @@ def dictsortreversed(value, arg):
decorated.sort()
decorated.reverse()
return [item[1] for item in decorated]
+dictsortreversed.is_safe = False
def first(value):
"""Returns the first item in a list."""
@@ -333,25 +405,36 @@ def first(value):
return value[0]
except IndexError:
return u''
+first.is_safe = True
def join(value, arg):
"""Joins a list with a string, like Python's ``str.join(list)``."""
try:
- return arg.join(map(force_unicode, value))
+ data = arg.join(map(force_unicode, value))
except AttributeError: # fail silently but nicely
return value
+ safe_args = reduce(lambda lhs, rhs: lhs and isinstance(rhs, SafeData),
+ value, True)
+ if safe_args:
+ return mark_safe(data)
+ else:
+ return data
+join.is_safe = True
def length(value):
"""Returns the length of the value - useful for lists."""
return len(value)
+length.is_safe = True
def length_is(value, arg):
"""Returns a boolean of whether the value's length is the argument."""
return len(value) == int(arg)
+length_is.is_safe = True
def random(value):
"""Returns a random item from the list."""
return random_module.choice(value)
+random.is_safe = True
def slice_(value, arg):
"""
@@ -372,8 +455,9 @@ def slice_(value, arg):
except (ValueError, TypeError):
return value # Fail silently.
+slice_.is_safe = True
-def unordered_list(value):
+def unordered_list(value, autoescape=None):
"""
Recursively takes a self-nested list and returns an HTML unordered list --
WITHOUT opening and closing <ul> tags.
@@ -394,6 +478,11 @@ def unordered_list(value):
</ul>
</li>
"""
+ if autoescape:
+ from django.utils.html import conditional_escape
+ escaper = conditional_escape
+ else:
+ escaper = lambda x: x
def convert_old_style_list(list_):
"""
Converts old style lists to the new easier to understand format.
@@ -443,12 +532,14 @@ def unordered_list(value):
sublist = _helper(sublist_item, tabs+1)
sublist = '\n%s<ul>\n%s\n%s</ul>\n%s' % (indent, sublist,
indent, indent)
- output.append('%s<li>%s%s</li>' % (indent, force_unicode(title),
- sublist))
+ output.append('%s<li>%s%s</li>' % (indent,
+ escaper(force_unicode(title)), sublist))
i += 1
return '\n'.join(output)
value, converted = convert_old_style_list(value)
- return _helper(value)
+ return mark_safe(_helper(value))
+unordered_list.is_safe = True
+unordered_list.needs_autoescape = True
###################
# INTEGERS #
@@ -457,6 +548,7 @@ def unordered_list(value):
def add(value, arg):
"""Adds the arg to the value."""
return int(value) + int(arg)
+add.is_safe = False
def get_digit(value, arg):
"""
@@ -476,6 +568,7 @@ def get_digit(value, arg):
return int(str(value)[-arg])
except IndexError:
return 0
+get_digit.is_safe = False
###################
# DATES #
@@ -489,6 +582,7 @@ def date(value, arg=None):
if arg is None:
arg = settings.DATE_FORMAT
return format(value, arg)
+date.is_safe = False
def time(value, arg=None):
"""Formats a time according to the given format."""
@@ -498,6 +592,7 @@ def time(value, arg=None):
if arg is None:
arg = settings.TIME_FORMAT
return time_format(value, arg)
+time.is_safe = False
def timesince(value, arg=None):
"""Formats a date as the time since that date (i.e. "4 days, 6 hours")."""
@@ -507,6 +602,7 @@ def timesince(value, arg=None):
if arg:
return timesince(arg, value)
return timesince(value)
+timesince.is_safe = False
def timeuntil(value, arg=None):
"""Formats a date as the time until that date (i.e. "4 days, 6 hours")."""
@@ -517,6 +613,7 @@ def timeuntil(value, arg=None):
if arg:
return timesince(arg, value)
return timesince(datetime.now(), value)
+timeuntil.is_safe = False
###################
# LOGIC #
@@ -525,16 +622,19 @@ def timeuntil(value, arg=None):
def default(value, arg):
"""If value is unavailable, use given default."""
return value or arg
+default.is_safe = False
def default_if_none(value, arg):
"""If value is None, use given default."""
if value is None:
return arg
return value
+default_if_none.is_safe = False
def divisibleby(value, arg):
"""Returns True if the value is devisible by the argument."""
return int(value) % int(arg) == 0
+divisibleby.is_safe = False
def yesno(value, arg=None):
"""
@@ -566,6 +666,7 @@ def yesno(value, arg=None):
if value:
return yes
return no
+yesno.is_safe = False
###################
# MISC #
@@ -588,29 +689,30 @@ def filesizeformat(bytes):
if bytes < 1024 * 1024 * 1024:
return ugettext("%.1f MB") % (bytes / (1024 * 1024))
return ugettext("%.1f GB") % (bytes / (1024 * 1024 * 1024))
+filesizeformat.is_safe = True
def pluralize(value, arg=u's'):
"""
Returns a plural suffix if the value is not 1. By default, 's' is used as
the suffix:
- * If value is 0, vote{{ value|plurlize }} displays "0 votes".
- * If value is 1, vote{{ value|plurlize }} displays "1 vote".
- * If value is 2, vote{{ value|plurlize }} displays "2 votes".
+ * If value is 0, vote{{ value|pluralize }} displays "0 votes".
+ * If value is 1, vote{{ value|pluralize }} displays "1 vote".
+ * If value is 2, vote{{ value|pluralize }} displays "2 votes".
If an argument is provided, that string is used instead:
- * If value is 0, class{{ value|plurlize:"es" }} displays "0 classes".
- * If value is 1, class{{ value|plurlize:"es" }} displays "1 class".
- * If value is 2, class{{ value|plurlize:"es" }} displays "2 classes".
+ * If value is 0, class{{ value|pluralize:"es" }} displays "0 classes".
+ * If value is 1, class{{ value|pluralize:"es" }} displays "1 class".
+ * If value is 2, class{{ value|pluralize:"es" }} displays "2 classes".
If the provided argument contains a comma, the text before the comma is
used for the singular case and the text after the comma is used for the
plural case:
- * If value is 0, cand{{ value|plurlize:"y,ies" }} displays "0 candies".
- * If value is 1, cand{{ value|plurlize:"y,ies" }} displays "1 candy".
- * If value is 2, cand{{ value|plurlize:"y,ies" }} displays "2 candies".
+ * If value is 0, cand{{ value|pluralize:"y,ies" }} displays "0 candies".
+ * If value is 1, cand{{ value|pluralize:"y,ies" }} displays "1 candy".
+ * If value is 2, cand{{ value|pluralize:"y,ies" }} displays "2 candies".
"""
if not u',' in arg:
arg = u',' + arg
@@ -631,11 +733,13 @@ def pluralize(value, arg=u's'):
except TypeError: # len() of unsized object.
pass
return singular_suffix
+pluralize.is_safe = False
def phone2numeric(value):
"""Takes a phone number and converts it in to its numerical equivalent."""
from django.utils.text import phone2numeric
return phone2numeric(value)
+phone2numeric.is_safe = True
def pprint(value):
"""A wrapper around pprint.pprint -- for debugging, really."""
@@ -644,6 +748,7 @@ def pprint(value):
return pformat(value)
except Exception, e:
return u"Error in formatting: %s" % force_unicode(e, errors="replace")
+pprint.is_safe = True
# Syntax: register.filter(name of filter, callback)
register.filter(add)
@@ -662,6 +767,7 @@ register.filter(filesizeformat)
register.filter(first)
register.filter(fix_ampersands)
register.filter(floatformat)
+register.filter(force_escape)
register.filter(get_digit)
register.filter(iriencode)
register.filter(join)
@@ -679,6 +785,7 @@ register.filter(pprint)
register.filter(removetags)
register.filter(random)
register.filter(rjust)
+register.filter(safe)
register.filter('slice', slice_)
register.filter(slugify)
register.filter(stringformat)
diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
index d3c18897eb..d91e30bbb5 100644
--- a/django/template/defaulttags.py
+++ b/django/template/defaulttags.py
@@ -14,9 +14,25 @@ from django.template import get_library, Library, InvalidTemplateLibrary
from django.conf import settings
from django.utils.encoding import smart_str, smart_unicode
from django.utils.itercompat import groupby
+from django.utils.safestring import mark_safe
register = Library()
+class AutoEscapeControlNode(Node):
+ """Implements the actions of the autoescape tag."""
+ def __init__(self, setting, nodelist):
+ self.setting, self.nodelist = setting, nodelist
+
+ def render(self, context):
+ old_setting = context.autoescape
+ context.autoescape = self.setting
+ output = self.nodelist.render(context)
+ context.autoescape = old_setting
+ if self.setting:
+ return mark_safe(output)
+ else:
+ return output
+
class CommentNode(Node):
def render(self, context):
return ''
@@ -393,6 +409,22 @@ class WithNode(Node):
return output
#@register.tag
+def autoescape(parser, token):
+ """
+ Force autoescape behaviour for this block.
+ """
+ args = token.contents.split()
+ if len(args) != 2:
+ raise TemplateSyntaxError("'Autoescape' tag requires exactly one argument.")
+ arg = args[1]
+ if arg not in (u'on', u'off'):
+ raise TemplateSyntaxError("'Autoescape' argument should be 'on' or 'off'")
+ nodelist = parser.parse(('endautoescape',))
+ parser.delete_first_token()
+ return AutoEscapeControlNode((arg == 'on'), nodelist)
+autoescape = register.tag(autoescape)
+
+#@register.tag
def comment(parser, token):
"""
Ignores everything between ``{% comment %}`` and ``{% endcomment %}``.
@@ -492,12 +524,15 @@ def do_filter(parser, token):
Sample usage::
- {% filter escape|lower %}
+ {% filter force_escape|lower %}
This text will be HTML-escaped, and will appear in lowercase.
{% endfilter %}
"""
_, rest = token.contents.split(None, 1)
filter_expr = parser.compile_filter("var|%s" % (rest))
+ for func, unused in filter_expr.filters:
+ if getattr(func, '_decorated_function', func).__name__ in ('escape', 'safe'):
+ raise TemplateSyntaxError('"filter %s" is not permitted. Use the "autoescape" tag instead.' % func.__name__)
nodelist = parser.parse(('endfilter',))
parser.delete_first_token()
return FilterNode(filter_expr, nodelist)
diff --git a/django/utils/encoding.py b/django/utils/encoding.py
index 4bda9caa50..2ab0db7432 100644
--- a/django/utils/encoding.py
+++ b/django/utils/encoding.py
@@ -3,6 +3,7 @@ import urllib
import datetime
from django.utils.functional import Promise
+from django.utils.safestring import SafeData, mark_safe
class DjangoUnicodeDecodeError(UnicodeDecodeError):
def __init__(self, obj, *args):
@@ -51,7 +52,10 @@ def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'):
else:
s = unicode(str(s), encoding, errors)
elif not isinstance(s, unicode):
- s = unicode(s, encoding, errors)
+ # Note: We use .decode() here, instead of unicode(s, encoding,
+ # errors), so that if s is a SafeString, it ends up being a
+ # SafeUnicode at the end.
+ s = s.decode(encoding, errors)
except UnicodeDecodeError, e:
raise DjangoUnicodeDecodeError(s, *e.args)
return s
diff --git a/django/utils/html.py b/django/utils/html.py
index ebd04d1b3c..8eeaa66330 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -3,6 +3,7 @@
import re
import string
+from django.utils.safestring import SafeData, mark_safe
from django.utils.encoding import force_unicode
from django.utils.functional import allow_lazy
@@ -27,16 +28,28 @@ del x # Temporary variable
def escape(html):
"Return the given HTML with ampersands, quotes and carets encoded."
- return force_unicode(html).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;')
+ return mark_safe(force_unicode(html).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;'))
escape = allow_lazy(escape, unicode)
-def linebreaks(value):
- "Convert newlines into <p> and <br />s."
+def conditional_escape(html):
+ """
+ Similar to escape(), except that it doesn't operate on pre-escaped strings.
+ """
+ if isinstance(html, SafeData):
+ return html
+ else:
+ return escape(html)
+
+def linebreaks(value, autoescape=False):
+ "Converts newlines into <p> and <br />s"
value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines
paras = re.split('\n{2,}', value)
- paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
+ if autoescape:
+ paras = [u'<p>%s</p>' % escape(p.strip()).replace('\n', '<br />') for p in paras]
+ else:
+ paras = [u'<p>%s</p>' % p.strip().replace('\n', '<br />') for p in paras]
return u'\n\n'.join(paras)
-linebreaks = allow_lazy(linebreaks, unicode)
+linebreaks = allow_lazy(linebreaks, unicode)
def strip_tags(value):
"Return the given HTML with all tags stripped."
@@ -58,7 +71,7 @@ def fix_ampersands(value):
return unencoded_ampersands_re.sub('&amp;', force_unicode(value))
fix_ampersands = allow_lazy(fix_ampersands, unicode)
-def urlize(text, trim_url_limit=None, nofollow=False):
+def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False):
"""
Convert any URLs in text into clickable links.
@@ -72,13 +85,19 @@ def urlize(text, trim_url_limit=None, nofollow=False):
If nofollow is True, the URLs in link text will get a rel="nofollow"
attribute.
"""
- trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+ if autoescape:
+ trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x)
+ else:
+ trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+ safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_unicode(text))
nofollow_attr = nofollow and ' rel="nofollow"' or ''
for i, word in enumerate(words):
match = punctuation_re.match(word)
if match:
lead, middle, trail = match.groups()
+ if safe_input:
+ middle = mark_safe(middle)
if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \
len(middle) > 0 and middle[0] in string.letters + string.digits and \
(middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))):
diff --git a/django/utils/safestring.py b/django/utils/safestring.py
new file mode 100644
index 0000000000..c7234af0f3
--- /dev/null
+++ b/django/utils/safestring.py
@@ -0,0 +1,124 @@
+"""
+Functions for working with "safe strings": strings that can be displayed safely
+without further escaping in HTML. Marking something as a "safe string" means
+that the producer of the string has already turned characters that should not
+be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
+"""
+from django.utils.functional import curry, Promise
+
+class EscapeData(object):
+ pass
+
+class EscapeString(str, EscapeData):
+ """
+ A string that should be HTML-escaped when output.
+ """
+ pass
+
+class EscapeUnicode(unicode, EscapeData):
+ """
+ A unicode object that should be HTML-escaped when output.
+ """
+ pass
+
+class SafeData(object):
+ pass
+
+class SafeString(str, SafeData):
+ """
+ A string subclass that has been specifically marked as "safe" (requires no
+ further escaping) for HTML output purposes.
+ """
+ def __add__(self, rhs):
+ """
+ Concatenating a safe string with another safe string or safe unicode
+ object is safe. Otherwise, the result is no longer safe.
+ """
+ if isinstance(rhs, SafeUnicode):
+ return SafeUnicode(self + rhs)
+ elif isinstance(rhs, SafeString):
+ return SafeString(self + rhs)
+ else:
+ return super(SafeString, self).__add__(rhs)
+
+ def __str__(self):
+ return self
+
+ def _proxy_method(self, *args, **kwargs):
+ """
+ Wrap a call to a normal unicode method up so that we return safe
+ results. The method that is being wrapped is passed in the 'method'
+ argument.
+ """
+ method = kwargs.pop('method')
+ data = method(self, *args, **kwargs)
+ if isinstance(data, str):
+ return SafeString(data)
+ else:
+ return SafeUnicode(data)
+
+ encode = curry(_proxy_method, method = str.encode)
+ decode = curry(_proxy_method, method = str.decode)
+
+class SafeUnicode(unicode, SafeData):
+ """
+ A unicode subclass that has been specifically marked as "safe" for HTML
+ output purposes.
+ """
+ def __add__(self, rhs):
+ """
+ Concatenating a safe unicode object with another safe string or safe
+ unicode object is safe. Otherwise, the result is no longer safe.
+ """
+ if isinstance(rhs, SafeData):
+ return SafeUnicode(self + rhs)
+ else:
+ return super(SafeUnicode, self).__add__(rhs)
+
+ def _proxy_method(self, *args, **kwargs):
+ """
+ Wrap a call to a normal unicode method up so that we return safe
+ results. The method that is being wrapped is passed in the 'method'
+ argument.
+ """
+ method = kwargs.pop('method')
+ data = method(self, *args, **kwargs)
+ if isinstance(data, str):
+ return SafeString(data)
+ else:
+ return SafeUnicode(data)
+
+ encode = curry(_proxy_method, method = unicode.encode)
+ decode = curry(_proxy_method, method = unicode.decode)
+
+def mark_safe(s):
+ """
+ Explicitly mark a string as safe for (HTML) output purposes. The returned
+ object can be used everywhere a string or unicode object is appropriate.
+
+ Can be called multiple times on a single string.
+ """
+ if isinstance(s, SafeData):
+ return s
+ if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str):
+ return SafeString(s)
+ if isinstance(s, (unicode, Promise)):
+ return SafeUnicode(s)
+ return SafeString(str(s))
+
+def mark_for_escaping(s):
+ """
+ Explicitly mark a string as requiring HTML escaping upon output. Has no
+ effect on SafeData subclasses.
+
+ Can be called multiple times on a single string (the resulting escaping is
+ only applied once).
+ """
+ if isinstance(s, (SafeData, EscapeData)):
+ return s
+ if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str):
+ return EscapeString(s)
+ if isinstance(s, (unicode, Promise)):
+ return EscapeUnicode(s)
+ return EscapeString(str(s))
+
diff --git a/django/views/debug.py b/django/views/debug.py
index 717de1eb34..7c45af230a 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -333,7 +333,6 @@ TECHNICAL_500_TEMPLATE = """
</script>
</head>
<body>
-
<div id="summary">
<h1>{{ exception_type }} at {{ request.path|escape }}</h1>
<h2>{{ exception_value|escape }}</h2>
@@ -395,7 +394,7 @@ TECHNICAL_500_TEMPLATE = """
<div id="template">
<h2>Template error</h2>
<p>In template <code>{{ template_info.name }}</code>, error at line <strong>{{ template_info.line }}</strong></p>
- <h3>{{ template_info.message|escape }}</h3>
+ <h3>{{ template_info.message }}</h3>
<table class="source{% if template_info.top %} cut-top{% endif %}{% ifnotequal template_info.bottom template_info.total %} cut-bottom{% endifnotequal %}">
{% for source_line in template_info.source_lines %}
{% ifequal source_line.0 template_info.line %}
@@ -413,6 +412,7 @@ TECHNICAL_500_TEMPLATE = """
<h2>Traceback <span>(innermost last)</span></h2>
<div class="commands"><a href="#" onclick="return switchPastebinFriendly(this);">Switch to copy-and-paste view</a></div>
<br/>
+ {% autoescape off %}
<div id="browserTraceback">
<ul class="traceback">
{% for frame in frames %}
@@ -422,11 +422,11 @@ TECHNICAL_500_TEMPLATE = """
{% if frame.context_line %}
<div class="context" id="c{{ frame.id }}">
{% if frame.pre_context %}
- <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol>
+ <ol start="{{ frame.pre_context_lineno }}" class="pre-context" id="pre{{ frame.id }}">{% for line in frame.pre_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line }}</li>{% endfor %}</ol>
{% endif %}
- <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line|escape }} <span>...</span></li></ol>
+ <ol start="{{ frame.lineno }}" class="context-line"><li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ frame.context_line }} <span>...</span></li></ol>
{% if frame.post_context %}
- <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line|escape }}</li>{% endfor %}</ol>
+ <ol start='{{ frame.lineno|add:"1" }}' class="post-context" id="post{{ frame.id }}">{% for line in frame.post_context %}<li onclick="toggle('pre{{ frame.id }}', 'post{{ frame.id }}')">{{ line }}</li>{% endfor %}</ol>
{% endif %}
</div>
{% endif %}
@@ -446,7 +446,7 @@ TECHNICAL_500_TEMPLATE = """
{% for var in frame.vars|dictsort:"0" %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
+ <td class="code"><div>{{ var.1|pprint }}</div></td>
</tr>
{% endfor %}
</tbody>
@@ -466,7 +466,7 @@ Traceback (most recent call last):<br/>
{% for frame in frames %}
File "{{ frame.filename }}" in {{ frame.function }}<br/>
{% if frame.context_line %}
- &nbsp;&nbsp;{{ frame.lineno }}. {{ frame.context_line|escape }}<br/>
+ &nbsp;&nbsp;{{ frame.lineno }}. {{ frame.context_line }}<br/>
{% endif %}
{% endfor %}<br/>
&nbsp;&nbsp;{{ exception_type }} at {{ request.path|escape }}<br/>
@@ -476,6 +476,7 @@ Traceback (most recent call last):<br/>
</tbody>
</table>
</div>
+ {% endautoescape %}
</div>
<div id="requestinfo">
@@ -494,7 +495,7 @@ Traceback (most recent call last):<br/>
{% for var in request.GET.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
+ <td class="code"><div>{{ var.1|pprint }}</div></td>
</tr>
{% endfor %}
</tbody>
@@ -516,7 +517,7 @@ Traceback (most recent call last):<br/>
{% for var in request.POST.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
+ <td class="code"><div>{{ var.1|pprint }}</div></td>
</tr>
{% endfor %}
</tbody>
@@ -538,7 +539,7 @@ Traceback (most recent call last):<br/>
{% for var in request.COOKIES.items %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
+ <td class="code"><div>{{ var.1|pprint }}</div></td>
</tr>
{% endfor %}
</tbody>
@@ -559,7 +560,7 @@ Traceback (most recent call last):<br/>
{% for var in request.META.items|dictsort:"0" %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
+ <td class="code"><div>{{ var.1|pprint }}</div></td>
</tr>
{% endfor %}
</tbody>
@@ -578,7 +579,7 @@ Traceback (most recent call last):<br/>
{% for var in settings.items|dictsort:"0" %}
<tr>
<td>{{ var.0 }}</td>
- <td class="code"><div>{{ var.1|pprint|escape }}</div></td>
+ <td class="code"><div>{{ var.1|pprint }}</div></td>
</tr>
{% endfor %}
</tbody>
@@ -593,7 +594,6 @@ Traceback (most recent call last):<br/>
display a standard 500 page.
</p>
</div>
-
</body>
</html>
"""
@@ -645,12 +645,12 @@ TECHNICAL_404_TEMPLATE = """
</p>
<ol>
{% for pattern in urlpatterns %}
- <li>{{ pattern|escape }}</li>
+ <li>{{ pattern }}</li>
{% endfor %}
</ol>
<p>The current URL, <code>{{ request_path|escape }}</code>, didn't match any of these.</p>
{% else %}
- <p>{{ reason|escape }}</p>
+ <p>{{ reason }}</p>
{% endif %}
</div>
diff --git a/docs/templates.txt b/docs/templates.txt
index 68dbfa3e63..b85f108bbe 100644
--- a/docs/templates.txt
+++ b/docs/templates.txt
@@ -299,6 +299,104 @@ it also defines the content that fills the hole in the *parent*. If there were
two similarly-named ``{% block %}`` tags in a template, that template's parent
wouldn't know which one of the blocks' content to use.
+Automatic HTML escaping
+=======================
+
+**New in Django development version**
+
+A very real problem when creating HTML (and other) output using templates and
+variable substitution is the possibility of accidently inserting some variable
+value that affects the resulting HTML. For example, a template fragment such as
+::
+
+ Hello, {{ name }}.
+
+seems like a harmless way to display the user's name. However, if you are
+displaying data that the user entered directly and they had entered their name as ::
+
+ <script>alert('hello')</script>
+
+this would always display a Javascript alert box when the page was loaded.
+Similarly, if you were displaying some data generated by another process and it
+contained a '<' symbol, you couldn't just dump this straight into your HTML,
+because it would be treated as the start of an element. The effects of these
+sorts of problems can vary from merely annoying to allowing exploits via `Cross
+Site Scripting`_ (XSS) attacks.
+
+.. _Cross Site Scripting: http://en.wikipedia.org/wiki/Cross-site_scripting
+
+In order to provide some protection against these problems, Django
+provides automatic (but controllable) HTML escaping for data coming from
+tempate variables. Inside this tag, any data that comes from template
+variables is examined to see if it contains one of the five HTML characters
+(<, >, ', " and &) that often need escaping and those characters are converted
+to their respective HTML entities. It causes no harm if a character is
+converted to an entity when it doesn't need to be, so all five characters are
+always converted.
+
+Since some variables will contain data that is *intended* to be rendered
+as HTML, template tag and filter writers can mark their output strings as
+requiring no further escaping. For example, the ``unordered_list`` filter is
+designed to return raw HTML and we want the template processor to simply
+display the results as returned, without applying any escaping. That is taken
+care of by the filter. The template author need do nothing special in that
+case.
+
+By default, automatic HTML escaping is always applied. However, sometimes you
+will not want this to occur (for example, if you're using the templating
+system to create an email). To control automatic escaping inside your template,
+wrap the affected content in the ``autoescape`` tag, like so::
+
+ {% autoescape off %}
+ Hello {{ name }}
+ {% endautoescape %}
+
+The auto-escaping tag passes its effect onto templates that extend the
+current one as well as templates included via the ``include`` tag, just like
+all block tags.
+
+The ``autoescape`` tag takes either ``on`` or ``off`` as its argument. At times, you might want to force auto-escaping when it would otherwise be disabled. For example::
+
+ Auto-escaping is on by default. Hello {{ name }}
+
+ {% autoescape off %}
+ This will not be auto-escaped: {{ data }}.
+
+ Nor this: {{ other_data }}
+ {% autoescape on %}
+ Auto-escaping applies again, {{ name }}
+ {% endautoescape %}
+ {% endautoescape %}
+
+For individual variables, the ``safe`` filter can also be used to indicate
+that the contents should not be automatically escaped::
+
+ This will be escaped: {{ data }}
+ This will not be escaped: {{ data|safe }}
+
+Think of *safe* as shorthand for *safe from further escaping* or *can be
+safely interpreted as HTML*. In this example, if ``data`` contains ``'<a>'``,
+the output will be::
+
+ This will be escaped: &lt;a&gt;
+ This will not be escaped: <a>
+
+Generally, you won't need to worry about auto-escaping very much. View
+developers and custom filter authors need to think about when their data
+shouldn't be escaped and mark it appropriately. They are in a better position
+to know when that should happen than the template author, so it is their
+responsibility. By default, all output is escaped unless the template
+processor is explicitly told otherwise.
+
+You should also note that if you are trying to write a template that might be
+used in situations where automatic escaping is enabled or disabled and you
+don't know which (such as when your template is included in other templates),
+you can safely write as if you were in an ``{% autoescape off %}`` situation.
+Scatter ``escape`` filters around for any variables that need escaping. When
+auto-escaping is on, these extra filters won't change the output -- any
+variables that use the ``escape`` filter do not have further automatic
+escaping applied to them.
+
Using the built-in reference
============================
@@ -374,6 +472,24 @@ available, and what they do.
Built-in tag reference
----------------------
+autoescape
+~~~~~~~~~~
+
+**New in Django development version**
+
+Control the current auto-escaping behaviour. This tag takes either ``on`` or
+``off`` as an argument and that determines whether auto-escaping is in effect
+inside the block.
+
+When auto-escaping is in effect, all variable content has HTML escaping applied
+to it before placing the result into the output (but after any filters have
+been applied). This is equivalent to manually applying the ``escape`` filter
+attached to each variable.
+
+The only exceptions are variables that are already marked as 'safe' from
+escaping, either by the code that populated the variable, or because it has
+the ``safe`` or ``escape`` filters applied.
+
block
~~~~~
@@ -452,7 +568,7 @@ just like in variable syntax.
Sample usage::
- {% filter escape|lower %}
+ {% filter force_escape|lower %}
This text will be HTML-escaped, and will appear in all lowercase.
{% endfilter %}
@@ -1076,6 +1192,10 @@ Returns true if the value is divisible by the argument.
escape
~~~~~~
+**New in Django development version:** The behaviour of this filter has
+changed slightly in the development version (the affects are only applied
+once, after all other filters).
+
Escapes a string's HTML. Specifically, it makes these replacements:
* ``"&"`` to ``"&amp;"``
@@ -1084,6 +1204,16 @@ Escapes a string's HTML. Specifically, it makes these replacements:
* ``'"'`` (double quote) to ``'&quot;'``
* ``"'"`` (single quote) to ``'&#39;'``
+The escaping is only applied when the string is output, so it does not matter
+where in a chained sequence of filters you put ``escape``: it will always be
+applied as though it were the last filter. If you want escaping to be applied
+immediately, use the ``force_escape`` filter.
+
+Applying ``escape`` to a variable that would normally have auto-escaping
+applied to the result will only result in one round of escaping being done. So
+it is safe to use this function even in auto-escaping environments. If you want
+multiple escaping passes to be applied, use the ``force_escape`` filter.
+
filesizeformat
~~~~~~~~~~~~~~
@@ -1140,6 +1270,17 @@ value Template Output
Using ``floatformat`` with no argument is equivalent to using ``floatformat``
with an argument of ``-1``.
+force_escape
+~~~~~~~~~~~~
+
+**New in Django development version**
+
+Applies HTML escaping to a string (see the ``escape`` filter for details).
+This filter is applied *immediately* and returns a new, escaped string. This
+is useful in the rare cases where you need multiple escaping or want to apply
+other filters to the escaped results. Normally, you want to use the ``escape``
+filter.
+
get_digit
~~~~~~~~~
@@ -1264,6 +1405,12 @@ Right-aligns the value in a field of a given width.
**Argument:** field size
+safe
+~~~~
+
+Marks a string as not requiring further HTML escaping prior to output. When
+autoescaping is off, this filter has no effect.
+
slice
~~~~~
diff --git a/docs/templates_python.txt b/docs/templates_python.txt
index bd105888ce..e4658f6461 100644
--- a/docs/templates_python.txt
+++ b/docs/templates_python.txt
@@ -219,13 +219,13 @@ be replaced with the name of the invalid variable.
While ``TEMPLATE_STRING_IF_INVALID`` can be a useful debugging tool,
it is a bad idea to turn it on as a 'development default'.
-
+
Many templates, including those in the Admin site, rely upon the
silence of the template system when a non-existent variable is
encountered. If you assign a value other than ``''`` to
``TEMPLATE_STRING_IF_INVALID``, you will experience rendering
problems with these templates and sites.
-
+
Generally, ``TEMPLATE_STRING_IF_INVALID`` should only be enabled
in order to debug a specific template problem, then cleared
once debugging is complete.
@@ -722,6 +722,95 @@ decorator instead::
If you leave off the ``name`` argument, as in the second example above, Django
will use the function's name as the filter name.
+Filters and auto-escaping
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**New in Django development version**
+
+When you are writing a custom filter, you need to give some thought to how
+this filter will interact with Django's auto-escaping behaviour. Firstly, you
+should realise that there are three types of strings that can be passed around
+inside the template code:
+
+ * raw strings are the native Python ``str`` or ``unicode`` types. On
+ output, they are escaped if auto-escaping is in effect and presented
+ unchanged, otherwise.
+
+ * "safe" strings are strings that are safe from further escaping at output
+ time. Any necessary escaping has already been done. They are commonly used
+ for output that contains raw HTML that is intended to be intrepreted on the
+ client side.
+
+ Internally, these strings are of type ``SafeString`` or ``SafeUnicode``,
+ although they share a common base class in ``SafeData``, so you can test
+ for them using code like::
+
+ if isinstance(value, SafeData):
+ # Do something with the "safe" string.
+
+ * strings which are marked as "needing escaping" are *always* escaped on
+ output, regardless of whether they are in an ``autoescape`` block or not.
+ These strings are only escaped once, however, even if auto-escaping
+ applies. This type of string is internally represented by the types
+ ``EscapeString`` and ``EscapeUnicode``. You will not normally need to worry
+ about these; they exist for the implementation of the ``escape`` filter.
+
+Inside your filter, you will need to think about three areas in order to be
+auto-escaping compliant:
+
+ 1. If your filter returns a string that is ready for direct output (it should
+ be considered a "safe" string), you should call
+ ``django.utils.safestring.mark_safe()`` on the result prior to returning.
+ This will turn the result into the appropriate ``SafeData`` type. This is
+ often the case when you are returning raw HTML, for example.
+
+ 2. If your filter is given a "safe" string, is it guaranteed to return a
+ "safe" string? If so, set the ``is_safe`` attribute on the function to be
+ ``True``. For example, a filter that replaced a word consisting only of
+ digits with the number spelt out in words is going to be
+ safe-string-preserving, since it cannot introduce any of the five dangerous
+ characters: <, >, ", ' or &. We can write::
+
+ @register.filter
+ def convert_to_words(value):
+ # ... implementation here ...
+ return result
+
+ convert_to_words.is_safe = True
+
+ Note that this filter does not return a universally safe result (it does
+ not return ``mark_safe(result)``) because if it is handed a raw string such
+ as '<a>', this will need further escaping in an auto-escape environment.
+ The ``is_safe`` attribute only talks about the the result when a safe
+ string is passed into the filter.
+
+ 3. Will your filter behave differently depending upon whether auto-escaping
+ is currently in effect or not? This is normally a concern when you are
+ returning mixed content (HTML elements mixed with user-supplied content).
+ For example, the ``ordered_list`` filter that ships with Django needs to
+ know whether to escape its content or not. It will always return a safe
+ string. Since it returns raw HTML, we cannot apply escaping to the
+ result -- it needs to be done in-situ.
+
+ For these cases, the filter function needs to be told what the current
+ auto-escaping setting is. Set the ``needs_autoescape`` attribute on the
+ filter to ``True`` and have your function take an extra argument called
+ ``autoescape`` with a default value of ``None``. When the filter is called,
+ the ``autoescape`` keyword argument will be ``True`` if auto-escaping is in
+ effect. For example, the ``unordered_list`` filter is written as::
+
+ def unordered_list(value, autoescape=None):
+ # ... lots of code here ...
+
+ return mark_safe(...)
+
+ unordered_list.is_safe = True
+ unordered_list.needs_autoescape = True
+
+By default, both the ``is_safe`` and ``needs_autoescape`` attributes are
+``False``. You do not need to specify them if ``False`` is an acceptable
+value.
+
Writing custom template tags
----------------------------
@@ -840,6 +929,43 @@ Ultimately, this decoupling of compilation and rendering results in an
efficient template system, because a template can render multiple context
without having to be parsed multiple times.
+Auto-escaping considerations
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The output from template tags is not automatically run through the
+auto-escaping filters. However, there are still a couple of things you should
+keep in mind when writing a template tag:
+
+If the ``render()`` function of your template stores the result in a context
+variable (rather than returning the result in a string), it should take care
+to call ``mark_safe()`` if appropriate. When the variable is ultimately
+rendered, it will be affected by the auto-escape setting in effect at the
+time, so content that should be safe from further escaping needs to be marked
+as such.
+
+Also, if your template tag creates a new context for performing some
+sub-rendering, you should be careful to set the auto-escape attribute to the
+current context's value. The ``__init__`` method for the ``Context`` class
+takes a parameter called ``autoescape`` that you can use for this purpose. For
+example::
+
+ def render(self, context):
+ # ...
+ new_context = Context({'var': obj}, autoescape=context.autoescape)
+ # ... Do something with new_context ...
+
+This is not a very common situation, but it is sometimes useful, particularly
+if you are rendering a template yourself. For example::
+
+ def render(self, context):
+ t = template.load_template('small_fragment.html')
+ return t.render(Context({'var': obj}, autoescape=context.autoescape))
+
+If we had neglected to pass in the current ``context.autoescape`` value to our
+new ``Context`` in this example, the results would have *always* been
+automatically escaped, which may not be the desired behaviour if the template
+tag is used inside a ``{% autoescape off %}`` block.
+
Registering the tag
~~~~~~~~~~~~~~~~~~~
@@ -917,7 +1043,7 @@ current context, available in the ``render`` method::
def __init__(self, date_to_be_formatted, format_string):
self.date_to_be_formatted = date_to_be_formatted
self.format_string = format_string
-
+
def render(self, context):
try:
actual_date = resolve_variable(self.date_to_be_formatted, context)
@@ -934,26 +1060,26 @@ format it accordingly.
``template.resolve_variable()`` is still available, but has been deprecated
in favor of a new ``template.Variable`` class. Using this class will usually
be more efficient than calling ``template.resolve_variable``
-
+
To use the ``Variable`` class, simply instantiate it with the name of the
variable to be resolved, and then call ``variable.resolve(context)``. So,
in the development version, the above example would be more correctly
written as:
-
+
.. parsed-literal::
-
+
class FormatTimeNode(template.Node):
def __init__(self, date_to_be_formatted, format_string):
self.date_to_be_formatted = **Variable(date_to_be_formatted)**
self.format_string = format_string
-
+
def render(self, context):
try:
actual_date = **self.date_to_be_formatted.resolve(context)**
return actual_date.strftime(self.format_string)
except template.VariableDoesNotExist:
return ''
-
+
Changes are highlighted in bold.
Variable resolution will throw a ``VariableDoesNotExist`` exception if it cannot
diff --git a/tests/regressiontests/defaultfilters/tests.py b/tests/regressiontests/defaultfilters/tests.py
index 270642d4a0..26d448900d 100644
--- a/tests/regressiontests/defaultfilters/tests.py
+++ b/tests/regressiontests/defaultfilters/tests.py
@@ -194,10 +194,10 @@ u'a stri to be maled'
>>> cut(u'a string to be mangled', 'strings')
u'a string to be mangled'
->>> escape(u'<some html & special characters > here')
+>>> force_escape(u'<some html & special characters > here')
u'&lt;some html &amp; special characters &gt; here'
->>> escape(u'<some html & special characters > here ĐÅ€£')
+>>> force_escape(u'<some html & special characters > here ĐÅ€£')
u'&lt;some html &amp; special characters &gt; here \xc4\x90\xc3\x85\xe2\x82\xac\xc2\xa3'
>>> linebreaks(u'line 1')
diff --git a/tests/regressiontests/forms/forms.py b/tests/regressiontests/forms/forms.py
index ed88e3a6bb..7c0cf8abf3 100644
--- a/tests/regressiontests/forms/forms.py
+++ b/tests/regressiontests/forms/forms.py
@@ -1554,7 +1554,7 @@ does not have help text, nothing will be output.
... </form>''')
>>> print t.render(Context({'form': UserRegistration(auto_id=False)}))
<form action="">
-<p>Username: <input type="text" name="username" maxlength="10" /><br />Good luck picking a username that doesn't already exist.</p>
+<p>Username: <input type="text" name="username" maxlength="10" /><br />Good luck picking a username that doesn&#39;t already exist.</p>
<p>Password1: <input type="password" name="password1" /></p>
<p>Password2: <input type="password" name="password2" /></p>
<input type="submit" />
diff --git a/tests/regressiontests/forms/tests.py b/tests/regressiontests/forms/tests.py
index 333b928700..53c3fc4b17 100644
--- a/tests/regressiontests/forms/tests.py
+++ b/tests/regressiontests/forms/tests.py
@@ -52,7 +52,7 @@ __test__ = {
'localflavor_sk_tests': localflavor_sk_tests,
'localflavor_uk_tests': localflavor_uk_tests,
'localflavor_us_tests': localflavor_us_tests,
- 'regressions_tests': regression_tests,
+ 'regression_tests': regression_tests,
'formset_tests': formset_tests,
'media_tests': media_tests,
'util_tests': util_tests,
diff --git a/tests/regressiontests/humanize/tests.py b/tests/regressiontests/humanize/tests.py
index 196488ba6e..6f60c6d6f9 100644
--- a/tests/regressiontests/humanize/tests.py
+++ b/tests/regressiontests/humanize/tests.py
@@ -3,6 +3,7 @@ from datetime import timedelta, date
from django.template import Template, Context, add_to_builtins
from django.utils.dateformat import DateFormat
from django.utils.translation import ugettext as _
+from django.utils.html import escape
add_to_builtins('django.contrib.humanize.templatetags.humanize')
@@ -15,7 +16,7 @@ class HumanizeTests(unittest.TestCase):
test_content = test_list[index]
t = Template('{{ test_content|%s }}' % method)
rendered = t.render(Context(locals())).strip()
- self.assertEqual(rendered, result_list[index],
+ self.assertEqual(rendered, escape(result_list[index]),
msg="%s test failed, produced %s, should've produced %s" % (method, rendered, result_list[index]))
def test_ordinal(self):
diff --git a/tests/regressiontests/templates/filters.py b/tests/regressiontests/templates/filters.py
new file mode 100644
index 0000000000..5d7129480c
--- /dev/null
+++ b/tests/regressiontests/templates/filters.py
@@ -0,0 +1,220 @@
+# coding: utf-8
+"""
+Tests for template filters (as opposed to template tags).
+
+The tests are hidden inside a function so that things like timestamps and
+timezones are only evaluated at the moment of execution and will therefore be
+consistent.
+"""
+
+from datetime import datetime, timedelta
+
+from django.utils.tzinfo import LocalTimezone
+from django.utils.safestring import mark_safe
+
+# RESULT SYNTAX --
+# 'template_name': ('template contents', 'context dict',
+# 'expected string output' or Exception class)
+def get_filter_tests():
+ now = datetime.now()
+ now_tz = datetime.now(LocalTimezone(now))
+ return {
+ # Default compare with datetime.now()
+ 'filter-timesince01' : ('{{ a|timesince }}', {'a': datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'),
+ 'filter-timesince02' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(days=1, minutes = 1)}, '1 day'),
+ 'filter-timesince03' : ('{{ a|timesince }}', {'a': datetime.now() - timedelta(hours=1, minutes=25, seconds = 10)}, '1 hour, 25 minutes'),
+
+ # Compare to a given parameter
+ 'filter-timesince04' : ('{{ a|timesince:b }}', {'a':now + timedelta(days=2), 'b':now + timedelta(days=1)}, '1 day'),
+ 'filter-timesince05' : ('{{ a|timesince:b }}', {'a':now + timedelta(days=2, minutes=1), 'b':now + timedelta(days=2)}, '1 minute'),
+
+ # Check that timezone is respected
+ 'filter-timesince06' : ('{{ a|timesince:b }}', {'a':now_tz + timedelta(hours=8), 'b':now_tz}, '8 hours'),
+
+ # Default compare with datetime.now()
+ 'filter-timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'),
+ 'filter-timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'),
+ 'filter-timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'),
+
+ # Compare to a given parameter
+ 'filter-timeuntil04' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=1), 'b':now - timedelta(days=2)}, '1 day'),
+ 'filter-timeuntil05' : ('{{ a|timeuntil:b }}', {'a':now - timedelta(days=2), 'b':now - timedelta(days=2, minutes=1)}, '1 minute'),
+
+ 'filter-addslash01': ("{% autoescape off %}{{ a|addslashes }} {{ b|addslashes }}{% endautoescape %}", {"a": "<a>'", "b": mark_safe("<a>'")}, ur"<a>\' <a>\'"),
+ 'filter-addslash02': ("{{ a|addslashes }} {{ b|addslashes }}", {"a": "<a>'", "b": mark_safe("<a>'")}, ur"&lt;a&gt;\&#39; <a>\'"),
+
+ 'filter-capfirst01': ("{% autoescape off %}{{ a|capfirst }} {{ b|capfirst }}{% endautoescape %}", {"a": "fred>", "b": mark_safe("fred&gt;")}, u"Fred> Fred&gt;"),
+ 'filter-capfirst02': ("{{ a|capfirst }} {{ b|capfirst }}", {"a": "fred>", "b": mark_safe("fred&gt;")}, u"Fred&gt; Fred&gt;"),
+
+ # Note that applying fix_ampsersands in autoescape mode leads to
+ # double escaping.
+ 'filter-fix_ampersands01': ("{% autoescape off %}{{ a|fix_ampersands }} {{ b|fix_ampersands }}{% endautoescape %}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&amp;b a&amp;b"),
+ 'filter-fix_ampersands02': ("{{ a|fix_ampersands }} {{ b|fix_ampersands }}", {"a": "a&b", "b": mark_safe("a&b")}, u"a&amp;amp;b a&amp;b"),
+
+ 'filter-floatformat01': ("{% autoescape off %}{{ a|floatformat }} {{ b|floatformat }}{% endautoescape %}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"),
+ 'filter-floatformat02': ("{{ a|floatformat }} {{ b|floatformat }}", {"a": "1.42", "b": mark_safe("1.42")}, u"1.4 1.4"),
+
+ # The contents of "linenumbers" is escaped according to the current
+ # autoescape setting.
+ 'filter-linenumbers01': ("{{ a|linenumbers }} {{ b|linenumbers }}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n&lt;two&gt;\nthree")}, u"1. one\n2. &lt;two&gt;\n3. three 1. one\n2. &lt;two&gt;\n3. three"),
+ 'filter-linenumbers02': ("{% autoescape off %}{{ a|linenumbers }} {{ b|linenumbers }}{% endautoescape %}", {"a": "one\n<two>\nthree", "b": mark_safe("one\n&lt;two&gt;\nthree")}, u"1. one\n2. <two>\n3. three 1. one\n2. &lt;two&gt;\n3. three"),
+
+ 'filter-lower01': ("{% autoescape off %}{{ a|lower }} {{ b|lower }}{% endautoescape %}", {"a": "Apple & banana", "b": mark_safe("Apple &amp; banana")}, u"apple & banana apple &amp; banana"),
+ 'filter-lower02': ("{{ a|lower }} {{ b|lower }}", {"a": "Apple & banana", "b": mark_safe("Apple &amp; banana")}, u"apple &amp; banana apple &amp; banana"),
+
+ # The make_list filter can destroy existing escaping, so the results are
+ # escaped.
+ 'filter-make_list01': ("{% autoescape off %}{{ a|make_list }}{% endautoescape %}", {"a": mark_safe("&")}, u"[u'&']"),
+ 'filter-make_list02': ("{{ a|make_list }}", {"a": mark_safe("&")}, u"[u&#39;&amp;&#39;]"),
+ 'filter-make_list03': ('{% autoescape off %}{{ a|make_list|stringformat:"s"|safe }}{% endautoescape %}', {"a": mark_safe("&")}, u"[u'&']"),
+ 'filter-make_list04': ('{{ a|make_list|stringformat:"s"|safe }}', {"a": mark_safe("&")}, u"[u'&']"),
+
+ # Running slugify on a pre-escaped string leads to odd behaviour,
+ # but the result is still safe.
+ 'filter-slugify01': ("{% autoescape off %}{{ a|slugify }} {{ b|slugify }}{% endautoescape %}", {"a": "a & b", "b": mark_safe("a &amp; b")}, u"a-b a-amp-b"),
+ 'filter-slugify02': ("{{ a|slugify }} {{ b|slugify }}", {"a": "a & b", "b": mark_safe("a &amp; b")}, u"a-b a-amp-b"),
+
+ # Notice that escaping is applied *after* any filters, so the string
+ # formatting here only needs to deal with pre-escaped characters.
+ 'filter-stringformat01': ('{% autoescape off %}.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.{% endautoescape %}', {"a": "a<b", "b": mark_safe("a<b")}, u". a<b. . a<b."),
+ 'filter-stringformat02': ('.{{ a|stringformat:"5s" }}. .{{ b|stringformat:"5s" }}.', {"a": "a<b", "b": mark_safe("a<b")}, u". a&lt;b. . a<b."),
+
+ # XXX No test for "title" filter; needs an actual object.
+
+ 'filter-truncatewords01': ('{% autoescape off %}{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}{% endautoescape %}', {"a": "alpha & bravo", "b": mark_safe("alpha &amp; bravo")}, u"alpha & ... alpha &amp; ..."),
+ 'filter-truncatewords02': ('{{ a|truncatewords:"2" }} {{ b|truncatewords:"2"}}', {"a": "alpha & bravo", "b": mark_safe("alpha &amp; bravo")}, u"alpha &amp; ... alpha &amp; ..."),
+
+ # The "upper" filter messes up entities (which are case-sensitive),
+ # so it's not safe for non-escaping purposes.
+ 'filter-upper01': ('{% autoescape off %}{{ a|upper }} {{ b|upper }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a &amp; b")}, u"A & B A &AMP; B"),
+ 'filter-upper02': ('{{ a|upper }} {{ b|upper }}', {"a": "a & b", "b": mark_safe("a &amp; b")}, u"A &amp; B A &amp;AMP; B"),
+
+ 'filter-urlize01': ('{% autoescape off %}{{ a|urlize }} {{ b|urlize }}{% endautoescape %}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/x=&y=" rel="nofollow">http://example.com/x=&y=</a> <a href="http://example.com?x=&y=" rel="nofollow">http://example.com?x=&y=</a>'),
+ 'filter-urlize02': ('{{ a|urlize }} {{ b|urlize }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/x=&y=" rel="nofollow">http://example.com/x=&amp;y=</a> <a href="http://example.com?x=&y=" rel="nofollow">http://example.com?x=&y=</a>'),
+ 'filter-urlize03': ('{% autoescape off %}{{ a|urlize }}{% endautoescape %}', {"a": mark_safe("a &amp; b")}, 'a &amp; b'),
+ 'filter-urlize04': ('{{ a|urlize }}', {"a": mark_safe("a &amp; b")}, 'a &amp; b'),
+
+ 'filter-urlizetrunc01': ('{% autoescape off %}{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}{% endautoescape %}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/x=&y=" rel="nofollow">http:...</a> <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'),
+ 'filter-urlizetrunc02': ('{{ a|urlizetrunc:"8" }} {{ b|urlizetrunc:"8" }}', {"a": "http://example.com/x=&y=", "b": mark_safe("http://example.com?x=&y=")}, u'<a href="http://example.com/x=&y=" rel="nofollow">http:...</a> <a href="http://example.com?x=&y=" rel="nofollow">http:...</a>'),
+
+ 'filter-wordcount01': ('{% autoescape off %}{{ a|wordcount }} {{ b|wordcount }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a &amp; b")}, "3 3"),
+ 'filter-wordcount02': ('{{ a|wordcount }} {{ b|wordcount }}', {"a": "a & b", "b": mark_safe("a &amp; b")}, "3 3"),
+
+ 'filter-wordwrap01': ('{% autoescape off %}{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}{% endautoescape %}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &\nb a &\nb"),
+ 'filter-wordwrap02': ('{{ a|wordwrap:"3" }} {{ b|wordwrap:"3" }}', {"a": "a & b", "b": mark_safe("a & b")}, u"a &amp;\nb a &\nb"),
+
+ 'filter-ljust01': ('{% autoescape off %}.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u".a&b . .a&b ."),
+ 'filter-ljust02': ('.{{ a|ljust:"5" }}. .{{ b|ljust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u".a&amp;b . .a&b ."),
+
+ 'filter-rjust01': ('{% autoescape off %}.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b. . a&b."),
+ 'filter-rjust02': ('.{{ a|rjust:"5" }}. .{{ b|rjust:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&amp;b. . a&b."),
+
+ 'filter-center01': ('{% autoescape off %}.{{ a|center:"5" }}. .{{ b|center:"5" }}.{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, u". a&b . . a&b ."),
+ 'filter-center02': ('.{{ a|center:"5" }}. .{{ b|center:"5" }}.', {"a": "a&b", "b": mark_safe("a&b")}, u". a&amp;b . . a&b ."),
+
+ 'filter-cut01': ('{% autoescape off %}{{ a|cut:"x" }} {{ b|cut:"x" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&amp;y")}, u"&y &amp;y"),
+ 'filter-cut02': ('{{ a|cut:"x" }} {{ b|cut:"x" }}', {"a": "x&y", "b": mark_safe("x&amp;y")}, u"&amp;y &amp;y"),
+ 'filter-cut03': ('{% autoescape off %}{{ a|cut:"&" }} {{ b|cut:"&" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&amp;y")}, u"xy xamp;y"),
+ 'filter-cut04': ('{{ a|cut:"&" }} {{ b|cut:"&" }}', {"a": "x&y", "b": mark_safe("x&amp;y")}, u"xy xamp;y"),
+ # Passing ';' to cut can break existing HTML entities, so those strings
+ # are auto-escaped.
+ 'filter-cut05': ('{% autoescape off %}{{ a|cut:";" }} {{ b|cut:";" }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&amp;y")}, u"x&y x&ampy"),
+ 'filter-cut06': ('{{ a|cut:";" }} {{ b|cut:";" }}', {"a": "x&y", "b": mark_safe("x&amp;y")}, u"x&amp;y x&amp;ampy"),
+
+ # The "escape" filter works the same whether autoescape is on or off,
+ # but it has no effect on strings already marked as safe.
+ 'filter-escape01': ('{{ a|escape }} {{ b|escape }}', {"a": "x&y", "b": mark_safe("x&y")}, u"x&amp;y x&y"),
+ 'filter-escape02': ('{% autoescape off %}{{ a|escape }} {{ b|escape }}{% endautoescape %}', {"a": "x&y", "b": mark_safe("x&y")}, "x&amp;y x&y"),
+
+ # It is only applied once, regardless of the number of times it
+ # appears in a chain.
+ 'filter-escape03': ('{% autoescape off %}{{ a|escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"),
+ 'filter-escape04': ('{{ a|escape|escape }}', {"a": "x&y"}, u"x&amp;y"),
+
+ # Force_escape is applied immediately. It can be used to provide
+ # double-escaping, for example.
+ 'filter-force-escape01': ('{% autoescape off %}{{ a|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"),
+ 'filter-force-escape02': ('{{ a|force_escape }}', {"a": "x&y"}, u"x&amp;y"),
+ 'filter-force-escape03': ('{% autoescape off %}{{ a|force_escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;amp;y"),
+ 'filter-force-escape04': ('{{ a|force_escape|force_escape }}', {"a": "x&y"}, u"x&amp;amp;y"),
+
+ # Because the result of force_escape is "safe", an additional
+ # escape filter has no effect.
+ 'filter-force-escape05': ('{% autoescape off %}{{ a|force_escape|escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"),
+ 'filter-force-escape06': ('{{ a|force_escape|escape }}', {"a": "x&y"}, u"x&amp;y"),
+ 'filter-force-escape07': ('{% autoescape off %}{{ a|escape|force_escape }}{% endautoescape %}', {"a": "x&y"}, u"x&amp;y"),
+ 'filter-force-escape07': ('{{ a|escape|force_escape }}', {"a": "x&y"}, u"x&amp;y"),
+
+ # The contents in "linebreaks" and "linebreaksbr" are escaped
+ # according to the current autoescape setting.
+ 'filter-linebreaks01': ('{{ a|linebreaks }} {{ b|linebreaks }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"<p>x&amp;<br />y</p> <p>x&<br />y</p>"),
+ 'filter-linebreaks02': ('{% autoescape off %}{{ a|linebreaks }} {{ b|linebreaks }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"<p>x&<br />y</p> <p>x&<br />y</p>"),
+
+ 'filter-linebreaksbr01': ('{{ a|linebreaksbr }} {{ b|linebreaksbr }}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&amp;<br />y x&<br />y"),
+ 'filter-linebreaksbr02': ('{% autoescape off %}{{ a|linebreaksbr }} {{ b|linebreaksbr }}{% endautoescape %}', {"a": "x&\ny", "b": mark_safe("x&\ny")}, u"x&<br />y x&<br />y"),
+
+ 'filter-safe01': ("{{ a }} -- {{ a|safe }}", {"a": u"<b>hello</b>"}, "&lt;b&gt;hello&lt;/b&gt; -- <b>hello</b>"),
+ 'filter-safe02': ("{% autoescape off %}{{ a }} -- {{ a|safe }}{% endautoescape %}", {"a": "<b>hello</b>"}, u"<b>hello</b> -- <b>hello</b>"),
+
+ 'filter-removetags01': ('{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, u"x &lt;p&gt;y&lt;/p&gt; x <p>y</p>"),
+ 'filter-removetags02': ('{% autoescape off %}{{ a|removetags:"a b" }} {{ b|removetags:"a b" }}{% endautoescape %}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, u"x <p>y</p> x <p>y</p>"),
+
+ 'filter-striptags01': ('{{ a|striptags }} {{ b|striptags }}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x y x y"),
+ 'filter-striptags02': ('{% autoescape off %}{{ a|striptags }} {{ b|striptags }}{% endautoescape %}', {"a": "<a>x</a> <p><b>y</b></p>", "b": mark_safe("<a>x</a> <p><b>y</b></p>")}, "x y x y"),
+
+ '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-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-slice01': ('{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}', {"a": "a&b", "b": mark_safe("a&b")}, "&amp;b &b"),
+ 'filter-slice02': ('{% autoescape off %}{{ a|slice:"1:3" }} {{ b|slice:"1:3" }}{% endautoescape %}', {"a": "a&b", "b": mark_safe("a&b")}, "&b &b"),
+
+ 'filter-unordered_list01': ('{{ a|unordered_list }}', {"a": ["x>", [["<y", []]]]}, "\t<li>x&gt;\n\t<ul>\n\t\t<li>&lt;y</li>\n\t</ul>\n\t</li>"),
+ 'filter-unordered_list02': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"),
+ 'filter-unordered_list03': ('{{ a|unordered_list }}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x&gt;\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"),
+ 'filter-unordered_list04': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [[mark_safe("<y"), []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"),
+ 'filter-unordered_list05': ('{% autoescape off %}{{ a|unordered_list }}{% endautoescape %}', {"a": ["x>", [["<y", []]]]}, "\t<li>x>\n\t<ul>\n\t\t<li><y</li>\n\t</ul>\n\t</li>"),
+
+ # If the input to "default" filter is marked as safe, then so is the
+ # output. However, if the default arg is used, auto-escaping kicks in
+ # (if enabled), because we cannot mark the default as safe.
+ #
+ # Note: we have to use {"a": ""} here, otherwise the invalid template
+ # variable string interferes with the test result.
+ 'filter-default01': ('{{ a|default:"x<" }}', {"a": ""}, "x&lt;"),
+ 'filter-default02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": ""}, "x<"),
+ 'filter-default03': ('{{ a|default:"x<" }}', {"a": mark_safe("x>")}, "x>"),
+ 'filter-default04': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": mark_safe("x>")}, "x>"),
+
+ 'filter-default_if_none01': ('{{ a|default:"x<" }}', {"a": None}, "x&lt;"),
+ 'filter-default_if_none02': ('{% autoescape off %}{{ a|default:"x<" }}{% endautoescape %}', {"a": None}, "x<"),
+
+ 'filter-phone2numeric01': ('{{ a|phone2numeric }} {{ b|phone2numeric }}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "&lt;1-800-2255-63&gt; <1-800-2255-63>"),
+ 'filter-phone2numeric02': ('{% autoescape off %}{{ a|phone2numeric }} {{ b|phone2numeric }}{% endautoescape %}', {"a": "<1-800-call-me>", "b": mark_safe("<1-800-call-me>") }, "<1-800-2255-63> <1-800-2255-63>"),
+
+ # Chaining a bunch of safeness-preserving filters should not alter
+ # the safe status either way.
+ 'chaining01': ('{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}', {"a": "a < b", "b": mark_safe("a < b")}, " A &lt; b . A < b "),
+ 'chaining02': ('{% autoescape off %}{{ a|capfirst|center:"7" }}.{{ b|capfirst|center:"7" }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, " A < b . A < b "),
+
+ # Using a filter that forces a string back to unsafe:
+ 'chaining03': ('{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}', {"a": "a < b", "b": mark_safe("a < b")}, "A &lt; .A < "),
+ 'chaining04': ('{% autoescape off %}{{ a|cut:"b"|capfirst }}.{{ b|cut:"b"|capfirst }}{% endautoescape %}', {"a": "a < b", "b": mark_safe("a < b")}, "A < .A < "),
+
+ # Using a filter that forces safeness does not lead to double-escaping
+ 'chaining05': ('{{ a|escape|capfirst }}', {"a": "a < b"}, "A &lt; b"),
+ 'chaining06': ('{% autoescape off %}{{ a|escape|capfirst }}{% endautoescape %}', {"a": "a < b"}, "A &lt; b"),
+
+ # Force to safe, then back (also showing why using force_escape too
+ # early in a chain can lead to unexpected results).
+ 'chaining07': ('{{ a|force_escape|cut:"b" }}', {"a": "a < b"}, "a &lt; "),
+ 'chaining08': ('{% autoescape off %}{{ a|force_escape|cut:"b" }}{% endautoescape %}', {"a": "a < b"}, "a &lt; "),
+ 'chaining09': ('{{ a|cut:"b"|force_escape }}', {"a": "a < b"}, "a &lt; "),
+ 'chaining10': ('{% autoescape off %}{{ a|cut:"b"|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a &lt; "),
+ 'chaining11': ('{{ a|cut:"b"|safe }}', {"a": "a < b"}, "a < "),
+ 'chaining12': ('{% autoescape off %}{{ a|cut:"b"|safe }}{% endautoescape %}', {"a": "a < b"}, "a < "),
+ 'chaining13': ('{{ a|safe|force_escape }}', {"a": "a < b"}, "a &lt; b"),
+ 'chaining14': ('{% autoescape off %}{{ a|safe|force_escape }}{% endautoescape %}', {"a": "a < b"}, "a &lt; b"),
+ }
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index 60f7d54145..d52e8f0abf 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/tests/regressiontests/templates/tests.py
@@ -14,9 +14,11 @@ from django import template
from django.template import loader
from django.template.loaders import app_directories, filesystem
from django.utils.translation import activate, deactivate, ugettext as _
+from django.utils.safestring import mark_safe
from django.utils.tzinfo import LocalTimezone
from unicode import unicode_tests
+import filters
# Some other tests we would like to run
__test__ = {
@@ -120,20 +122,97 @@ class Templates(unittest.TestCase):
['/dir1/index.html'])
def test_templates(self):
- # NOW and NOW_tz are used by timesince tag tests.
- NOW = datetime.now()
- NOW_tz = datetime.now(LocalTimezone(datetime.now()))
+ template_tests = self.get_template_tests()
+ filter_tests = filters.get_filter_tests()
+ # Quickly check that we aren't accidentally using a name in both
+ # template and filter tests.
+ overlapping_names = [name for name in filter_tests if name in
+ template_tests]
+ assert not overlapping_names, 'Duplicate test name(s): %s' % ', '.join(overlapping_names)
+
+ template_tests.update(filter_tests)
+
+ # Register our custom template loader.
+ def test_template_loader(template_name, template_dirs=None):
+ "A custom template loader that loads the unit-test templates."
+ try:
+ return (template_tests[template_name][0] , "test:%s" % template_name)
+ except KeyError:
+ raise template.TemplateDoesNotExist, template_name
+
+ old_template_loaders = loader.template_source_loaders
+ loader.template_source_loaders = [test_template_loader]
+
+ failures = []
+ tests = template_tests.items()
+ tests.sort()
+
+ # Turn TEMPLATE_DEBUG off, because tests assume that.
+ old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
+
+ # Set TEMPLATE_STRING_IF_INVALID to a known string
+ old_invalid = settings.TEMPLATE_STRING_IF_INVALID
+ expected_invalid_str = 'INVALID'
+
+ for name, vals in tests:
+ if isinstance(vals[2], tuple):
+ normal_string_result = vals[2][0]
+ invalid_string_result = vals[2][1]
+ if '%s' in invalid_string_result:
+ expected_invalid_str = 'INVALID %s'
+ invalid_string_result = invalid_string_result % vals[2][2]
+ template.invalid_var_format_string = True
+ else:
+ normal_string_result = vals[2]
+ invalid_string_result = vals[2]
+
+ if 'LANGUAGE_CODE' in vals[1]:
+ activate(vals[1]['LANGUAGE_CODE'])
+ else:
+ activate('en-us')
+
+ for invalid_str, result in [('', normal_string_result),
+ (expected_invalid_str, invalid_string_result)]:
+ settings.TEMPLATE_STRING_IF_INVALID = invalid_str
+ try:
+ test_template = loader.get_template(name)
+ output = self.render(test_template, vals)
+ except Exception, e:
+ if e.__class__ != result:
+ failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e))
+ continue
+ if output != result:
+ failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output))
+
+ if 'LANGUAGE_CODE' in vals[1]:
+ deactivate()
+
+ if template.invalid_var_format_string:
+ expected_invalid_str = 'INVALID'
+ template.invalid_var_format_string = False
+
+ loader.template_source_loaders = old_template_loaders
+ deactivate()
+ settings.TEMPLATE_DEBUG = old_td
+ settings.TEMPLATE_STRING_IF_INVALID = old_invalid
+
+ self.assertEqual(failures, [], '\n'.join(failures))
+
+ def render(self, test_template, vals):
+ return test_template.render(template.Context(vals[1]))
+
+ def get_template_tests(self):
# SYNTAX --
# 'template_name': ('template contents', 'context dict', 'expected string output' or Exception class)
- TEMPLATE_TESTS = {
-
- ### BASIC SYNTAX ##########################################################
+ return {
+ ### BASIC SYNTAX ################################################
# Plain text should go through the template parser untouched
'basic-syntax01': ("something cool", {}, "something cool"),
- # Variables should be replaced with their value in the current context
+ # Variables should be replaced with their value in the current
+ # context
'basic-syntax02': ("{{ headline }}", {'headline':'Success'}, "Success"),
# More than one replacement variable is allowed in a template
@@ -240,7 +319,8 @@ class Templates(unittest.TestCase):
'filter-syntax09': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"),
# Escaped string as argument
- 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}', {"var": None}, ' endquote" hah'),
+ 'filter-syntax10': (r'{{ var|default_if_none:" endquote\" hah" }}',
+ {"var": None}, ' endquote&quot; hah'),
# Variable as argument
'filter-syntax11': (r'{{ var|default_if_none:var2 }}', {"var": None, "var2": "happy"}, 'happy'),
@@ -760,38 +840,6 @@ class Templates(unittest.TestCase):
# 'now03' : ('{% now "j \"n\" Y"%}', {}, str(datetime.now().day) + '"' + str(datetime.now().month) + '"' + str(datetime.now().year)),
# 'now04' : ('{% now "j \nn\n Y"%}', {}, str(datetime.now().day) + '\n' + str(datetime.now().month) + '\n' + str(datetime.now().year))
- ### TIMESINCE TAG ##################################################
- # Default compare with datetime.now()
- 'timesince01' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=-1, seconds = -10)}, '1 minute'),
- 'timesince02' : ('{{ a|timesince }}', {'a':(datetime.now() - timedelta(days=1, minutes = 1))}, '1 day'),
- 'timesince03' : ('{{ a|timesince }}', {'a':(datetime.now() -
- timedelta(hours=1, minutes=25, seconds = 10))}, '1 hour, 25 minutes'),
-
- # Compare to a given parameter
- 'timesince04' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2), 'b':NOW + timedelta(days=1)}, '1 day'),
- 'timesince05' : ('{{ a|timesince:b }}', {'a':NOW + timedelta(days=2, minutes=1), 'b':NOW + timedelta(days=2)}, '1 minute'),
-
- # Check that timezone is respected
- 'timesince06' : ('{{ a|timesince:b }}', {'a':NOW_tz + timedelta(hours=8), 'b':NOW_tz}, '8 hours'),
-
- # Check times in the future.
- 'timesince07' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(minutes=1, seconds=10)}, '0 minutes'),
- 'timesince08' : ('{{ a|timesince }}', {'a':datetime.now() + timedelta(days=1, minutes=1)}, '0 minutes'),
-
- ### TIMEUNTIL TAG ##################################################
- # Default compare with datetime.now()
- 'timeuntil01' : ('{{ a|timeuntil }}', {'a':datetime.now() + timedelta(minutes=2, seconds = 10)}, '2 minutes'),
- 'timeuntil02' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(days=1, seconds = 10))}, '1 day'),
- 'timeuntil03' : ('{{ a|timeuntil }}', {'a':(datetime.now() + timedelta(hours=8, minutes=10, seconds = 10))}, '8 hours, 10 minutes'),
-
- # Compare to a given parameter
- 'timeuntil04' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=1), 'b':NOW - timedelta(days=2)}, '1 day'),
- 'timeuntil05' : ('{{ a|timeuntil:b }}', {'a':NOW - timedelta(days=2), 'b':NOW - timedelta(days=2, minutes=1)}, '1 minute'),
-
- # Check times in the past.
- 'timeuntil07' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(minutes=1, seconds=10)}, '0 minutes'),
- 'timeuntil08' : ('{{ a|timeuntil }}', {'a':datetime.now() - timedelta(days=1, minutes=1)}, '0 minutes'),
-
### URL TAG ########################################################
# Successes
'url01' : ('{% url regressiontests.templates.views.client client.id %}', {'client': {'id': 1}}, '/url_tag/client/1/'),
@@ -819,72 +867,31 @@ class Templates(unittest.TestCase):
'cache08' : ('{% load cache %}{% cache %}{% endcache %}', {}, template.TemplateSyntaxError),
'cache09' : ('{% load cache %}{% cache 1 %}{% endcache %}', {}, template.TemplateSyntaxError),
'cache10' : ('{% load cache %}{% cache foo bar %}{% endcache %}', {}, template.TemplateSyntaxError),
- }
- # Register our custom template loader.
- def test_template_loader(template_name, template_dirs=None):
- "A custom template loader that loads the unit-test templates."
- try:
- return (TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name)
- except KeyError:
- raise template.TemplateDoesNotExist, template_name
+ ### AUTOESCAPE TAG ##############################################
+ 'autoescape-tag01': ("{% autoescape off %}hello{% endautoescape %}", {}, "hello"),
+ 'autoescape-tag02': ("{% autoescape off %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "<b>hello</b>"),
+ 'autoescape-tag03': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>hello</b>"}, "&lt;b&gt;hello&lt;/b&gt;"),
- old_template_loaders = loader.template_source_loaders
- loader.template_source_loaders = [test_template_loader]
+ # Autoescape disabling and enabling nest in a predictable way.
+ 'autoescape-tag04': ("{% autoescape off %}{{ first }} {% autoescape on%}{{ first }}{% endautoescape %}{% endautoescape %}", {"first": "<a>"}, "<a> &lt;a&gt;"),
- failures = []
- tests = TEMPLATE_TESTS.items()
- tests.sort()
+ 'autoescape-tag05': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": "<b>first</b>"}, "&lt;b&gt;first&lt;/b&gt;"),
- # Turn TEMPLATE_DEBUG off, because tests assume that.
- old_td, settings.TEMPLATE_DEBUG = settings.TEMPLATE_DEBUG, False
+ # Strings (ASCII or unicode) already marked as "safe" are not
+ # auto-escaped
+ 'autoescape-tag06': ("{{ first }}", {"first": mark_safe("<b>first</b>")}, "<b>first</b>"),
+ 'autoescape-tag07': ("{% autoescape on %}{{ first }}{% endautoescape %}", {"first": mark_safe(u"<b>Apple</b>")}, u"<b>Apple</b>"),
- # Set TEMPLATE_STRING_IF_INVALID to a known string
- old_invalid = settings.TEMPLATE_STRING_IF_INVALID
- expected_invalid_str = 'INVALID'
+ # String arguments to filters, if used in the result, are escaped,
+ # too.
+ 'basic-syntax08': (r'{% autoescape on %}{{ var|default_if_none:" endquote\" hah" }}{% endautoescape %}', {"var": None}, ' endquote&quot; hah'),
- for name, vals in tests:
- if isinstance(vals[2], tuple):
- normal_string_result = vals[2][0]
- invalid_string_result = vals[2][1]
- if '%s' in invalid_string_result:
- expected_invalid_str = 'INVALID %s'
- invalid_string_result = invalid_string_result % vals[2][2]
- template.invalid_var_format_string = True
- else:
- normal_string_result = vals[2]
- invalid_string_result = vals[2]
-
- if 'LANGUAGE_CODE' in vals[1]:
- activate(vals[1]['LANGUAGE_CODE'])
- else:
- activate('en-us')
-
- for invalid_str, result in [('', normal_string_result),
- (expected_invalid_str, invalid_string_result)]:
- settings.TEMPLATE_STRING_IF_INVALID = invalid_str
- try:
- output = loader.get_template(name).render(template.Context(vals[1]))
- except Exception, e:
- if e.__class__ != result:
- failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Got %s, exception: %s" % (invalid_str, name, e.__class__, e))
- continue
- if output != result:
- failures.append("Template test (TEMPLATE_STRING_IF_INVALID='%s'): %s -- FAILED. Expected %r, got %r" % (invalid_str, name, result, output))
-
- if 'LANGUAGE_CODE' in vals[1]:
- deactivate()
-
- if template.invalid_var_format_string:
- expected_invalid_str = 'INVALID'
- template.invalid_var_format_string = False
-
- loader.template_source_loaders = old_template_loaders
- deactivate()
- settings.TEMPLATE_DEBUG = old_td
- settings.TEMPLATE_STRING_IF_INVALID = old_invalid
-
- self.assertEqual(failures, [], '\n'.join(failures))
+ # The "safe" and "escape" filters cannot work due to internal
+ # implementation details (fortunately, the (no)autoescape block
+ # tags can be used in those cases)
+ 'autoescape-filtertag01': ("{{ first }}{% filter safe %}{{ first }} x<y{% endfilter %}", {"first": "<a>"}, template.TemplateSyntaxError),
+ }
if __name__ == "__main__":
unittest.main()