diff --git a/flashcards/migrations/0018_flashcard_score.py b/flashcards/migrations/0018_flashcard_score.py new file mode 100644 index 0000000..23714c2 --- /dev/null +++ b/flashcards/migrations/0018_flashcard_score.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals +from math import log1p, exp + +from django.db import models, migrations +from django.utils.timezone import make_aware + + +def update_scores(apps, schema_editor): + Flashcard = apps.get_model("flashcards", "Flashcard") + for card in Flashcard.objects.all(): + def seconds_since_epoch(dt): + from datetime import datetime + + epoch = make_aware(datetime.utcfromtimestamp(0)) + delta = dt - epoch + return delta.total_seconds() + + z = 0 + rate = 1.0 / 3600 + for vote in card.userflashcard_set.iterator(): + t = seconds_since_epoch(vote.pulled) + u = max(z, rate * t) + v = min(z, rate * t) + z = u + log1p(exp(v - u)) + card.score = z + card.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('flashcards', '0017_auto_20150601_2001'), + ] + + operations = [ + migrations.AddField( + model_name='flashcard', + name='score', + field=models.FloatField(default=0), + preserve_default=False, + ), + migrations.RunPython(update_scores), + ] diff --git a/flashcards/models.py b/flashcards/models.py index 36aa465..379b554 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.tokens import default_token_generator from django.core.cache import cache from django.core.exceptions import ValidationError -from django.core.exceptions import PermissionDenied, SuspiciousOperation +from django.core.exceptions import PermissionDenied from django.core.mail import send_mail from django.core.validators import MinLengthValidator from django.db import IntegrityError @@ -23,6 +23,8 @@ from cached_property import cached_property + + # Hack to fix AbstractUser before subclassing it AbstractUser._meta.get_field('email')._unique = True @@ -127,6 +129,8 @@ class User(AbstractUser, SimpleEmailConfirmationUserMixin): except IntegrityError: raise FlashcardAlreadyPulledException() + flashcard.refresh_score() + import flashcards.pushes flashcards.pushes.push_feed_event('score_change', flashcard) @@ -135,13 +139,14 @@ class User(AbstractUser, SimpleEmailConfirmationUserMixin): def unpull(self, flashcard): if not self.is_in_section(flashcard.section): raise ValueError("User not in the section this flashcard belongs to") - try: user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) except UserFlashcard.DoesNotExist: raise FlashcardNotInDeckException() user_card.delete() + flashcard.refresh_score() + import flashcards.pushes flashcards.pushes.push_feed_event('score_change', flashcard) @@ -271,6 +276,7 @@ class Flashcard(Model): material_date = DateTimeField(default=now, help_text="The date with which the card is associated") previous = ForeignKey('Flashcard', null=True, blank=True, default=None, help_text="The previous version of this card, if one exists") + score = FloatField(default=0) author = ForeignKey(User) is_hidden = BooleanField(default=False) hide_reason = CharField(blank=True, null=True, max_length=255, default='', @@ -284,6 +290,10 @@ class Flashcard(Model): def __unicode__(self): return u'' % self.text + def refresh_score(self): + self.score = self.calculate_score + self.save() + @classmethod def push(cls, **kwargs): card = cls(**kwargs) @@ -311,16 +321,6 @@ class Flashcard(Model): def is_in_deck(self, user): return self.userflashcard_set.filter(user=user).exists() - def add_to_deck(self, user): - if not user.is_in_section(self.section): - raise PermissionDenied("You don't have the permission to add this card") - try: - user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) - except IntegrityError: - raise SuspiciousOperation("The flashcard is already in the user's deck") - user_flashcard.save() - return user_flashcard - def edit(self, user, new_data): """ Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. @@ -347,7 +347,7 @@ class Flashcard(Model): self.pk = None self.mask = new_data.get('mask', self.mask) self.save() - self.add_to_deck(user) + user.pull(self) else: user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) user_card.mask = new_data.get('mask', user_card.mask) @@ -372,7 +372,7 @@ class Flashcard(Model): hide.delete() @cached_property - def score(self): + def calculate_score(self): def seconds_since_epoch(dt): from datetime import datetime @@ -526,8 +526,7 @@ class Section(Model): return '%s %s' % (self.department_abbreviation, self.course_num) def get_feed_for_user(self, user, page=1): - cards = list(self.get_cards_for_user(user)[(page - 1) * self.PAGE_SIZE:page * self.PAGE_SIZE]) - cards.sort(key=lambda x: -x.score) + cards = self.get_cards_for_user(user).order_by('-score')[(page - 1) * self.PAGE_SIZE:page * self.PAGE_SIZE] return cards def get_cards_for_user(self, user):