Comma separated lists in django templates

Question:

If fruits is the list ['apples', 'oranges', 'pears'],

is there a quick way using django template tags to produce “apples, oranges, and pears”?

I know it’s not difficult to do this using a loop and {% if counter.last %} statements, but because I’m going to use this repeatedly I think I’m going to have to learn how to write custom tags filters, and I don’t want to reinvent the wheel if it’s already been done.

As an extension, my attempts to drop the Oxford Comma (ie return “apples, oranges and pears”) are even messier.

Asked By: Alasdair

||

Answers:

First choice: use the existing join template tag.

http://docs.djangoproject.com/en/dev/ref/templates/builtins/#join

Here’s their example

{{ value|join:" // " }}

Second choice: do it in the view.

fruits_text = ", ".join( fruits )

Provide fruits_text to the template for rendering.

Answered By: S.Lott

I would suggest a custom django templating filter rather than a custom tag — filter is handier and simpler (where appropriate, like here). {{ fruits | joinby:", " }} looks like what I’d want to have for the purpose… with a custom joinby filter:

def joinby(value, arg):
    return arg.join(value)

which as you see is simplicity itself!

Answered By: Alex Martelli

Here’s the filter I wrote to solve my problem (it doesn’t include the Oxford comma)

def join_with_commas(obj_list):
    """Takes a list of objects and returns their string representations,
    separated by commas and with 'and' between the penultimate and final items
    For example, for a list of fruit objects:
    [<Fruit: apples>, <Fruit: oranges>, <Fruit: pears>] -> 'apples, oranges and pears'
    """
    if not obj_list:
        return ""
    l=len(obj_list)
    if l==1:
        return u"%s" % obj_list[0]
    else:    
        return ", ".join(str(obj) for obj in obj_list[:l-1]) 
                + " and " + str(obj_list[l-1])

To use it in the template: {{ fruits|join_with_commas }}

Answered By: Alasdair

Here’s a super simple solution. Put this code into comma.html:

{% if not forloop.last %}{% ifequal forloop.revcounter 2 %} and {% else %}, {% endifequal %}{% else %}{% endif %}

And now wherever you’d put the comma, include “comma.html” instead:

{% for cat in cats %}
Kitty {{cat.name}}{% include "comma.html" %}
{% endfor %}

Update: @user3748764 gives us a slightly more compact version, without the deprecated ifequal syntax:

{% if not forloop.first %}{% if forloop.last %} and {% else %}, {% endif %}{% endif %}

Note that it should be used before the element, not after.

If you want a ‘.’ on the end of Michael Matthew Toomim’s answer, then use:

{% if not forloop.last %}{% ifequal forloop.revcounter 2 %} and {% else %}, {% endifequal %}{% else %}{% endif %}{% if forloop.last %}.{% endif %}
Answered By: Todd Davies

Django doesn’t have support for this out-of-the-box. You can define a custom filter for this:

from django import template


register = template.Library()


@register.filter
def join_and(value):
    """Given a list of strings, format them with commas and spaces, but
    with 'and' at the end.

    >>> join_and(['apples', 'oranges', 'pears'])
    "apples, oranges, and pears"

    """
    # convert numbers to strings
    value = [str(item) for item in value]

    if len(value) == 1:
        return value[0]

    # join all but the last element
    all_but_last = ", ".join(value[:-1])
    return "%s, and %s" % (all_but_last, value[-1])

However, if you want to deal with something more complex than just lists of strings, you’ll have to use an explicit {% for x in y %} loop in your template.

Answered By: Wilfred Hughes

If you like one-liners:

@register.filter
def lineup(ls): return ', '.join(ls[:-1])+' and '+ls[-1] if len(ls)>1 else ls[0]

and then in the template:

{{ fruits|lineup }}
Answered By: fmalina

On the Django template this all you need to do for establishing a comma after each fruit. The comma will stop once its reached the last fruit.

{% if not forloop.last %}, {% endif %}
Answered By: Tommygun

I would simply use ', '.join(['apples', 'oranges', 'pears']) before sending it to the template as a context data.

UPDATE:

data = ['apples', 'oranges', 'pears']
print(', '.join(data[0:-1]) + ' and ' + data[-1])

You will get apples, oranges and pears output.

Answered By: yigidix

All of the answers here fail one or more of the following:

  • They rewrite something (poorly!) that’s in the standard template library (ack, top answer!)
  • They don’t use and for the last item.
  • They lack a serial (oxford) comma.
  • They use negative indexing, which won’t work for django querysets.
  • They don’t usually handle string sanitation properly.

Here’s my entry into this canon. First, the tests:

class TestTextFilters(TestCase):

    def test_oxford_zero_items(self):
        self.assertEqual(oxford_comma([]), '')

    def test_oxford_one_item(self):
        self.assertEqual(oxford_comma(['a']), 'a')

    def test_oxford_two_items(self):
        self.assertEqual(oxford_comma(['a', 'b']), 'a and b')

    def test_oxford_three_items(self):
        self.assertEqual(oxford_comma(['a', 'b', 'c']), 'a, b, and c')

And now the code. Yes, it gets a bit messy, but you’ll see that it doesn’t use negative indexing:

from django.utils.encoding import force_text
from django.utils.html import conditional_escape
from django.utils.safestring import mark_safe

@register.filter(is_safe=True, needs_autoescape=True)
def oxford_comma(l, autoescape=True):
    """Join together items in a list, separating them with commas or ', and'"""
    l = map(force_text, l)
    if autoescape:
        l = map(conditional_escape, l)

    num_items = len(l)
    if num_items == 0:
        s = ''
    elif num_items == 1:
        s = l[0]
    elif num_items == 2:
        s = l[0] + ' and ' + l[1]
    elif num_items > 2:
        for i, item in enumerate(l):
            if i == 0:
                # First item
                s = item
            elif i == (num_items - 1):
                # Last item.
                s += ', and ' + item
            else:
                # Items in the middle
                s += ', ' + item

    return mark_safe(s)

You can use this in a django template with:

{% load my_filters %}
{{ items|oxford_comma }}
Answered By: mlissner

I think the simplest solution might be:

@register.filter
def comma_list(p_values: Iterable[str]) -> List[str]:
    values = list(p_values)
    if len(values) > 1:
        values[-1] = u'and %s' % values[-1]
    if len(values) > 2:
        return u', '.join(values)
    return u' '.join(values)

Answered By: mannysz