Commit 435643e5a2945c496a9c50406b2e3badd3bf39f0

Authored by Andrew Buss
1 parent 22f26c6fac
Exists in master

scratch that

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

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