Commit 5c84eec6b068a290f86cb13eccd79553dd978f2d

Authored by Rohan Rangray
1 parent 7d64912c69
Exists in master

Added fix to adapt the algorithm to when the user reviews the card

Showing 1 changed file with 1 additions and 0 deletions Inline Diff

flashcards/models.py View file @ 5c84eec
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
from datetime import timedelta 3 3 from datetime import timedelta
4 4
from gcm import GCM 5 5 from gcm import GCM
from django.contrib.auth.models import AbstractUser, UserManager 6 6 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 7 7 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 8 8 from django.core.cache import cache
from django.core.exceptions import ValidationError 9 9 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied 10 10 from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail 11 11 from django.core.mail import send_mail
from django.core.validators import MinLengthValidator 12 12 from django.core.validators import MinLengthValidator
from django.db import IntegrityError 13 13 from django.db import IntegrityError
from django.db.models import * 14 14 from django.db.models import *
from django.utils.timezone import now, make_aware 15 15 from django.utils.timezone import now, make_aware
from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY 16 16 from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY
from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress 17 17 from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress
from fields import MaskField 18 18 from fields import MaskField
from cached_property import cached_property 19 19 from cached_property import cached_property
20 20
21 21
# Hack to fix AbstractUser before subclassing it 22 22 # Hack to fix AbstractUser before subclassing it
23 23
AbstractUser._meta.get_field('email')._unique = True 24 24 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 25 25 AbstractUser._meta.get_field('username')._unique = False
26 26
27 27
class EmailOnlyUserManager(UserManager): 28 28 class EmailOnlyUserManager(UserManager):
""" 29 29 """
A tiny extension of Django's UserManager which correctly creates users 30 30 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 31 31 without usernames (using emails instead).
""" 32 32 """
33 33
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 34 34 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 35 35 """
Creates and saves a User with the given email and password. 36 36 Creates and saves a User with the given email and password.
""" 37 37 """
email = self.normalize_email(email) 38 38 email = self.normalize_email(email)
user = self.model(email=email, 39 39 user = self.model(email=email,
is_staff=is_staff, is_active=True, 40 40 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 41 41 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 42 42 date_joined=now(), **extra_fields)
user.set_password(password) 43 43 user.set_password(password)
user.save(using=self._db) 44 44 user.save(using=self._db)
user.send_confirmation_email() 45 45 user.send_confirmation_email()
return user 46 46 return user
47 47
def create_user(self, email, password=None, **extra_fields): 48 48 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 49 49 user = self._create_user(email, password, False, False, **extra_fields)
50 50
return user 51 51 return user
52 52
def create_superuser(self, email, password, **extra_fields): 53 53 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 54 54 return self._create_user(email, password, True, True, **extra_fields)
55 55
56 56
class FlashcardAlreadyPulledException(Exception): 57 57 class FlashcardAlreadyPulledException(Exception):
pass 58 58 pass
59 59
60 60
class FlashcardNotInDeckException(Exception): 61 61 class FlashcardNotInDeckException(Exception):
pass 62 62 pass
63 63
64 64
class FlashcardNotHiddenException(Exception): 65 65 class FlashcardNotHiddenException(Exception):
pass 66 66 pass
67 67
68 68
class FlashcardAlreadyHiddenException(Exception): 69 69 class FlashcardAlreadyHiddenException(Exception):
pass 70 70 pass
71 71
72 72
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 73 73 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 74 74 """
An extension of Django's default user model. 75 75 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 76 76 We use email as the username field, and include enrolled sections here
""" 77 77 """
objects = EmailOnlyUserManager() 78 78 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 79 79 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 80 80 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 81 81 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 82 82 confirmed_email = BooleanField(default=False)
registration_id = CharField(null=True, default=None, max_length=4096) 83 83 registration_id = CharField(null=True, default=None, max_length=4096)
last_notified = DateTimeField(null=True, default=None) 84 84 last_notified = DateTimeField(null=True, default=None)
85 85
@property 86 86 @property
def locked(self): 87 87 def locked(self):
if self.confirmed_email: return False 88 88 if self.confirmed_email: return False
return (now() - self.date_joined).days > 0 89 89 return (now() - self.date_joined).days > 0
90 90
def send_confirmation_email(self): 91 91 def send_confirmation_email(self):
body = ''' 92 92 body = '''
Visit the following link to confirm your email address: 93 93 Visit the following link to confirm your email address:
%sapp/verifyemail/%s 94 94 %sapp/verifyemail/%s
95 95
If you did not register for Flashy, no action is required. 96 96 If you did not register for Flashy, no action is required.
''' 97 97 '''
send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key), 98 98 send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key),
"noreply@flashy.cards", [self.email]) 99 99 "noreply@flashy.cards", [self.email])
100 100
def notify(self): 101 101 def notify(self):
gcm = GCM(GCM_API_KEY) 102 102 gcm = GCM(GCM_API_KEY)
gcm.plaintext_request( 103 103 gcm.plaintext_request(
registration_id=self.registration_id, 104 104 registration_id=self.registration_id,
data={'poop': 'data'} 105 105 data={'poop': 'data'}
) 106 106 )
self.last_notified = now() 107 107 self.last_notified = now()
self.save() 108 108 self.save()
109 109
def set_registration_id(self, token): 110 110 def set_registration_id(self, token):
self.registration_id = token 111 111 self.registration_id = token
self.save() 112 112 self.save()
113 113
def is_in_section(self, section): 114 114 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 115 115 return self.sections.filter(pk=section.pk).exists()
116 116
def pull(self, flashcard): 117 117 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 118 118 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 119 119 raise ValueError("User not in the section this flashcard belongs to")
120 120
try: 121 121 try:
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 122 122 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
except IntegrityError: 123 123 except IntegrityError:
raise FlashcardAlreadyPulledException() 124 124 raise FlashcardAlreadyPulledException()
125 125
flashcard.refresh_score() 126 126 flashcard.refresh_score()
127 127
import flashcards.pushes 128 128 import flashcards.pushes
129 129
flashcards.pushes.push_feed_event('score_change', flashcard) 130 130 flashcards.pushes.push_feed_event('score_change', flashcard)
flashcards.pushes.push_deck_event('card_pulled', flashcard, self) 131 131 flashcards.pushes.push_deck_event('card_pulled', flashcard, self)
132 132
def unpull(self, flashcard): 133 133 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 134 134 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 135 135 raise ValueError("User not in the section this flashcard belongs to")
try: 136 136 try:
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 137 137 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
except UserFlashcard.DoesNotExist: 138 138 except UserFlashcard.DoesNotExist:
raise FlashcardNotInDeckException() 139 139 raise FlashcardNotInDeckException()
user_card.delete() 140 140 user_card.delete()
141 141
flashcard.refresh_score() 142 142 flashcard.refresh_score()
143 143
import flashcards.pushes 144 144 import flashcards.pushes
145 145
flashcards.pushes.push_feed_event('score_change', flashcard) 146 146 flashcards.pushes.push_feed_event('score_change', flashcard)
flashcards.pushes.push_deck_event('card_unpulled', flashcard, self) 147 147 flashcards.pushes.push_deck_event('card_unpulled', flashcard, self)
148 148
def get_deck(self, section): 149 149 def get_deck(self, section):
if not self.is_in_section(section): 150 150 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 151 151 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 152 152 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
153 153
def request_password_reset(self): 154 154 def request_password_reset(self):
token = default_token_generator.make_token(self) 155 155 token = default_token_generator.make_token(self)
156 156
body = ''' 157 157 body = '''
Visit the following link to reset your password: 158 158 Visit the following link to reset your password:
%sapp/resetpassword/%d/%s 159 159 %sapp/resetpassword/%d/%s
160 160
If you did not request a password reset, no action is required. 161 161 If you did not request a password reset, no action is required.
''' 162 162 '''
163 163
send_mail("Flashy password reset", 164 164 send_mail("Flashy password reset",
body % (ABSOLUTE_URL_ROOT, self.pk, token), 165 165 body % (ABSOLUTE_URL_ROOT, self.pk, token),
"noreply@flashy.cards", 166 166 "noreply@flashy.cards",
[self.email]) 167 167 [self.email])
168 168
@classmethod 169 169 @classmethod
def confirm_email(cls, confirmation_key): 170 170 def confirm_email(cls, confirmation_key):
# This will raise an exception if the email address is invalid 171 171 # This will raise an exception if the email address is invalid
address = EmailAddress.objects.confirm(confirmation_key, save=True).email 172 172 address = EmailAddress.objects.confirm(confirmation_key, save=True).email
user = cls.objects.get(email=address) 173 173 user = cls.objects.get(email=address)
user.confirmed_email = True 174 174 user.confirmed_email = True
user.save() 175 175 user.save()
return address 176 176 return address
177 177
def by_retention(self, sections, material_date_begin, material_date_end): 178 178 def by_retention(self, sections, material_date_begin, material_date_end):
user_flashcard_filter = UserFlashcard.objects.filter( 179 179 user_flashcard_filter = UserFlashcard.objects.filter(
user=self, flashcard__section__pk__in=sections, 180 180 user=self, flashcard__section__pk__in=sections,
flashcard__material_date__gte=material_date_begin, 181 181 flashcard__material_date__gte=material_date_begin,
flashcard__material_date__lte=material_date_end 182 182 flashcard__material_date__lte=material_date_end
) 183 183 )
184 184
if not user_flashcard_filter.exists(): 185 185 if not user_flashcard_filter.exists():
raise ValidationError("No matching flashcard found in your decks") 186 186 raise ValidationError("No matching flashcard found in your decks")
187 187
return user_flashcard_filter.order_by('next_review') 188 188 return user_flashcard_filter.order_by('next_review')
189 189
190 190
class UserFlashcard(Model): 191 191 class UserFlashcard(Model):
""" 192 192 """
Represents the relationship between a user and a flashcard by: 193 193 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 194 194 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 195 195 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 196 196 3. A user has a flashcard hidden from them
""" 197 197 """
user = ForeignKey('User') 198 198 user = ForeignKey('User')
mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card") 199 199 mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 200 200 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 201 201 flashcard = ForeignKey('Flashcard')
next_review = DateTimeField(null=True) 202 202 next_review = DateTimeField(null=True)
last_interval = IntegerField(default=1) 203 203 last_interval = IntegerField(default=1)
last_response_factor = FloatField(default=2.5) 204 204 last_response_factor = FloatField(default=2.5)
205 205
q_dict = {(False, False): 0, (False, True): 1, (False, None): 2, 206 206 q_dict = {(False, False): 0, (False, True): 1, (False, None): 2,
(True, False): 3, (True, None): 4, (True, True): 5} 207 207 (True, False): 3, (True, None): 4, (True, True): 5}
208 208
def get_mask(self): 209 209 def get_mask(self):
if self.mask is None: 210 210 if self.mask is None:
return self.flashcard.mask 211 211 return self.flashcard.mask
return self.mask 212 212 return self.mask
213 213
def save(self, force_insert=False, force_update=False, using=None, 214 214 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 215 215 update_fields=None):
if self.pk is None: 216 216 if self.pk is None:
self.next_review = now() + timedelta(days=1) 217 217 self.next_review = now() + timedelta(days=1)
super(UserFlashcard, self).save(force_insert=force_insert, force_update=force_update, 218 218 super(UserFlashcard, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 219 219 using=using, update_fields=update_fields)
220 220
def review(self, user_flashcard_quiz): 221 221 def review(self, user_flashcard_quiz):
q = self.q_dict[(user_flashcard_quiz.response == user_flashcard_quiz.blanked_word), user_flashcard_quiz.correct] 222 222 q = self.q_dict[(user_flashcard_quiz.response == user_flashcard_quiz.blanked_word), user_flashcard_quiz.correct]
if self.last_interval == 1: 223 223 if self.last_interval == 1:
self.last_interval = 6 224 224 self.last_interval = 6
else: 225 225 else:
self.last_response_factor = min(1.3, self.last_response_factor + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))) 226 226 self.last_response_factor = min(1.3, self.last_response_factor + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)))
227 self.last_interval += (now() - self.next_review).days
self.last_interval = int(round(self.last_interval * self.last_response_factor)) 227 228 self.last_interval = int(round(self.last_interval * self.last_response_factor))
self.next_review = user_flashcard_quiz.when + timedelta(days=self.last_interval) 228 229 self.next_review = user_flashcard_quiz.when + timedelta(days=self.last_interval)
self.save() 229 230 self.save()
230 231
class Meta: 231 232 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 232 233 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 233 234 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 234 235 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 235 236 # By default, order by most recently pulled
ordering = ['-pulled'] 236 237 ordering = ['-pulled']
237 238
def __unicode__(self): 238 239 def __unicode__(self):
return '%s has %s' % (str(self.user), str(self.flashcard)) 239 240 return '%s has %s' % (str(self.user), str(self.flashcard))
240 241
241 242
class FlashcardHide(Model): 242 243 class FlashcardHide(Model):
""" 243 244 """
Represents the property of a flashcard being hidden by a user. 244 245 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 245 246 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 246 247 If reason is null, the flashcard was just hidden.
If reason is not null, the flashcard was reported, and reason is the reason why it was reported. 247 248 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 248 249 """
user = ForeignKey('User') 249 250 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 250 251 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 251 252 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 252 253 hidden = DateTimeField(auto_now_add=True)
253 254
class Meta: 254 255 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 255 256 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 256 257 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 257 258 index_together = ["user", "flashcard"]
258 259
def __unicode__(self): 259 260 def __unicode__(self):
return u'%s hid %s' % (unicode(self.user), unicode(self.flashcard)) 260 261 return u'%s hid %s' % (unicode(self.user), unicode(self.flashcard))
261 262
262 263
class Flashcard(Model): 263 264 class Flashcard(Model):
text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)]) 264 265 text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)])
section = ForeignKey('Section', help_text='The section with which the card is associated') 265 266 section = ForeignKey('Section', help_text='The section with which the card is associated')
pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") 266 267 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(default=now, help_text="The date with which the card is associated") 267 268 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 268 269 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 269 270 help_text="The previous version of this card, if one exists")
score = FloatField(default=0) 270 271 score = FloatField(default=0)
author = ForeignKey(User) 271 272 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 272 273 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 273 274 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 274 275 help_text="Reason for hiding this card")
mask = MaskField(null=True, blank=True, help_text="The mask on the card") 275 276 mask = MaskField(null=True, blank=True, help_text="The mask on the card")
276 277
class Meta: 277 278 class Meta:
# By default, order by most recently pushed 278 279 # By default, order by most recently pushed
ordering = ['-pushed'] 279 280 ordering = ['-pushed']
280 281
def __unicode__(self): 281 282 def __unicode__(self):
return u'<flashcard: %s>' % self.text 282 283 return u'<flashcard: %s>' % self.text
283 284
def refresh_score(self): 284 285 def refresh_score(self):
self.score = self.calculate_score 285 286 self.score = self.calculate_score
self.save() 286 287 self.save()
287 288
@classmethod 288 289 @classmethod
def push(cls, **kwargs): 289 290 def push(cls, **kwargs):
card = cls(**kwargs) 290 291 card = cls(**kwargs)
card.save() 291 292 card.save()
card.author.pull(card) 292 293 card.author.pull(card)
import flashcards.pushes 293 294 import flashcards.pushes
294 295
flashcards.pushes.push_feed_event('new_card', card) 295 296 flashcards.pushes.push_feed_event('new_card', card)
296 297
return card 297 298 return card
298 299
@property 299 300 @property
def material_week_num(self): 300 301 def material_week_num(self):
return (self.material_date - QUARTER_START).days / 7 + 1 301 302 return (self.material_date - QUARTER_START).days / 7 + 1
302 303
def is_hidden_from(self, user): 303 304 def is_hidden_from(self, user):
""" 304 305 """
A card can be hidden globally, but if a user has the card in their deck, 305 306 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 306 307 this visibility overrides a global hide.
:param user: 307 308 :param user:
:return: Whether the card is hidden from the user. 308 309 :return: Whether the card is hidden from the user.
""" 309 310 """
if hasattr(self, 'is_not_hidden') and self.is_not_hidden: return False 310 311 if hasattr(self, 'is_not_hidden') and self.is_not_hidden: return False
return self.is_hidden or self.flashcardhide_set.filter(user=user).exists() 311 312 return self.is_hidden or self.flashcardhide_set.filter(user=user).exists()
312 313
def is_in_deck(self, user): 313 314 def is_in_deck(self, user):
if hasattr(self, 'userflashcard_id'): return self.userflashcard_id 314 315 if hasattr(self, 'userflashcard_id'): return self.userflashcard_id
return self.userflashcard_set.filter(user=user).exists() 315 316 return self.userflashcard_set.filter(user=user).exists()
316 317
def edit(self, user, new_data): 317 318 def edit(self, user, new_data):
""" 318 319 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 319 320 Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard.
Sets up everything correctly so this object, when saved, will result in the appropriate changes. 320 321 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 321 322 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 322 323 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 323 324 """
324 325
# content_changed is True iff either material_date or text were changed 325 326 # content_changed is True iff either material_date or text were changed
content_changed = False 326 327 content_changed = False
# create_new is True iff the user editing this card is the author of this card 327 328 # create_new is True iff the user editing this card is the author of this card
# and there are no other users with this card in their decks 328 329 # and there are no other users with this card in their decks
create_new = user != self.author or \ 329 330 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 330 331 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 331 332 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 332 333 content_changed = True
self.material_date = new_data['material_date'] 333 334 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 334 335 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 335 336 content_changed = True
self.text = new_data['text'] 336 337 self.text = new_data['text']
if create_new and content_changed: 337 338 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 338 339 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 339 340 self.previous_id = self.pk
self.pk = None 340 341 self.pk = None
self.mask = new_data.get('mask', self.mask) 341 342 self.mask = new_data.get('mask', self.mask)
self.save() 342 343 self.save()
user.pull(self) 343 344 user.pull(self)
else: 344 345 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 345 346 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 346 347 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 347 348 user_card.save()
return self 348 349 return self
349 350
def hide_by_user(self, user, reason=None): 350 351 def hide_by_user(self, user, reason=None):
import flashcards.pushes 351 352 import flashcards.pushes
352 353
flashcards.pushes.push_deck_event('card_hidden', self, user) 353 354 flashcards.pushes.push_deck_event('card_hidden', self, user)
if self.is_in_deck(user): user.unpull(self) 354 355 if self.is_in_deck(user): user.unpull(self)
hide, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 355 356 hide, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
hide.reason = reason 356 357 hide.reason = reason
hide.save() 357 358 hide.save()
358 359
def unhide_by_user(self, user, reason=None): 359 360 def unhide_by_user(self, user, reason=None):
import flashcards.pushes 360 361 import flashcards.pushes
361 362
flashcards.pushes.push_deck_event('card_unhidden', self, user) 362 363 flashcards.pushes.push_deck_event('card_unhidden', self, user)
hide = self.flashcardhide_set.get(user=user) 363 364 hide = self.flashcardhide_set.get(user=user)
hide.delete() 364 365 hide.delete()
365 366
@cached_property 366 367 @cached_property
def calculate_score(self): 367 368 def calculate_score(self):
def seconds_since_epoch(dt): 368 369 def seconds_since_epoch(dt):
from datetime import datetime 369 370 from datetime import datetime
370 371
epoch = make_aware(datetime.utcfromtimestamp(0)) 371 372 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 372 373 delta = dt - epoch
return delta.total_seconds() 373 374 return delta.total_seconds()
374 375
z = 0 375 376 z = 0
rate = 1.0 / 120 376 377 rate = 1.0 / 120
for vote in self.userflashcard_set.iterator(): 377 378 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 378 379 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 379 380 u = max(z, rate * t)
v = min(z, rate * t) 380 381 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 381 382 z = u + log1p(exp(v - u))
return z 382 383 return z
383 384
@classmethod 384 385 @classmethod
def cards_visible_to(cls, user): 385 386 def cards_visible_to(cls, user):
""" 386 387 """
:param user: 387 388 :param user:
:return: A queryset with all cards that should be visible to a user. 388 389 :return: A queryset with all cards that should be visible to a user.
""" 389 390 """
# All flashcards where the author is either confirmed, or the user 390 391 # All flashcards where the author is either confirmed, or the user
rqs = cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)) 391 392 rqs = cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user))
# Exclude hidden cards 392 393 # Exclude hidden cards
rqs = rqs.exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 393 394 rqs = rqs.exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
# rqs = rqs.prefetch_related('userflashcard_set') 394 395 # rqs = rqs.prefetch_related('userflashcard_set')
# rqs = rqs.aggregate(Count(userflashcard__user=user)) 395 396 # rqs = rqs.aggregate(Count(userflashcard__user=user))
# Annotate the cards so we don't have to check if they're hidden in the future 396 397 # Annotate the cards so we don't have to check if they're hidden in the future
return rqs.annotate(is_not_hidden=Value(True, output_field=BooleanField())) 397 398 return rqs.annotate(is_not_hidden=Value(True, output_field=BooleanField()))
398 399
@classmethod 399 400 @classmethod
def cards_hidden_by(cls, user): 400 401 def cards_hidden_by(cls, user):
return cls.objects.filter(flashcardhide__user=user) 401 402 return cls.objects.filter(flashcardhide__user=user)
402 403
403 404
class UserFlashcardQuiz(Model): 404 405 class UserFlashcardQuiz(Model):
""" 405 406 """
An event of a user being quizzed on a flashcard. 406 407 An event of a user being quizzed on a flashcard.
""" 407 408 """
user_flashcard = ForeignKey(UserFlashcard) 408 409 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 409 410 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=40, blank=True, help_text="The character range which was blanked") 410 411 blanked_word = CharField(max_length=40, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response") 411 412 response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 412 413 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
413 414
def __unicode__(self): 414 415 def __unicode__(self):
return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard)) 415 416 return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard))
416 417
def save(self, force_insert=False, force_update=False, using=None, 417 418 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 418 419 update_fields=None):
super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update, 419 420 super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 420 421 using=using, update_fields=update_fields)
self.user_flashcard.review(self) 421 422 self.user_flashcard.review(self)
422 423
def status(self): 423 424 def status(self):
""" 424 425 """
There are three stages of a quiz object: 425 426 There are three stages of a quiz object:
1. the user has been shown the card 426 427 1. the user has been shown the card
2. the user has answered the card 427 428 2. the user has answered the card
3. the user has self-evaluated their response's correctness 428 429 3. the user has self-evaluated their response's correctness
429 430
:return: string (evaluated, answered, viewed) 430 431 :return: string (evaluated, answered, viewed)
""" 431 432 """
if self.correct is not None: return "evaluated" 432 433 if self.correct is not None: return "evaluated"
if self.response: return "answered" 433 434 if self.response: return "answered"
return "viewed" 434 435 return "viewed"
435 436
436 437
class Section(Model): 437 438 class Section(Model):
""" 438 439 """
A UCSD course taught by an instructor during a quarter. 439 440 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 440 441 We use the term "section" to avoid collision with the builtin keyword "class"
We index gratuitously to support autofill and because this is primarily read-only 441 442 We index gratuitously to support autofill and because this is primarily read-only
""" 442 443 """
department = CharField(db_index=True, max_length=50) 443 444 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 444 445 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 445 446 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 446 447 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 447 448 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 448 449 quarter = CharField(db_index=True, max_length=4)
PAGE_SIZE = 40 449 450 PAGE_SIZE = 40
450 451
@classmethod 451 452 @classmethod
def search(cls, terms): 452 453 def search(cls, terms):
""" 453 454 """
Search all fields of all sections for a particular set of terms 454 455 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 455 456 A matching section must match at least one field on each term
:param terms:iterable 456 457 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 457 458 :return: Matching QuerySet ordered by department and course number
""" 458 459 """
final_q = Q() 459 460 final_q = Q()
for term in terms: 460 461 for term in terms:
q = Q(department__icontains=term) 461 462 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 462 463 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 463 464 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 464 465 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 465 466 q |= Q(instructor__icontains=term)
final_q &= q 466 467 final_q &= q
qs = cls.objects.filter(final_q) 467 468 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 468 469 # Have the database cast the course number to an integer so it will sort properly
# ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically 469 470 # ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"}) 470 471 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 471 472 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 472 473 return qs
473 474
@property 474 475 @property
def is_whitelisted(self): 475 476 def is_whitelisted(self):
""" 476 477 """
:return: whether a whitelist exists for this section 477 478 :return: whether a whitelist exists for this section
""" 478 479 """
return self.whitelist.exists() 479 480 return self.whitelist.exists()
480 481
def is_user_on_whitelist(self, user): 481 482 def is_user_on_whitelist(self, user):
""" 482 483 """
:return: whether the user is on the waitlist for this section 483 484 :return: whether the user is on the waitlist for this section
""" 484 485 """
return self.whitelist.filter(email=user.email).exists() 485 486 return self.whitelist.filter(email=user.email).exists()
486 487
def is_user_enrolled(self, user): 487 488 def is_user_enrolled(self, user):
return self.user_set.filter(pk=user.pk).exists() 488 489 return self.user_set.filter(pk=user.pk).exists()
489 490