/
fields.py
130 lines (96 loc) · 4.66 KB
/
fields.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""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, connection):
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)