Commit 5bff7d66e030a49ffc4fe368e4ee2b591f23adb8

Authored by Andrew Buss
1 parent 3817d63468
Exists in master

change course sorting for sidebar

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

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