Commit 8a4e3756dd9b5ffa33d65432d4fba5562736adae

Authored by Andrew Buss
Exists in master

Merge branch 'master' of git.ucsd.edu:110swag/flashy-backend

Showing 1 changed file Inline Diff

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