Commit f66dc6a912c163e3ce1b526a867ef9e5ad2e9e2b

Authored by Rohan Rangray
1 parent 137e48f922
Exists in master

If the client's registration id is invalid, remove it

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

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