At the very beginning of creating articles and questions on the forum, I added a simple counter of views on this content.
This counter was an ordinary field of integer type and each time a page was requested it was incremented by one.
But not so long ago, I replaced this counter with a counter model for registering unique views. I just did it because I think it's prettier.
The counter counts unique visitors either by IP address, if the user is not authorized on the site, or by user account, if this user is authorized on the site.
So let's learn how to do it.
Viewer model
Responsible for counting unique users who viewed the site.
class Viewer(models.Model): ipaddress = models.GenericIPAddressField("IP address", blank=True, null=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True)
As you can see, there are only two fields in this model, the first is the IP Address, and the second is the foreign key to the user model
Adding a counter to the content model
And now let's add a counter to some site content model, for example, to the article model Article
class Article(models.Model): viewers = models.ManyToManyField(Viewer) # Some another code
Adding code is done as a Many-To-Many relationship. Since the unique visitor in this case will be one, but he can view many other articles. At the same time, the article can be viewed by many other users.
How to increase the content view counter
Most likely, some view class or view function will be responsible for displaying articles on your site. Personally, I prefer to use modern Class Based View in Django.
Therefore, I will write a mixin that can be used with DetailView to register unique views of content on the site.
class CountViewerMixin: def get(self, request, *args, **kwargs): response = super().get(request, *args, **kwargs) if hasattr(self.object, 'viewers'): viewer, created = Viewer.objects.get_or_create( ipaddress=None if request.user.is_authenticated else get_client_ip(request), user=request.user if request.user.is_authenticated else None ) if self.object.viewers.filter(id=viewer.id).count() == 0: self.object.viewers.add(viewer) return response
It can be seen from the code that the mixin overrides the get method, checks that the content object exists in the View and only after that gets a unique visitor, and after checking that the visitor does not yet exist among the visitors who have viewed the content, adds it to the content views.
Thus, the content object itself is not modified, unlike the counter in the form of an integer field in the content model. What can actually complicate the code is if you have content model save method overrides or you have written receiver functions to handle save signals from the model. In the case of the Viewer counter, which is presented in this article, signals such as post_save on the content model simply do not work.
In the Content View, the mixin connection will look like this:
class ArticleView(CountViewerMixin, DetailView): # some another code
Usage in template
Inside the template, you can get the number of views as follows
{{ object.viewers.count }}
get_client_ip
The mixin also has a function to get an ip address from an HTTP request. I have already described this special function for getting the user's ip address from a request in one of the articles .
And in my latest revision for Django 3, it looks like this.
def get_client_ip(request): """ Get client ip address from HTTP request :param request: HTTP request :return: IP Address """ x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') return x_forwarded_for.split(',')[-1].strip() if x_forwarded_for else request.META.get('REMOTE_ADDR')
Conclusion
Such a counter will make it easier to maintain the code if you have reactions to saving content, and will also reduce the impact of cheats on content counters.