In my blog, I wanted an easy way to count the number of views for a specific blog post without having to rely on Google Analytics. With the recent browser updates, libraries like Google Analytics are often disabled and shouldn't be relied on.

Therefor, I wanted a solution which does the count server-side and store them in the database.

The de-facto solution for doing this with Django is a module called django-hitcount. Getting it installed is the same as with every Django extension. We first use the pip utility to get the module installed:

$ pip install django-hitcount

Then, we need to add it to the list of installed applications in the settings.py file of our app:

mysite/settings.py

INSTALLED_APPS = [
    ...
    'hitcount',
    ...
]

I then updated the model class to allow sorting on the hitcount:

blog/models.py

from django.db import models
from django.contrib.contenttypes.fields import GenericRelation

from hitcount.models import HitCountMixin, HitCount

class Post(models.Model, HitCountMixin):
    title = models.CharField(max_length=255, unique=True)
    slug = models.SlugField(max_length=255, unique=True)
    published_on = models.DateTimeField(blank=True, default=None, null=True)
    content = models.TextField(blank=True)

    hit_count_generic = GenericRelation(
        HitCount, object_id_field='object_pk',
        related_query_name='hit_count_generic_relation'
    )

    def current_hit_count(self):
        return self.hit_count.hits

The GenericRelation allows you so query the posts sorting them by their hitcount:

Post.objects.order_by("hit_count_generic__hits")

The current_hit_count makes it easy to display the number of views in the template:

blog/templates/blog/post_detail.html

{% if post.current_hit_count > 1 %}
    | {{ post.current_hit_count }} views
{% endif %}

Since I'm using class-based views for my blog detail pages, I can update the view to:

blog/views.py

from .models import Post

from hitcount.views import HitCountDetailView

class PostDetail(HitCountDetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    count_hit = True

    def get_context_data(self, **kwargs):
        context = super(PostDetail, self).get_context_data(**kwargs)
        context.update({
            'popular_posts': Post.objects.order_by('-hit_count_generic__hits')[:3],
        })
        return context

Instead of extending from DetailView, I'm now extending from HitCountDetailView instead. For the curious, HitCountDetailView is a combination of DetailView along with HitCountMixin which adds the hit counting capabilities.

To actually count the hits for this view, you need to add count_hit=True. This will count every time the view is rendered. I also added the get_context_data method so that I can add the list of popular posts to the blogpost detail page. To get these, I'm getting the posts ordered by their hit count in descending order, limiting the result to 3.

To show the related posts in your template, you can use:

blog/templates/blog/post_detail.html

<h3>Popular Posts</h3>
{% for p in popular_posts %}
    <p>{{p.title}}</p>
{% endfor %}

Last but not least, I wanted to add the view count to the list view of the blog posts in the admin view:

blog/admin.py

from .models import Post 

from django.contrib import admin

class PostAdmin(admin.ModelAdmin):
    list_display = ('title', 'formatted_hit_count',)

    def formatted_hit_count(self, obj):
        return obj.current_hit_count if obj.current_hit_count > 0 else '-'
    formatted_hit_count.admin_order_field = 'hit_count'
    formatted_hit_count.short_description = 'Hits'

admin.site.register(Post, PostAdmin)

The complete documentation for Django hitcount can be found here.

Related Posts

  • Migrating your Wagtail site to a different database engine
  • Making publish the default action in Wagtail
  • Outputting a Django queryset as JSON
  • Programatically creating redirects in Wagtail
  • Generating slugs and redirects with Django