2

Hello I am making django app, i would like to add visit counter feature but seperate for each item. I think it would be a nice feature.

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['comments'] = Comment.objects.filter(item=self.object)
        context['form'] = CommentCreationForm()
        num_visits = self.request.session.get('num_visits', 0)
        self.request.session['num_visits'] = num_visits + 1
        context['visits'] = num_visits
        return context
Brian Destura
  • 11,487
  • 3
  • 18
  • 34
  • 1
    With the above implementation, you only specify how many times the person in this session has visited an object, you thus count it for *per user*, and the visits of one user have no effect on the visits of another user. – Willem Van Onsem Aug 29 '21 at 17:57
  • So i should modify models rather than using vanilla session and visits. –  Aug 29 '21 at 18:02
  • add a field inside the item model and count it with every click. – Faisal Nazik Aug 29 '21 at 18:03
  • @MaceiejWojtkowiak: if you want to count the total number of visitors, yes. Otherwise once the session is gone (for example the user restarts the browser), normally the count will be back to zero for that user, and furthermore other user's visits would not make any difference – Willem Van Onsem Aug 29 '21 at 18:03
  • Can you share the model of the `self.object` item? – Willem Van Onsem Aug 29 '21 at 18:05

1 Answers1

2

Currently what you are implementing is a counter per session. Indeed this means that if a user starts a session on your page, first they will see zero, later one, and so on. But that will only count how many times that user visits a page in the session. Visits from other users will not make any difference.

If you want to keep track of the total number of visits per user, you will need to let the visiting data persist. You can do that with an extra model that for example each time creates a new record when a (registered) user visits a page, or we can work with a simple counter. If we want to prevent counting the same user multiple times if they visit the same object multiple times, it makes more sense to work with a ManyToManyField to the user.

Option 1: simple IntegerField

A simple implementation that simply counts the number of visits, and thus count the same user twice if that user visits the object twice can be implemented with an extra IntegerField to count the number of visits, this looks like. We can write an abstract model for that:

class WithVisitCounter(models.Model):
    visits = models.IntegerField(editable=False, default=0)

    class Meta:
        abstract = True

and then let the model inherit from this:

class BlogPost(WithVisitCounter, models.Model):
    # ⋮

then we can make a mixin WithVisitCounterMixin:

from django.views.generic.detail import SingleObjectMixin

class WithVisitCounterMixin(SingleObjectMixin):

    def get_object(self, *args, **kwargs):
        obj = super().get_object(*args, **kwargs)
        old_visit = obj.visits
        obj.visits = F('visits') + 1
        obj.save(updated_fields=['visits'])
        obj.visits = old_visit + 1
        return obj

    def get_context_data(self, *args, **kwargs):
        cd = super().get_context_data(*args, **kwargs)
        cd['visits'] = self.object.visits
        return cd

Then we can use this Mixin in all views that have a SingleObjectMixin like a DetailView and an UpdateView:

class BlogPostDetailView(WithVisitCounterMixin, DetailView):
    # ⋮

This will pass the number of visitors as visits to the context data, so you can render this with {{ visits }}, or with {{ object.visits }} if the object is passed to the template.

Option 2: A ManyToManyField to the user model

The first option does not take into account users that visit the same object multiple times. This means that the same user can visit the page twenty times, and that will be seen as twenty independent visits.

We can in that case define an abstract model that will add links to users, with:

from django.conf import settings

class WithVisitCounter(models.Model):
    visitors = models.ManyToManyField(
        to=settings.AUTH_USER_MODEL,
        related_name='%(model_name)s_visits'
    )

    class Meta:
        abstract = True

class BlogPost(WithVisitCounter, models.Model):
    # ⋮

Then we can define a WithVisitCounterMixin just like we did for the first option. In this case we will add a link from the object to the logged in user:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.detail import SingleObjectMixin

class WithVisitCounterMixin(SingleObjectMixin):

    def get_object(self, *args, **kwargs):
        obj = super().get_object(*args, **kwargs)
        obj.visitors.add(self.request.user)
        return obj

    def get_context_data(self, *args, **kwargs):
        cd = super().get_context_data(*args, **kwargs)
        cd['visits'] = self.object.visitors.count()
        return cd

for that single object, we can then get the visitors by counting the number of records for the .visitors of the self.object.

We can thus use that mixin in a DetailView or UpdateView as well:

class BlogPostDetailView(WithVisitCounterMixin, DetailView):
    # ⋮

We can then again use {{ visits }} for the number of visitors for that item.

Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555