Commit 5c1348fee89a45afb07f214b9855b0b351009a3a
1 parent
d17b18bb4f
Exists in
master
halfway denormalize score
Showing 2 changed files with 58 additions and 16 deletions Side-by-side Diff
flashcards/migrations/0018_flashcard_score.py
View file @
5c1348f
1 | +# -*- coding: utf-8 -*- | |
2 | +from __future__ import unicode_literals | |
3 | +from math import log1p, exp | |
4 | + | |
5 | +from django.db import models, migrations | |
6 | +from django.utils.timezone import make_aware | |
7 | + | |
8 | + | |
9 | +def update_scores(apps, schema_editor): | |
10 | + Flashcard = apps.get_model("flashcards", "Flashcard") | |
11 | + for card in Flashcard.objects.all(): | |
12 | + def seconds_since_epoch(dt): | |
13 | + from datetime import datetime | |
14 | + | |
15 | + epoch = make_aware(datetime.utcfromtimestamp(0)) | |
16 | + delta = dt - epoch | |
17 | + return delta.total_seconds() | |
18 | + | |
19 | + z = 0 | |
20 | + rate = 1.0 / 3600 | |
21 | + for vote in card.userflashcard_set.iterator(): | |
22 | + t = seconds_since_epoch(vote.pulled) | |
23 | + u = max(z, rate * t) | |
24 | + v = min(z, rate * t) | |
25 | + z = u + log1p(exp(v - u)) | |
26 | + card.score = z | |
27 | + card.save() | |
28 | + | |
29 | + | |
30 | +class Migration(migrations.Migration): | |
31 | + dependencies = [ | |
32 | + ('flashcards', '0017_auto_20150601_2001'), | |
33 | + ] | |
34 | + | |
35 | + operations = [ | |
36 | + migrations.AddField( | |
37 | + model_name='flashcard', | |
38 | + name='score', | |
39 | + field=models.FloatField(default=0), | |
40 | + preserve_default=False, | |
41 | + ), | |
42 | + migrations.RunPython(update_scores), | |
43 | + ] |
flashcards/models.py
View file @
5c1348f
... | ... | @@ -7,7 +7,7 @@ |
7 | 7 | from django.contrib.auth.tokens import default_token_generator |
8 | 8 | from django.core.cache import cache |
9 | 9 | from django.core.exceptions import ValidationError |
10 | -from django.core.exceptions import PermissionDenied, SuspiciousOperation | |
10 | +from django.core.exceptions import PermissionDenied | |
11 | 11 | from django.core.mail import send_mail |
12 | 12 | from django.core.validators import MinLengthValidator |
13 | 13 | from django.db import IntegrityError |
... | ... | @@ -23,6 +23,8 @@ |
23 | 23 | |
24 | 24 | |
25 | 25 | |
26 | + | |
27 | + | |
26 | 28 | # Hack to fix AbstractUser before subclassing it |
27 | 29 | |
28 | 30 | AbstractUser._meta.get_field('email')._unique = True |
... | ... | @@ -127,6 +129,8 @@ |
127 | 129 | except IntegrityError: |
128 | 130 | raise FlashcardAlreadyPulledException() |
129 | 131 | |
132 | + flashcard.refresh_score() | |
133 | + | |
130 | 134 | import flashcards.pushes |
131 | 135 | |
132 | 136 | flashcards.pushes.push_feed_event('score_change', flashcard) |
133 | 137 | |
... | ... | @@ -135,13 +139,14 @@ |
135 | 139 | def unpull(self, flashcard): |
136 | 140 | if not self.is_in_section(flashcard.section): |
137 | 141 | raise ValueError("User not in the section this flashcard belongs to") |
138 | - | |
139 | 142 | try: |
140 | 143 | user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) |
141 | 144 | except UserFlashcard.DoesNotExist: |
142 | 145 | raise FlashcardNotInDeckException() |
143 | 146 | user_card.delete() |
144 | 147 | |
148 | + flashcard.refresh_score() | |
149 | + | |
145 | 150 | import flashcards.pushes |
146 | 151 | |
147 | 152 | flashcards.pushes.push_feed_event('score_change', flashcard) |
... | ... | @@ -271,6 +276,7 @@ |
271 | 276 | material_date = DateTimeField(default=now, help_text="The date with which the card is associated") |
272 | 277 | previous = ForeignKey('Flashcard', null=True, blank=True, default=None, |
273 | 278 | help_text="The previous version of this card, if one exists") |
279 | + score = FloatField(default=0) | |
274 | 280 | author = ForeignKey(User) |
275 | 281 | is_hidden = BooleanField(default=False) |
276 | 282 | hide_reason = CharField(blank=True, null=True, max_length=255, default='', |
... | ... | @@ -284,6 +290,10 @@ |
284 | 290 | def __unicode__(self): |
285 | 291 | return u'<flashcard: %s>' % self.text |
286 | 292 | |
293 | + def refresh_score(self): | |
294 | + self.score = self.calculate_score | |
295 | + self.save() | |
296 | + | |
287 | 297 | @classmethod |
288 | 298 | def push(cls, **kwargs): |
289 | 299 | card = cls(**kwargs) |
... | ... | @@ -311,16 +321,6 @@ |
311 | 321 | def is_in_deck(self, user): |
312 | 322 | return self.userflashcard_set.filter(user=user).exists() |
313 | 323 | |
314 | - def add_to_deck(self, user): | |
315 | - if not user.is_in_section(self.section): | |
316 | - raise PermissionDenied("You don't have the permission to add this card") | |
317 | - try: | |
318 | - user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) | |
319 | - except IntegrityError: | |
320 | - raise SuspiciousOperation("The flashcard is already in the user's deck") | |
321 | - user_flashcard.save() | |
322 | - return user_flashcard | |
323 | - | |
324 | 324 | def edit(self, user, new_data): |
325 | 325 | """ |
326 | 326 | Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. |
... | ... | @@ -347,7 +347,7 @@ |
347 | 347 | self.pk = None |
348 | 348 | self.mask = new_data.get('mask', self.mask) |
349 | 349 | self.save() |
350 | - self.add_to_deck(user) | |
350 | + user.pull(self) | |
351 | 351 | else: |
352 | 352 | user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) |
353 | 353 | user_card.mask = new_data.get('mask', user_card.mask) |
... | ... | @@ -372,7 +372,7 @@ |
372 | 372 | hide.delete() |
373 | 373 | |
374 | 374 | @cached_property |
375 | - def score(self): | |
375 | + def calculate_score(self): | |
376 | 376 | def seconds_since_epoch(dt): |
377 | 377 | from datetime import datetime |
378 | 378 | |
... | ... | @@ -526,8 +526,7 @@ |
526 | 526 | return '%s %s' % (self.department_abbreviation, self.course_num) |
527 | 527 | |
528 | 528 | def get_feed_for_user(self, user, page=1): |
529 | - cards = list(self.get_cards_for_user(user)[(page - 1) * self.PAGE_SIZE:page * self.PAGE_SIZE]) | |
530 | - cards.sort(key=lambda x: -x.score) | |
529 | + cards = self.get_cards_for_user(user).order_by('-score')[(page - 1) * self.PAGE_SIZE:page * self.PAGE_SIZE] | |
531 | 530 | return cards |
532 | 531 | |
533 | 532 | def get_cards_for_user(self, user): |