Commit a222348da766d93d98186160c037a00a003a3043

Authored by Andrew Buss
1 parent 0f0d54faac
Exists in master

let a user see his own cards

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

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