I am working on a blog webapp and I would like to monitor the number of views a post has. I have decided to use the post detail view to count. And to keep the counter from incrementing for a particular user, I am going to allow only logged users to count and a user can count only once.
this is my post model
class Post(models.Model):
title = models.CharField(max_length=150)
content = models.TextField(max_length=3000, null=True, blank=True)
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
viewers = models.TextField(default="", null=True, blank=True)
numViews = models.IntegerField(default=0)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})
I am basically using a field 'viewers' to track users already counted and 'numviews' as the counter.
My views.py function
class PostDetailView(DetailView):
model = Post
def get(self, request, *args, **kwargs):
# increment post views whenever request is made
postObject = self.get_object()
user = request.user
if postObject.viewers == None:
postObject.viewers = ""
postObject.save()
if user.is_authenticated and user.username not in postObject.viewers:
# increment
postObject.numViews += 1
postObject.save()
# add username to viewers list
postObject.viewers+=user.username
postObject.save()
return super().get(request, *args, **kwargs)
In the views function, I am checking if the username is already appended to the viewers string, else append it and increment the counter. Is there a more efficient way of doing this? In the case the blog gets thousands of views, it is going to be a very long string of usernames.
In the case the blog gets thousands of views, it is going to be a very long string of usernames.
It will not only be inefficient, but also can result in the wrong result. If you have three users foo
, bar
and foobar
, then if foo
visits the page, and bar
visits the page, then if foobar
visits the page, their view will not be taken into account.
An other problem is that there can be race conditions here: if two users visit the same post approximately at the same time, it is possible that the database ends up only updating the object for one user, and thus it is like the other user never visited the page.
Normally you determine who has viewed the post through a ManyToManyField
[Django-doc]:
from django.conf import settings
class Post(models.Model):
title = models.CharField(max_length=150)
content = models.TextField(max_length=3000, null=True, blank=True)
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
viewers = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='viewed_posts'
editable=False
)
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'pk': self.pk})
Then we can implement the view as:
from django.db.models import Count
class PostDetailView(DetailView):
model = Post
queryset = Post.objects.annotate(
num_views=Count('viewers')
)
def get_context_data(self, *args, **kwargs):
if self.request.user.is_authenticated:
__, created = Post.viewers.through.objects.get_or_create(
post=self.object,
user=self.request.user
)
if created:
self.object.num_views += 1
return super().get_context_data(*args, **kwargs)