~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to djangoratings/fields.py

  • Committer: franku
  • Date: 2018-11-21 17:54:32 UTC
  • mfrom: (508.1.1 widelands)
  • Revision ID: somal@arcor.de-20181121175432-8rc3h0332xmgmma4
merged trunk, resolved conflicts

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(
14
 
        'djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS')
15
 
 
16
 
from django.contrib.contenttypes.models import ContentType
17
 
 
18
 
__all__ = ('Rating', 'RatingField', 'AnonymousRatingField')
19
 
 
20
 
try:
21
 
    from hashlib import md5
22
 
except ImportError:
23
 
    from md5 import new as md5
24
 
 
25
 
try:
26
 
    from django.utils.timezone import now
27
 
except ImportError:
28
 
    now = datetime.now
29
 
 
30
 
 
31
 
def md5_hexdigest(value):
32
 
    return md5(value).hexdigest()
33
 
 
34
 
 
35
 
class Rating(object):
36
 
 
37
 
    def __init__(self, score, votes):
38
 
        self.score = score
39
 
        self.votes = votes
40
 
 
41
 
 
42
 
class RatingManager(object):
43
 
 
44
 
    def __init__(self, instance, field):
45
 
        self.content_type = None
46
 
        self.instance = instance
47
 
        self.field = field
48
 
 
49
 
        self.votes_field_name = '%s_votes' % (self.field.name,)
50
 
        self.score_field_name = '%s_score' % (self.field.name,)
51
 
 
52
 
    def get_percent(self):
53
 
        """get_percent()
54
 
 
55
 
        Returns the weighted percentage of the score from min-max values
56
 
 
57
 
        """
58
 
        if not (self.votes and self.score):
59
 
            return 0
60
 
        return 100 * (self.get_rating() / self.field.range)
61
 
 
62
 
    def get_real_percent(self):
63
 
        """get_real_percent()
64
 
 
65
 
        Returns the unmodified percentage of the score based on a 0-point scale.
66
 
 
67
 
        """
68
 
        if not (self.votes and self.score):
69
 
            return 0
70
 
        return 100 * (self.get_real_rating() / self.field.range)
71
 
 
72
 
    def get_ratings(self):
73
 
        """get_ratings()
74
 
 
75
 
        Returns a Vote QuerySet for this rating field.
76
 
 
77
 
        """
78
 
        return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key)
79
 
 
80
 
    def get_rating(self):
81
 
        """get_rating()
82
 
 
83
 
        Returns the weighted average rating.
84
 
 
85
 
        """
86
 
        if not (self.votes and self.score):
87
 
            return 0
88
 
        return float(self.score) / (self.votes + self.field.weight)
89
 
 
90
 
    def get_opinion_percent(self):
91
 
        """get_opinion_percent()
92
 
 
93
 
        Returns a neutral-based percentage.
94
 
 
95
 
        """
96
 
        return (self.get_percent() + 100) / 2
97
 
 
98
 
    def get_real_rating(self):
99
 
        """get_rating()
100
 
 
101
 
        Returns the unmodified average rating.
102
 
 
103
 
        """
104
 
        if not (self.votes and self.score):
105
 
            return 0
106
 
        return float(self.score) / self.votes
107
 
 
108
 
    def get_rating_for_user(self, user, ip_address=None, cookies={}):
109
 
        """get_rating_for_user(user, ip_address=None, cookie=None)
110
 
 
111
 
        Returns the rating for a user or anonymous IP."""
112
 
        kwargs = dict(
113
 
            content_type=self.get_content_type(),
114
 
            object_id=self.instance.pk,
115
 
            key=self.field.key,
116
 
        )
117
 
 
118
 
        if not (user and user.is_authenticated):
119
 
            if not ip_address:
120
 
                raise ValueError('``user`` or ``ip_address`` must be present.')
121
 
            kwargs['user__isnull'] = True
122
 
            kwargs['ip_address'] = ip_address
123
 
        else:
124
 
            kwargs['user'] = user
125
 
 
126
 
        use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
127
 
        if 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)
132
 
            if cookie:
133
 
                kwargs['cookie'] = cookie
134
 
            else:
135
 
                kwargs['cookie__isnull'] = True
136
 
 
137
 
        try:
138
 
            rating = Vote.objects.get(**kwargs)
139
 
            return rating.score
140
 
        except Vote.MultipleObjectsReturned:
141
 
            pass
142
 
        except Vote.DoesNotExist:
143
 
            pass
144
 
        return
145
 
 
146
 
    def get_iterable_range(self):
147
 
        # started from 1, because 0 is equal to delete
148
 
        return range(1, self.field.range)
149
 
 
150
 
    def add(self, score, user, ip_address, cookies={}, commit=True):
151
 
        """add(score, user, ip_address)
152
 
 
153
 
        Used to add a rating to an object.
154
 
 
155
 
        """
156
 
        try:
157
 
            score = int(score)
158
 
        except (ValueError, TypeError):
159
 
            raise InvalidRating('%s is not a valid choice for %s' %
160
 
                                (score, self.field.name))
161
 
 
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
167
 
 
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))
171
 
 
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,))
175
 
 
176
 
        if is_anonymous:
177
 
            user = None
178
 
 
179
 
        defaults = dict(
180
 
            score=score,
181
 
            ip_address=ip_address,
182
 
        )
183
 
 
184
 
        kwargs = dict(
185
 
            content_type=self.get_content_type(),
186
 
            object_id=self.instance.pk,
187
 
            key=self.field.key,
188
 
            user=user,
189
 
        )
190
 
        if not user:
191
 
            kwargs['ip_address'] = ip_address
192
 
 
193
 
        use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
194
 
        if 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)
202
 
            if not cookie:
203
 
                kwargs['cookie__isnull'] = True
204
 
            kwargs['cookie'] = cookie
205
 
 
206
 
        try:
207
 
            rating, created = Vote.objects.get(**kwargs), False
208
 
        except Vote.DoesNotExist:
209
 
            if delete:
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'],
216
 
                    key=kwargs['key'],
217
 
                    ip_address=ip_address,
218
 
                ).count()
219
 
                if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
220
 
                    raise IPLimitReached()
221
 
            kwargs.update(defaults)
222
 
            if use_cookies:
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
229
 
 
230
 
        has_changed = False
231
 
        if not created:
232
 
            if self.field.can_change_vote:
233
 
                has_changed = True
234
 
                self.score -= rating.score
235
 
                # you can delete your vote only if you have permission to
236
 
                # change your vote
237
 
                if not delete:
238
 
                    rating.score = score
239
 
                    rating.save()
240
 
                else:
241
 
                    self.votes -= 1
242
 
                    rating.delete()
243
 
            else:
244
 
                raise CannotChangeVote()
245
 
        else:
246
 
            has_changed = True
247
 
            self.votes += 1
248
 
        if has_changed:
249
 
            if not delete:
250
 
                self.score += rating.score
251
 
            if commit:
252
 
                self.instance.save()
253
 
            #setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes))
254
 
 
255
 
            defaults = dict(
256
 
                score=self.score,
257
 
                votes=self.votes,
258
 
            )
259
 
 
260
 
            kwargs = dict(
261
 
                content_type=self.get_content_type(),
262
 
                object_id=self.instance.pk,
263
 
                key=self.field.key,
264
 
            )
265
 
 
266
 
            try:
267
 
                score, created = Score.objects.get(**kwargs), False
268
 
            except Score.DoesNotExist:
269
 
                kwargs.update(defaults)
270
 
                score, created = Score.objects.create(**kwargs), True
271
 
 
272
 
            if not created:
273
 
                score.__dict__.update(defaults)
274
 
                score.save()
275
 
 
276
 
        # return value
277
 
        adds = {}
278
 
        if use_cookies:
279
 
            adds['cookie_name'] = cookie_name
280
 
            adds['cookie'] = cookie
281
 
        if delete:
282
 
            adds['deleted'] = True
283
 
        return adds
284
 
 
285
 
    def delete(self, user, ip_address, cookies={}, commit=True):
286
 
        return self.add(0, user, ip_address, cookies, commit)
287
 
 
288
 
    def _get_votes(self, default=None):
289
 
        return getattr(self.instance, self.votes_field_name, default)
290
 
 
291
 
    def _set_votes(self, value):
292
 
        return setattr(self.instance, self.votes_field_name, value)
293
 
 
294
 
    votes = property(_get_votes, _set_votes)
295
 
 
296
 
    def _get_score(self, default=None):
297
 
        return getattr(self.instance, self.score_field_name, default)
298
 
 
299
 
    def _set_score(self, value):
300
 
        return setattr(self.instance, self.score_field_name, value)
301
 
 
302
 
    score = property(_get_score, _set_score)
303
 
 
304
 
    def get_content_type(self):
305
 
        if self.content_type is None:
306
 
            self.content_type = ContentType.objects.get_for_model(
307
 
                self.instance)
308
 
        return self.content_type
309
 
 
310
 
    def _update(self, commit=False):
311
 
        """Forces an update of this rating (useful for when Vote objects are
312
 
        removed)."""
313
 
        votes = Vote.objects.filter(
314
 
            content_type=self.get_content_type(),
315
 
            object_id=self.instance.pk,
316
 
            key=self.field.key,
317
 
        )
318
 
        obj_score = sum([v.score for v in votes])
319
 
        obj_votes = len(votes)
320
 
 
321
 
        score, created = Score.objects.get_or_create(
322
 
            content_type=self.get_content_type(),
323
 
            object_id=self.instance.pk,
324
 
            key=self.field.key,
325
 
            defaults=dict(
326
 
                score=obj_score,
327
 
                votes=obj_votes,
328
 
            )
329
 
        )
330
 
        if not created:
331
 
            score.score = obj_score
332
 
            score.votes = obj_votes
333
 
            score.save()
334
 
        self.score = obj_score
335
 
        self.votes = obj_votes
336
 
        if commit:
337
 
            self.instance.save()
338
 
 
339
 
 
340
 
class RatingCreator(object):
341
 
 
342
 
    def __init__(self, field):
343
 
        self.field = field
344
 
        self.votes_field_name = '%s_votes' % (self.field.name,)
345
 
        self.score_field_name = '%s_score' % (self.field.name,)
346
 
 
347
 
    def __get__(self, instance, type=None):
348
 
        if instance is None:
349
 
            return self.field
350
 
            #raise AttributeError('Can only be accessed via an instance.')
351
 
        return RatingManager(instance, self.field)
352
 
 
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)
357
 
        else:
358
 
            raise TypeError("%s value must be a Rating instance, not '%r'" % (
359
 
                self.field.name, value))
360
 
 
361
 
 
362
 
class RatingField(IntegerField):
363
 
    """A rating field contributes two columns to the model instead of the
364
 
    standard single column."""
365
 
 
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)
380
 
 
381
 
    def contribute_to_class(self, cls, name):
382
 
        self.name = name
383
 
 
384
 
        # Votes tally field
385
 
        self.votes_field = PositiveIntegerField(
386
 
            editable=False, default=0, blank=True)
387
 
        cls.add_to_class('%s_votes' % (self.name,), self.votes_field)
388
 
 
389
 
        # Score sum 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)
393
 
 
394
 
        self.key = md5_hexdigest(self.name)
395
 
 
396
 
        field = RatingCreator(self)
397
 
 
398
 
        if not hasattr(cls, '_djangoratings'):
399
 
            cls._djangoratings = []
400
 
        cls._djangoratings.append(self)
401
 
 
402
 
        setattr(cls, name, field)
403
 
 
404
 
    def get_db_prep_save(self, value):
405
 
        # XXX: what happens here?
406
 
        pass
407
 
 
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'):
413
 
        #     lookup_type =
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]
419
 
        else:
420
 
            return super(RatingField, self).get_db_prep_lookup(lookup_type, value)
421
 
 
422
 
    def formfield(self, **kwargs):
423
 
        defaults = {'form_class': forms.RatingField}
424
 
        defaults.update(kwargs)
425
 
        return super(RatingField, self).formfield(**defaults)
426
 
 
427
 
    # TODO: flatten_data method
428
 
 
429
 
 
430
 
class AnonymousRatingField(RatingField):
431
 
 
432
 
    def __init__(self, *args, **kwargs):
433
 
        kwargs['allow_anonymous'] = True
434
 
        super(AnonymousRatingField, self).__init__(*args, **kwargs)