19

I'm faced with a problem wherein I'm trying to create a QuerySet with the results ordered not by a field on a model, but instead ordered by the result of a value returned by a method on the model.

To wit:

class MyModel(models.Model):

someAttributes = models.TextField(blank=True)

@property
def calculate_rating(self):
<do some calculation and return integer>

Given that, how can I construct a QuerySet that orders the results by the value for each instance as returned by calculate_rating()?

In trying to simply use order_by(), I get an error:

Cannot resolve keyword 'average_rating' into field.

Can anybody provide some ideas?

Michael Place
  • 2,986
  • 1
  • 19
  • 18
  • possible duplicate of [Sorting a Django QuerySet by a property (not a field) of the Model](http://stackoverflow.com/questions/4175749/sorting-a-django-queryset-by-a-property-not-a-field-of-the-model) – Anto May 08 '14 at 15:48

3 Answers3

22

order_by is for database stuff. Try to use sorted instead:

sorted(MyModel.objects.all(),  key=lambda m: m.calculate_rating)
Alexandre
  • 1,245
  • 1
  • 13
  • 22
10

There is no way to do this. One thing you can do is to create a separate database field for that model and save the calculated rating in it. You can probably override the save method of the model and do the calculations there, after that you can only refer to the value.

You can also sort the returned QuerySet using Python sorted. Take into account that the sorting approach using the built-in sorted function increases a lot the computational complexity and it's not a good idea to use such code in production.

For more information you can check this answer: https://stackoverflow.com/a/981802/1869597

Community
  • 1
  • 1
Jordan Jambazov
  • 3,460
  • 1
  • 19
  • 40
3

The best way to achieve what you want, is by using the Case and When statements. This allows you to keep the final result in QuerySet instead of list ;)

ratings_tuples = [(r.id, r.calc_rating) for r in MyModel.objects.all()]  
ratings_list = sorted(ratings_tuples, key = lambda x: x[1])  # Sort by second tuple elem

So here is our sorted list ratings_list that holds IDs and ratings of the elements in a sorted way

pk_list = [idx for idx, rating in ratings_list]  
from django.db.models import Case, When
preserved = Case(*[When(pk=pk, then=pos) for pos, pk in enumerate(pk_list)])  
queryset = MyModel.objects.filter(pk__in=pk_list).order_by(preserved)  

Now, using Case and When statements, we are able to do the queryset sorting itself by the IDs from the list we already have. (Django Docs for details).
Had a similar situation and needed a solution. So found some details on this post https://stackoverflow.com/a/37648265/4271452, it works.

Michael Hays
  • 2,947
  • 3
  • 20
  • 30
Iulian Pinzaru
  • 400
  • 3
  • 10
  • Nice! It's less clear what is going on here than with `sorted` to be sure, but there are many cases where it is necessary to keep the result as a queryset. – Michael Hays Apr 13 '19 at 21:49