Commit 34586f534d3b6991792cc06b29f15bc7614b7057

Authored by Rohan Rangray
1 parent 7dcd94c018
Exists in master

Fixed MaskFieldSerializer to accept both lists and their string represenations.

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

flashcards/serializers.py View file @ 34586f5
from json import dumps, loads 1 1 from json import dumps, loads
2 2
from django.utils.datetime_safe import datetime 3 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 4 4 from django.utils.timezone import now
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz 5 5 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 6 6 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 7 7 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty 8 8 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 9 9 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 10 10 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 11 11 from flashy.settings import QUARTER_END, QUARTER_START
12 12
13 13
class EmailSerializer(Serializer): 14 14 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 15 email = EmailField(required=True)
16 16
17 17
class EmailPasswordSerializer(EmailSerializer): 18 18 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 19 password = CharField(required=True)
20 20
21 21
class RegistrationSerializer(EmailPasswordSerializer): 22 22 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 23 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 24
25 25
class PasswordResetRequestSerializer(EmailSerializer): 26 26 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 27 def validate_email(self, value):
try: 28 28 try:
User.objects.get(email=value) 29 29 User.objects.get(email=value)
return value 30 30 return value
except User.DoesNotExist: 31 31 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 32 raise serializers.ValidationError('No user exists with that email')
33 33
34 34
class PasswordResetSerializer(Serializer): 35 35 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 36 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 37 uid = IntegerField(required=True)
token = CharField(required=True) 38 38 token = CharField(required=True)
39 39
def validate_uid(self, value): 40 40 def validate_uid(self, value):
try: 41 41 try:
User.objects.get(id=value) 42 42 User.objects.get(id=value)
return value 43 43 return value
except User.DoesNotExist: 44 44 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 45 raise serializers.ValidationError('Could not verify reset token')
46 46
47 47
class UserUpdateSerializer(Serializer): 48 48 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 49 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 50 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 51 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 52 # reset_token = CharField(required=False)
53 53
def validate(self, data): 54 54 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 55 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 56 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 57 return data
58 58
59 59
class Password(Serializer): 60 60 class Password(Serializer):
email = EmailField(required=True) 61 61 email = EmailField(required=True)
password = CharField(required=True) 62 62 password = CharField(required=True)
63 63
64 64
class LecturePeriodSerializer(ModelSerializer): 65 65 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 66 class Meta:
model = LecturePeriod 67 67 model = LecturePeriod
exclude = 'id', 'section' 68 68 exclude = 'id', 'section'
69 69
70 70
class SectionSerializer(ModelSerializer): 71 71 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 72 72 lecture_times = CharField()
short_name = CharField() 73 73 short_name = CharField()
long_name = CharField() 74 74 long_name = CharField()
75 75
class Meta: 76 76 class Meta:
model = Section 77 77 model = Section
78 78
79 79
class DeepSectionSerializer(SectionSerializer): 80 80 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 81 81 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
82 82
83 83
class UserSerializer(ModelSerializer): 84 84 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 85 85 email = EmailField(required=False)
sections = SectionSerializer(many=True) 86 86 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 87 87 is_confirmed = BooleanField()
88 88
class Meta: 89 89 class Meta:
model = User 90 90 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 91 91 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
92 92
93 93
class MaskFieldSerializer(serializers.Field): 94 94 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 95 95 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 96 96 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 97 97 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 98 98 'overlap': 'Ensure this field does not have overlapping intervals.'
} 99 99 }
100 100
def to_representation(self, value): 101 101 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 102 102 return dumps(list(self._make_mask(value)))
103 103
def to_internal_value(self, value): 104 104 def to_internal_value(self, value):
return self._make_mask(loads(value)) 105 105 if not isinstance(value, list):
106 value = loads(value)
107 return self._make_mask(value)
106 108
def _make_mask(self, data): 107 109 def _make_mask(self, data):
try: 108 110 try:
mask = FlashcardMask(data) 109 111 mask = FlashcardMask(data)
except ValueError: 110 112 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 111 113 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 112 114 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 113 115 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 114 116 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 115 117 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 116 118 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 117 119 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 118 120 return mask
119 121
120 122
class FlashcardSerializer(ModelSerializer): 121 123 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 122 124 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 123 125 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 124 126 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 125 127 mask = MaskFieldSerializer(allow_null=True)
score = IntegerField(read_only=True) 126 128 score = IntegerField(read_only=True)
127 129
def validate_material_date(self, value): 128 130 def validate_material_date(self, value):
# TODO: make this dynamic 129 131 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 130 132 if QUARTER_START <= value <= QUARTER_END:
return value 131 133 return value
else: 132 134 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 133 135 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
134 136
def validate_pushed(self, value): 135 137 def validate_pushed(self, value):
if value > datetime.now(): 136 138 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 137 139 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 138 140 return value
139 141
def validate_mask(self, value): 140 142 def validate_mask(self, value):
if value is None: 141 143 if value is None:
return None 142 144 return None
if len(self.initial_data['text']) < value.max_offset(): 143 145 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 144 146 raise serializers.ValidationError("Mask out of bounds")
return value 145 147 return value
146 148
class Meta: 147 149 class Meta:
model = Flashcard 148 150 model = Flashcard
exclude = 'author', 'previous' 149 151 exclude = 'author', 'previous'
150 152
151 153
class FlashcardUpdateSerializer(serializers.Serializer): 152 154 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 153 155 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 154 156 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 155 157 mask = MaskFieldSerializer(required=False)
156 158
def validate_material_date(self, date): 157 159 def validate_material_date(self, date):
if date > QUARTER_END: 158 160 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 159 161 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 160 162 return date
161 163
def validate(self, attrs): 162 164 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 163 165 # 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']): 164 166 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 165 167 raise serializers.ValidationError("No new value passed in")
return attrs 166 168 return attrs
167 169
168 170
class QuizRequestSerializer(serializers.Serializer): 169 171 class QuizRequestSerializer(serializers.Serializer):
sections = ListField(child=IntegerField(min_value=1), required=False) 170 172 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 171 173 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 172 174 material_date_end = DateTimeField(default=QUARTER_END)
173 175
def update(self, instance, validated_data): 174 176 def update(self, instance, validated_data):
pass 175 177 pass
176 178
def create(self, validated_data): 177 179 def create(self, validated_data):
return validated_data 178 180 return validated_data
179 181
def validate_material_date_begin(self, value): 180 182 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 181 183 if QUARTER_START <= value <= QUARTER_END:
return value 182 184 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 183 185 raise serializers.ValidationError("Invalid begin date for the flashcard range")
184 186
def validate_material_date_end(self, value): 185 187 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 186 188 if QUARTER_START <= value <= QUARTER_END:
return value 187 189 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 188 190 raise serializers.ValidationError("Invalid end date for the flashcard range")
189 191
def validate_sections(self, value): 190 192 def validate_sections(self, value):
if value is None: 191 193 if value is None:
return Section.objects.all() 192 194 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 193 195 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 194 196 if not section_filter.exists():
raise serializers.ValidationError("Those aren't valid sections") 195 197 raise serializers.ValidationError("Those aren't valid sections")
return value 196 198 return value
197 199
def validate(self, attrs): 198 200 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 199 201 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 200 202 raise serializers.ValidationError("Invalid range")
if 'sections' not in attrs: 201 203 if 'sections' not in attrs:
attrs['sections'] = self.validate_sections(None) 202 204 attrs['sections'] = self.validate_sections(None)
return attrs 203 205 return attrs
204 206
flashcards/views.py View file @ 34586f5
from random import sample 1 1 from random import sample
2 2
import django 3 3 import django
from django.contrib import auth 4 4 from django.contrib import auth
from django.shortcuts import get_object_or_404 5 5 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer 6 6 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.notifications import notify_new_card 8 8 from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 9 9 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 10 10 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 11 11 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer 12 12 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 13 13 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 14 14 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 15 15 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 16 16 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 17 17 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 18 18 from django.core.mail import send_mail
from django.contrib.auth import authenticate 19 19 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 20 20 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 21 21 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 22 22 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 23 23 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 24 24 from simple_email_confirmation import EmailAddress
25 25
26 26
class SectionViewSet(ReadOnlyModelViewSet): 27 27 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 28 28 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 29 29 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 30 30 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 31 31 permission_classes = [IsAuthenticated]
32 32
@detail_route(methods=['GET']) 33 33 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 34 34 def flashcards(self, request, pk):
""" 35 35 """
Gets flashcards for a section, excluding hidden cards. 36 36 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 37 37 Returned in strictly chronological order (material date).
""" 38 38 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() 39 39 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 40 40 return Response(FlashcardSerializer(flashcards, many=True).data)
41 41
@detail_route(methods=['POST']) 42 42 @detail_route(methods=['POST'])
def enroll(self, request, pk): 43 43 def enroll(self, request, pk):
""" 44 44 """
Add the current user to a specified section 45 45 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. 46 46 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 47 47 ---
view_mocker: flashcards.api.mock_no_params 48 48 view_mocker: flashcards.api.mock_no_params
""" 49 49 """
try: 50 50 try:
self.get_object().enroll(request.user) 51 51 self.get_object().enroll(request.user)
except django.core.exceptions.PermissionDenied as e: 52 52 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 53 53 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 54 54 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 55 55 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 56 56 return Response(status=HTTP_204_NO_CONTENT)
57 57
@detail_route(methods=['POST']) 58 58 @detail_route(methods=['POST'])
def drop(self, request, pk): 59 59 def drop(self, request, pk):
""" 60 60 """
Remove the current user from a specified section 61 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 62 62 If the user is not in the class, 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().drop(request.user) 67 67 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 68 68 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 69 69 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 70 70 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 71 71 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 72 72 return Response(status=HTTP_204_NO_CONTENT)
73 73
@list_route(methods=['GET']) 74 74 @list_route(methods=['GET'])
def search(self, request): 75 75 def search(self, request):
""" 76 76 """
Returns a list of sections which match a user's query 77 77 Returns a list of sections which match a user's query
--- 78 78 ---
parameters: 79 79 parameters:
- name: q 80 80 - name: q
description: space-separated list of terms 81 81 description: space-separated list of terms
required: true 82 82 required: true
type: form 83 83 type: form
response_serializer: SectionSerializer 84 84 response_serializer: SectionSerializer
""" 85 85 """
query = request.GET.get('q', None) 86 86 query = request.GET.get('q', None)
if not query: return Response('[]') 87 87 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 88 88 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 89 89 data = SectionSerializer(qs, many=True).data
return Response(data) 90 90 return Response(data)
91 91
@detail_route(methods=['GET']) 92 92 @detail_route(methods=['GET'])
def deck(self, request, pk): 93 93 def deck(self, request, pk):
""" 94 94 """
Gets the contents of a user's deck for a given section. 95 95 Gets the contents of a user's deck for a given section.
""" 96 96 """
qs = request.user.get_deck(self.get_object()) 97 97 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 98 98 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 99 99 return Response(serializer.data)
100 100
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) 101 101 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 102 102 def ordered_deck(self, request, pk):
""" 103 103 """
Get a chronological order by material_date of flashcards for a section. 104 104 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 105 105 This excludes hidden card.
""" 106 106 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 107 107 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 108 108 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 109 109 return Response(serializer.data)
110 110
@detail_route(methods=['GET']) 111 111 @detail_route(methods=['GET'])
def feed(self, request, pk): 112 112 def feed(self, request, pk):
""" 113 113 """
Gets the contents of a user's feed for a section. 114 114 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 115 115 Exclude cards that are already in the user's deck
""" 116 116 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 117 117 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 118 118 return Response(serializer.data)
119 119
120 120
class UserSectionListView(ListAPIView): 121 121 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 122 122 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 123 123 permission_classes = [IsAuthenticated]
124 124
def get_queryset(self): 125 125 def get_queryset(self):
return self.request.user.sections.all() 126 126 return self.request.user.sections.all()
127 127
def paginate_queryset(self, queryset): return None 128 128 def paginate_queryset(self, queryset): return None
129 129
130 130
class UserDetail(GenericAPIView): 131 131 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 132 132 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 133 133 permission_classes = [IsAuthenticated]
134 134
def patch(self, request, format=None): 135 135 def patch(self, request, format=None):
""" 136 136 """
Updates the user's password, or verifies their email address 137 137 Updates the user's password, or verifies their email address
--- 138 138 ---
request_serializer: UserUpdateSerializer 139 139 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 140 140 response_serializer: UserSerializer
""" 141 141 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 142 142 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 143 143 data.is_valid(raise_exception=True)
data = data.validated_data 144 144 data = data.validated_data
145 145
if 'new_password' in data: 146 146 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 147 147 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 148 148 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 149 149 request.user.set_password(data['new_password'])
request.user.save() 150 150 request.user.save()
151 151
if 'confirmation_key' in data: 152 152 if 'confirmation_key' in data:
try: 153 153 try:
request.user.confirm_email(data['confirmation_key']) 154 154 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 155 155 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 156 156 raise ValidationError('confirmation_key is invalid')
157 157
return Response(UserSerializer(request.user).data) 158 158 return Response(UserSerializer(request.user).data)
159 159
def get(self, request, format=None): 160 160 def get(self, request, format=None):
""" 161 161 """
Return data about the user 162 162 Return data about the user
--- 163 163 ---
response_serializer: UserSerializer 164 164 response_serializer: UserSerializer
""" 165 165 """
serializer = UserSerializer(request.user, context={'request': request}) 166 166 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 167 167 return Response(serializer.data)
168 168
def delete(self, request): 169 169 def delete(self, request):
""" 170 170 """
Irrevocably delete the user and their data 171 171 Irrevocably delete the user and their data
172 172
Yes, really 173 173 Yes, really
""" 174 174 """
request.user.delete() 175 175 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 176 176 return Response(status=HTTP_204_NO_CONTENT)
177 177
178 178
@api_view(['POST']) 179 179 @api_view(['POST'])
def register(request, format=None): 180 180 def register(request, format=None):
""" 181 181 """
Register a new user 182 182 Register a new user
--- 183 183 ---
request_serializer: EmailPasswordSerializer 184 184 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 185 185 response_serializer: UserSerializer
""" 186 186 """
data = RegistrationSerializer(data=request.data) 187 187 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 188 188 data.is_valid(raise_exception=True)
189 189
User.objects.create_user(**data.validated_data) 190 190 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 191 191 user = authenticate(**data.validated_data)
auth.login(request, user) 192 192 auth.login(request, user)
193 193
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 194 194 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
195 195
196 196
@api_view(['POST']) 197 197 @api_view(['POST'])
def login(request): 198 198 def login(request):
""" 199 199 """
Authenticates user and returns user data if valid. 200 200 Authenticates user and returns user data if valid.
--- 201 201 ---
request_serializer: EmailPasswordSerializer 202 202 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 203 203 response_serializer: UserSerializer
""" 204 204 """
205 205
data = EmailPasswordSerializer(data=request.data) 206 206 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 207 207 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 208 208 user = authenticate(**data.validated_data)
209 209
if user is None: 210 210 if user is None:
raise AuthenticationFailed('Invalid email or password') 211 211 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 212 212 if not user.is_active:
raise NotAuthenticated('Account is disabled') 213 213 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 214 214 auth.login(request, user)
return Response(UserSerializer(request.user).data) 215 215 return Response(UserSerializer(request.user).data)
216 216
217 217
@api_view(['POST']) 218 218 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 219 219 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 220 220 def logout(request, format=None):
""" 221 221 """
Logs the authenticated user out. 222 222 Logs the authenticated user out.
""" 223 223 """
auth.logout(request) 224 224 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 225 225 return Response(status=HTTP_204_NO_CONTENT)
226 226
227 227
@api_view(['POST']) 228 228 @api_view(['POST'])
def request_password_reset(request, format=None): 229 229 def request_password_reset(request, format=None):
""" 230 230 """
Send a password reset token/link to the provided email. 231 231 Send a password reset token/link to the provided email.
--- 232 232 ---
request_serializer: PasswordResetRequestSerializer 233 233 request_serializer: PasswordResetRequestSerializer
""" 234 234 """
data = PasswordResetRequestSerializer(data=request.data) 235 235 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 236 236 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 237 237 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 238 238 return Response(status=HTTP_204_NO_CONTENT)
239 239
240 240
@api_view(['POST']) 241 241 @api_view(['POST'])
def reset_password(request, format=None): 242 242 def reset_password(request, format=None):
""" 243 243 """
Updates user's password to new password if token is valid. 244 244 Updates user's password to new password if token is valid.
--- 245 245 ---
request_serializer: PasswordResetSerializer 246 246 request_serializer: PasswordResetSerializer
""" 247 247 """
data = PasswordResetSerializer(data=request.data) 248 248 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 249 249 data.is_valid(raise_exception=True)
250 250
user = User.objects.get(id=data['uid'].value) 251 251 user = User.objects.get(id=data['uid'].value)
# Check token validity. 252 252 # Check token validity.
253 253
if default_token_generator.check_token(user, data['token'].value): 254 254 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 255 255 user.set_password(data['new_password'].value)
user.save() 256 256 user.save()
else: 257 257 else:
raise ValidationError('Could not verify reset token') 258 258 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 259 259 return Response(status=HTTP_204_NO_CONTENT)
260 260
261 261
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 262 262 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 263 263 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 264 264 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 265 265 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
266 266
# Override create in CreateModelMixin 267 267 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 268 268 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 269 269 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 270 270 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 271 271 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 272 272 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 273 273 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 274 274 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 275 275 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 276 276 self.perform_create(flashcard)
notify_new_card(flashcard) 277 277 notify_new_card(flashcard)
headers = self.get_success_headers(data) 278 278 headers = self.get_success_headers(data)
request.user.pull(flashcard) 279 279 request.user.pull(flashcard)
response_data = FlashcardSerializer(flashcard).data 280 280 response_data = FlashcardSerializer(flashcard).data
281 281
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 282 282 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
283 283
@detail_route(methods=['POST']) 284 284 @detail_route(methods=['POST'])
def unhide(self, request, pk): 285 285 def unhide(self, request, pk):
""" 286 286 """
Unhide the given card 287 287 Unhide the given card
--- 288 288 ---
view_mocker: flashcards.api.mock_no_params 289 289 view_mocker: flashcards.api.mock_no_params
""" 290 290 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 291 291 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 292 292 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 293 293 return Response(status=HTTP_204_NO_CONTENT)
294 294
@detail_route(methods=['POST']) 295 295 @detail_route(methods=['POST'])
def report(self, request, pk): 296 296 def report(self, request, pk):
""" 297 297 """
Hide the given card 298 298 Hide the given card
--- 299 299 ---
view_mocker: flashcards.api.mock_no_params 300 300 view_mocker: flashcards.api.mock_no_params
""" 301 301 """
self.get_object().report(request.user) 302 302 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 303 303 return Response(status=HTTP_204_NO_CONTENT)
304 304
hide = report 305 305 hide = report
306 306
@detail_route(methods=['POST']) 307 307 @detail_route(methods=['POST'])
def pull(self, request, pk): 308 308 def pull(self, request, pk):
""" 309 309 """
Pull a card from the live feed into the user's deck. 310 310 Pull a card from the live feed into the user's deck.
--- 311 311 ---
view_mocker: flashcards.api.mock_no_params 312 312 view_mocker: flashcards.api.mock_no_params
""" 313 313 """
314 314
request.user.pull(self.get_object()) 315 315 request.user.pull(self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 316 316 return Response(status=HTTP_204_NO_CONTENT)
317 317
318 318
@detail_route(methods=['POST']) 319 319 @detail_route(methods=['POST'])
def unpull(self, request, pk): 320 320 def unpull(self, request, pk):
""" 321 321 """
Unpull a card from the user's deck 322 322 Unpull a card from the user's deck
--- 323 323 ---
view_mocker: flashcards.api.mock_no_params 324 324 view_mocker: flashcards.api.mock_no_params
""" 325 325 """
user = request.user 326 326 user = request.user
flashcard = self.get_object() 327 327 flashcard = self.get_object()
user.unpull(flashcard) 328 328 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 329 329 return Response(status=HTTP_204_NO_CONTENT)
330 330
def partial_update(self, request, *args, **kwargs): 331 331 def partial_update(self, request, *args, **kwargs):
""" 332 332 """
Edit settings related to a card for the user. 333 333 Edit settings related to a card for the user.
--- 334 334 ---
request_serializer: FlashcardUpdateSerializer 335 335 request_serializer: FlashcardUpdateSerializer
""" 336 336 """
user = request.user 337 337 user = request.user
flashcard = self.get_object() 338 338 flashcard = self.get_object()
data = FlashcardUpdateSerializer(data=request.data) 339 339 data = FlashcardUpdateSerializer(data=request.data)