Commit 0b516c412bbfdd8918efdc5edc2c137800ae5df7

Authored by Rohan Rangray
1 parent 4ff8077bc2
Exists in master

Added potential fix to get SQL expdecay sorting working on postgres

Showing 2 changed files with 23 additions and 4 deletions Inline Diff

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