~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to djangoratings/fields.py

  • Committer: Holger Rapp
  • Date: 2016-08-08 10:06:42 UTC
  • mto: This revision was merged to the branch mainline in revision 419.
  • Revision ID: sirver@gmx.de-20160808100642-z62vwqitxoyl5fh4
Added the apt-get update script I run every 30 days.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from django.db.models import IntegerField, PositiveIntegerField
 
2
from django.conf import settings
 
3
 
 
4
import forms
 
5
import itertools
 
6
from datetime import datetime
 
7
 
 
8
from models import Vote, Score
 
9
from default_settings import RATINGS_VOTES_PER_IP
 
10
from exceptions import *
 
11
 
 
12
if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS:
 
13
    raise ImportError("djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS")
 
14
 
 
15
from django.contrib.contenttypes.models import ContentType
 
16
 
 
17
__all__ = ('Rating', 'RatingField', 'AnonymousRatingField')
 
18
 
 
19
try:
 
20
    from hashlib import md5
 
21
except ImportError:
 
22
    from md5 import new as md5
 
23
 
 
24
try:
 
25
    from django.utils.timezone import now
 
26
except ImportError:
 
27
    now = datetime.now
 
28
 
 
29
def md5_hexdigest(value):
 
30
    return md5(value).hexdigest()
 
31
 
 
32
class Rating(object):
 
33
    def __init__(self, score, votes):
 
34
        self.score = score
 
35
        self.votes = votes
 
36
 
 
37
class RatingManager(object):
 
38
    def __init__(self, instance, field):
 
39
        self.content_type = None
 
40
        self.instance = instance
 
41
        self.field = field
 
42
        
 
43
        self.votes_field_name = "%s_votes" % (self.field.name,)
 
44
        self.score_field_name = "%s_score" % (self.field.name,)
 
45
    
 
46
    def get_percent(self):
 
47
        """get_percent()
 
48
        
 
49
        Returns the weighted percentage of the score from min-max values"""
 
50
        if not (self.votes and self.score):
 
51
            return 0
 
52
        return 100 * (self.get_rating() / self.field.range)
 
53
    
 
54
    def get_real_percent(self):
 
55
        """get_real_percent()
 
56
        
 
57
        Returns the unmodified percentage of the score based on a 0-point scale."""
 
58
        if not (self.votes and self.score):
 
59
            return 0
 
60
        return 100 * (self.get_real_rating() / self.field.range)
 
61
    
 
62
    def get_ratings(self):
 
63
        """get_ratings()
 
64
        
 
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)
 
67
        
 
68
    def get_rating(self):
 
69
        """get_rating()
 
70
        
 
71
        Returns the weighted average rating."""
 
72
        if not (self.votes and self.score):
 
73
            return 0
 
74
        return float(self.score)/(self.votes+self.field.weight)
 
75
    
 
76
    def get_opinion_percent(self):
 
77
        """get_opinion_percent()
 
78
        
 
79
        Returns a neutral-based percentage."""
 
80
        return (self.get_percent()+100)/2
 
81
 
 
82
    def get_real_rating(self):
 
83
        """get_rating()
 
84
        
 
85
        Returns the unmodified average rating."""
 
86
        if not (self.votes and self.score):
 
87
            return 0
 
88
        return float(self.score)/self.votes
 
89
    
 
90
    def get_rating_for_user(self, user, ip_address=None, cookies={}):
 
91
        """get_rating_for_user(user, ip_address=None, cookie=None)
 
92
        
 
93
        Returns the rating for a user or anonymous IP."""
 
94
        kwargs = dict(
 
95
            content_type    = self.get_content_type(),
 
96
            object_id       = self.instance.pk,
 
97
            key             = self.field.key,
 
98
        )
 
99
 
 
100
        if not (user and user.is_authenticated()):
 
101
            if not ip_address:
 
102
                raise ValueError('``user`` or ``ip_address`` must be present.')
 
103
            kwargs['user__isnull'] = True
 
104
            kwargs['ip_address'] = ip_address
 
105
        else:
 
106
            kwargs['user'] = user
 
107
        
 
108
        use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
 
109
        if 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)
 
113
            if cookie:    
 
114
                kwargs['cookie'] = cookie
 
115
            else:
 
116
                kwargs['cookie__isnull'] = True
 
117
            
 
118
        try:
 
119
            rating = Vote.objects.get(**kwargs)
 
120
            return rating.score
 
121
        except Vote.MultipleObjectsReturned:
 
122
            pass
 
123
        except Vote.DoesNotExist:
 
124
            pass
 
125
        return
 
126
    
 
127
    def get_iterable_range(self):
 
128
        return range(1, self.field.range) #started from 1, because 0 is equal to delete
 
129
        
 
130
    def add(self, score, user, ip_address, cookies={}, commit=True):
 
131
        """add(score, user, ip_address)
 
132
        
 
133
        Used to add a rating to an object."""
 
134
        try:
 
135
            score = int(score)
 
136
        except (ValueError, TypeError):
 
137
            raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
 
138
        
 
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
 
143
        
 
144
        if score < 0 or score > self.field.range:
 
145
            raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
 
146
 
 
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,))
 
150
        
 
151
        if is_anonymous:
 
152
            user = None
 
153
        
 
154
        defaults = dict(
 
155
            score = score,
 
156
            ip_address = ip_address,
 
157
        )
 
158
        
 
159
        kwargs = dict(
 
160
            content_type    = self.get_content_type(),
 
161
            object_id       = self.instance.pk,
 
162
            key             = self.field.key,
 
163
            user            = user,
 
164
        )
 
165
        if not user:
 
166
            kwargs['ip_address'] = ip_address
 
167
        
 
168
        use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
 
169
        if 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
 
174
            if not cookie:
 
175
                kwargs['cookie__isnull'] = True
 
176
            kwargs['cookie'] = cookie
 
177
 
 
178
        try:
 
179
            rating, created = Vote.objects.get(**kwargs), False
 
180
        except Vote.DoesNotExist:
 
181
            if delete:
 
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'],
 
187
                    key=kwargs['key'],
 
188
                    ip_address=ip_address,
 
189
                ).count()
 
190
                if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
 
191
                    raise IPLimitReached()
 
192
            kwargs.update(defaults)
 
193
            if use_cookies:
 
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
 
198
            
 
199
        has_changed = False
 
200
        if not created:
 
201
            if self.field.can_change_vote:
 
202
                has_changed = True
 
203
                self.score -= rating.score
 
204
                # you can delete your vote only if you have permission to change your vote
 
205
                if not delete:
 
206
                    rating.score = score
 
207
                    rating.save()
 
208
                else:
 
209
                    self.votes -= 1
 
210
                    rating.delete()
 
211
            else:
 
212
                raise CannotChangeVote()
 
213
        else:
 
214
            has_changed = True
 
215
            self.votes += 1
 
216
        if has_changed:
 
217
            if not delete:
 
218
                self.score += rating.score
 
219
            if commit:
 
220
                self.instance.save()
 
221
            #setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes))
 
222
            
 
223
            defaults = dict(
 
224
                score   = self.score,
 
225
                votes   = self.votes,
 
226
            )
 
227
            
 
228
            kwargs = dict(
 
229
                content_type    = self.get_content_type(),
 
230
                object_id       = self.instance.pk,
 
231
                key             = self.field.key,
 
232
            )
 
233
            
 
234
            try:
 
235
                score, created = Score.objects.get(**kwargs), False
 
236
            except Score.DoesNotExist:
 
237
                kwargs.update(defaults)
 
238
                score, created = Score.objects.create(**kwargs), True
 
239
            
 
240
            if not created:
 
241
                score.__dict__.update(defaults)
 
242
                score.save()
 
243
        
 
244
        # return value
 
245
        adds = {}
 
246
        if use_cookies:
 
247
            adds['cookie_name'] = cookie_name
 
248
            adds['cookie'] = cookie
 
249
        if delete:
 
250
            adds['deleted'] = True
 
251
        return adds
 
252
 
 
253
    def delete(self, user, ip_address, cookies={}, commit=True):
 
254
        return self.add(0, user, ip_address, cookies, commit)
 
255
    
 
256
    def _get_votes(self, default=None):
 
257
        return getattr(self.instance, self.votes_field_name, default)
 
258
    
 
259
    def _set_votes(self, value):
 
260
        return setattr(self.instance, self.votes_field_name, value)
 
261
        
 
262
    votes = property(_get_votes, _set_votes)
 
263
 
 
264
    def _get_score(self, default=None):
 
265
        return getattr(self.instance, self.score_field_name, default)
 
266
    
 
267
    def _set_score(self, value):
 
268
        return setattr(self.instance, self.score_field_name, value)
 
269
        
 
270
    score = property(_get_score, _set_score)
 
271
 
 
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
 
276
    
 
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,
 
283
        )
 
284
        obj_score = sum([v.score for v in votes])
 
285
        obj_votes = len(votes)
 
286
 
 
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,
 
291
            defaults        = dict(
 
292
                score       = obj_score,
 
293
                votes       = obj_votes,
 
294
            )
 
295
        )
 
296
        if not created:
 
297
            score.score = obj_score
 
298
            score.votes = obj_votes
 
299
            score.save()
 
300
        self.score = obj_score
 
301
        self.votes = obj_votes
 
302
        if commit:
 
303
            self.instance.save()
 
304
 
 
305
class RatingCreator(object):
 
306
    def __init__(self, field):
 
307
        self.field = field
 
308
        self.votes_field_name = "%s_votes" % (self.field.name,)
 
309
        self.score_field_name = "%s_score" % (self.field.name,)
 
310
 
 
311
    def __get__(self, instance, type=None):
 
312
        if instance is None:
 
313
            return self.field
 
314
            #raise AttributeError('Can only be accessed via an instance.')
 
315
        return RatingManager(instance, self.field)
 
316
 
 
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)
 
321
        else:
 
322
            raise TypeError("%s value must be a Rating instance, not '%r'" % (self.field.name, value))
 
323
 
 
324
class RatingField(IntegerField):
 
325
    """
 
326
    A rating field contributes two columns to the model instead of the standard single column.
 
327
    """
 
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)
 
341
    
 
342
    def contribute_to_class(self, cls, name):
 
343
        self.name = name
 
344
 
 
345
        # Votes tally field
 
346
        self.votes_field = PositiveIntegerField(
 
347
            editable=False, default=0, blank=True)
 
348
        cls.add_to_class("%s_votes" % (self.name,), self.votes_field)
 
349
 
 
350
        # Score sum 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)
 
354
 
 
355
        self.key = md5_hexdigest(self.name)
 
356
 
 
357
        field = RatingCreator(self)
 
358
 
 
359
        if not hasattr(cls, '_djangoratings'):
 
360
            cls._djangoratings = []
 
361
        cls._djangoratings.append(self)
 
362
 
 
363
        setattr(cls, name, field)
 
364
 
 
365
    def get_db_prep_save(self, value):
 
366
        # XXX: what happens here?
 
367
        pass
 
368
 
 
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'):
 
374
        #     lookup_type = 
 
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]
 
380
        else:
 
381
            return super(RatingField, self).get_db_prep_lookup(lookup_type, value)
 
382
 
 
383
    def formfield(self, **kwargs):
 
384
        defaults = {'form_class': forms.RatingField}
 
385
        defaults.update(kwargs)
 
386
        return super(RatingField, self).formfield(**defaults)
 
387
 
 
388
    # TODO: flatten_data method
 
389
 
 
390
 
 
391
class AnonymousRatingField(RatingField):
 
392
    def __init__(self, *args, **kwargs):
 
393
        kwargs['allow_anonymous'] = True
 
394
        super(AnonymousRatingField, self).__init__(*args, **kwargs)