Commit 196fb56ae73ff715c6ef23a21c795b6612172ca4

Authored by Rohan Rangray
1 parent c21d1a7f55
Exists in master

Another fix in the query

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

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