diff --git a/django/utils/html.py b/django/utils/html.py
index 25605bea044..ec7b28d330a 100644
--- a/django/utils/html.py
+++ b/django/utils/html.py
@@ -87,8 +87,8 @@ def format_html(format_string, *args, **kwargs):
def format_html_join(sep, format_string, args_generator):
"""
- A wrapper format_html, for the common case of a group of arguments that need
- to be formatted using the same format string, and then joined using
+ A wrapper of format_html, for the common case of a group of arguments that
+ need to be formatted using the same format string, and then joined using
'sep'. 'sep' is also passed through conditional_escape.
'args_generator' should be an iterator that returns the sequence of 'args'
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index ee04d77d321..a862d558757 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -449,17 +449,25 @@ subclass::
* If the string given is a method of the model, ``ModelAdmin`` or a
callable, Django will HTML-escape the output by default. If you'd
rather not escape the output of the method, give the method an
- ``allow_tags`` attribute whose value is ``True``.
+ ``allow_tags`` attribute whose value is ``True``. However, to avoid an
+ XSS vulnerability, you should use :func:`~django.utils.html.format_html`
+ to escape user-provided inputs.
Here's a full example model::
+ from django.utils.html import format_html
+
class Person(models.Model):
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
color_code = models.CharField(max_length=6)
def colored_name(self):
- return '%s %s' % (self.color_code, self.first_name, self.last_name)
+ return format_html('{1} {2}',
+ self.color_code,
+ self.first_name,
+ self.last_name)
+
colored_name.allow_tags = True
class PersonAdmin(admin.ModelAdmin):
@@ -500,12 +508,17 @@ subclass::
For example::
+ from django.utils.html import format_html
+
class Person(models.Model):
first_name = models.CharField(max_length=50)
color_code = models.CharField(max_length=6)
def colored_first_name(self):
- return '%s' % (self.color_code, self.first_name)
+ return format_html('{1}',
+ self.color_code,
+ self.first_name)
+
colored_first_name.allow_tags = True
colored_first_name.admin_order_field = 'first_name'
@@ -817,19 +830,27 @@ subclass::
the admin interface to provide feedback on the status of the objects being
edited, for example::
+ from django.utils.html import format_html_join
+ from django.utils.safestring import mark_safe
+
class PersonAdmin(ModelAdmin):
readonly_fields = ('address_report',)
def address_report(self, instance):
- return ", ".join(instance.get_full_address()) or \
- "I can't determine this address."
+ # assuming get_full_address() returns a list of strings
+ # for each line of the address and you want to separate each
+ # line by a linebreak
+ return format_html_join(
+ mark_safe('
'),
+ '{0}',
+ ((line,) for line in instance.get_full_address()),
+ ) or "I can't determine this address."
# short_description functions like a model field's verbose_name
address_report.short_description = "Address"
# in this example, we have used HTML tags in the output
address_report.allow_tags = True
-
.. attribute:: ModelAdmin.save_as
Set ``save_as`` to enable a "save as" feature on admin change forms.
diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt
index 20192ed006a..e9b29602acf 100644
--- a/docs/ref/utils.txt
+++ b/docs/ref/utils.txt
@@ -541,6 +541,19 @@ escaping HTML.
through :func:`conditional_escape` which (ultimately) calls
:func:`~django.utils.encoding.force_text` on the values.
+.. function:: format_html_join(sep, format_string, args_generator)
+
+ A wrapper of :func:`format_html`, for the common case of a group of
+ arguments that need to be formatted using the same format string, and then
+ joined using ``sep``. ``sep`` is also passed through
+ :func:`conditional_escape`.
+
+ ``args_generator`` should be an iterator that returns the sequence of
+ ``args`` that will be passed to :func:`format_html`. For example::
+
+ format_html_join('\n', "