1
from django.db.models import IntegerField, PositiveIntegerField
2
from django.conf import settings
6
from datetime import datetime
8
from models import Vote, Score
9
from default_settings import RATINGS_VOTES_PER_IP
10
from exceptions import *
12
if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS:
13
raise ImportError("djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS")
15
from django.contrib.contenttypes.models import ContentType
17
__all__ = ('Rating', 'RatingField', 'AnonymousRatingField')
20
from hashlib import md5
22
from md5 import new as md5
25
from django.utils.timezone import now
29
def md5_hexdigest(value):
30
return md5(value).hexdigest()
33
def __init__(self, score, votes):
37
class RatingManager(object):
38
def __init__(self, instance, field):
39
self.content_type = None
40
self.instance = instance
43
self.votes_field_name = "%s_votes" % (self.field.name,)
44
self.score_field_name = "%s_score" % (self.field.name,)
46
def get_percent(self):
49
Returns the weighted percentage of the score from min-max values"""
50
if not (self.votes and self.score):
52
return 100 * (self.get_rating() / self.field.range)
54
def get_real_percent(self):
57
Returns the unmodified percentage of the score based on a 0-point scale."""
58
if not (self.votes and self.score):
60
return 100 * (self.get_real_rating() / self.field.range)
62
def get_ratings(self):
65
Returns a Vote QuerySet for this rating field."""
66
return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key)
71
Returns the weighted average rating."""
72
if not (self.votes and self.score):
74
return float(self.score)/(self.votes+self.field.weight)
76
def get_opinion_percent(self):
77
"""get_opinion_percent()
79
Returns a neutral-based percentage."""
80
return (self.get_percent()+100)/2
82
def get_real_rating(self):
85
Returns the unmodified average rating."""
86
if not (self.votes and self.score):
88
return float(self.score)/self.votes
90
def get_rating_for_user(self, user, ip_address=None, cookies={}):
91
"""get_rating_for_user(user, ip_address=None, cookie=None)
93
Returns the rating for a user or anonymous IP."""
95
content_type = self.get_content_type(),
96
object_id = self.instance.pk,
100
if not (user and user.is_authenticated()):
102
raise ValueError('``user`` or ``ip_address`` must be present.')
103
kwargs['user__isnull'] = True
104
kwargs['ip_address'] = ip_address
106
kwargs['user'] = user
108
use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
110
# TODO: move 'vote-%d.%d.%s' to settings or something
111
cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
112
cookie = cookies.get(cookie_name)
114
kwargs['cookie'] = cookie
116
kwargs['cookie__isnull'] = True
119
rating = Vote.objects.get(**kwargs)
121
except Vote.MultipleObjectsReturned:
123
except Vote.DoesNotExist:
127
def get_iterable_range(self):
128
return range(1, self.field.range) #started from 1, because 0 is equal to delete
130
def add(self, score, user, ip_address, cookies={}, commit=True):
131
"""add(score, user, ip_address)
133
Used to add a rating to an object."""
136
except (ValueError, TypeError):
137
raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
139
delete = (score == 0)
140
if delete and not self.field.allow_delete:
141
raise CannotDeleteVote("you are not allowed to delete votes for %s" % (self.field.name,))
142
# ... you're also can't delete your vote if you haven't permissions to change it. I leave this case for CannotChangeVote
144
if score < 0 or score > self.field.range:
145
raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
147
is_anonymous = (user is None or not user.is_authenticated())
148
if is_anonymous and not self.field.allow_anonymous:
149
raise AuthRequired("user must be a user, not '%r'" % (user,))
156
ip_address = ip_address,
160
content_type = self.get_content_type(),
161
object_id = self.instance.pk,
162
key = self.field.key,
166
kwargs['ip_address'] = ip_address
168
use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
170
defaults['cookie'] = now().strftime('%Y%m%d%H%M%S%f') # -> md5_hexdigest?
171
# TODO: move 'vote-%d.%d.%s' to settings or something
172
cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
173
cookie = cookies.get(cookie_name) # try to get existent cookie value
175
kwargs['cookie__isnull'] = True
176
kwargs['cookie'] = cookie
179
rating, created = Vote.objects.get(**kwargs), False
180
except Vote.DoesNotExist:
182
raise CannotDeleteVote("attempt to find and delete your vote for %s is failed" % (self.field.name,))
183
if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
184
num_votes = Vote.objects.filter(
185
content_type=kwargs['content_type'],
186
object_id=kwargs['object_id'],
188
ip_address=ip_address,
190
if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
191
raise IPLimitReached()
192
kwargs.update(defaults)
194
# record with specified cookie was not found ...
195
cookie = defaults['cookie'] # ... thus we need to replace old cookie (if presented) with new one
196
kwargs.pop('cookie__isnull', '') # ... and remove 'cookie__isnull' (if presented) from .create()'s **kwargs
197
rating, created = Vote.objects.create(**kwargs), True
201
if self.field.can_change_vote:
203
self.score -= rating.score
204
# you can delete your vote only if you have permission to change your vote
212
raise CannotChangeVote()
218
self.score += rating.score
221
#setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes))
229
content_type = self.get_content_type(),
230
object_id = self.instance.pk,
231
key = self.field.key,
235
score, created = Score.objects.get(**kwargs), False
236
except Score.DoesNotExist:
237
kwargs.update(defaults)
238
score, created = Score.objects.create(**kwargs), True
241
score.__dict__.update(defaults)
247
adds['cookie_name'] = cookie_name
248
adds['cookie'] = cookie
250
adds['deleted'] = True
253
def delete(self, user, ip_address, cookies={}, commit=True):
254
return self.add(0, user, ip_address, cookies, commit)
256
def _get_votes(self, default=None):
257
return getattr(self.instance, self.votes_field_name, default)
259
def _set_votes(self, value):
260
return setattr(self.instance, self.votes_field_name, value)
262
votes = property(_get_votes, _set_votes)
264
def _get_score(self, default=None):
265
return getattr(self.instance, self.score_field_name, default)
267
def _set_score(self, value):
268
return setattr(self.instance, self.score_field_name, value)
270
score = property(_get_score, _set_score)
272
def get_content_type(self):
273
if self.content_type is None:
274
self.content_type = ContentType.objects.get_for_model(self.instance)
275
return self.content_type
277
def _update(self, commit=False):
278
"""Forces an update of this rating (useful for when Vote objects are removed)."""
279
votes = Vote.objects.filter(
280
content_type = self.get_content_type(),
281
object_id = self.instance.pk,
282
key = self.field.key,
284
obj_score = sum([v.score for v in votes])
285
obj_votes = len(votes)
287
score, created = Score.objects.get_or_create(
288
content_type = self.get_content_type(),
289
object_id = self.instance.pk,
290
key = self.field.key,
297
score.score = obj_score
298
score.votes = obj_votes
300
self.score = obj_score
301
self.votes = obj_votes
305
class RatingCreator(object):
306
def __init__(self, field):
308
self.votes_field_name = "%s_votes" % (self.field.name,)
309
self.score_field_name = "%s_score" % (self.field.name,)
311
def __get__(self, instance, type=None):
314
#raise AttributeError('Can only be accessed via an instance.')
315
return RatingManager(instance, self.field)
317
def __set__(self, instance, value):
318
if isinstance(value, Rating):
319
setattr(instance, self.votes_field_name, value.votes)
320
setattr(instance, self.score_field_name, value.score)
322
raise TypeError("%s value must be a Rating instance, not '%r'" % (self.field.name, value))
324
class RatingField(IntegerField):
326
A rating field contributes two columns to the model instead of the standard single column.
328
def __init__(self, *args, **kwargs):
329
if 'choices' in kwargs:
330
raise TypeError("%s invalid attribute 'choices'" % (self.__class__.__name__,))
331
self.can_change_vote = kwargs.pop('can_change_vote', False)
332
self.weight = kwargs.pop('weight', 0)
333
self.range = kwargs.pop('range', 2)
334
self.allow_anonymous = kwargs.pop('allow_anonymous', False)
335
self.use_cookies = kwargs.pop('use_cookies', False)
336
self.allow_delete = kwargs.pop('allow_delete', False)
337
kwargs['editable'] = False
338
kwargs['default'] = 0
339
kwargs['blank'] = True
340
super(RatingField, self).__init__(*args, **kwargs)
342
def contribute_to_class(self, cls, name):
346
self.votes_field = PositiveIntegerField(
347
editable=False, default=0, blank=True)
348
cls.add_to_class("%s_votes" % (self.name,), self.votes_field)
351
self.score_field = IntegerField(
352
editable=False, default=0, blank=True)
353
cls.add_to_class("%s_score" % (self.name,), self.score_field)
355
self.key = md5_hexdigest(self.name)
357
field = RatingCreator(self)
359
if not hasattr(cls, '_djangoratings'):
360
cls._djangoratings = []
361
cls._djangoratings.append(self)
363
setattr(cls, name, field)
365
def get_db_prep_save(self, value):
366
# XXX: what happens here?
369
def get_db_prep_lookup(self, lookup_type, value):
370
# TODO: hack in support for __score and __votes
371
# TODO: order_by on this field should use the weighted algorithm
372
raise NotImplementedError(self.get_db_prep_lookup)
373
# if lookup_type in ('score', 'votes'):
375
# return self.score_field.get_db_prep_lookup()
376
if lookup_type == 'exact':
377
return [self.get_db_prep_save(value)]
378
elif lookup_type == 'in':
379
return [self.get_db_prep_save(v) for v in value]
381
return super(RatingField, self).get_db_prep_lookup(lookup_type, value)
383
def formfield(self, **kwargs):
384
defaults = {'form_class': forms.RatingField}
385
defaults.update(kwargs)
386
return super(RatingField, self).formfield(**defaults)
388
# TODO: flatten_data method
391
class AnonymousRatingField(RatingField):
392
def __init__(self, *args, **kwargs):
393
kwargs['allow_anonymous'] = True
394
super(AnonymousRatingField, self).__init__(*args, **kwargs)