Commit 2c00c7d077ff106cf94b0af346231c3e43d291bc

Authored by Rohan Rangray
1 parent c95e5d254d
Exists in master

Refactored soem code. Moved study sorting into the User model

Showing 2 changed files with 33 additions and 33 deletions Inline Diff

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