Commit 5c40ced6df8f3a71c8b4d06a2c7a9f33f62227b5

Authored by Andrew Buss
1 parent 89492339c3
Exists in master

migrate default score value

Showing 2 changed files with 23 additions and 0 deletions Inline Diff

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