26
27
except ImportError:
29
31
def md5_hexdigest(value):
30
32
return md5(value).hexdigest()
32
35
class Rating(object):
33
37
def __init__(self, score, votes):
37
42
class RatingManager(object):
38
44
def __init__(self, instance, field):
39
45
self.content_type = None
40
46
self.instance = instance
43
self.votes_field_name = "%s_votes" % (self.field.name,)
44
self.score_field_name = "%s_score" % (self.field.name,)
49
self.votes_field_name = '%s_votes' % (self.field.name,)
50
self.score_field_name = '%s_score' % (self.field.name,)
46
52
def get_percent(self):
49
Returns the weighted percentage of the score from min-max values"""
55
Returns the weighted percentage of the score from min-max values
50
58
if not (self.votes and self.score):
52
60
return 100 * (self.get_rating() / self.field.range)
54
62
def get_real_percent(self):
55
63
"""get_real_percent()
57
Returns the unmodified percentage of the score based on a 0-point scale."""
65
Returns the unmodified percentage of the score based on a 0-point scale.
58
68
if not (self.votes and self.score):
60
70
return 100 * (self.get_real_rating() / self.field.range)
62
72
def get_ratings(self):
65
Returns a Vote QuerySet for this rating field."""
75
Returns a Vote QuerySet for this rating field.
66
78
return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key)
68
80
def get_rating(self):
71
Returns the weighted average rating."""
83
Returns the weighted average rating.
72
86
if not (self.votes and self.score):
74
return float(self.score)/(self.votes+self.field.weight)
88
return float(self.score) / (self.votes + self.field.weight)
76
90
def get_opinion_percent(self):
77
91
"""get_opinion_percent()
79
Returns a neutral-based percentage."""
80
return (self.get_percent()+100)/2
93
Returns a neutral-based percentage.
96
return (self.get_percent() + 100) / 2
82
98
def get_real_rating(self):
85
Returns the unmodified average rating."""
101
Returns the unmodified average rating.
86
104
if not (self.votes and self.score):
88
return float(self.score)/self.votes
106
return float(self.score) / self.votes
90
108
def get_rating_for_user(self, user, ip_address=None, cookies={}):
91
109
"""get_rating_for_user(user, ip_address=None, cookie=None)
93
111
Returns the rating for a user or anonymous IP."""
95
content_type = self.get_content_type(),
96
object_id = self.instance.pk,
113
content_type=self.get_content_type(),
114
object_id=self.instance.pk,
100
118
if not (user and user.is_authenticated()):
104
122
kwargs['ip_address'] = ip_address
106
124
kwargs['user'] = user
108
126
use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
110
128
# 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?
129
cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs[
130
'object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
112
131
cookie = cookies.get(cookie_name)
114
133
kwargs['cookie'] = cookie
116
135
kwargs['cookie__isnull'] = True
119
138
rating = Vote.objects.get(**kwargs)
120
139
return rating.score
123
142
except Vote.DoesNotExist:
127
146
def get_iterable_range(self):
128
return range(1, self.field.range) #started from 1, because 0 is equal to delete
147
# started from 1, because 0 is equal to delete
148
return range(1, self.field.range)
130
150
def add(self, score, user, ip_address, cookies={}, commit=True):
131
151
"""add(score, user, ip_address)
133
Used to add a rating to an object."""
153
Used to add a rating to an object.
135
157
score = int(score)
136
158
except (ValueError, TypeError):
137
raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
159
raise InvalidRating('%s is not a valid choice for %s' %
160
(score, self.field.name))
139
162
delete = (score == 0)
140
163
if delete and not self.field.allow_delete:
141
raise CannotDeleteVote("you are not allowed to delete votes for %s" % (self.field.name,))
164
raise CannotDeleteVote(
165
'you are not allowed to delete votes for %s' % (self.field.name,))
142
166
# ... you're also can't delete your vote if you haven't permissions to change it. I leave this case for CannotChangeVote
144
168
if score < 0 or score > self.field.range:
145
raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
169
raise InvalidRating('%s is not a valid choice for %s' %
170
(score, self.field.name))
147
172
is_anonymous = (user is None or not user.is_authenticated())
148
173
if is_anonymous and not self.field.allow_anonymous:
149
174
raise AuthRequired("user must be a user, not '%r'" % (user,))
156
ip_address = ip_address,
181
ip_address=ip_address,
160
content_type = self.get_content_type(),
161
object_id = self.instance.pk,
162
key = self.field.key,
185
content_type=self.get_content_type(),
186
object_id=self.instance.pk,
166
191
kwargs['ip_address'] = ip_address
168
193
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?
195
defaults['cookie'] = now().strftime(
196
'%Y%m%d%H%M%S%f') # -> md5_hexdigest?
171
197
# 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
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)
175
203
kwargs['cookie__isnull'] = True
176
204
kwargs['cookie'] = cookie
179
207
rating, created = Vote.objects.get(**kwargs), False
180
208
except Vote.DoesNotExist:
182
raise CannotDeleteVote("attempt to find and delete your vote for %s is failed" % (self.field.name,))
210
raise CannotDeleteVote(
211
'attempt to find and delete your vote for %s is failed' % (self.field.name,))
183
212
if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
184
213
num_votes = Vote.objects.filter(
185
214
content_type=kwargs['content_type'],
192
221
kwargs.update(defaults)
194
223
# 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
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', '')
197
228
rating, created = Vote.objects.create(**kwargs), True
199
230
has_changed = False
201
232
if self.field.can_change_vote:
202
233
has_changed = True
203
234
self.score -= rating.score
204
# you can delete your vote only if you have permission to change your vote
235
# you can delete your vote only if you have permission to
206
238
rating.score = score
220
252
self.instance.save()
221
253
#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,
261
content_type=self.get_content_type(),
262
object_id=self.instance.pk,
235
267
score, created = Score.objects.get(**kwargs), False
236
268
except Score.DoesNotExist:
237
269
kwargs.update(defaults)
238
270
score, created = Score.objects.create(**kwargs), True
241
273
score.__dict__.update(defaults)
253
285
def delete(self, user, ip_address, cookies={}, commit=True):
254
286
return self.add(0, user, ip_address, cookies, commit)
256
288
def _get_votes(self, default=None):
257
289
return getattr(self.instance, self.votes_field_name, default)
259
291
def _set_votes(self, value):
260
292
return setattr(self.instance, self.votes_field_name, value)
262
294
votes = property(_get_votes, _set_votes)
264
296
def _get_score(self, default=None):
265
297
return getattr(self.instance, self.score_field_name, default)
267
299
def _set_score(self, value):
268
300
return setattr(self.instance, self.score_field_name, value)
270
302
score = property(_get_score, _set_score)
272
304
def get_content_type(self):
273
305
if self.content_type is None:
274
self.content_type = ContentType.objects.get_for_model(self.instance)
306
self.content_type = ContentType.objects.get_for_model(
275
308
return self.content_type
277
310
def _update(self, commit=False):
278
"""Forces an update of this rating (useful for when Vote objects are removed)."""
311
"""Forces an update of this rating (useful for when Vote objects are
279
313
votes = Vote.objects.filter(
280
content_type = self.get_content_type(),
281
object_id = self.instance.pk,
282
key = self.field.key,
314
content_type=self.get_content_type(),
315
object_id=self.instance.pk,
284
318
obj_score = sum([v.score for v in votes])
285
319
obj_votes = len(votes)
287
321
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,
322
content_type=self.get_content_type(),
323
object_id=self.instance.pk,
303
337
self.instance.save()
305
340
class RatingCreator(object):
306
342
def __init__(self, field):
307
343
self.field = field
308
self.votes_field_name = "%s_votes" % (self.field.name,)
309
self.score_field_name = "%s_score" % (self.field.name,)
344
self.votes_field_name = '%s_votes' % (self.field.name,)
345
self.score_field_name = '%s_score' % (self.field.name,)
311
347
def __get__(self, instance, type=None):
312
348
if instance is None:
319
355
setattr(instance, self.votes_field_name, value.votes)
320
356
setattr(instance, self.score_field_name, value.score)
322
raise TypeError("%s value must be a Rating instance, not '%r'" % (self.field.name, value))
358
raise TypeError("%s value must be a Rating instance, not '%r'" % (
359
self.field.name, value))
324
362
class RatingField(IntegerField):
326
A rating field contributes two columns to the model instead of the standard single column.
363
"""A rating field contributes two columns to the model instead of the
364
standard single column."""
328
366
def __init__(self, *args, **kwargs):
329
367
if 'choices' in kwargs:
330
raise TypeError("%s invalid attribute 'choices'" % (self.__class__.__name__,))
368
raise TypeError("%s invalid attribute 'choices'" %
369
(self.__class__.__name__,))
331
370
self.can_change_vote = kwargs.pop('can_change_vote', False)
332
371
self.weight = kwargs.pop('weight', 0)
333
372
self.range = kwargs.pop('range', 2)
338
377
kwargs['default'] = 0
339
378
kwargs['blank'] = True
340
379
super(RatingField, self).__init__(*args, **kwargs)
342
381
def contribute_to_class(self, cls, name):
345
384
# Votes tally field
346
385
self.votes_field = PositiveIntegerField(
347
386
editable=False, default=0, blank=True)
348
cls.add_to_class("%s_votes" % (self.name,), self.votes_field)
387
cls.add_to_class('%s_votes' % (self.name,), self.votes_field)
350
389
# Score sum field
351
390
self.score_field = IntegerField(
352
391
editable=False, default=0, blank=True)
353
cls.add_to_class("%s_score" % (self.name,), self.score_field)
392
cls.add_to_class('%s_score' % (self.name,), self.score_field)
355
394
self.key = md5_hexdigest(self.name)