Commit 2f03453104dc13dd3344c73d92a01a2620c23a9c

Authored by Rohan Rangray
1 parent 92415314e1
Exists in master

Fixed typo in study

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

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