Commit 390189eb63a6a8f4c0bc4fd584072d4834da7173

Authored by Andrew Buss
1 parent 19f62c6f7e
Exists in master

handle pulling a card that's already in deck more gracefully

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

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