Commit cab847f4e84d1b4ad865e7791ff842c1f1c57027

Authored by Rohan Rangray
1 parent 23e4b7a71e
Exists in master

Fixed bug in study score update

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

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