Commit 3f97f29f084459082d6060d166cdfd28c4220e33

Authored by Andrew Buss
1 parent 31a37c406b
Exists in master

improve error handling for card pull/push

Showing 2 changed files with 25 additions and 8 deletions Inline Diff

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