-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
bug 634402 bug 634263 move mdn apps into kuma; functional but mis-styled
- Loading branch information
1 parent
a4920d2
commit a7cfcfd
Showing
120 changed files
with
9,100 additions
and
173 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
Oops, something went wrong.