diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 7372c9414d..bb6e8bf4c2 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import re import sys import warnings +from collections import namedtuple from datetime import datetime from itertools import cycle as itertools_cycle, groupby @@ -335,6 +336,9 @@ class LoremNode(Node): return '\n\n'.join(paras) +GroupedResult = namedtuple('GroupedResult', ['grouper', 'list']) + + class RegroupNode(Node): def __init__(self, target, expression, var_name): self.target, self.expression = target, expression @@ -355,7 +359,7 @@ class RegroupNode(Node): # List of dictionaries in the format: # {'grouper': 'key', 'list': [list of contents]}. context[self.var_name] = [ - {'grouper': key, 'list': list(val)} + GroupedResult(grouper=key, list=list(val)) for key, val in groupby(obj_list, lambda obj: self.resolve_expression(obj, context)) ] diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index 1a8fe600ac..9a5bd4abba 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -896,13 +896,36 @@ resulting list. Here, we're regrouping the ``cities`` list by the ``country`` attribute and calling the result ``country_list``. ``{% regroup %}`` produces a list (in this case, ``country_list``) of -**group objects**. Each group object has two attributes: +**group objects**. Group objects are instances of +:py:func:`~collections.namedtuple` with two fields: * ``grouper`` -- the item that was grouped by (e.g., the string "India" or "Japan"). * ``list`` -- a list of all items in this group (e.g., a list of all cities with country='India'). +.. versionchanged:: 1.11 + + The group object was changed from a dictionary to a + :py:func:`~collections.namedtuple`. + +Because ``{% regroup %}`` produces :py:func:`~collections.namedtuple` objects, +you can also write the previous example as:: + + {% regroup cities by country as country_list %} + + + Note that ``{% regroup %}`` does not order its input! Our example relies on the fact that the ``cities`` list was ordered by ``country`` in the first place. If the ``cities`` list did *not* order its members by ``country``, the diff --git a/docs/releases/1.11.txt b/docs/releases/1.11.txt index c8282f70d6..67c247c86e 100644 --- a/docs/releases/1.11.txt +++ b/docs/releases/1.11.txt @@ -300,6 +300,10 @@ Templates supports context processors by setting the ``'context_processors'`` option in :setting:`OPTIONS `. +* The :ttag:`regroup` tag now returns ``namedtuple``\s instead of dictionaries + so you can unpack the group object directly in a loop, e.g. + ``{% for grouper, list in regrouped %}``. + Tests ~~~~~ diff --git a/tests/template_tests/syntax_tests/test_regroup.py b/tests/template_tests/syntax_tests/test_regroup.py index d747c016f8..2c0135587e 100644 --- a/tests/template_tests/syntax_tests/test_regroup.py +++ b/tests/template_tests/syntax_tests/test_regroup.py @@ -100,3 +100,22 @@ class RegroupTagTests(SimpleTestCase): def test_regroup08(self): with self.assertRaises(TemplateSyntaxError): self.engine.get_template('regroup08') + + @setup({'regroup_unpack': '{% regroup data by bar as grouped %}' + '{% for grouper, group in grouped %}' + '{{ grouper }}:' + '{% for item in group %}' + '{{ item.foo }}' + '{% endfor %},' + '{% endfor %}'}) + def test_regroup_unpack(self): + output = self.engine.render_to_string('regroup_unpack', { + 'data': [ + {'foo': 'c', 'bar': 1}, + {'foo': 'd', 'bar': 1}, + {'foo': 'a', 'bar': 2}, + {'foo': 'b', 'bar': 2}, + {'foo': 'x', 'bar': 3}, + ], + }) + self.assertEqual(output, '1:cd,2:ab,3:x,')