Commit 93e9f2896e2ab150b320647ecbb7be1b36a7335e

Authored by Andrew Buss
1 parent 01721260e6
Exists in master

oops; don't actually save

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

flashcards/models.py View file @ 93e9f28
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()
self.save() 354
import flashcards.pushes 355 354 import flashcards.pushes
flashcards.pushes.push_deck_event('card_fixed', self, user) 356 355 flashcards.pushes.push_deck_event('card_fixed', self, user)
return self 357 356 return self
358 357
def hide_by_user(self, user, reason=None): 359 358 def hide_by_user(self, user, reason=None):
import flashcards.pushes 360 359 import flashcards.pushes
361 360
flashcards.pushes.push_deck_event('card_hidden', self, user) 362 361 flashcards.pushes.push_deck_event('card_hidden', self, user)
if self.is_in_deck(user): user.unpull(self) 363 362 if self.is_in_deck(user): user.unpull(self)
hide, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 364 363 hide, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
hide.reason = reason 365 364 hide.reason = reason
hide.save() 366 365 hide.save()
367 366
def unhide_by_user(self, user, reason=None): 368 367 def unhide_by_user(self, user, reason=None):
import flashcards.pushes 369 368 import flashcards.pushes
370 369
flashcards.pushes.push_deck_event('card_unhidden', self, user) 371 370 flashcards.pushes.push_deck_event('card_unhidden', self, user)
hide = self.flashcardhide_set.get(user=user) 372 371 hide = self.flashcardhide_set.get(user=user)
hide.delete() 373 372 hide.delete()
374 373
@cached_property 375 374 @cached_property
def calculate_score(self): 376 375 def calculate_score(self):
def seconds_since_epoch(dt): 377 376 def seconds_since_epoch(dt):
from datetime import datetime 378 377 from datetime import datetime
379 378
epoch = make_aware(datetime.utcfromtimestamp(0)) 380 379 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 381 380 delta = dt - epoch
return delta.total_seconds() 382 381 return delta.total_seconds()
383 382
z = 0 384 383 z = 0
rate = 1.0 / 120 385 384 rate = 1.0 / 120
for vote in self.userflashcard_set.iterator(): 386 385 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 387 386 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 388 387 u = max(z, rate * t)
v = min(z, rate * t) 389 388 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 390 389 z = u + log1p(exp(v - u))
return z 391 390 return z
392 391
@classmethod 393 392 @classmethod
def cards_visible_to(cls, user): 394 393 def cards_visible_to(cls, user):
""" 395 394 """
:param user: 396 395 :param user:
:return: A queryset with all cards that should be visible to a user. 397 396 :return: A queryset with all cards that should be visible to a user.
""" 398 397 """
# All flashcards where the author is either confirmed, or the user 399 398 # All flashcards where the author is either confirmed, or the user
rqs = cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)) 400 399 rqs = cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user))
# Exclude hidden cards 401 400 # Exclude hidden cards
rqs = rqs.exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 402 401 rqs = rqs.exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
# rqs = rqs.prefetch_related('userflashcard_set') 403 402 # rqs = rqs.prefetch_related('userflashcard_set')
# rqs = rqs.aggregate(Count(userflashcard__user=user)) 404 403 # rqs = rqs.aggregate(Count(userflashcard__user=user))
# Annotate the cards so we don't have to check if they're hidden in the future 405 404 # 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())) 406 405 return rqs.annotate(is_not_hidden=Value(True, output_field=BooleanField()))
407 406
@classmethod 408 407 @classmethod
def cards_hidden_by(cls, user): 409 408 def cards_hidden_by(cls, user):
return cls.objects.filter(flashcardhide__user=user) 410 409 return cls.objects.filter(flashcardhide__user=user)
411 410
412 411
class UserFlashcardQuiz(Model): 413 412 class UserFlashcardQuiz(Model):
""" 414 413 """
An event of a user being quizzed on a flashcard. 415 414 An event of a user being quizzed on a flashcard.
""" 416 415 """
user_flashcard = ForeignKey(UserFlashcard) 417 416 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 418 417 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=255, blank=True, help_text="The character range which was blanked") 419 418 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") 420 419 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") 421 420 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
422 421
def __unicode__(self): 423 422 def __unicode__(self):
return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard)) 424 423 return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard))
425 424
def save(self, force_insert=False, force_update=False, using=None, 426 425 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 427 426 update_fields=None):
super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update, 428 427 super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 429 428 using=using, update_fields=update_fields)
self.user_flashcard.review(self) 430 429 self.user_flashcard.review(self)
431 430
def status(self): 432 431 def status(self):
""" 433 432 """
There are three stages of a quiz object: 434 433 There are three stages of a quiz object:
1. the user has been shown the card 435 434 1. the user has been shown the card
2. the user has answered the card 436 435 2. the user has answered the card
3. the user has self-evaluated their response's correctness 437 436 3. the user has self-evaluated their response's correctness
438 437
:return: string (evaluated, answered, viewed) 439 438 :return: string (evaluated, answered, viewed)
""" 440 439 """
if self.correct is not None: return "evaluated" 441 440 if self.correct is not None: return "evaluated"
if self.response: return "answered" 442 441 if self.response: return "answered"
return "viewed" 443 442 return "viewed"
444 443
445 444
class Section(Model): 446 445 class Section(Model):
""" 447 446 """
A UCSD course taught by an instructor during a quarter. 448 447 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 449 448 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 450 449 We index gratuitously to support autofill and because this is primarily read-only
""" 451 450 """
department = CharField(db_index=True, max_length=50) 452 451 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 453 452 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 454 453 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 455 454 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 456 455 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 457 456 quarter = CharField(db_index=True, max_length=4)
PAGE_SIZE = 40 458 457 PAGE_SIZE = 40
459 458
@classmethod 460 459 @classmethod
def search(cls, terms): 461 460 def search(cls, terms):
""" 462 461 """
Search all fields of all sections for a particular set of terms 463 462 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 464 463 A matching section must match at least one field on each term
:param terms:iterable 465 464 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 466 465 :return: Matching QuerySet ordered by department and course number
""" 467 466 """
final_q = Q() 468 467 final_q = Q()
for term in terms: 469 468 for term in terms:
q = Q(department__icontains=term) 470 469 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 471 470 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 472 471 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 473 472 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 474 473 q |= Q(instructor__icontains=term)
final_q &= q 475 474 final_q &= q
qs = cls.objects.filter(final_q).prefetch_related('whitelist') 476 475 qs = cls.objects.filter(final_q).prefetch_related('whitelist')
# Have the database cast the course number to an integer so it will sort properly 477 476 # 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 478 477 # 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)"}) 479 478 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 480 479 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 481 480 return qs
482 481
@property 483 482 @property
def is_whitelisted(self): 484 483 def is_whitelisted(self):
""" 485 484 """
:return: whether a whitelist exists for this section 486 485 :return: whether a whitelist exists for this section
""" 487 486 """
data = cache.get("section_%d_is_whitelisted" % self.pk) 488 487 data = cache.get("section_%d_is_whitelisted" % self.pk)
if data is None: 489 488 if data is None:
data = self.whitelist.exists() 490 489 data = self.whitelist.exists()
cache.set("section_%d_is_whitelisted" % self.pk, data, 24 * 60 * 60) 491 490 cache.set("section_%d_is_whitelisted" % self.pk, data, 24 * 60 * 60)
return data 492 491 return data
493 492
def is_user_on_whitelist(self, user): 494 493 def is_user_on_whitelist(self, user):
""" 495 494 """
:return: whether the user is on the waitlist for this section 496 495 :return: whether the user is on the waitlist for this section
""" 497 496 """
return self.whitelist.filter(email=user.email).exists() 498 497 return self.whitelist.filter(email=user.email).exists()
499 498
def is_user_enrolled(self, user): 500 499 def is_user_enrolled(self, user):
return self.user_set.filter(pk=user.pk).exists() 501 500 return self.user_set.filter(pk=user.pk).exists()
502 501