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.
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.
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:
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?
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.)