Django custom order_by algorithm

Viewed 88

I want to create a formula for trending posts. I've done this so far:

def hot(request):
    posts = Post.objects.all()
    for post in posts:
        likes = int(post.likes)
        views = int(post.blog_view)
        rep = int(post.author.profile.reputation)
        d0 = timezone.now()
        d1 = post.date_posted
        days = (d0 - d1).days
        trending_score = (3/(1+days**(1/3)))*(0.5*views+0.25*rep+2.5*likes)

The formula is at trending_score variable, and each time, it returns a number for its trending score. The higher the trending_score, the more trending it is.

Now I want to implement this in django using order_by, or something else:

def hot(request):
    posts = Post.objects.all()
    for post in posts:
        likes = int(post.likes)
        views = int(post.blog_view)
        rep = int(post.author.profile.reputation)
        d0 = timezone.now()
        d1 = post.date_posted
        days = (d0 - d1).days
        trending_score = (3/(1+days**(1/3)))*(0.5*views+0.25*rep+2.5*likes)
    context = {
        Post.objects.all().order_by(-trending_score)
    }
    return render(request, 'blog/popularity.html', context)

I obviously knew this wasn't going to work, because I put trending_score in the for loop and context is outside of it, so it wasn't going to work. The error was:Invalid order_by arguments: [-21.75] But I have no idea how else I can do this. Any help in doing this will be appreciated.

2 Answers

You can annotate your queryset with values calculated by your formula and then order it based on the annotation. The main thing is to translate your formula to the representation that django ORM understands such that it would convert to a proper SQL statement. Something like this should work:

from django.db.models import F, FloatField, DurationField
from django.db.models.functions import Cast, Now, Extract


days = Cast(
    Extract(Cast(Now() - F('date_posted'), DurationField()), 'days'),
    FloatField()
)
views = Cast('views', FloatField())
rep = Cast('post.author.profile.reputation', FloatField())
likes = Cast('likes', FloatField())

Post.objects.all().annotate(
    trending_score=(3./(1+days**(1/3)))*(.5*views+.25*rep+2.5*likes)
).order_by('-trending_score')

The simplest fix I can think of is, create a new list to store the results. That would allow you to get results ordered on a particular attribute without having to change your models or anything.

def hot(request):
    posts = Post.objects.all()
    result = []
    for post in posts:
        likes = int(post.likes)
        views = int(post.blog_view)
        rep = int(post.author.profile.reputation)
        d0 = timezone.now()
        d1 = post.date_posted
        days = (d0 - d1).days
        trending_score = (3/(1+days**(1/3)))*(0.5*views+0.25*rep+2.5*likes)
        result.append((trending_score, post))  #added a tuple of value-object
    context = {
        sorted(result, reverse=True)
    }
    return render(request, 'blog/popularity.html', context)

On your HTML page, instead of

{% for post in posts %}

You can use

{% for object in posts %}
post = object[1]
Related