Skip to content

Commit

Permalink
initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
ddanier committed Jul 18, 2011
0 parents commit 79aa751
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 0 deletions.
28 changes: 28 additions & 0 deletions LICENSE
@@ -0,0 +1,28 @@
Copyright (c) 2010
Team23 GbR <info@team23.de> / David Danier <david.danier@team23.de>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of Team23 nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

1 change: 1 addition & 0 deletions django_hits/__init__.py
@@ -0,0 +1 @@

108 changes: 108 additions & 0 deletions django_hits/migrations/0001_initial.py
@@ -0,0 +1,108 @@
# encoding: utf-8
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models

class Migration(SchemaMigration):

def forwards(self, orm):

# Adding model 'Hit'
db.create_table('django_hits_hit', (
('visits', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
('object_pk', self.gf('django.db.models.fields.CharField')(max_length=255)),
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True)),
('views', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
))
db.send_create_signal('django_hits', ['Hit'])

# Adding unique constraint on 'Hit', fields ['content_type', 'object_pk']
db.create_unique('django_hits_hit', ['content_type_id', 'object_pk'])

# Adding model 'HitLog'
db.create_table('django_hits_hitlog', (
('ip', self.gf('django.db.models.fields.IPAddressField')(max_length=15, null=True)),
('when', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
('hit', self.gf('django.db.models.fields.related.ForeignKey')(related_name='log', to=orm['django_hits.Hit'])),
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='hits_log', null=True, to=orm['auth.User'])),
))
db.send_create_signal('django_hits', ['HitLog'])

# Adding unique constraint on 'HitLog', fields ['hit', 'user', 'ip']
db.create_unique('django_hits_hitlog', ['hit_id', 'user_id', 'ip'])


def backwards(self, orm):

# Deleting model 'Hit'
db.delete_table('django_hits_hit')

# Removing unique constraint on 'Hit', fields ['content_type', 'object_pk']
db.delete_unique('django_hits_hit', ['content_type_id', 'object_pk'])

# Deleting model 'HitLog'
db.delete_table('django_hits_hitlog')

# Removing unique constraint on 'HitLog', fields ['hit', 'user', 'ip']
db.delete_unique('django_hits_hitlog', ['hit_id', 'user_id', 'ip'])


models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'django_hits.hit': {
'Meta': {'unique_together': "(('content_type', 'object_pk'),)", 'object_name': 'Hit'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_pk': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'views': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
'visits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
'django_hits.hitlog': {
'Meta': {'unique_together': "(('hit', 'user', 'ip'),)", 'object_name': 'HitLog'},
'hit': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'log'", 'to': "orm['django_hits.Hit']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ip': ('django.db.models.fields.IPAddressField', [], {'max_length': '15', 'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'hits_log'", 'null': 'True', 'to': "orm['auth.User']"}),
'when': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'})
}
}

complete_apps = ['django_hits']
Empty file.
97 changes: 97 additions & 0 deletions django_hits/models.py
@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*-
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.contrib.auth.models import User
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from datetime import datetime, timedelta

from django.db.models import signals


class HitManager(models.Manager):
def get_for(self, obj):
from django.db import backend
if isinstance(obj, models.Model):
content_type = ContentType.objects.get_for_model(obj.__class__)
object_pk = getattr(obj, obj._meta.pk.column)
try:
return self.get_or_create(content_type=content_type, object_pk=object_pk)[0]
except backend.IntegrityError: # catch race condition
return self.get(content_type=content_type, object_pk=object_pk)
elif isinstance(obj, (str, unicode)):
try:
return self.get_or_create(content_type__isnull=True, object_pk=obj)[0]
except backend.IntegrityError: # catch race condition
return self.get(content_type__isnull=True, object_pk=obj)
else:
raise Exception("Don't know what to do with this obj!?")

def hit(self, obj, user, ip):
hit = self.get_for(obj)
hit.hit(user, ip)
return hit


class Hit(models.Model):
content_type = models.ForeignKey(ContentType, null=True)
object_pk = models.CharField(max_length=255) # TextField not possible, because unique_together is needed, must be enough
content_object = generic.GenericForeignKey(ct_field="content_type", fk_field="object_pk")
views = models.PositiveIntegerField(default=0) # page hits/views
visits = models.PositiveIntegerField(default=0) # unique visits

objects = HitManager()

# TODO: Transaction-Management needed?
@transaction.commit_manually
def hit(self, user, ip):
from django.db import backend
if self.has_hit_from(user, ip):
self.update_hit_from(user, ip)
Hit.objects.filter(pk=self.pk).update(views=models.F('views') + 1)
self.views += 1
transaction.commit()
return True
try:
self.log.create(user=user, ip=ip)
except backend.IntegrityError: # catch race condition
# log-extry was already created
# happens when users double-click or reload to fast
# (we ignore this)
transaction.rollback()
return False
Hit.objects.filter(pk=self.pk).update(views=models.F('views') + 1, visits=models.F('visits') + 1)
self.views += 1
self.visits += 1
transaction.commit()
return True

def has_hit_from(self, user, ip):
self.clear_log()
if self.log.filter(user=user, ip=ip).count():
return True
else:
return False

def update_hit_from(self, user, ip):
self.log.filter(user=user, ip=ip).update(when=datetime.now())

def clear_log(self):
timespan = datetime.now() - timedelta(days=30)
for l in self.log.filter(when__lt=timespan).order_by('-when')[25:]:
l.delete()

class Meta:
unique_together = (('content_type', 'object_pk'),)


class HitLog(models.Model):
hit = models.ForeignKey(Hit, related_name='log')
user = models.ForeignKey(User, related_name='hits_log', null=True)
ip = models.IPAddressField(null=True)
when = models.DateTimeField(default=datetime.now)

class Meta:
unique_together = (('hit', 'user', 'ip'),)

1 change: 1 addition & 0 deletions django_hits/templatetags/__init__.py
@@ -0,0 +1 @@

82 changes: 82 additions & 0 deletions django_hits/templatetags/hit_tags.py
@@ -0,0 +1,82 @@
from django import template
from django_hits.models import Hit

register = template.Library()


class HitNode(template.Node):
def __init__(self, context_var_name, var_name, count):
self.context_var_name = context_var_name
self.var_name = var_name
self.count = count

def render(self, context):
if not hasattr(context, '_hit_cache_'):
context._hit_cache_ = {}
try:
obj = self.context_var_name.resolve(context)
user = template.Variable('user').resolve(context)
except template.VariableDoesNotExist:
return ''
if user.is_anonymous():
user = None
count = self.count
ip = None
if count:
try:
request = template.Variable('request').resolve(context)
if 'REMOTE_ADDR' in request.META:
ip = request.META['REMOTE_ADDR']
except template.VariableDoesNotExist:
pass
was_counted = False
if obj in context._hit_cache_:
hit, was_counted = context._hit_cache_[obj]
else:
hit = Hit.objects.get_for(obj)
if count and not was_counted:
was_counted = hit.hit(user, ip)
context._hit_cache_[obj] = (hit, was_counted)
if self.var_name:
context[self.var_name] = hit
return ''


@register.tag
def get_hit(parser, token):
'''
{% get_hit for obj as hit %}
{% get_hit for "static_page" as hit %}
'''
tokens = token.split_contents()
if not len(tokens) in (5,):
raise template.TemplateSyntaxError, "%r tag requires 4 or 5" % tokens[0]
if tokens[1] != 'for':
raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
context_var_name = parser.compile_filter(tokens[2])
if tokens[3] != 'as':
raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0]
var_name = tokens[4]
return HitNode(context_var_name, var_name, False)

@register.tag
def count_hit(parser, token):
'''
{% count_hit for obj %}
{% count_hit for obj as hit %}
{% count_hit for "static_page" %}
{% count_hit for "static_page" as hit %}
'''
tokens = token.split_contents()
if not len(tokens) in (3, 5,):
raise template.TemplateSyntaxError, "%r tag requires 4 or 5" % tokens[0]
if tokens[1] != 'for':
raise template.TemplateSyntaxError, "Second argument in %r tag must be 'for'" % tokens[0]
context_var_name = parser.compile_filter(tokens[2])
var_name = None
if len(tokens) > 3:
if tokens[3] != 'as':
raise template.TemplateSyntaxError, "Fourth argument in %r must be 'as'" % tokens[0]
var_name = tokens[4]
return HitNode(context_var_name, var_name, True)

0 comments on commit 79aa751

Please sign in to comment.