Commit 25c6d69732163db260f6dab151195e85e12bbaa5

Authored by Rohan Rangray
1 parent dc942208b9
Exists in master

Changed registration_token to registration_id for increased consisitency

Showing 5 changed files with 26 additions and 7 deletions Inline Diff

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