50

I am sure that someone has a pluggable app (or tutorial) out there that approximates this, but I have having trouble finding it: I want to be able to track the number of "views" a particular object has (just like a question here on stackoverflow has a "view count").

If the user isn't logged in, I wouldn't mind attempting to place a cookie (or log an IP) so they can't inadvertently run up the view count by refreshing the page; and if a user is logged in, only allow them one "view" across sessions/browsers/IP addresses. I don't think I need it any fancier than that.

I figure the best way to do this is with Middleware that is decoupled from the various models I want to track and using an F expression (of sorts) -- other questions on StackOverflow have alluded to this (1), (2), (3).

But I wonder if this code exists out in the wild already -- because I am not the savviest coder and I'm sure someone could do it better. Smile.

Have you seen it?

thornomad
  • 6,707
  • 10
  • 53
  • 78

6 Answers6

56

I am not sure if it's in the best taste to answer my own question but, after a bit of work, I put together an app that solves the problems in earnest: django-hitcount.

You can read about how to use it at the documentation page.

The ideas for django-hitcount came came from both of my two original answers (Teebes -and- vikingosegundo), which really got me started thinking about the whole thing.

This is my first attempt at sharing a pluggable app with the community and hope someone else finds it useful. Thanks!

Community
  • 1
  • 1
thornomad
  • 6,707
  • 10
  • 53
  • 78
  • 5
    Hitcount seems overcomplicated for this task. Especially using models for counting hits can be real heavy. I would recommend (as I did in my project) to use Cache instead. Smart cache names+timeouts are dealing with problem nicely and it is extremely fast. – thedk Oct 21 '12 at 13:16
  • great app, thanks! Does it automatically filter out search engines' hits? – Dennis Golomazov Aug 16 '13 at 09:48
  • 1
    @DenisGolomazov - well, it uses Javascript to track the hit, and usually a search engine wouldn't be executing the javascript (I would think) ... so yes: it should ignore a search engine by virtue of the search engine not executing the javascript on the page. – thornomad Sep 02 '13 at 16:24
  • @thornomad Documentation page seems to be down. On git-hub only is the instalation guide, but I would like to use it, and I don't know where to find the documentation. Thanks! – cor May 12 '14 at 09:50
  • 1
    @cor - it should be up and running now, sorry about that. – thornomad May 12 '14 at 15:13
  • This doesn't seem to work -> the viewcount is always 1 – Henry Zhu May 22 '16 at 01:08
33

You should use the django built-in session framework, it already does a lot of this for you. I implemented this in the following way with a Q&A app where I wanted to track views:

in models.py:

class QuestionView(models.Model):
    question = models.ForeignKey(Question, related_name='questionviews', on_delete=models.CASCADE)
    ip = models.CharField(max_length=40)
    session = models.CharField(max_length=40)
    created = models.DateTimeField(default=datetime.datetime.now())

in views.py:

def record_view(request, question_id):

    question = get_object_or_404(Question, pk=question_id)

    if not QuestionView.objects.filter(
                    question=question,
                    session=request.session.session_key):
        view = QuestionView(question=question,
                            ip=request.META['REMOTE_ADDR'],
                            created=datetime.datetime.now(),
                            session=request.session.session_key)
        view.save()

    return HttpResponse(u"%s" % QuestionView.objects.filter(question=question).count())

Vikingosegundo is probably right though that using content-type is probably the more reusable solution but definitely don't reinvent the wheel in terms of tracking sessions, Django already does that!

Last thing, you should probably have the view that records the hit be either called via Ajax or a css link so that search engines don't rev up your counts.

Hope that helps!

Maran Sowthri
  • 829
  • 6
  • 14
Teebes
  • 2,056
  • 2
  • 14
  • 11
  • That does help - how you used the session information and everything is going to be useful. I like vikingosegundo's approach too - which is more generic. If I can't find anything else, I may combine the two. And, will have to keep in mind search engines - I hadn't thought of that. But they may include a certain header, that could be checked ... no? – thornomad Oct 21 '09 at 22:50
  • You can definitely check the headers. this previous question http://stackoverflow.com/questions/45824/counting-number-of-views-for-a-page-ignoring-search-engines has some very good info on this (non django specific). – Teebes Oct 21 '09 at 23:19
  • It makes me a good sense to count views for a page. – miltonbhowmick Dec 09 '20 at 18:19
  • What's the point of `ip` and `created` if you don't use them in the example? – Chris Jul 26 '22 at 19:11
14

You could create a generic Hit model

class Hit(models.Model):
    date = models.DateTimeField(auto_now=True)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey('content_type', 'object_id')

In your view.py you write this function:

def render_to_response_hit_count(request,template_path,keys,response):
    for  key in keys:
        for i in response[key]:
             Hit(content_object=i).save()
    return render_to_response(template_path, response)

and the views that you are interested in return

return render_to_response_hit_count(request,   'map/list.html',['list',],
        {
            'list': l,
        })

This approach gives you the power, not only to count the hit, but to filter the hit-history by time, contenttype and so on...

As the hit-table might be growing fast, you should think about a deletion strategy.

vikingosegundo
  • 52,040
  • 14
  • 137
  • 178
  • Yea - I did see your code, and it did raise my attention! Smile. However, was hoping for something already in an app that I could just import and then use ... but, I may try and combine your Hit model (and the generic aspects) with the session suggestions of @Teebes. Thanks. – thornomad Oct 21 '09 at 22:48
  • Sure, u should combine them. with session u get the informations about single users. and with my approach u can control the views to be triggered without writing the same code over and over again. take that to ur solution. – vikingosegundo Oct 21 '09 at 23:13
  • Typo: DateTimeFiles should read as DateTimeField, shouldn't it? – Meilo Jan 24 '12 at 13:50
  • Thank you @vikingsegundo for this helpful answer. You should update it for Django v2/v3. Also, you could add sample template code. – enoren5 Oct 02 '20 at 10:21
  • @Angeles89 you sound quite demanding. I haven’t used django in years. Unlikely that I will update this. But guess what: you can add those changes yourself. – vikingosegundo Oct 02 '20 at 10:41
  • I wasn't demanding a change. I merely made two potential suggestions. Maybe I will update your answer. :) – enoren5 Oct 02 '20 at 10:47
  • @vikingosegundo, How do i access hits in template ? – Lars Jan 01 '21 at 08:52
  • @Progam same techniques as you'll find in djangos documentation apply. – vikingosegundo Jan 02 '21 at 23:32
9

I know this question is an old one and also thornomad has put an app to solve the problem and inspire me with me solution. I would like to share this solution since I didn't find much information about this topic and it may help someone else. My approach is to make a generic model can be used with any view based on the view path (url).

models.py

class UrlHit(models.Model):
    url     = models.URLField()
    hits    = models.PositiveIntegerField(default=0)

    def __str__(self):
        return str(self.url)

    def increase(self):
        self.hits += 1
        self.save()


class HitCount(models.Model):
    url_hit = models.ForeignKey(UrlHit, editable=False, on_delete=models.CASCADE)
    ip      = models.CharField(max_length=40)
    session = models.CharField(max_length=40)
    date    = models.DateTimeField(auto_now=True)

views.py

def get_client_ip(request):
    x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
    if x_forwarded_for:
        ip = x_forwarded_for.split(',')[0]
    else:
        ip = request.META.get('REMOTE_ADDR')
    return ip

def hit_count(request):
    if not request.session.session_key:
        request.session.save()
    s_key = request.session.session_key
    ip = get_client_ip(request)
    url, url_created = UrlHit.objects.get_or_create(url=request.path)

    if url_created:
        track, created = HitCount.objects.get_or_create(url_hit=url, ip=ip, session=s_key)
        if created:
            url.increase()
            request.session[ip] = ip
            request.session[request.path] = request.path
    else:
        if ip and request.path not in request.session:
            track, created = HitCount.objects.get_or_create(url_hit=url, ip=ip, session=s_key)
            if created:
                url.increase()
                request.session[ip] = ip
                request.session[request.path] = request.path
    return url.hits
Radico
  • 336
  • 1
  • 5
  • 19
  • Thanks, I like your approach. Please could you elaborate further how to increment the views when someone visits a page i.e. could you provide code for the urls.py – MGLondon Sep 01 '19 at 12:05
  • 1
    The function can be invoked inside the view that you need to count its visits and since the function returns the hits number, it can be added to the context data and used in the template. However, I have converted this function to a decorator [here](https://github.com/Radi85/django-website/blob/master/track/views.py) – Radico Sep 02 '19 at 11:33
  • 2
    Thank you Radico. I have used your approach on my website and it is working pretty awesome. I utilized it in a class based view :) – MGLondon Sep 02 '19 at 18:27
  • The viewer's count will not be unique right? we will receive duplicated count for the same user, since its visit count – Thomas John Mar 30 '20 at 06:21
5

I did this by creating a model PageViews and making a column "Hits" in it. Every time when Homepage url is hit. I increment the first and only row of column Hit and render it to the template. Here how it looks.

Views.py

def Home(request):

    if(PageView.objects.count()<=0):
        x=PageView.objects.create()
        x.save()
    else:
        x=PageView.objects.all()[0]
        x.hits=x.hits+1
        x.save()
    context={'page':x.hits}
    return  render(request,'home.html',context=context)

Models.py

class PageView(models.Model):
    hits=models.IntegerField(default=0)
Pankaj Mishra
  • 550
  • 6
  • 18
3

I did it using cookies. Don't know if it's a good idea to do that or not. The following code looks for an already set cookie first if it exists it increases the total_view counter if it is not there the it increases both total_views and unique_views. Both total_views and unique_views are a field of a Django model.

def view(request):
    ...
    cookie_state = request.COOKIES.get('viewed_post_%s' % post_name_slug)
    response = render_to_response('community/post.html',context_instance=RequestContext(request, context_dict))
    if cookie_state:
        Post.objects.filter(id=post.id).update(total_views=F('total_views') + 1)
    else:
        Post.objects.filter(id=post.id).update(unique_views=F('unique_views') + 1)
        Post.objects.filter(id=post.id).update(total_views=F('total_views') + 1)
                        response.set_cookie('viewed_post_%s' % post_name_slug , True, max_age=2678400)
    return response
Mudit Jain
  • 43
  • 7
  • Hi @MuditJain , I couldn't understand `cookie_state = request.COOKIES.get('viewed_post_%s' % post_name_slug)` how can I get the cookie for my url like: `re_path('gigs/(?P[0-9]+)/$', views.gig_detail, name='gig-detail')` – Abdul Rehman Apr 18 '19 at 06:17