Order by model method result

I have a News model that I use a separate model called NewsView to store the number of views, and I wrote a method inside News called count_views to get the number of views. Now I want to order the news list based on the most viewed in the view, but because I get the number of views through a method, this is not possible with order_by. Is there another way to do this?

class NewsView(models.Model):
    user = models.ForeignKey('account.User', on_delete=models.SET_NULL, null=True)
    news = models.ForeignKey('News', on_delete=models.CASCADE)
    viewed_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = 'user', 'news'


class News(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published')
    ]

    image = models.ImageField(upload_to='news/', blank=True, null=True)
    cropping = ImageRatioField('image', '1200x675')
    author = models.ForeignKey('account.Profile', on_delete=models.SET_NULL, null=True)
    title = models.CharField(max_length=250)
    content = models.TextField()
    category = models.ManyToManyField('Category', related_name='news')
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    published_date = models.DateTimeField(blank=True, null=True)
    created_date = models.DateTimeField(auto_now_add=True)
    updated_date = models.DateTimeField(auto_now=True)

    objects = models.Manager()
    published = PublishedManager()  # Create a new manager to get published news

    class Meta:
        verbose_name_plural = 'News'
        ordering = '-published_date', 'title'

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('news:detail', kwargs={'pk': self.pk})

    def count_views(self):
        return NewsView.objects.filter(news=self).count()

Which news list? Did you omit something in your post?

You didn’t need to query NewsView directly from inside News in the count_views method. You could leverage the reverse lookup already in Django by specifying a reverse name in NewsView. Just add related_name to news column in NewsView:

class NewsView(models.Model):
    user = models.ForeignKey('account.User', on_delete=models.SET_NULL, null=True)
    news = models.ForeignKey(
        News, 
        on_delete=models.CASCADE, 
        related_name="views"
    )
    viewed_at = models.DateTimeField(auto_now_add=True)

then modify count_views in News to:

def count_views(self):
        return self.views.count()

Also notice that you do not need to quote the Model names or first parameter in a models.ForeignKey() field.

If you are getting a List of News and you want them ordered according to number of views, you may need to annotate and aggregate the resulting queryset.

1 Like

Side note: They may or may not, depending upon other factors in the file that may not be shown here. (Removing the quotes could require other changes be made.) However, there’s no issue with keeping the quotes.

I believe an annotation would be appropriate and sufficient. From what I see here, I don’t believe there’s a need for an aggragate.

1 Like
class IndexView(ListView):
    model = models.News
    template_name = 'news/news_list.html'
    context_object_name = 'news_list'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['categories'] = models.Category.objects.annotate(
            news_count=Count('news')
        ).order_by('-news_count', 'name')
        return context

    def get_queryset(self):
        if self.request.user.role == 'admin':
            news = self.model.objects.all()
        else:
            news = self.model.published.all()

        # Search result
        if q := self.request.GET.get('q'):
            news = news.filter(
                Q(title__icontains=q) |
                Q(content__icontains=q) |
                Q(category__name__icontains=q) |
                Q(author__first_name__icontains=q) |
                Q(author__last_name__icontains=q)
            ).distinct()

        # Filters result
        if (order := self.request.GET.get('order')) and order == 'view':
            news = news.order_by('-count_views')

        return news

This is what I want to do. I want to allow the user to filter the news based on the number of views (Filter result section in get_queryset method).
But count_views is a method and I get this error:

FieldError at /news/
Cannot resolve keyword 'count_views' into field. Choices are: author, author_id, category, comments, content, created_date, cropping, id, image, newsview, published_date, status, title, updated_date

And this is because count_views only returns the result and doesn’t store it in the database, so I can’t order the queryset based on that. Can I do something else?

This is correct.

Yes, you can annotate the queryset with the count. See the docs at Aggregation | Django documentation | Django for more details and examples. (You’ll find an example that matches your requirement almost exactly.)