108

Is it possible to sort a set of related items in a DJango template?

That is: this code (with HTML tags omitted for clarity):

{% for event in eventsCollection %}
   {{ event.location }}
   {% for attendee in event.attendee_set.all %}
     {{ attendee.first_name }} {{ attendee.last_name }}
   {% endfor %}
 {% endfor %}

displays almost exactly want I want. The only thing I want to change is I the list of attendees to be sorted by last name. I've tried saying something like this:

{% for event in events %}
   {{ event.location }}
   {% for attendee in event.attendee_set.order_by__last_name %}
     {{ attendee.first_name }} {{ attendee.last_name }}
   {% endfor %}
 {% endfor %}

Alas, the above syntax doesn't work (it produces an empty list) and neither does any other variation I have thought of (lot's of syntax errors reported, but no joy).

I could, of course, produce some kind of array of sorted attendee lists in my view, but that is an ugly and fragile (and did I mention ugly) solution.

Needless to say, but I'll say it anyway, I have perused the on-line docs and searched Stack Overflow and the archives of django-user without finding anything helpful (ah, if only a query set were a dictionary dictsort would do the job, but it's not and it doesn't)

==============================================

Edited to add additional thoughts after accepting Tawmas's answer.


Tawmas addressed the issue exactly as I presented it -- although the solution was not what I expected. As a result I learned a useful technique that can be used in other situations as well.

Tom's answer proposed an approach I had already mentioned in my OP and tentatively rejected as being "ugly".

The "ugly" was a gut reaction, and I wanted to clarify what was wrong with it. In doing so I realized that the reason it was an ugly approach was because I was hung up on the idea of passing a query set to the template to be rendered. If I relax that requirement, there is an un-ugly approach that should work.

I haven't tried this yet, but suppose that rather than passing the queryset, the view code iterated through the query set producing a list of Events, then decorated each Event with a query set for the corresponding attendees which WAS sorted (or filtered, or whatever) in the desired way. Something like so:

eventCollection = []   
events = Event.object.[filtered and sorted to taste]
for event in events:
   event.attendee_list = event.attendee_set.[filtered and sorted to taste]
   eventCollection.append(event)

Now the template becomes:

{% for event in events %}
   {{ event.location }}
   {% for attendee in event.attendee_list %}
     {{ attendee.first_name }} {{ attendee.last_name }}
   {% endfor %}
 {% endfor %}

The downside is the view has to "actualize" all of the events at once which could be a problem if there were large numbers of events. Of course one could add pagination, but that complicates the view considerably.

The upside is the "prepare the data to be displayed" code is in the view where it belongs letting the template focus on formatting the data provided by the view for display. This is right and proper.

So my plan is to use Tawmas' technique for large tables and the above technique for small tables, with the definition of large and small left to the reader (grin.)

shad0w_wa1k3r
  • 12,955
  • 8
  • 67
  • 90
Dale Wilson
  • 9,166
  • 3
  • 34
  • 52

4 Answers4

174

You can use template filter dictsort https://docs.djangoproject.com/en/dev/ref/templates/builtins/#std:templatefilter-dictsort

This should work:

{% for event in eventsCollection %}
   {{ event.location }}
   {% for attendee in event.attendee_set.all|dictsort:"last_name" %}
     {{ attendee.first_name }} {{ attendee.last_name }}
   {% endfor %}
 {% endfor %}
tariwan
  • 1,749
  • 1
  • 10
  • 3
  • 40
    Great ! And for those who wonder, there is also `dictsortreversed` : https://docs.djangoproject.com/en/dev/ref/templates/builtins/#dictsortreversed – Mickaël Oct 26 '14 at 13:51
  • 1
    To quote from my original post: ah, if only a query set were a dictionary dictsort would do the job, but it's not and it doesn't. – Dale Wilson Dec 19 '14 at 17:26
  • I think this should be less in practice, and let model handle the sorting before fetching the records. – acpmasquerade Feb 07 '15 at 13:24
  • 1
    @DaleWilson, I've actually got `dictsort` working properly on code almost exactly like yours. Interestingly it seems to work fine on querysets. – mlissner Aug 26 '15 at 02:17
  • @mlissner Cool. I guess they fixed it in the four years since this question was asked. – Dale Wilson Aug 26 '15 at 15:12
  • 3
    And just for further clarity, spaces matter: `{% for attendee in event.attendee_set.all|dictsort:"last_name" %}` sorts the attendees, but `{% for attendee in event.attendee_set.all | dictsort:"last_name" %}` attempts to sort the output of the for loop and breaks the `for`. – mattsl Jan 08 '18 at 10:05
  • Thank you so much this is super helpful for encrypted data sets where the database order by will be ordering by the encrypted data. – gabeio Sep 05 '19 at 19:48
157

You need to specify the ordering in the attendee model, like this. For example (assuming your model class is named Attendee):

class Attendee(models.Model):
    class Meta:
        ordering = ['last_name']

See the manual for further reference.

EDIT. Another solution is to add a property to your Event model, that you can access from your template:

class Event(models.Model):
# ...
@property
def sorted_attendee_set(self):
    return self.attendee_set.order_by('last_name')

You could define more of these as you need them...

tawmas
  • 7,443
  • 3
  • 25
  • 24
  • Thanks for your comment, but this only works if I want to make the display order a permanent property of the attendee, which I don't. I might, for example, wish to display the attendees sorted by the date when their registration was received, so I would know which attendees should be notified that there was no room for them. – Dale Wilson Jun 30 '11 at 21:57
  • I added an alternate solution for you. It should allow you the flexibility you need. – tawmas Jun 30 '11 at 22:39
  • @Mark Indeed it does. As far as I understand `@property` is overkill here as there's no getters or setters involved: http://stackoverflow.com/questions/1554546/when-and-how-to-use-the-builtin-function-property-in-python – Rick Westera Jan 22 '14 at 21:46
  • 1
    Although not strictly needed for the template, I find that the @property here makes for cleaner access **from the application code**, although there is a small performance penalty involved (< 30 ns on my laptop). This is of course a matter of style and highly subjective. – tawmas Jan 22 '14 at 23:57
  • In case you want to sort the set with respect to two attributes, this is the command: class Meta: ordering = ['last_name', 'first_name'] – Tms91 Jul 09 '19 at 13:55
  • To do in reverse `ordering = ['-last_name']` – Apoorv pandey Oct 27 '21 at 06:18
8

One solution is to make a custom templatag:

@register.filter
def order_by(queryset, args):
    args = [x.strip() for x in args.split(',')]
    return queryset.order_by(*args)

use like this:

{% for image in instance.folder.files|order_by:"original_filename" %}
   ...
{% endfor %}
matinfo
  • 437
  • 4
  • 4
0

regroup should be able to do what you want, but is there a reason you can't order them the way you want back in the view?

Tom
  • 22,301
  • 5
  • 63
  • 96
  • To order them in the view I would need to iterate through the events and for each event create a container of some sort of the attendees related to that event in properly sorted order, then pass that entire collection of containers to the template in a form that would let the template find the appropriate collection of attendees as _it_ iterated through the events. As I said, it could be done, but it's not a good solution. It requires far too much coupling between the view and the template, parallel iterations, etc. I was hoping for a better solution to what should be a common problem. – Dale Wilson Jun 30 '11 at 21:54
  • I looked at regroup and it did not appear to do what I wanted. in particular the documentation says: [quote] Note that {% regroup %} does not order its input! Our example relies on the fact that the people list was ordered by gender in the first place. [endquote] – Dale Wilson Jun 30 '11 at 22:00