Commit 1f74d60b267ebbfeda462dba8a9569b660ad0bf2

Authored by Andrew Buss
1 parent cf248fe509
Exists in master

add material week number

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

flashcards/models.py View file @ 1f74d60
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
3 3
from django.contrib.auth.models import AbstractUser, UserManager 4 4 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 5 5 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 6 6 from django.core.cache import cache
from django.core.exceptions import ValidationError 7 7 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 8 8 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 9 9 from django.core.mail import send_mail
from django.db import IntegrityError 10 10 from django.db import IntegrityError
from django.db.models import * 11 11 from django.db.models import *
from django.utils.timezone import now, make_aware 12 12 from django.utils.timezone import now, make_aware
13 from flashy.settings import QUARTER_START
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 13 14 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 14 15 from fields import MaskField
from cached_property import cached_property 15 16 from cached_property import cached_property
16 17
17 18
18 19
19 20
# Hack to fix AbstractUser before subclassing it 20 21 # Hack to fix AbstractUser before subclassing it
21 22
AbstractUser._meta.get_field('email')._unique = True 22 23 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 23 24 AbstractUser._meta.get_field('username')._unique = False
24 25
25 26
class EmailOnlyUserManager(UserManager): 26 27 class EmailOnlyUserManager(UserManager):
""" 27 28 """
A tiny extension of Django's UserManager which correctly creates users 28 29 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 29 30 without usernames (using emails instead).
""" 30 31 """
31 32
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 32 33 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 33 34 """
Creates and saves a User with the given email and password. 34 35 Creates and saves a User with the given email and password.
""" 35 36 """
email = self.normalize_email(email) 36 37 email = self.normalize_email(email)
user = self.model(email=email, 37 38 user = self.model(email=email,
is_staff=is_staff, is_active=True, 38 39 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 39 40 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 40 41 date_joined=now(), **extra_fields)
user.set_password(password) 41 42 user.set_password(password)
user.save(using=self._db) 42 43 user.save(using=self._db)
return user 43 44 return user
44 45
def create_user(self, email, password=None, **extra_fields): 45 46 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 46 47 user = self._create_user(email, password, False, False, **extra_fields)
body = ''' 47 48 body = '''
Visit the following link to confirm your email address: 48 49 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 49 50 https://flashy.cards/app/verifyemail/%s
50 51
If you did not register for Flashy, no action is required. 51 52 If you did not register for Flashy, no action is required.
''' 52 53 '''
53 54
assert send_mail("Flashy email verification", 54 55 assert send_mail("Flashy email verification",
body % user.confirmation_key, 55 56 body % user.confirmation_key,
"noreply@flashy.cards", 56 57 "noreply@flashy.cards",
[user.email]) 57 58 [user.email])
return user 58 59 return user
59 60
def create_superuser(self, email, password, **extra_fields): 60 61 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 61 62 return self._create_user(email, password, True, True, **extra_fields)
62 63
63 64
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 64 65 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 65 66 """
An extension of Django's default user model. 66 67 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 67 68 We use email as the username field, and include enrolled sections here
""" 68 69 """
objects = EmailOnlyUserManager() 69 70 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 70 71 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 71 72 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 72 73 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 73 74 confirmed_email = BooleanField(default=False)
74 75
def is_in_section(self, section): 75 76 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 76 77 return self.sections.filter(pk=section.pk).exists()
77 78
def pull(self, flashcard): 78 79 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 79 80 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 80 81 raise ValueError("User not in the section this flashcard belongs to")
81 82
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 82 83 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.save() 83 84 user_card.save()
84 85
import flashcards.notifications 85 86 import flashcards.notifications
86 87
flashcards.notifications.notify_score_change(flashcard) 87 88 flashcards.notifications.notify_score_change(flashcard)
flashcards.notifications.notify_pull(flashcard) 88 89 flashcards.notifications.notify_pull(flashcard)
89 90
def unpull(self, flashcard): 90 91 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 91 92 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 92 93 raise ValueError("User not in the section this flashcard belongs to")
93 94
try: 94 95 try:
import flashcards.notifications 95 96 import flashcards.notifications
96 97
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 97 98 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
user_card.delete() 98 99 user_card.delete()
flashcards.notifications.notify_score_change(flashcard) 99 100 flashcards.notifications.notify_score_change(flashcard)
except UserFlashcard.DoesNotExist: 100 101 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 101 102 raise ValueError('Cannot unpull card that is not pulled.')
102 103
def get_deck(self, section): 103 104 def get_deck(self, section):
if not self.is_in_section(section): 104 105 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 105 106 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 106 107 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
107 108
def request_password_reset(self): 108 109 def request_password_reset(self):
token = default_token_generator.make_token(self) 109 110 token = default_token_generator.make_token(self)
110 111
body = ''' 111 112 body = '''
Visit the following link to reset your password: 112 113 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 113 114 https://flashy.cards/app/resetpassword/%d/%s
114 115
If you did not request a password reset, no action is required. 115 116 If you did not request a password reset, no action is required.
''' 116 117 '''
117 118
send_mail("Flashy password reset", 118 119 send_mail("Flashy password reset",
body % (self.pk, token), 119 120 body % (self.pk, token),
"noreply@flashy.cards", 120 121 "noreply@flashy.cards",
[self.email]) 121 122 [self.email])
122 123
def confirm_email(self, confirmation_key): 123 124 def confirm_email(self, confirmation_key):
# This will raise an exception if the email address is invalid 124 125 # This will raise an exception if the email address is invalid
self.email_address_set.confirm(confirmation_key, save=True) 125 126 self.email_address_set.confirm(confirmation_key, save=True)
self.confirmed_email = True 126 127 self.confirmed_email = True
self.save() 127 128 self.save()
128 129
129 130
class UserFlashcard(Model): 130 131 class UserFlashcard(Model):
""" 131 132 """
Represents the relationship between a user and a flashcard by: 132 133 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 133 134 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 134 135 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 135 136 3. A user has a flashcard hidden from them
""" 136 137 """
user = ForeignKey('User') 137 138 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 138 139 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 139 140 help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 140 141 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 141 142 flashcard = ForeignKey('Flashcard')
142 143
def get_mask(self): 143 144 def get_mask(self):
if self.mask is None: 144 145 if self.mask is None:
return self.flashcard.mask 145 146 return self.flashcard.mask
return self.mask 146 147 return self.mask
147 148
class Meta: 148 149 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 149 150 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 150 151 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 151 152 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 152 153 # By default, order by most recently pulled
ordering = ['-pulled'] 153 154 ordering = ['-pulled']
154 155
155 156
class FlashcardHide(Model): 156 157 class FlashcardHide(Model):
""" 157 158 """
Represents the property of a flashcard being hidden by a user. 158 159 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 159 160 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 160 161 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. 161 162 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 162 163 """
user = ForeignKey('User') 163 164 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 164 165 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 165 166 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 166 167 hidden = DateTimeField(auto_now_add=True)
167 168
class Meta: 168 169 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 169 170 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 170 171 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 171 172 index_together = ["user", "flashcard"]
172 173
173 174
class Flashcard(Model): 174 175 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 175 176 text = CharField(max_length=255, help_text='The text on the card')
section = ForeignKey('Section', help_text='The section with which the card is associated') 176 177 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") 177 178 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") 178 179 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 179 180 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 180 181 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 181 182 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 182 183 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 183 184 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 184 185 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 185 186 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
186 187
class Meta: 187 188 class Meta:
# By default, order by most recently pushed 188 189 # By default, order by most recently pushed
ordering = ['-pushed'] 189 190 ordering = ['-pushed']
191
192 @property
193 def material_week_num(self):
194 return (self.material_date-QUARTER_START).days/7+1
190 195
def is_hidden_from(self, user): 191 196 def is_hidden_from(self, user):
""" 192 197 """
A card can be hidden globally, but if a user has the card in their deck, 193 198 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 194 199 this visibility overrides a global hide.
:param user: 195 200 :param user:
:return: Whether the card is hidden from the user. 196 201 :return: Whether the card is hidden from the user.
""" 197 202 """
return self.is_hidden or self.flashcardhide_set.filter(user=user).exists() 198 203 return self.is_hidden or self.flashcardhide_set.filter(user=user).exists()
199 204
def hide_from(self, user, reason=None): 200 205 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 201 206 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 202 207 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 203 208 if not created:
raise ValidationError("The card has already been hidden.") 204 209 raise ValidationError("The card has already been hidden.")
obj.save() 205 210 obj.save()
206 211
def is_in_deck(self, user): 207 212 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 208 213 return self.userflashcard_set.filter(user=user).exists()
209 214
def add_to_deck(self, user): 210 215 def add_to_deck(self, user):
if not user.is_in_section(self.section): 211 216 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 212 217 raise PermissionDenied("You don't have the permission to add this card")
try: 213 218 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 214 219 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 215 220 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 216 221 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 217 222 user_flashcard.save()
return user_flashcard 218 223 return user_flashcard
219 224
def edit(self, user, new_data): 220 225 def edit(self, user, new_data):
""" 221 226 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 222 227 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. 223 228 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 224 229 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 225 230 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 226 231 """
227 232
# content_changed is True iff either material_date or text were changed 228 233 # content_changed is True iff either material_date or text were changed
content_changed = False 229 234 content_changed = False
# create_new is True iff the user editing this card is the author of this card 230 235 # 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 231 236 # and there are no other users with this card in their decks
create_new = user != self.author or \ 232 237 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 233 238 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 234 239 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 235 240 content_changed = True
self.material_date = new_data['material_date'] 236 241 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 237 242 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 238 243 content_changed = True
self.text = new_data['text'] 239 244 self.text = new_data['text']
if create_new and content_changed: 240 245 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 241 246 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 242 247 self.previous_id = self.pk
self.pk = None 243 248 self.pk = None
self.mask = new_data.get('mask', self.mask) 244 249 self.mask = new_data.get('mask', self.mask)
self.save() 245 250 self.save()
self.add_to_deck(user) 246 251 self.add_to_deck(user)
else: 247 252 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 248 253 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 249 254 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 250 255 user_card.save()
return self 251 256 return self
252 257
def report(self, user, reason=None): 253 258 def report(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 254 259 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 255 260 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 256 261 obj.reason = reason
obj.save() 257 262 obj.save()
258 263
@cached_property 259 264 @cached_property
def score(self): 260 265 def score(self):
def seconds_since_epoch(dt): 261 266 def seconds_since_epoch(dt):
from datetime import datetime 262 267 from datetime import datetime
263 268
epoch = make_aware(datetime.utcfromtimestamp(0)) 264 269 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 265 270 delta = dt - epoch
return delta.total_seconds() 266 271 return delta.total_seconds()
267 272
z = 0 268 273 z = 0
rate = 1.0 / 3600 269 274 rate = 1.0 / 3600
for vote in self.userflashcard_set.iterator(): 270 275 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 271 276 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 272 277 u = max(z, rate * t)
v = min(z, rate * t) 273 278 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 274 279 z = u + log1p(exp(v - u))
return z 275 280 return z
276 281
@classmethod 277 282 @classmethod
def cards_visible_to(cls, user): 278 283 def cards_visible_to(cls, user):
""" 279 284 """
:param user: 280 285 :param user:
:return: A queryset with all cards that should be visible to a user. 281 286 :return: A queryset with all cards that should be visible to a user.
""" 282 287 """
return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user) 283 288 return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)
).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 284 289 ).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
285 290
@classmethod 286 291 @classmethod
def cards_hidden_by(cls, user): 287 292 def cards_hidden_by(cls, user):
return cls.objects.filter(flashcardhide__user=user) 288 293 return cls.objects.filter(flashcardhide__user=user)
289 294
290 295
class UserFlashcardQuiz(Model): 291 296 class UserFlashcardQuiz(Model):
""" 292 297 """
An event of a user being quizzed on a flashcard. 293 298 An event of a user being quizzed on a flashcard.
""" 294 299 """
user_flashcard = ForeignKey(UserFlashcard) 295 300 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 296 301 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 297 302 blanked_word = CharField(max_length=8, 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") 298 303 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") 299 304 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
300 305
def status(self): 301 306 def status(self):
""" 302 307 """
There are three stages of a quiz object: 303 308 There are three stages of a quiz object:
1. the user has been shown the card 304 309 1. the user has been shown the card
2. the user has answered the card 305 310 2. the user has answered the card
3. the user has self-evaluated their response's correctness 306 311 3. the user has self-evaluated their response's correctness
307 312
:return: string (evaluated, answered, viewed) 308 313 :return: string (evaluated, answered, viewed)
""" 309 314 """
if self.correct is not None: return "evaluated" 310 315 if self.correct is not None: return "evaluated"
if self.response: return "answered" 311 316 if self.response: return "answered"
return "viewed" 312 317 return "viewed"
313 318
314 319
class Section(Model): 315 320 class Section(Model):
""" 316 321 """
A UCSD course taught by an instructor during a quarter. 317 322 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 318 323 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 319 324 We index gratuitously to support autofill and because this is primarily read-only
""" 320 325 """
department = CharField(db_index=True, max_length=50) 321 326 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 322 327 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 323 328 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 324 329 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 325 330 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 326 331 quarter = CharField(db_index=True, max_length=4)
327 332
@classmethod 328 333 @classmethod
def search(cls, terms): 329 334 def search(cls, terms):
""" 330 335 """
Search all fields of all sections for a particular set of terms 331 336 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 332 337 A matching section must match at least one field on each term
:param terms:iterable 333 338 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 334 339 :return: Matching QuerySet ordered by department and course number
""" 335 340 """
final_q = Q() 336 341 final_q = Q()
for term in terms: 337 342 for term in terms:
q = Q(department__icontains=term) 338 343 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 339 344 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 340 345 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 341 346 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 342 347 q |= Q(instructor__icontains=term)
final_q &= q 343 348 final_q &= q
qs = cls.objects.filter(final_q) 344 349 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 345 350 # 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 346 351 # 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)"}) 347 352 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 348 353 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 349 354 return qs
350 355
@property 351 356 @property
def is_whitelisted(self): 352 357 def is_whitelisted(self):
""" 353 358 """
:return: whether a whitelist exists for this section 354 359 :return: whether a whitelist exists for this section
""" 355 360 """
return self.whitelist.exists() 356 361 return self.whitelist.exists()
357 362
def is_user_on_whitelist(self, user): 358 363 def is_user_on_whitelist(self, user):
""" 359 364 """
:return: whether the user is on the waitlist for this section 360 365 :return: whether the user is on the waitlist for this section
""" 361 366 """
return self.whitelist.filter(email=user.email).exists() 362 367 return self.whitelist.filter(email=user.email).exists()
363 368
def enroll(self, user): 364 369 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 365 370 if user.sections.filter(pk=self.pk).exists():
flashcards/serializers.py View file @ 1f74d60
from json import dumps, loads 1 1 from json import dumps, loads
2 2
from django.utils.datetime_safe import datetime 3 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 4 4 from django.utils.timezone import now
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz 5 5 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 6 6 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 7 7 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \ 8 8 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \
SerializerMethodField, FloatField 9 9 SerializerMethodField, FloatField
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 10 10 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 11 11 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 12 12 from flashy.settings import QUARTER_END, QUARTER_START
13 13
14 14
class EmailSerializer(Serializer): 15 15 class EmailSerializer(Serializer):
email = EmailField(required=True) 16 16 email = EmailField(required=True)
17 17
18 18
class EmailPasswordSerializer(EmailSerializer): 19 19 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 20 20 password = CharField(required=True)
21 21
22 22
class RegistrationSerializer(EmailPasswordSerializer): 23 23 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 24 24 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
25 25
26 26
class PasswordResetRequestSerializer(EmailSerializer): 27 27 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 28 28 def validate_email(self, value):
try: 29 29 try:
User.objects.get(email=value) 30 30 User.objects.get(email=value)
return value 31 31 return value
except User.DoesNotExist: 32 32 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 33 33 raise serializers.ValidationError('No user exists with that email')
34 34
35 35
class PasswordResetSerializer(Serializer): 36 36 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 37 37 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 38 38 uid = IntegerField(required=True)
token = CharField(required=True) 39 39 token = CharField(required=True)
40 40
def validate_uid(self, value): 41 41 def validate_uid(self, value):
try: 42 42 try:
User.objects.get(id=value) 43 43 User.objects.get(id=value)
return value 44 44 return value
except User.DoesNotExist: 45 45 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 46 46 raise serializers.ValidationError('Could not verify reset token')
47 47
48 48
class UserUpdateSerializer(Serializer): 49 49 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 50 50 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 51 51 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 52 52 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 53 53 # reset_token = CharField(required=False)
54 54
def validate(self, data): 55 55 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 56 56 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 57 57 raise serializers.ValidationError('old_password is required to set a new_password')
return data 58 58 return data
59 59
60 60
class Password(Serializer): 61 61 class Password(Serializer):
email = EmailField(required=True) 62 62 email = EmailField(required=True)
password = CharField(required=True) 63 63 password = CharField(required=True)
64 64
65 65
class LecturePeriodSerializer(ModelSerializer): 66 66 class LecturePeriodSerializer(ModelSerializer):
class Meta: 67 67 class Meta:
model = LecturePeriod 68 68 model = LecturePeriod
exclude = 'id', 'section' 69 69 exclude = 'id', 'section'
70 70
71 71
class SectionSerializer(ModelSerializer): 72 72 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 73 73 lecture_times = CharField()
short_name = CharField() 74 74 short_name = CharField()
long_name = CharField() 75 75 long_name = CharField()
76 76
class Meta: 77 77 class Meta:
model = Section 78 78 model = Section
79 79
80 80
class DeepSectionSerializer(SectionSerializer): 81 81 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 82 82 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
83 83
84 84
class UserSerializer(ModelSerializer): 85 85 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 86 86 email = EmailField(required=False)
sections = SectionSerializer(many=True) 87 87 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 88 88 is_confirmed = BooleanField()
89 89
class Meta: 90 90 class Meta:
model = User 91 91 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 92 92 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
93 93
94 94
class MaskFieldSerializer(serializers.Field): 95 95 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 96 96 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 97 97 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 98 98 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 99 99 'overlap': 'Ensure this field does not have overlapping intervals.'
} 100 100 }
101 101
def to_representation(self, value): 102 102 def to_representation(self, value):
return map(list, self._make_mask(value)) 103 103 return map(list, self._make_mask(value))
104 104
def to_internal_value(self, value): 105 105 def to_internal_value(self, value):
if not isinstance(value, list): 106 106 if not isinstance(value, list):
value = loads(value) 107 107 value = loads(value)
return self._make_mask(value) 108 108 return self._make_mask(value)
109 109
def _make_mask(self, data): 110 110 def _make_mask(self, data):
try: 111 111 try:
mask = FlashcardMask(data) 112 112 mask = FlashcardMask(data)
except ValueError: 113 113 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 114 114 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 115 115 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 116 116 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 117 117 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 118 118 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 119 119 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 120 120 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 121 121 return mask
122 122
123 123
class FlashcardSerializer(ModelSerializer): 124 124 class FlashcardSerializer(ModelSerializer):
is_hidden = SerializerMethodField() 125 125 is_hidden = SerializerMethodField()
is_in_deck = SerializerMethodField() 126 126 is_in_deck = SerializerMethodField()
# hide_reason = CharField(read_only=True) 127 127 # hide_reason = CharField(read_only=True)
128 material_week_num = IntegerField(read_only=True)
material_date = DateTimeField(default=now) 128 129 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 129 130 mask = MaskFieldSerializer(allow_null=True)
score = FloatField(read_only=True) 130 131 score = FloatField(read_only=True)
131 132
def validate_material_date(self, value): 132 133 def validate_material_date(self, value):
# TODO: make this dynamic 133 134 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 134 135 if QUARTER_START <= value <= QUARTER_END:
return value 135 136 return value
else: 136 137 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 137 138 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
138 139
def validate_pushed(self, value): 139 140 def validate_pushed(self, value):
if value > datetime.now(): 140 141 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 141 142 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 142 143 return value
143 144
def validate_mask(self, value): 144 145 def validate_mask(self, value):
if value is None: 145 146 if value is None:
return None 146 147 return None
if len(self.initial_data['text']) < value.max_offset(): 147 148 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 148 149 raise serializers.ValidationError("Mask out of bounds")
return value 149 150 return value
150 151
def get_is_hidden(self, obj): 151 152 def get_is_hidden(self, obj):
if 'user' not in self.context: return False 152 153 if 'user' not in self.context: return False
return obj.is_hidden_from(self.context['user']) 153 154 return obj.is_hidden_from(self.context['user'])
154 155
def get_is_in_deck(self, obj): 155 156 def get_is_in_deck(self, obj):
if 'user' not in self.context: return False 156 157 if 'user' not in self.context: return False
return obj.is_in_deck(self.context['user']) 157 158 return obj.is_in_deck(self.context['user'])
158 159
class Meta: 159 160 class Meta:
model = Flashcard 160 161 model = Flashcard
exclude = 'author', 'previous' 161 162 exclude = 'author', 'previous'
162 163
163 164
class FlashcardUpdateSerializer(serializers.Serializer): 164 165 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 165 166 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 166 167 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 167 168 mask = MaskFieldSerializer(required=False)
168 169
def validate_material_date(self, date): 169 170 def validate_material_date(self, date):
if date > QUARTER_END: 170 171 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 171 172 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 172 173 return date
173 174
def validate(self, attrs): 174 175 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 175 176 # Make sure that at least one of the attributes was passed in
if not any(i in attrs for i in ['material_date', 'text', 'mask']): 176 177 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 177 178 raise serializers.ValidationError("No new value passed in")
return attrs 178 179 return attrs
179 180
180 181
class QuizRequestSerializer(serializers.Serializer): 181 182 class QuizRequestSerializer(serializers.Serializer):
sections = ListField(child=IntegerField(min_value=1), required=False) 182 183 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 183 184 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 184 185 material_date_end = DateTimeField(default=QUARTER_END)
185 186
def update(self, instance, validated_data): 186 187 def update(self, instance, validated_data):
pass 187 188 pass
188 189
def create(self, validated_data): 189 190 def create(self, validated_data):
return validated_data 190 191 return validated_data
191 192
def validate_material_date_begin(self, value): 192 193 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 193 194 if QUARTER_START <= value <= QUARTER_END:
return value 194 195 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 195 196 raise serializers.ValidationError("Invalid begin date for the flashcard range")
196 197
def validate_material_date_end(self, value): 197 198 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 198 199 if QUARTER_START <= value <= QUARTER_END:
return value 199 200 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 200 201 raise serializers.ValidationError("Invalid end date for the flashcard range")
201 202
def validate_sections(self, value): 202 203 def validate_sections(self, value):
if value is None: 203 204 if value is None:
return Section.objects.all() 204 205 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 205 206 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 206 207 if not section_filter.exists():
raise serializers.ValidationError("Those aren't valid sections") 207 208 raise serializers.ValidationError("Those aren't valid sections")
return value 208 209 return value
209 210
def validate(self, attrs): 210 211 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 211 212 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 212 213 raise serializers.ValidationError("Invalid range")