Commit 01721260e6c11539212aed83d33d3b63b3300628

Authored by Andrew Buss
1 parent 81749b02fe
Exists in master

add flashcard fix event

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

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