~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to djangoratings/fields.py

  • Committer: franku
  • Date: 2018-05-06 08:20:39 UTC
  • mto: This revision was merged to the branch mainline in revision 494.
  • Revision ID: somal@arcor.de-20180506082039-v8n40alffhi2ulct
This is an Attribute error

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)