[Fixed]-Django grouping queryset by first letter?

22👍

There’s a template tag for this, if all you care about is its presentation on the page. First, define an organizational principle in the class. In your case, it’s the first letter:

class Item(models.Model):
    ...

    def first_letter(self):
        return self.name and self.name[0] or ''

And then define a regroup in the template, using the first_letter call:

{% regroup items by first_letter as letter_list %}
<ul> 
{% for letter in letter_list %}
  <li>{{ letter.grouper }}
    <ul>
        {% for item in letter.list %}
        <li>{{ item.name }}</li>
        {% endfor %}
    </ul>
  </li>
{% endfor %}
</ul>

7👍

Just wanted to add that if you use this and your item has a lower-case first character it will be a separate group. I added upper to it.

return self.name and self.name.upper()[0] or ''
👤Ross

5👍

Alternatively you could use slice inline in the template without the need for a first_letter method on your model.

{% regroup items by name|slice:":1" as letter_list %}
<ul> 
{% for letter in letter_list %}
  <li>{{ letter.grouper }}
    <ul>
        {% for item in letter.list %}
        <li>{{ item.name }}</li>
        {% endfor %}
    </ul>
  </li>
{% endfor %}
</ul>

1👍

Even easier. You can group by first leter just in ‘regroup’:

{% regroup items|dictsort:"name" by name.0 as item_letter %}
<ul>
{% for letter in item_letter %}
    <h4>{{ letter.grouper|title }}</h4>
    {% for i in letter.list|dictsort:"name" %}
        <li>{{ i.name }}</li>
    {% endfor %}
{% empty %}
    <span>There is no items yet...</span>
{% endfor %}
</ul>

name.0 in this case the same as item.name[0] in Python.

Tested in Django 1.10

1👍

For Django REST you can do like this,

import string
import collections

from rest_framework.response import Response
from rest_framework import status, viewsets

def groupby(self, request):
    result = []
    for i in list(string.ascii_uppercase):
        c = City.objects.filter(name__startswith=i)
        if c:
            result.append((i, map((lambda x: x['name']),list(c.values('name')))
            ))
    return Response(collections.OrderedDict(sorted(dict(result).items())), status=status.HTTP_200_OK)

City Models

class City(models.Model):
    """All features model"""

    name = models.CharField(max_length=99)

Response

{
    "A": [
        "Adelanto",
        "Azusa",
        "Alameda",
        "Albany",
        "Alhambra",
        "Anaheim"
    ],
    "B": [
        "Belmont",
        "Berkeley",
        "Beverly Hills",
        "Big Sur",
        "Burbank"
    ],
    ......

}

0👍

This is another take for doing in straight Django and Python. The other solution offered was terribly inefficient

import itertools

collector = {}
item_qs = Item.objects.all().order_by('name')
for alphabet_letter, items in itertools.groupby(item_qs, lambda x: x.name[0].lower()):
    # you can do any modifications you need at this point
    # you can do another loop if you want or a dictionary comprehension
    collector[alphabet_letter] = items  

What does this give you? A single query to the db.

Should I use a collector? No, you should maybe use a yield this is just a proof of concept.

What ever you do, DO NOT add a query call inside a loop.

Leave a comment