Commit 34a8edc2e1758454dbc46449a65e3dd4c237a692

Authored by Andrew Buss
1 parent e7a281e866
Exists in master

ignore effects of unverified users

Showing 3 changed files with 31 additions and 2 deletions Inline Diff

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