Commit 0f0d54faac3b82eb8508919e4303408ff7315259

Authored by Andrew Buss
1 parent 34a8edc2e1
Exists in master

wait differently

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

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