Commit 9d3d5e9047849a7b2aa65d5f1f3bca61e08bcaa6

Authored by Andrew Buss
1 parent 804b11e233
Exists in master

card decay to 1/e in 2 minutes

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

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