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:
14
'djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS')
16
from django.contrib.contenttypes.models import ContentType
18
__all__ = ('Rating', 'RatingField', 'AnonymousRatingField')
21
from hashlib import md5
23
from md5 import new as md5
26
from django.utils.timezone import now
31
def md5_hexdigest(value):
32
return md5(value).hexdigest()
37
def __init__(self, score, votes):
42
class RatingManager(object):
44
def __init__(self, instance, field):
45
self.content_type = None
46
self.instance = instance
49
self.votes_field_name = '%s_votes' % (self.field.name,)
50
self.score_field_name = '%s_score' % (self.field.name,)
52
def get_percent(self):
55
Returns the weighted percentage of the score from min-max values
58
if not (self.votes and self.score):
60
return 100 * (self.get_rating() / self.field.range)
62
def get_real_percent(self):
65
Returns the unmodified percentage of the score based on a 0-point scale.
68
if not (self.votes and self.score):
70
return 100 * (self.get_real_rating() / self.field.range)
72
def get_ratings(self):
75
Returns a Vote QuerySet for this rating field.
78
return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key)
83
Returns the weighted average rating.
86
if not (self.votes and self.score):
88
return float(self.score) / (self.votes + self.field.weight)
90
def get_opinion_percent(self):
91
"""get_opinion_percent()
93
Returns a neutral-based percentage.
96
return (self.get_percent() + 100) / 2
98
def get_real_rating(self):
101
Returns the unmodified average rating.
104
if not (self.votes and self.score):
106
return float(self.score) / self.votes
108
def get_rating_for_user(self, user, ip_address=None, cookies={}):
109
"""get_rating_for_user(user, ip_address=None, cookie=None)
111
Returns the rating for a user or anonymous IP."""
113
content_type=self.get_content_type(),
114
object_id=self.instance.pk,
118
if not (user and user.is_authenticated):
120
raise ValueError('``user`` or ``ip_address`` must be present.')
121
kwargs['user__isnull'] = True
122
kwargs['ip_address'] = ip_address
124
kwargs['user'] = user
126
use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
128
# TODO: move 'vote-%d.%d.%s' to settings or something
129
cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs[
130
'object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
131
cookie = cookies.get(cookie_name)
133
kwargs['cookie'] = cookie
135
kwargs['cookie__isnull'] = True
138
rating = Vote.objects.get(**kwargs)
140
except Vote.MultipleObjectsReturned:
142
except Vote.DoesNotExist:
146
def get_iterable_range(self):
147
# started from 1, because 0 is equal to delete
148
return range(1, self.field.range)
150
def add(self, score, user, ip_address, cookies={}, commit=True):
151
"""add(score, user, ip_address)
153
Used to add a rating to an object.
158
except (ValueError, TypeError):
159
raise InvalidRating('%s is not a valid choice for %s' %
160
(score, self.field.name))
162
delete = (score == 0)
163
if delete and not self.field.allow_delete:
164
raise CannotDeleteVote(
165
'you are not allowed to delete votes for %s' % (self.field.name,))
166
# ... you're also can't delete your vote if you haven't permissions to change it. I leave this case for CannotChangeVote
168
if score < 0 or score > self.field.range:
169
raise InvalidRating('%s is not a valid choice for %s' %
170
(score, self.field.name))
172
is_anonymous = (user is None or not user.is_authenticated)
173
if is_anonymous and not self.field.allow_anonymous:
174
raise AuthRequired("user must be a user, not '%r'" % (user,))
181
ip_address=ip_address,
185
content_type=self.get_content_type(),
186
object_id=self.instance.pk,
191
kwargs['ip_address'] = ip_address
193
use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
195
defaults['cookie'] = now().strftime(
196
'%Y%m%d%H%M%S%f') # -> md5_hexdigest?
197
# TODO: move 'vote-%d.%d.%s' to settings or something
198
cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs[
199
'object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
200
# try to get existent cookie value
201
cookie = cookies.get(cookie_name)
203
kwargs['cookie__isnull'] = True
204
kwargs['cookie'] = cookie
207
rating, created = Vote.objects.get(**kwargs), False
208
except Vote.DoesNotExist:
210
raise CannotDeleteVote(
211
'attempt to find and delete your vote for %s is failed' % (self.field.name,))
212
if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
213
num_votes = Vote.objects.filter(
214
content_type=kwargs['content_type'],
215
object_id=kwargs['object_id'],
217
ip_address=ip_address,
219
if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
220
raise IPLimitReached()
221
kwargs.update(defaults)
223
# record with specified cookie was not found ...
224
# ... thus we need to replace old cookie (if presented) with new one
225
cookie = defaults['cookie']
226
# ... and remove 'cookie__isnull' (if presented) from .create()'s **kwargs
227
kwargs.pop('cookie__isnull', '')
228
rating, created = Vote.objects.create(**kwargs), True
232
if self.field.can_change_vote:
234
self.score -= rating.score
235
# you can delete your vote only if you have permission to
244
raise CannotChangeVote()
250
self.score += rating.score
253
#setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes))
261
content_type=self.get_content_type(),
262
object_id=self.instance.pk,
267
score, created = Score.objects.get(**kwargs), False
268
except Score.DoesNotExist:
269
kwargs.update(defaults)
270
score, created = Score.objects.create(**kwargs), True
273
score.__dict__.update(defaults)
279
adds['cookie_name'] = cookie_name
280
adds['cookie'] = cookie
282
adds['deleted'] = True
285
def delete(self, user, ip_address, cookies={}, commit=True):
286
return self.add(0, user, ip_address, cookies, commit)
288
def _get_votes(self, default=None):
289
return getattr(self.instance, self.votes_field_name, default)
291
def _set_votes(self, value):
292
return setattr(self.instance, self.votes_field_name, value)
294
votes = property(_get_votes, _set_votes)
296
def _get_score(self, default=None):
297
return getattr(self.instance, self.score_field_name, default)
299
def _set_score(self, value):
300
return setattr(self.instance, self.score_field_name, value)
302
score = property(_get_score, _set_score)
304
def get_content_type(self):
305
if self.content_type is None:
306
self.content_type = ContentType.objects.get_for_model(
308
return self.content_type
310
def _update(self, commit=False):
311
"""Forces an update of this rating (useful for when Vote objects are
313
votes = Vote.objects.filter(
314
content_type=self.get_content_type(),
315
object_id=self.instance.pk,
318
obj_score = sum([v.score for v in votes])
319
obj_votes = len(votes)
321
score, created = Score.objects.get_or_create(
322
content_type=self.get_content_type(),
323
object_id=self.instance.pk,
331
score.score = obj_score
332
score.votes = obj_votes
334
self.score = obj_score
335
self.votes = obj_votes
340
class RatingCreator(object):
342
def __init__(self, field):
344
self.votes_field_name = '%s_votes' % (self.field.name,)
345
self.score_field_name = '%s_score' % (self.field.name,)
347
def __get__(self, instance, type=None):
350
#raise AttributeError('Can only be accessed via an instance.')
351
return RatingManager(instance, self.field)
353
def __set__(self, instance, value):
354
if isinstance(value, Rating):
355
setattr(instance, self.votes_field_name, value.votes)
356
setattr(instance, self.score_field_name, value.score)
358
raise TypeError("%s value must be a Rating instance, not '%r'" % (
359
self.field.name, value))
362
class RatingField(IntegerField):
363
"""A rating field contributes two columns to the model instead of the
364
standard single column."""
366
def __init__(self, *args, **kwargs):
367
if 'choices' in kwargs:
368
raise TypeError("%s invalid attribute 'choices'" %
369
(self.__class__.__name__,))
370
self.can_change_vote = kwargs.pop('can_change_vote', False)
371
self.weight = kwargs.pop('weight', 0)
372
self.range = kwargs.pop('range', 2)
373
self.allow_anonymous = kwargs.pop('allow_anonymous', False)
374
self.use_cookies = kwargs.pop('use_cookies', False)
375
self.allow_delete = kwargs.pop('allow_delete', False)
376
kwargs['editable'] = False
377
kwargs['default'] = 0
378
kwargs['blank'] = True
379
super(RatingField, self).__init__(*args, **kwargs)
381
def contribute_to_class(self, cls, name):
385
self.votes_field = PositiveIntegerField(
386
editable=False, default=0, blank=True)
387
cls.add_to_class('%s_votes' % (self.name,), self.votes_field)
390
self.score_field = IntegerField(
391
editable=False, default=0, blank=True)
392
cls.add_to_class('%s_score' % (self.name,), self.score_field)
394
self.key = md5_hexdigest(self.name)
396
field = RatingCreator(self)
398
if not hasattr(cls, '_djangoratings'):
399
cls._djangoratings = []
400
cls._djangoratings.append(self)
402
setattr(cls, name, field)
404
def get_db_prep_save(self, value):
405
# XXX: what happens here?
408
def get_db_prep_lookup(self, lookup_type, value):
409
# TODO: hack in support for __score and __votes
410
# TODO: order_by on this field should use the weighted algorithm
411
raise NotImplementedError(self.get_db_prep_lookup)
412
# if lookup_type in ('score', 'votes'):
414
# return self.score_field.get_db_prep_lookup()
415
if lookup_type == 'exact':
416
return [self.get_db_prep_save(value)]
417
elif lookup_type == 'in':
418
return [self.get_db_prep_save(v) for v in value]
420
return super(RatingField, self).get_db_prep_lookup(lookup_type, value)
422
def formfield(self, **kwargs):
423
defaults = {'form_class': forms.RatingField}
424
defaults.update(kwargs)
425
return super(RatingField, self).formfield(**defaults)
427
# TODO: flatten_data method
430
class AnonymousRatingField(RatingField):
432
def __init__(self, *args, **kwargs):
433
kwargs['allow_anonymous'] = True
434
super(AnonymousRatingField, self).__init__(*args, **kwargs)