Django grouping queryset by first letter?

2020-02-03 07:11发布

问题:

I have a QuerySet like:

items = Item.objects.all()

Item has a 'name' field. In the template I want to show:

  • A
  • Axes
  • Alcohol
  • B
  • Bazookas
  • C
  • Coins
  • Cartridges
  • S
  • Swords
  • Sparrows

So the items are ordered and group by the first letter. Missing letters are omitted. Does anyone have any ideas?

回答1:

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>


回答2:

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 ''


回答3:

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>


回答4:

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



回答5:

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"
    ],
    ......

}


回答6:

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.