pythondjangoblogs

Best way to monitor number of views on a blog post in Django


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.


Solution

  • 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)