Grouping (not 'ordering') objects into an alphabetical list on a single page

Hi there,

I’d be really grateful for some help in cracking this problem. I’m struggling, but I feel like there must be a really simple/obvious way of doing this…

Objective:

I’m trying to display a list of objects in an alphabetic list for a directory page. Something pretty much like this: List of film and television directors - Wikipedia

To avoid having to click on the link, the list would be:

Surname starts with A
• Anderson, Wes

Surname starts with B
• Burton, Tim

Surname starts with C
• Coppola, Sofia

etc…

A simplified version of the model would be:

class Person(model.Models):
     first_name = model.CharField
     last_name = model.CharField

     def __str__(self):
          return "{}, {}".format(self.last_name, self.first_name)

Route 1. In the template?

At first I thought there might be a way of doing this solely in the template using tags and filters. The syntax may be wrong, but in my head something like:

{{ person.last_name|startswith:"a" }}

However looking at the documentation I can’t see a template tag that would fulfill that requirement. Also reading through other discussions, a common recommendation is not putting that much logic in the template.

Route 2. In the view?

If not filtering in the template, after that I thought it would be possible to filter the objects within the view and send each portion separately.

def person_list(request):
     startswith_a = Person.objects.filter(last_name__startswith="a")
     startswith_b = Person.objects.filter(last_name__startswith="b")
     startswith_c = Person.objects.filter(last_name__startswith="c")
     etc.
     etc.
     return render(request, "directory/person-list.html", {"startswith_a": startswith_a, "startswith_b": startswith_b})

But that feels wrong as I’d have to hard code the alphabet into the view definition, and then pass all 26 (or more) sets into the view.

Route 3. In the model?

Finally I thought it might be possible to have a new field in the model which would affix its location in the directory. This could be updated manually or (I guess?) by customising the save function on the model to automatically assign based on the first character of the last_name field.

class Person(model.Models):
     directory_position = model.CharField  # eg. "a"
     first_name = model.CharField # eg. "Wes"
     last_name = model.CharField # eg. "Anderson"

     def __str__(self):
          return "{}, {}".format(self.last_name, self.first_name)

With that set, it would be possible to use template tags to show the relevant objects in each portion of the page.

{{ person.directory_position="a" }}

But that still feels a bit hacky because the template would have to iterate over the whole returned list for each of the 26 characters (and more for special characters).

Racking my brains for other solutions… Is there a custom model method I could write that would self filter for each letter?

Again, I feel like I must be missing something obvious. There’s probably a single line of code that will do all of this (especially in Python!) but I just can’t figure it out.

Thanks in advance!!

In your view, use a query set ordered by last name. Then use itertools.groupby to transform that into an iterable-of-iterables grouped by letter: itertools — Functions creating iterators for efficient looping — Python 3.9.5 documentation

You’ll want a key function like:

def first_letter_upper(string):
    return string[0].upper()
2 Likes

Thanks @adamchainz , I didn’t know about itertools.groupby so that’s really helpful. I’m writing the view with groupby, still finding my around it but getting there slowly!