Skip to content

Commit

Permalink
bug 634402 bug 634263 move mdn apps into kuma; functional but mis-styled
Browse files Browse the repository at this point in the history
  • Loading branch information
groovecoder committed Feb 23, 2011
1 parent a4920d2 commit a7cfcfd
Show file tree
Hide file tree
Showing 120 changed files with 9,100 additions and 173 deletions.
10 changes: 10 additions & 0 deletions apps/actioncounters/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
actioncounters
==============

This is a Django app intended to help count actions on model content objects.
Examples of actions include things such as "view", "download", and "like".
An attempt is made to constrain each counted action to a limit per unique user.

Inspired in part by:
* <https://github.com/thornomad/django-hitcount>
* <https://github.com/dcramer/django-ratings>
Empty file added apps/actioncounters/__init__.py
Empty file.
8 changes: 8 additions & 0 deletions apps/actioncounters/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from django.contrib import admin

from .models import ActionCounterUnique

class ActionCounterUniqueAdmin(admin.ModelAdmin):
list_display = ( 'content_object', 'name', 'total', 'user', 'ip',
'user_agent', 'modified', )
admin.site.register(ActionCounterUnique, ActionCounterUniqueAdmin)
130 changes: 130 additions & 0 deletions apps/actioncounters/fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Fields for action counters
See also: djangoratings for inspiration
"""
import logging

from django.conf import settings
from django.db import models
from django.db.models import F


RECENT_PERIOD = getattr(settings, 'ACTION_COUNTER_RECENT_PERIOD', 60 * 60 * 24)


class ActionCounterField(models.IntegerField):
"""An action counter field contributes two columns to the model - one for
the current total count, and another for a recent history window count."""

def __init__(self, *args, **kwargs):

self.max_total_per_unique = kwargs.pop('max_total_per_unique', 1)
self.min_total_per_unique = kwargs.pop('min_total_per_unique', 0)

super(ActionCounterField, self).__init__(*args, **kwargs)

def contribute_to_class(self, cls, name):
self.name = name

self.total_field = models.IntegerField(editable=False, default=0, blank=True, db_index=True)
cls.add_to_class('%s_total' % (self.name,), self.total_field)

self.recent_field = models.IntegerField(editable=False, default=0, blank=True, db_index=True)
cls.add_to_class('%s_recent' % (self.name,), self.recent_field)

# TODO: Could maybe include a JSON-formatted history list of recent rollups

t = ActionCounterCreator(self)
setattr(cls, name, t)

def get_db_prep_save(self, value):
pass


class ActionCounterCreator(object):
def __init__(self, field):
self.field = field
self.votes_field_name = "%s_votes" % (self.field.name,)
self.score_field_name = "%s_score" % (self.field.name,)

def __get__(self, instance, type=None):
if instance is None:
return self.field
return ActionCounterManager(instance, self.field)

def __set__(self, instance, value):
raise TypeError("%s cannot be set directly")


class ActionCounterManager(object):

def __init__(self, instance, field):
self.content_type = None
self.instance = instance
self.field = field
self.name = field.name

self.total_field_name = "%s_total" % (self.name,)
self.recent_field_name = "%s_recent" % (self.name,)

def _get_total(self, default=0):
return getattr(self.instance, self.total_field_name, default)
def _set_total(self, value):
return setattr(self.instance, self.total_field_name, value)
total = property(_get_total, _set_total)

def _get_recent(self, default=0):
return getattr(self.instance, self.recent_field_name, default)
def _set_recent(self, value):
return setattr(self.instance, self.recent_field_name, value)
recent = property(_get_recent, _set_recent)

def _get_counter_for_request(self, request, do_create=True):
from .models import ActionCounterUnique
counter, created = ActionCounterUnique.objects.get_unique_for_request(
self.instance, self.name, request, do_create)
return counter

def get_total_for_request(self, request):
counter = self._get_counter_for_request(request, False)
return counter and counter.total or 0

def increment(self, request):
counter = self._get_counter_for_request(request)
ok = counter.increment(self.field.min_total_per_unique,
self.field.max_total_per_unique)
if ok: self._change_total(1)

def decrement(self, request):
counter = self._get_counter_for_request(request)
ok = counter.decrement(self.field.min_total_per_unique,
self.field.max_total_per_unique)
if ok: self._change_total(-1)

def _change_total(self, delta):
# This is ugly, but results in a single-column UPDATE like so:
#
# UPDATE `actioncounters_testmodel`
# SET `likes_total` = `actioncounters_testmodel`.`likes_total` + 1
# WHERE `actioncounters_testmodel`.`id` = 1
#
# This also avoids updating datestamps and doing a more verbose query.
# TODO: Find a less-ugly way to do this.
m_cls = self.instance.__class__
qs = m_cls.objects.all().filter(pk=self.instance.pk)
update_kwargs = {
"%s" % self.total_field_name: F(self.total_field_name) + delta
}
qs.update(**update_kwargs)

# HACK: This value change is just for the benefit of local code,
# may possibly fall out of sync with the actual database if there's a
# race condition. A subsequent save() could clobber concurrent counter
# changes.
self.total = self.total + delta

# HACK: Invalidate this object in cache-machine, if the method is available.
if hasattr(m_cls.objects, 'invalidate'):
m_cls.objects.invalidate(self.instance)


113 changes: 113 additions & 0 deletions apps/actioncounters/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Models for activity counters"""

import logging

from django.db import models
from django.conf import settings
from django.db.models import F

from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import User
from django.contrib.contenttypes import generic

from django.utils.translation import ugettext_lazy as _

from .utils import get_ip, get_unique
from .fields import ActionCounterField


class TestModel(models.Model):
# TODO: Find a way to move this to tests.py and create only during testing!

title = models.CharField(max_length=255, blank=False, unique=True)

likes = ActionCounterField()
views = ActionCounterField(max_total_per_unique=5)
frobs = ActionCounterField(min_total_per_unique=-5)
boogs = ActionCounterField(min_total_per_unique=-5, max_total_per_unique=5)

def __unicode__(self):
return unicode(self.pk)


class ActionCounterUniqueManager(models.Manager):
"""Manager for action hits"""

def get_unique_for_request(self, object, action_name, request, create=True):
"""Get a unique counter for the given request, with the option to
refrain from creating a new one if the intent is just to check
existence."""
content_type = ContentType.objects.get_for_model(object)
user, ip, user_agent, session_key = get_unique(request)
if create:
return self.get_or_create(
content_type=content_type, object_pk=object.pk,
name=action_name,
ip=ip, user_agent=user_agent, user=user,
session_key=session_key,
defaults=dict( total=0 ))
else:
try:
return (
self.get(
content_type=content_type, object_pk=object.pk,
name=action_name,
ip=ip, user_agent=user_agent, user=user,
session_key=session_key,),
False
)
except ActionCounterUnique.DoesNotExist:
return ( None, False )


class ActionCounterUnique(models.Model):
"""Action counter for a unique request / user"""
objects = ActionCounterUniqueManager()

content_type = models.ForeignKey(
ContentType,
verbose_name="content type",
related_name="content_type_set_for_%(class)s",)
object_pk = models.CharField( _('object ID'),
max_length=32)
content_object = generic.GenericForeignKey(
'content_type', 'object_pk')
name = models.CharField( _('name of the action'),
max_length=64, db_index=True, blank=False)

total = models.IntegerField()

ip = models.CharField(max_length=40, editable=False,
db_index=True, blank=True, null=True)
session_key = models.CharField(max_length=40, editable=False,
db_index=True, blank=True, null=True)
user_agent = models.CharField(max_length=255, editable=False,
db_index=True, blank=True, null=True)
user = models.ForeignKey(User, editable=False,
db_index=True, blank=True, null=True)

modified = models.DateTimeField(
_('date last modified'),
auto_now=True, blank=False)

def increment(self, min=0, max=1):
return self._change_total(1, min, max)

def decrement(self, min=0, max=1):
return self._change_total(-1, min, max)

def _change_total(self, delta, min, max):
# TODO: This seems like a race condition. Maybe there's a way to do something like
# UPDATE actioncounterunique SET total = min(max(field_total+1, min), max) WHERE ...
# ...and if that's doable, how to detect whether the total changed
result = (self.total + delta)
if result > max: return False
if result < min: return False
if result == 0:
# Don't keep zero entries around in the table.
self.delete()
else:
self.total = F('total') + delta
self.save()
return True

Loading

0 comments on commit a7cfcfd

Please sign in to comment.