Commit 336f8126dd46c4f7dd5d7ef7be3ced53af2f644b

Authored by Andrew Buss
1 parent 1f74d60b26
Exists in master

reduce char limit to 140

Showing 2 changed files with 35 additions and 5 deletions Inline Diff

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