Commit ad724d791652285149b9c9a1dd6618f53794c3ee

Authored by Andrew Buss
1 parent fe44f16085
Exists in master

is_hidden specific to user

Showing 3 changed files with 20 additions and 20 deletions Inline Diff

flashcards/models.py View file @ ad724d7
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
3 3
from django.contrib.auth.models import AbstractUser, UserManager 4 4 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 5 5 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 6 6 from django.core.cache import cache
from django.core.exceptions import ValidationError 7 7 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 8 8 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 9 9 from django.core.mail import send_mail
from django.db import IntegrityError 10 10 from django.db import IntegrityError
from django.db.models import * 11 11 from django.db.models import *
from django.utils.timezone import now, make_aware 12 12 from django.utils.timezone import now, make_aware
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 13 13 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 14 14 from fields import MaskField
from cached_property import cached_property 15 15 from cached_property import cached_property
16 16
17 17
18 18
19
20
21
22
23
# Hack to fix AbstractUser before subclassing it 24 19 # Hack to fix AbstractUser before subclassing it
25 20
AbstractUser._meta.get_field('email')._unique = True 26 21 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 27 22 AbstractUser._meta.get_field('username')._unique = False
28 23
29 24
class EmailOnlyUserManager(UserManager): 30 25 class EmailOnlyUserManager(UserManager):
""" 31 26 """
A tiny extension of Django's UserManager which correctly creates users 32 27 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 33 28 without usernames (using emails instead).
""" 34 29 """
35 30
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 36 31 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 37 32 """
Creates and saves a User with the given email and password. 38 33 Creates and saves a User with the given email and password.
""" 39 34 """
email = self.normalize_email(email) 40 35 email = self.normalize_email(email)
user = self.model(email=email, 41 36 user = self.model(email=email,
is_staff=is_staff, is_active=True, 42 37 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 43 38 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 44 39 date_joined=now(), **extra_fields)
user.set_password(password) 45 40 user.set_password(password)
user.save(using=self._db) 46 41 user.save(using=self._db)
return user 47 42 return user
48 43
def create_user(self, email, password=None, **extra_fields): 49 44 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 50 45 user = self._create_user(email, password, False, False, **extra_fields)
body = ''' 51 46 body = '''
Visit the following link to confirm your email address: 52 47 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 53 48 https://flashy.cards/app/verifyemail/%s
54 49
If you did not register for Flashy, no action is required. 55 50 If you did not register for Flashy, no action is required.
''' 56 51 '''
57 52
assert send_mail("Flashy email verification", 58 53 assert send_mail("Flashy email verification",
body % user.confirmation_key, 59 54 body % user.confirmation_key,
"noreply@flashy.cards", 60 55 "noreply@flashy.cards",
[user.email]) 61 56 [user.email])
return user 62 57 return user
63 58
def create_superuser(self, email, password, **extra_fields): 64 59 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 65 60 return self._create_user(email, password, True, True, **extra_fields)
66 61
67 62
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 68 63 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 69 64 """
An extension of Django's default user model. 70 65 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 71 66 We use email as the username field, and include enrolled sections here
""" 72 67 """
objects = EmailOnlyUserManager() 73 68 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 74 69 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 75 70 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 76 71 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 77 72 confirmed_email = BooleanField(default=False)
78 73
def is_in_section(self, section): 79 74 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 80 75 return self.sections.filter(pk=section.pk).exists()
81 76
def pull(self, flashcard): 82 77 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 83 78 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 84 79 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 85 80 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = now() 86 81 user_card.pulled = now()
user_card.save() 87 82 user_card.save()
import flashcards.notifications 88 83 import flashcards.notifications
89 84
flashcards.notifications.notify_score_change(flashcard) 90 85 flashcards.notifications.notify_score_change(flashcard)
flashcards.notifications.notify_pull(flashcard) 91 86 flashcards.notifications.notify_pull(flashcard)
92 87
def unpull(self, flashcard): 93 88 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 94 89 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 95 90 raise ValueError("User not in the section this flashcard belongs to")
96 91
try: 97 92 try:
import flashcards.notifications 98 93 import flashcards.notifications
99 94
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 100 95 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
user_card.delete() 101 96 user_card.delete()
flashcards.notifications.notify_score_change(flashcard) 102 97 flashcards.notifications.notify_score_change(flashcard)
except UserFlashcard.DoesNotExist: 103 98 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 104 99 raise ValueError('Cannot unpull card that is not pulled.')
105 100
def get_deck(self, section): 106 101 def get_deck(self, section):
if not self.is_in_section(section): 107 102 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 108 103 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 109 104 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
110 105
def request_password_reset(self): 111 106 def request_password_reset(self):
token = default_token_generator.make_token(self) 112 107 token = default_token_generator.make_token(self)
113 108
body = ''' 114 109 body = '''
Visit the following link to reset your password: 115 110 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 116 111 https://flashy.cards/app/resetpassword/%d/%s
117 112
If you did not request a password reset, no action is required. 118 113 If you did not request a password reset, no action is required.
''' 119 114 '''
120 115
send_mail("Flashy password reset", 121 116 send_mail("Flashy password reset",
body % (self.pk, token), 122 117 body % (self.pk, token),
"noreply@flashy.cards", 123 118 "noreply@flashy.cards",
[self.email]) 124 119 [self.email])
125 120
def confirm_email(self, confirmation_key): 126 121 def confirm_email(self, confirmation_key):
# This will raise an exception if the email address is invalid 127 122 # This will raise an exception if the email address is invalid
self.email_address_set.confirm(confirmation_key, save=True) 128 123 self.email_address_set.confirm(confirmation_key, save=True)
self.confirmed_email = True 129 124 self.confirmed_email = True
self.save() 130 125 self.save()
131 126
132 127
class UserFlashcard(Model): 133 128 class UserFlashcard(Model):
""" 134 129 """
Represents the relationship between a user and a flashcard by: 135 130 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 136 131 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 137 132 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 138 133 3. A user has a flashcard hidden from them
""" 139 134 """
user = ForeignKey('User') 140 135 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 141 136 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 142 137 help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 143 138 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 144 139 flashcard = ForeignKey('Flashcard')
145 140
def get_mask(self): 146 141 def get_mask(self):
if self.mask is None: 147 142 if self.mask is None:
return self.flashcard.mask 148 143 return self.flashcard.mask
return self.mask 149 144 return self.mask
150 145
class Meta: 151 146 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 152 147 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 153 148 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 154 149 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 155 150 # By default, order by most recently pulled
ordering = ['-pulled'] 156 151 ordering = ['-pulled']
157 152
158 153
class FlashcardHide(Model): 159 154 class FlashcardHide(Model):
""" 160 155 """
Represents the property of a flashcard being hidden by a user. 161 156 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 162 157 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 163 158 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. 164 159 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 165 160 """
user = ForeignKey('User') 166 161 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 167 162 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 168 163 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 169 164 hidden = DateTimeField(auto_now_add=True)
170 165
class Meta: 171 166 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 172 167 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 173 168 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 174 169 index_together = ["user", "flashcard"]
175 170
176 171
class Flashcard(Model): 177 172 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 178 173 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') 179 174 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") 180 175 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") 181 176 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 182 177 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 183 178 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 184 179 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 185 180 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 186 181 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 187 182 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 188 183 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
189 184
class Meta: 190 185 class Meta:
# By default, order by most recently pushed 191 186 # By default, order by most recently pushed
ordering = ['-pushed'] 192 187 ordering = ['-pushed']
193 188
def is_hidden_from(self, user): 194 189 def is_hidden_from(self, user):
""" 195 190 """
A card can be hidden globally, but if a user has the card in their deck, 196 191 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 197 192 this visibility overrides a global hide.
:param user: 198 193 :param user:
:return: Whether the card is hidden from the user. 199 194 :return: Whether the card is hidden from the user.
""" 200 195 """
if self.userflashcard_set.filter(user=user).exists(): return False 201 196 return self.is_hidden or self.flashcardhide_set.filter(user=user).exists()
if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True 202
return False 203
204 197
def hide_from(self, user, reason=None): 205 198 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 206 199 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 207 200 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 208 201 if not created:
raise ValidationError("The card has already been hidden.") 209 202 raise ValidationError("The card has already been hidden.")
obj.save() 210 203 obj.save()
211 204
def is_in_deck(self, user): 212 205 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 213 206 return self.userflashcard_set.filter(user=user).exists()
214 207
def add_to_deck(self, user): 215 208 def add_to_deck(self, user):
if not user.is_in_section(self.section): 216 209 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 217 210 raise PermissionDenied("You don't have the permission to add this card")
try: 218 211 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 219 212 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 220 213 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 221 214 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 222 215 user_flashcard.save()
return user_flashcard 223 216 return user_flashcard
224 217
def edit(self, user, new_data): 225 218 def edit(self, user, new_data):
""" 226 219 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 227 220 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 221 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 229 222 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 230 223 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 231 224 """
232 225
# content_changed is True iff either material_date or text were changed 233 226 # content_changed is True iff either material_date or text were changed
content_changed = False 234 227 content_changed = False
# create_new is True iff the user editing this card is the author of this card 235 228 # 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 229 # and there are no other users with this card in their decks
create_new = user != self.author or \ 237 230 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 238 231 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 239 232 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 240 233 content_changed = True
self.material_date = new_data['material_date'] 241 234 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 242 235 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 243 236 content_changed = True
self.text = new_data['text'] 244 237 self.text = new_data['text']
if create_new and content_changed: 245 238 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 246 239 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 247 240 self.previous_id = self.pk
self.pk = None 248 241 self.pk = None
self.mask = new_data.get('mask', self.mask) 249 242 self.mask = new_data.get('mask', self.mask)
self.save() 250 243 self.save()
self.add_to_deck(user) 251 244 self.add_to_deck(user)
else: 252 245 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 253 246 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 254 247 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 255 248 user_card.save()
return self 256 249 return self
257 250
def report(self, user, reason=None): 258 251 def report(self, user, reason=None):
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 259 252 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 260 253 obj.reason = reason
obj.save() 261 254 obj.save()
262 255
@cached_property 263 256 @cached_property
def score(self): 264 257 def score(self):
def seconds_since_epoch(dt): 265 258 def seconds_since_epoch(dt):
from datetime import datetime 266 259 from datetime import datetime
260
epoch = make_aware(datetime.utcfromtimestamp(0)) 267 261 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 268 262 delta = dt - epoch
return delta.total_seconds() 269 263 return delta.total_seconds()
270 264
z = 0 271 265 z = 0
rate = 1.0 / 3600 272 266 rate = 1.0 / 3600
for vote in self.userflashcard_set.iterator(): 273 267 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 274 268 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 275 269 u = max(z, rate * t)
v = min(z, rate * t) 276 270 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 277 271 z = u + log1p(exp(v - u))
return z 278 272 return z
279 273
@classmethod 280 274 @classmethod
def cards_visible_to(cls, user): 281 275 def cards_visible_to(cls, user):
""" 282 276 """
:param user: 283 277 :param user:
:return: A queryset with all cards that should be visible to a user. 284 278 :return: A queryset with all cards that should be visible to a user.
""" 285 279 """
return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user) 286 280 return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)
).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 287 281 ).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
282
283 @classmethod
284 def cards_hidden_by(cls, user):
285 return cls.objects.filter(flashcardhide__user=user)
288 286
289 287
class UserFlashcardQuiz(Model): 290 288 class UserFlashcardQuiz(Model):
""" 291 289 """
An event of a user being quizzed on a flashcard. 292 290 An event of a user being quizzed on a flashcard.
""" 293 291 """
user_flashcard = ForeignKey(UserFlashcard) 294 292 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 295 293 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 296 294 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response") 297 295 response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 298 296 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
299 297
def status(self): 300 298 def status(self):
""" 301 299 """
There are three stages of a quiz object: 302 300 There are three stages of a quiz object:
1. the user has been shown the card 303 301 1. the user has been shown the card
2. the user has answered the card 304 302 2. the user has answered the card
3. the user has self-evaluated their response's correctness 305 303 3. the user has self-evaluated their response's correctness
306 304
:return: string (evaluated, answered, viewed) 307 305 :return: string (evaluated, answered, viewed)
""" 308 306 """
if self.correct is not None: return "evaluated" 309 307 if self.correct is not None: return "evaluated"
if self.response: return "answered" 310 308 if self.response: return "answered"
return "viewed" 311 309 return "viewed"
312 310
313 311
class Section(Model): 314 312 class Section(Model):
""" 315 313 """
A UCSD course taught by an instructor during a quarter. 316 314 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 317 315 We use the term "section" to avoid collision with the builtin keyword "class"
We index gratuitously to support autofill and because this is primarily read-only 318 316 We index gratuitously to support autofill and because this is primarily read-only
""" 319 317 """
department = CharField(db_index=True, max_length=50) 320 318 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 321 319 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 322 320 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 323 321 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 324 322 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 325 323 quarter = CharField(db_index=True, max_length=4)
326 324
@classmethod 327 325 @classmethod
def search(cls, terms): 328 326 def search(cls, terms):
""" 329 327 """
Search all fields of all sections for a particular set of terms 330 328 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 331 329 A matching section must match at least one field on each term
:param terms:iterable 332 330 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 333 331 :return: Matching QuerySet ordered by department and course number
""" 334 332 """
final_q = Q() 335 333 final_q = Q()
for term in terms: 336 334 for term in terms:
q = Q(department__icontains=term) 337 335 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 338 336 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 339 337 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 340 338 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 341 339 q |= Q(instructor__icontains=term)
final_q &= q 342 340 final_q &= q
qs = cls.objects.filter(final_q) 343 341 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 344 342 # Have the database cast the course number to an integer so it will sort properly
# ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically 345 343 # ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"}) 346 344 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 347 345 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 348 346 return qs
349 347
@property 350 348 @property
def is_whitelisted(self): 351 349 def is_whitelisted(self):
""" 352 350 """
:return: whether a whitelist exists for this section 353 351 :return: whether a whitelist exists for this section
""" 354 352 """
return self.whitelist.exists() 355 353 return self.whitelist.exists()
356 354
def is_user_on_whitelist(self, user): 357 355 def is_user_on_whitelist(self, user):
""" 358 356 """
:return: whether the user is on the waitlist for this section 359 357 :return: whether the user is on the waitlist for this section
""" 360 358 """
return self.whitelist.filter(email=user.email).exists() 361 359 return self.whitelist.filter(email=user.email).exists()
362 360
def enroll(self, user): 363 361 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 364 362 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 365 363 raise ValidationError('User is already enrolled in this section')
flashcards/serializers.py View file @ ad724d7
from json import dumps, loads 1 1 from json import dumps, loads
2 2
from django.utils.datetime_safe import datetime 3 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 4 4 from django.utils.timezone import now
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz 5 5 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 6 6 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 7 7 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty 8 8 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \
9 SerializerMethodField
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 9 10 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 10 11 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 11 12 from flashy.settings import QUARTER_END, QUARTER_START
12 13
13 14
class EmailSerializer(Serializer): 14 15 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 16 email = EmailField(required=True)
16 17
17 18
class EmailPasswordSerializer(EmailSerializer): 18 19 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 20 password = CharField(required=True)
20 21
21 22
class RegistrationSerializer(EmailPasswordSerializer): 22 23 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 24 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 25
25 26
class PasswordResetRequestSerializer(EmailSerializer): 26 27 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 28 def validate_email(self, value):
try: 28 29 try:
User.objects.get(email=value) 29 30 User.objects.get(email=value)
return value 30 31 return value
except User.DoesNotExist: 31 32 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 33 raise serializers.ValidationError('No user exists with that email')
33 34
34 35
class PasswordResetSerializer(Serializer): 35 36 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 37 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 38 uid = IntegerField(required=True)
token = CharField(required=True) 38 39 token = CharField(required=True)
39 40
def validate_uid(self, value): 40 41 def validate_uid(self, value):
try: 41 42 try:
User.objects.get(id=value) 42 43 User.objects.get(id=value)
return value 43 44 return value
except User.DoesNotExist: 44 45 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 46 raise serializers.ValidationError('Could not verify reset token')
46 47
47 48
class UserUpdateSerializer(Serializer): 48 49 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 50 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 51 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 52 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 53 # reset_token = CharField(required=False)
53 54
def validate(self, data): 54 55 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 56 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 57 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 58 return data
58 59
59 60
class Password(Serializer): 60 61 class Password(Serializer):
email = EmailField(required=True) 61 62 email = EmailField(required=True)
password = CharField(required=True) 62 63 password = CharField(required=True)
63 64
64 65
class LecturePeriodSerializer(ModelSerializer): 65 66 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 67 class Meta:
model = LecturePeriod 67 68 model = LecturePeriod
exclude = 'id', 'section' 68 69 exclude = 'id', 'section'
69 70
70 71
class SectionSerializer(ModelSerializer): 71 72 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 72 73 lecture_times = CharField()
short_name = CharField() 73 74 short_name = CharField()
long_name = CharField() 74 75 long_name = CharField()
75 76
class Meta: 76 77 class Meta:
model = Section 77 78 model = Section
78 79
79 80
class DeepSectionSerializer(SectionSerializer): 80 81 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 81 82 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
82 83
83 84
class UserSerializer(ModelSerializer): 84 85 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 85 86 email = EmailField(required=False)
sections = SectionSerializer(many=True) 86 87 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 87 88 is_confirmed = BooleanField()
88 89
class Meta: 89 90 class Meta:
model = User 90 91 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 91 92 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
92 93
93 94
class MaskFieldSerializer(serializers.Field): 94 95 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 95 96 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 96 97 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 97 98 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 98 99 'overlap': 'Ensure this field does not have overlapping intervals.'
} 99 100 }
100 101
def to_representation(self, value): 101 102 def to_representation(self, value):
return map(list, self._make_mask(value)) 102 103 return map(list, self._make_mask(value))
103 104
def to_internal_value(self, value): 104 105 def to_internal_value(self, value):
if not isinstance(value, list): 105 106 if not isinstance(value, list):
value = loads(value) 106 107 value = loads(value)
return self._make_mask(value) 107 108 return self._make_mask(value)
108 109
def _make_mask(self, data): 109 110 def _make_mask(self, data):
try: 110 111 try:
mask = FlashcardMask(data) 111 112 mask = FlashcardMask(data)
except ValueError: 112 113 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 113 114 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 114 115 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 115 116 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 116 117 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 117 118 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 118 119 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 119 120 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 120 121 return mask
121 122
122 123
class FlashcardSerializer(ModelSerializer): 123 124 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 124 125 is_hidden = SerializerMethodField()
# hide_reason = CharField(read_only=True) 125 126 # hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 126 127 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 127 128 mask = MaskFieldSerializer(allow_null=True)
score = IntegerField(read_only=True) 128 129 score = IntegerField(read_only=True)
129 130
def validate_material_date(self, value): 130 131 def validate_material_date(self, value):
# TODO: make this dynamic 131 132 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 132 133 if QUARTER_START <= value <= QUARTER_END:
return value 133 134 return value
else: 134 135 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 135 136 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
136 137
def validate_pushed(self, value): 137 138 def validate_pushed(self, value):
if value > datetime.now(): 138 139 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 139 140 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 140 141 return value
141 142
def validate_mask(self, value): 142 143 def validate_mask(self, value):
if value is None: 143 144 if value is None:
return None 144 145 return None
if len(self.initial_data['text']) < value.max_offset(): 145 146 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 146 147 raise serializers.ValidationError("Mask out of bounds")
return value 147 148 return value
149
150 def get_is_hidden(self, obj):
151 if 'user' not in self.context: return False
152 return obj.is_hidden_from(self.context['user'])
148 153
class Meta: 149 154 class Meta:
model = Flashcard 150 155 model = Flashcard
exclude = 'author', 'previous' 151 156 exclude = 'author', 'previous'
152 157
153 158
class FlashcardUpdateSerializer(serializers.Serializer): 154 159 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 155 160 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 156 161 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 157 162 mask = MaskFieldSerializer(required=False)
158 163
def validate_material_date(self, date): 159 164 def validate_material_date(self, date):
if date > QUARTER_END: 160 165 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 161 166 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 162 167 return date
163 168
def validate(self, attrs): 164 169 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 165 170 # Make sure that at least one of the attributes was passed in
if not any(i in attrs for i in ['material_date', 'text', 'mask']): 166 171 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 167 172 raise serializers.ValidationError("No new value passed in")
return attrs 168 173 return attrs
169 174
170 175
class QuizRequestSerializer(serializers.Serializer): 171 176 class QuizRequestSerializer(serializers.Serializer):
sections = ListField(child=IntegerField(min_value=1), required=False) 172 177 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 173 178 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 174 179 material_date_end = DateTimeField(default=QUARTER_END)
175 180
def update(self, instance, validated_data): 176 181 def update(self, instance, validated_data):
pass 177 182 pass
178 183
def create(self, validated_data): 179 184 def create(self, validated_data):
return validated_data 180 185 return validated_data
181 186
def validate_material_date_begin(self, value): 182 187 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 183 188 if QUARTER_START <= value <= QUARTER_END:
return value 184 189 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 185 190 raise serializers.ValidationError("Invalid begin date for the flashcard range")
186 191
def validate_material_date_end(self, value): 187 192 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 188 193 if QUARTER_START <= value <= QUARTER_END:
return value 189 194 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 190 195 raise serializers.ValidationError("Invalid end date for the flashcard range")
191 196
def validate_sections(self, value): 192 197 def validate_sections(self, value):
if value is None: 193 198 if value is None:
return Section.objects.all() 194 199 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 195 200 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 196 201 if not section_filter.exists():
raise serializers.ValidationError("Those aren't valid sections") 197 202 raise serializers.ValidationError("Those aren't valid sections")
return value 198 203 return value
199 204
def validate(self, attrs): 200 205 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 201 206 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 202 207 raise serializers.ValidationError("Invalid range")
if 'sections' not in attrs: 203 208 if 'sections' not in attrs:
attrs['sections'] = self.validate_sections(None) 204 209 attrs['sections'] = self.validate_sections(None)
return attrs 205 210 return attrs
206 211
flashcards/views.py View file @ ad724d7
from random import sample 1
2
import django 3 1 import django
from django.contrib import auth 4 2 from django.contrib import auth
from django.shortcuts import get_object_or_404 5 3 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer 6 4 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz 7 5 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.notifications import notify_new_card 8 6 from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 9 7 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 10 8 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 11 9 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer 12 10 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 13 11 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 14 12 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 15 13 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 16 14 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 17 15 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 18 16 from django.core.mail import send_mail
from django.contrib.auth import authenticate 19 17 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 20 18 from django.contrib.auth.tokens import default_token_generator
from django.db.models import Count, F 21
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 22 19 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 23 20 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 24 21 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 25 22 from simple_email_confirmation import EmailAddress
from math import e 26
27 23
28 24
class SectionViewSet(ReadOnlyModelViewSet): 29 25 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 30 26 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 31 27 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 32 28 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 33 29 permission_classes = [IsAuthenticated]
34 30
@detail_route(methods=['GET']) 35 31 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 36 32 def flashcards(self, request, pk):
""" 37 33 """
Gets flashcards for a section, excluding hidden cards. 38 34 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 39 35 Returned in strictly chronological order (material date).
""" 40 36 """
37 flashcards = Flashcard.cards_visible_to(request.user)
if 'hidden' in request.GET: 41 38 if 'hidden' in request.GET:
flashcards = Flashcard.objects 42 39 if request.get['hidden'] == 'only':
else: 43 40 flashcards = Flashcard.cards_hidden_by(request.user)
flashcards = Flashcard.cards_visible_to(request.user) 44 41 else:
42 flashcards |= Flashcard.cards_hidden_by(request.user)
flashcards = flashcards.filter(section=self.get_object()).all() 45 43 flashcards = flashcards.filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 46 44 return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data)
47 45
@detail_route(methods=['POST']) 48 46 @detail_route(methods=['POST'])
def enroll(self, request, pk): 49 47 def enroll(self, request, pk):
""" 50 48 """
Add the current user to a specified section 51 49 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 52 50 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 53 51 ---
view_mocker: flashcards.api.mock_no_params 54 52 view_mocker: flashcards.api.mock_no_params
""" 55 53 """
try: 56 54 try:
self.get_object().enroll(request.user) 57 55 self.get_object().enroll(request.user)
except django.core.exceptions.PermissionDenied as e: 58 56 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 59 57 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 60 58 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 61 59 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 62 60 return Response(status=HTTP_204_NO_CONTENT)
63 61
@detail_route(methods=['POST']) 64 62 @detail_route(methods=['POST'])
def drop(self, request, pk): 65 63 def drop(self, request, pk):
""" 66 64 """
Remove the current user from a specified section 67 65 Remove the current user from a specified section
If the user is not in the class, the request will fail. 68 66 If the user is not in the class, the request will fail.
--- 69 67 ---
view_mocker: flashcards.api.mock_no_params 70 68 view_mocker: flashcards.api.mock_no_params
""" 71 69 """
try: 72 70 try:
self.get_object().drop(request.user) 73 71 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 74 72 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 75 73 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 76 74 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 77 75 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 78 76 return Response(status=HTTP_204_NO_CONTENT)
79 77
@list_route(methods=['GET']) 80 78 @list_route(methods=['GET'])
def search(self, request): 81 79 def search(self, request):
""" 82 80 """
Returns a list of sections which match a user's query 83 81 Returns a list of sections which match a user's query
--- 84 82 ---
parameters: 85 83 parameters:
- name: q 86 84 - name: q
description: space-separated list of terms 87 85 description: space-separated list of terms
required: true 88 86 required: true
type: form 89 87 type: form
response_serializer: SectionSerializer 90 88 response_serializer: SectionSerializer
""" 91 89 """
query = request.GET.get('q', None) 92 90 query = request.GET.get('q', None)
if not query: return Response('[]') 93 91 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 94 92 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 95 93 data = SectionSerializer(qs, many=True).data
return Response(data) 96 94 return Response(data)
97 95
@detail_route(methods=['GET']) 98 96 @detail_route(methods=['GET'])
def deck(self, request, pk): 99 97 def deck(self, request, pk):
""" 100 98 """
Gets the contents of a user's deck for a given section. 101 99 Gets the contents of a user's deck for a given section.
""" 102 100 """
qs = request.user.get_deck(self.get_object()) 103 101 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 104 102 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 105 103 return Response(serializer.data)
106 104
@detail_route(methods=['GET']) 107 105 @detail_route(methods=['GET'])
def feed(self, request, pk): 108 106 def feed(self, request, pk):
""" 109 107 """
Gets the contents of a user's feed for a section. 110 108 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 111 109 Exclude cards that are already in the user's deck
""" 112 110 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 113 111 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 114 112 return Response(serializer.data)
115 113
116 114
class UserSectionListView(ListAPIView): 117 115 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 118 116 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 119 117 permission_classes = [IsAuthenticated]
120 118
def get_queryset(self): 121 119 def get_queryset(self):
return self.request.user.sections.all() 122 120 return self.request.user.sections.all()
123 121
def paginate_queryset(self, queryset): return None 124 122 def paginate_queryset(self, queryset): return None
125 123
126 124
class UserDetail(GenericAPIView): 127 125 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 128 126 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 129 127 permission_classes = [IsAuthenticated]
130 128
def patch(self, request, format=None): 131 129 def patch(self, request, format=None):
""" 132 130 """
Updates the user's password, or verifies their email address 133 131 Updates the user's password, or verifies their email address
--- 134 132 ---
request_serializer: UserUpdateSerializer 135 133 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 136 134 response_serializer: UserSerializer
""" 137 135 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 138 136 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 139 137 data.is_valid(raise_exception=True)
data = data.validated_data 140 138 data = data.validated_data
141 139
if 'new_password' in data: 142 140 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 143 141 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 144 142 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 145 143 request.user.set_password(data['new_password'])
request.user.save() 146 144 request.user.save()
147 145
if 'confirmation_key' in data: 148 146 if 'confirmation_key' in data:
try: 149 147 try:
request.user.confirm_email(data['confirmation_key']) 150 148 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 151 149 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 152 150 raise ValidationError('confirmation_key is invalid')
153 151
return Response(UserSerializer(request.user).data) 154 152 return Response(UserSerializer(request.user).data)
155 153
def get(self, request, format=None): 156 154 def get(self, request, format=None):
""" 157 155 """
Return data about the user 158 156 Return data about the user
--- 159 157 ---
response_serializer: UserSerializer 160 158 response_serializer: UserSerializer
""" 161 159 """
serializer = UserSerializer(request.user, context={'request': request}) 162 160 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 163 161 return Response(serializer.data)
164 162
def delete(self, request): 165 163 def delete(self, request):
""" 166 164 """
Irrevocably delete the user and their data 167 165 Irrevocably delete the user and their data
168 166
Yes, really 169 167 Yes, really
""" 170 168 """
request.user.delete() 171 169 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 172 170 return Response(status=HTTP_204_NO_CONTENT)
173 171
174 172
@api_view(['POST']) 175 173 @api_view(['POST'])
def register(request, format=None): 176 174 def register(request, format=None):
""" 177 175 """
Register a new user 178 176 Register a new user
--- 179 177 ---
request_serializer: EmailPasswordSerializer 180 178 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 181 179 response_serializer: UserSerializer
""" 182 180 """
data = RegistrationSerializer(data=request.data) 183 181 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 184 182 data.is_valid(raise_exception=True)
185 183
User.objects.create_user(**data.validated_data) 186 184 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 187 185 user = authenticate(**data.validated_data)
auth.login(request, user) 188 186 auth.login(request, user)
189 187
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 190 188 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
191 189
192 190
@api_view(['POST']) 193 191 @api_view(['POST'])
def login(request): 194 192 def login(request):
""" 195 193 """
Authenticates user and returns user data if valid. 196 194 Authenticates user and returns user data if valid.
--- 197 195 ---
request_serializer: EmailPasswordSerializer 198 196 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 199 197 response_serializer: UserSerializer
""" 200 198 """
201 199
data = EmailPasswordSerializer(data=request.data) 202 200 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 203 201 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 204 202 user = authenticate(**data.validated_data)
205 203
if user is None: 206 204 if user is None:
raise AuthenticationFailed('Invalid email or password') 207 205 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 208 206 if not user.is_active:
raise NotAuthenticated('Account is disabled') 209 207 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 210 208 auth.login(request, user)
return Response(UserSerializer(request.user).data) 211 209 return Response(UserSerializer(request.user).data)
212 210
213 211
@api_view(['POST']) 214 212 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 215 213 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 216 214 def logout(request, format=None):
""" 217 215 """
Logs the authenticated user out. 218 216 Logs the authenticated user out.
""" 219 217 """
auth.logout(request) 220 218 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 221 219 return Response(status=HTTP_204_NO_CONTENT)
222 220
223 221
@api_view(['POST']) 224 222 @api_view(['POST'])
def request_password_reset(request, format=None): 225 223 def request_password_reset(request, format=None):
""" 226 224 """
Send a password reset token/link to the provided email. 227 225 Send a password reset token/link to the provided email.
--- 228 226 ---
request_serializer: PasswordResetRequestSerializer 229 227 request_serializer: PasswordResetRequestSerializer
""" 230 228 """
data = PasswordResetRequestSerializer(data=request.data) 231 229 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 232 230 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 233 231 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 234 232 return Response(status=HTTP_204_NO_CONTENT)
235 233
236 234
@api_view(['POST']) 237 235 @api_view(['POST'])
def reset_password(request, format=None): 238 236 def reset_password(request, format=None):
""" 239 237 """
Updates user's password to new password if token is valid. 240 238 Updates user's password to new password if token is valid.
--- 241 239 ---
request_serializer: PasswordResetSerializer 242 240 request_serializer: PasswordResetSerializer
""" 243 241 """
data = PasswordResetSerializer(data=request.data) 244 242 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 243 data.is_valid(raise_exception=True)
246 244
user = User.objects.get(id=data['uid'].value) 247 245 user = User.objects.get(id=data['uid'].value)
# Check token validity. 248 246 # Check token validity.
249 247
if default_token_generator.check_token(user, data['token'].value): 250 248 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 251 249 user.set_password(data['new_password'].value)
user.save() 252 250 user.save()
else: 253 251 else:
raise ValidationError('Could not verify reset token') 254 252 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 255 253 return Response(status=HTTP_204_NO_CONTENT)
256 254
257 255
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 258 256 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 259 257 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 260 258 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 261 259 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
# Override create in CreateModelMixin 262 260 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 263 261 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 264 262 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 265 263 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 266 264 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 267 265 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 268 266 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 269 267 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 270 268 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 271 269 self.perform_create(flashcard)
notify_new_card(flashcard) 272 270 notify_new_card(flashcard)
headers = self.get_success_headers(data) 273 271 headers = self.get_success_headers(data)
request.user.pull(flashcard) 274 272 request.user.pull(flashcard)
response_data = FlashcardSerializer(flashcard).data 275 273 response_data = FlashcardSerializer(flashcard).data
276 274
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 277 275 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
278 276
@detail_route(methods=['POST']) 279 277 @detail_route(methods=['POST'])
def unhide(self, request, pk): 280 278 def unhide(self, request, pk):
""" 281 279 """
Unhide the given card 282 280 Unhide the given card
--- 283 281 ---
view_mocker: flashcards.api.mock_no_params 284 282 view_mocker: flashcards.api.mock_no_params
""" 285 283 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 286 284 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 287 285 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 288 286 return Response(status=HTTP_204_NO_CONTENT)
289 287
@detail_route(methods=['POST']) 290 288 @detail_route(methods=['POST'])
def report(self, request, pk): 291 289 def report(self, request, pk):
""" 292 290 """
Hide the given card 293 291 Hide the given card
--- 294 292 ---
view_mocker: flashcards.api.mock_no_params 295 293 view_mocker: flashcards.api.mock_no_params
""" 296 294 """
self.get_object().report(request.user) 297 295 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 298 296 return Response(status=HTTP_204_NO_CONTENT)
299 297
hide = report 300 298 hide = report
301 299
@detail_route(methods=['POST']) 302 300 @detail_route(methods=['POST'])
def pull(self, request, pk): 303 301 def pull(self, request, pk):
""" 304 302 """
Pull a card from the live feed into the user's deck. 305 303 Pull a card from the live feed into the user's deck.
--- 306 304 ---
view_mocker: flashcards.api.mock_no_params 307 305 view_mocker: flashcards.api.mock_no_params
""" 308 306 """
309 307
request.user.pull(self.get_object()) 310 308 request.user.pull(self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 311 309 return Response(status=HTTP_204_NO_CONTENT)
312 310
313
@detail_route(methods=['POST']) 314 311 @detail_route(methods=['POST'])
def unpull(self, request, pk): 315 312 def unpull(self, request, pk):
""" 316 313 """
Unpull a card from the user's deck 317 314 Unpull a card from the user's deck
--- 318 315 ---
view_mocker: flashcards.api.mock_no_params 319 316 view_mocker: flashcards.api.mock_no_params
""" 320 317 """
user = request.user 321 318 user = request.user
flashcard = self.get_object() 322 319 flashcard = self.get_object()
user.unpull(flashcard) 323 320 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 324 321 return Response(status=HTTP_204_NO_CONTENT)
325 322
def partial_update(self, request, *args, **kwargs): 326 323 def partial_update(self, request, *args, **kwargs):
""" 327 324 """
Edit settings related to a card for the user. 328 325 Edit settings related to a card for the user.
--- 329 326 ---
request_serializer: FlashcardUpdateSerializer 330 327 request_serializer: FlashcardUpdateSerializer
""" 331 328 """
user = request.user 332 329 user = request.user
flashcard = self.get_object() 333 330 flashcard = self.get_object()
data = FlashcardUpdateSerializer(data=request.data) 334 331 data = FlashcardUpdateSerializer(data=request.data)
data.is_valid(raise_exception=True) 335 332 data.is_valid(raise_exception=True)
new_flashcard = data.validated_data 336 333 new_flashcard = data.validated_data
new_flashcard = flashcard.edit(user, new_flashcard) 337 334 new_flashcard = flashcard.edit(user, new_flashcard)
return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK) 338 335 return Response(FlashcardSerializer(new_flashcard, context={'user': request.user}).data, status=HTTP_200_OK)
339 336
340 337
class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): 341 338 class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin):
permission_classes = [IsAuthenticated, IsFlashcardReviewer] 342 339 permission_classes = [IsAuthenticated, IsFlashcardReviewer]
queryset = UserFlashcardQuiz.objects.all() 343 340 queryset = UserFlashcardQuiz.objects.all()
344 341
def get_serializer_class(self): 345 342 def get_serializer_class(self):
if self.request.method == 'POST': 346 343 if self.request.method == 'POST':
return QuizRequestSerializer 347 344 return QuizRequestSerializer
return QuizAnswerRequestSerializer 348 345 return QuizAnswerRequestSerializer
349 346