Commit 99572cfcce0be3f08b38bdb02f3f3d541c7e8d55

Authored by Andrew Buss
1 parent d370a0e684
Exists in master

unpull before reporting

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

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