Commit 33f8a47a8b87874c0ce2e7080fdb43079d2e147b

Authored by Andrew Buss
1 parent f0ac627bcd
Exists in master

enforce 24 hour grace period on email verification

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

flashcards/api.py View file @ 33f8a47
1 from django.utils.timezone import now
from flashcards.models import Flashcard, UserFlashcardQuiz 1 2 from flashcards.models import Flashcard, UserFlashcardQuiz
3 from rest_framework.exceptions import PermissionDenied
from rest_framework.pagination import PageNumberPagination 2 4 from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission 3 5 from rest_framework.permissions import BasePermission
4 6
7 mock_no_params = lambda x: None
5 8
mock_no_params = lambda x:None 6
7 9
class StandardResultsSetPagination(PageNumberPagination): 8 10 class StandardResultsSetPagination(PageNumberPagination):
page_size = 40 9 11 page_size = 40
page_size_query_param = 'page_size' 10 12 page_size_query_param = 'page_size'
max_page_size = 1000 11 13 max_page_size = 1000
12 14
13 15
class UserDetailPermissions(BasePermission): 14 16 class UserDetailPermissions(BasePermission):
""" 15 17 """
Permissions for the user detail view. Anonymous users may only POST. 16 18 Permissions for the user detail view. Anonymous users may only POST.
""" 17 19 """
18 20
def has_object_permission(self, request, view, obj): 19 21 def has_object_permission(self, request, view, obj):
if request.method == 'POST': 20 22 if request.method == 'POST':
return True 21 23 return True
return request.user.is_authenticated() 22 24 return request.user.is_authenticated()
23 25
24 26
class IsEnrolledInAssociatedSection(BasePermission): 25 27 class IsEnrolledInAssociatedSection(BasePermission):
def has_object_permission(self, request, view, obj): 26 28 def has_object_permission(self, request, view, obj):
if obj is None: 27 29 if obj is None:
return True 28 30 return True
assert type(obj) is Flashcard 29 31 assert type(obj) is Flashcard
return request.user.is_in_section(obj.section) 30 32 return request.user.is_in_section(obj.section)
31 33
32 34
class IsFlashcardReviewer(BasePermission): 33 35 class IsFlashcardReviewer(BasePermission):
def has_object_permission(self, request, view, obj): 34 36 def has_object_permission(self, request, view, obj):
if obj is None: 35 37 if obj is None:
return True 36 38 return True
assert type(obj) is UserFlashcardQuiz 37 39 assert type(obj) is UserFlashcardQuiz
return request.user == obj.user_flashcard.user 38 40 return request.user == obj.user_flashcard.user
41
42
43 class IsAuthenticatedAndConfirmed(BasePermission):
flashcards/views.py View file @ 33f8a47
import django 1 1 import django
from django.contrib import auth 2 2 from django.contrib import auth
from django.db import IntegrityError 3 3 from django.db import IntegrityError
from django.shortcuts import get_object_or_404 4 4 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer 5 5 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \
6 IsAuthenticatedAndConfirmed
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz 6 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.notifications import notify_new_card 7 8 from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 8 9 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 9 10 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 10 11 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer 11 12 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 12 13 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 13 14 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 14 15 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 15 16 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 16 17 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 17 18 from django.core.mail import send_mail
from django.contrib.auth import authenticate 18 19 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 19 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 20 21 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 21 22 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 22 23 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 23 24 from simple_email_confirmation import EmailAddress
24 25
25 26
class SectionViewSet(ReadOnlyModelViewSet): 26 27 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 27 28 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 28 29 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 29 30 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 30 31 permission_classes = [IsAuthenticatedAndConfirmed]
31 32
@detail_route(methods=['GET']) 32 33 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 33 34 def flashcards(self, request, pk):
""" 34 35 """
Gets flashcards for a section, excluding hidden cards. 35 36 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 36 37 Returned in strictly chronological order (material date).
""" 37 38 """
flashcards = Flashcard.cards_visible_to(request.user) 38 39 flashcards = Flashcard.cards_visible_to(request.user)
if 'hidden' in request.GET: 39 40 if 'hidden' in request.GET:
if request.GET['hidden'] == 'only': 40 41 if request.GET['hidden'] == 'only':
flashcards = Flashcard.cards_hidden_by(request.user) 41 42 flashcards = Flashcard.cards_hidden_by(request.user)
else: 42 43 else:
flashcards |= Flashcard.cards_hidden_by(request.user) 43 44 flashcards |= Flashcard.cards_hidden_by(request.user)
flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all() 44 45 flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all()
return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data) 45 46 return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data)
46 47
@detail_route(methods=['POST']) 47 48 @detail_route(methods=['POST'])
def enroll(self, request, pk): 48 49 def enroll(self, request, pk):
""" 49 50 """
Add the current user to a specified section 50 51 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. 51 52 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 52 53 ---
view_mocker: flashcards.api.mock_no_params 53 54 view_mocker: flashcards.api.mock_no_params
""" 54 55 """
try: 55 56 try:
self.get_object().enroll(request.user) 56 57 self.get_object().enroll(request.user)
except django.core.exceptions.PermissionDenied as e: 57 58 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 58 59 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 59 60 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 60 61 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 61 62 return Response(status=HTTP_204_NO_CONTENT)
62 63
@detail_route(methods=['POST']) 63 64 @detail_route(methods=['POST'])
def drop(self, request, pk): 64 65 def drop(self, request, pk):
""" 65 66 """
Remove the current user from a specified section 66 67 Remove the current user from a specified section
If the user is not in the class, the request will fail. 67 68 If the user is not in the class, the request will fail.
--- 68 69 ---
view_mocker: flashcards.api.mock_no_params 69 70 view_mocker: flashcards.api.mock_no_params
""" 70 71 """
try: 71 72 try:
self.get_object().drop(request.user) 72 73 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 73 74 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 74 75 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 75 76 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 76 77 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 77 78 return Response(status=HTTP_204_NO_CONTENT)
78 79
@list_route(methods=['GET']) 79 80 @list_route(methods=['GET'])
def search(self, request): 80 81 def search(self, request):
""" 81 82 """
Returns a list of sections which match a user's query 82 83 Returns a list of sections which match a user's query
--- 83 84 ---
parameters: 84 85 parameters:
- name: q 85 86 - name: q
description: space-separated list of terms 86 87 description: space-separated list of terms
required: true 87 88 required: true
type: form 88 89 type: form
response_serializer: SectionSerializer 89 90 response_serializer: SectionSerializer
""" 90 91 """
query = request.GET.get('q', None) 91 92 query = request.GET.get('q', None)
if not query: return Response('[]') 92 93 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 93 94 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 94 95 data = SectionSerializer(qs, many=True).data
return Response(data) 95 96 return Response(data)
96 97
@detail_route(methods=['GET']) 97 98 @detail_route(methods=['GET'])
def deck(self, request, pk): 98 99 def deck(self, request, pk):
""" 99 100 """
Gets the contents of a user's deck for a given section. 100 101 Gets the contents of a user's deck for a given section.
""" 101 102 """
qs = request.user.get_deck(self.get_object()) 102 103 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 103 104 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 104 105 return Response(serializer.data)
105 106
@detail_route(methods=['GET']) 106 107 @detail_route(methods=['GET'])
def feed(self, request, pk): 107 108 def feed(self, request, pk):
""" 108 109 """
Gets the contents of a user's feed for a section. 109 110 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 110 111 Exclude cards that are already in the user's deck
""" 111 112 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True, 112 113 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True,
context={'user': request.user}) 113 114 context={'user': request.user})
return Response(serializer.data) 114 115 return Response(serializer.data)
115 116
116 117
class UserSectionListView(ListAPIView): 117 118 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 118 119 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 119 120 permission_classes = [IsAuthenticatedAndConfirmed]
120 121
def get_queryset(self): 121 122 def get_queryset(self):
return self.request.user.sections.all() 122 123 return self.request.user.sections.all()
123 124
def paginate_queryset(self, queryset): return None 124 125 def paginate_queryset(self, queryset): return None
125 126
126 127
class UserDetail(GenericAPIView): 127 128 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 128 129 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 129 130 permission_classes = [IsAuthenticatedAndConfirmed]
130 131
def patch(self, request, format=None): 131 132 def patch(self, request, format=None):
""" 132 133 """
Updates the user's password, or verifies their email address 133 134 Updates the user's password, or verifies their email address
--- 134 135 ---
request_serializer: UserUpdateSerializer 135 136 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 136 137 response_serializer: UserSerializer
""" 137 138 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 138 139 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 139 140 data.is_valid(raise_exception=True)
data = data.validated_data 140 141 data = data.validated_data
141 142
if 'new_password' in data: 142 143 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 143 144 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 144 145 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 145 146 request.user.set_password(data['new_password'])
request.user.save() 146 147 request.user.save()
147 148
if 'confirmation_key' in data: 148 149 if 'confirmation_key' in data:
try: 149 150 try:
request.user.confirm_email(data['confirmation_key']) 150 151 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 151 152 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 152 153 raise ValidationError('confirmation_key is invalid')
153 154
return Response(UserSerializer(request.user).data) 154 155 return Response(UserSerializer(request.user).data)
155 156
def get(self, request, format=None): 156 157 def get(self, request, format=None):
""" 157 158 """
Return data about the user 158 159 Return data about the user
--- 159 160 ---
response_serializer: UserSerializer 160 161 response_serializer: UserSerializer
""" 161 162 """
serializer = UserSerializer(request.user, context={'request': request}) 162 163 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 163 164 return Response(serializer.data)
164 165
def delete(self, request): 165 166 def delete(self, request):
""" 166 167 """
Irrevocably delete the user and their data 167 168 Irrevocably delete the user and their data
168 169
Yes, really 169 170 Yes, really
""" 170 171 """
request.user.delete() 171 172 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 172 173 return Response(status=HTTP_204_NO_CONTENT)
173 174
174 175
@api_view(['POST']) 175 176 @api_view(['POST'])
def register(request, format=None): 176 177 def register(request, format=None):
""" 177 178 """
Register a new user 178 179 Register a new user
--- 179 180 ---
request_serializer: EmailPasswordSerializer 180 181 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 181 182 response_serializer: UserSerializer
""" 182 183 """
data = RegistrationSerializer(data=request.data) 183 184 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 184 185 data.is_valid(raise_exception=True)
185 186
User.objects.create_user(**data.validated_data) 186 187 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 187 188 user = authenticate(**data.validated_data)
auth.login(request, user) 188 189 auth.login(request, user)
189 190
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 190 191 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
191 192
192 193
@api_view(['POST']) 193 194 @api_view(['POST'])
def login(request): 194 195 def login(request):
""" 195 196 """
Authenticates user and returns user data if valid. 196 197 Authenticates user and returns user data if valid.
--- 197 198 ---
request_serializer: EmailPasswordSerializer 198 199 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 199 200 response_serializer: UserSerializer
""" 200 201 """
201 202
data = EmailPasswordSerializer(data=request.data) 202 203 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 203 204 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 204 205 user = authenticate(**data.validated_data)
205 206
if user is None: 206 207 if user is None:
raise AuthenticationFailed('Invalid email or password') 207 208 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 208 209 if not user.is_active:
raise NotAuthenticated('Account is disabled') 209 210 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 210 211 auth.login(request, user)
return Response(UserSerializer(request.user).data) 211 212 return Response(UserSerializer(request.user).data)
212 213
213 214
@api_view(['POST']) 214 215 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 215 216 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 216 217 def logout(request, format=None):
""" 217 218 """
Logs the authenticated user out. 218 219 Logs the authenticated user out.
""" 219 220 """
auth.logout(request) 220 221 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 221 222 return Response(status=HTTP_204_NO_CONTENT)
222 223
223 224
@api_view(['POST']) 224 225 @api_view(['POST'])
def request_password_reset(request, format=None): 225 226 def request_password_reset(request, format=None):
""" 226 227 """
Send a password reset token/link to the provided email. 227 228 Send a password reset token/link to the provided email.
--- 228 229 ---
request_serializer: PasswordResetRequestSerializer 229 230 request_serializer: PasswordResetRequestSerializer
""" 230 231 """
data = PasswordResetRequestSerializer(data=request.data) 231 232 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 232 233 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 233 234 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 234 235 return Response(status=HTTP_204_NO_CONTENT)
235 236
236 237
@api_view(['POST']) 237 238 @api_view(['POST'])
def reset_password(request, format=None): 238 239 def reset_password(request, format=None):
""" 239 240 """
Updates user's password to new password if token is valid. 240 241 Updates user's password to new password if token is valid.
--- 241 242 ---
request_serializer: PasswordResetSerializer 242 243 request_serializer: PasswordResetSerializer
""" 243 244 """
data = PasswordResetSerializer(data=request.data) 244 245 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 246 data.is_valid(raise_exception=True)
246 247
user = User.objects.get(id=data['uid'].value) 247 248 user = User.objects.get(id=data['uid'].value)
# Check token validity. 248 249 # Check token validity.
249 250
if default_token_generator.check_token(user, data['token'].value): 250 251 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 251 252 user.set_password(data['new_password'].value)
user.save() 252 253 user.save()
else: 253 254 else:
raise ValidationError('Could not verify reset token') 254 255 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 255 256 return Response(status=HTTP_204_NO_CONTENT)
256 257
257 258
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 258 259 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 259 260 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 260 261 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 261 262 permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection]
# Override create in CreateModelMixin 262 263 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 263 264 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 264 265 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 265 266 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 266 267 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 267 268 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 268 269 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 269 270 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 270 271 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 271 272 self.perform_create(flashcard)
notify_new_card(flashcard) 272 273 notify_new_card(flashcard)
headers = self.get_success_headers(data) 273 274 headers = self.get_success_headers(data)
request.user.pull(flashcard) 274 275 request.user.pull(flashcard)
response_data = FlashcardSerializer(flashcard).data 275 276 response_data = FlashcardSerializer(flashcard).data
276 277
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 277 278 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
278 279
@detail_route(methods=['POST']) 279 280 @detail_route(methods=['POST'])
def unhide(self, request, pk): 280 281 def unhide(self, request, pk):
""" 281 282 """
Unhide the given card 282 283 Unhide the given card
--- 283 284 ---
view_mocker: flashcards.api.mock_no_params 284 285 view_mocker: flashcards.api.mock_no_params
""" 285 286 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 286 287 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 287 288 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 288 289 return Response(status=HTTP_204_NO_CONTENT)
289 290
@detail_route(methods=['POST']) 290 291 @detail_route(methods=['POST'])
def report(self, request, pk): 291 292 def report(self, request, pk):
""" 292 293 """
Hide the given card 293 294 Hide the given card
--- 294 295 ---
view_mocker: flashcards.api.mock_no_params 295 296 view_mocker: flashcards.api.mock_no_params
""" 296 297 """
self.get_object().report(request.user) 297 298 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 298 299 return Response(status=HTTP_204_NO_CONTENT)
299 300
hide = report 300 301 hide = report
301 302
@detail_route(methods=['POST']) 302 303 @detail_route(methods=['POST'])
def pull(self, request, pk): 303 304 def pull(self, request, pk):
""" 304 305 """
Pull a card from the live feed into the user's deck. 305 306 Pull a card from the live feed into the user's deck.
--- 306 307 ---
view_mocker: flashcards.api.mock_no_params 307 308 view_mocker: flashcards.api.mock_no_params
""" 308 309 """
try: 309 310 try:
request.user.pull(self.get_object()) 310 311 request.user.pull(self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 311 312 return Response(status=HTTP_204_NO_CONTENT)
except IntegrityError, e: 312 313 except IntegrityError, e:
raise ValidationError('Cannot pull a card already in deck') 313 314 raise ValidationError('Cannot pull a card already in deck')
314 315
@detail_route(methods=['POST']) 315 316 @detail_route(methods=['POST'])
def unpull(self, request, pk): 316 317 def unpull(self, request, pk):
""" 317 318 """
Unpull a card from the user's deck 318 319 Unpull a card from the user's deck
--- 319 320 ---
view_mocker: flashcards.api.mock_no_params 320 321 view_mocker: flashcards.api.mock_no_params
""" 321 322 """
user = request.user 322 323 user = request.user
flashcard = self.get_object() 323 324 flashcard = self.get_object()
user.unpull(flashcard) 324 325 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 325 326 return Response(status=HTTP_204_NO_CONTENT)
326 327
def partial_update(self, request, *args, **kwargs): 327 328 def partial_update(self, request, *args, **kwargs):
""" 328 329 """
Edit settings related to a card for the user. 329 330 Edit settings related to a card for the user.
--- 330 331 ---
request_serializer: FlashcardUpdateSerializer 331 332 request_serializer: FlashcardUpdateSerializer
""" 332 333 """
user = request.user 333 334 user = request.user
flashcard = self.get_object() 334 335 flashcard = self.get_object()
data = FlashcardUpdateSerializer(data=request.data) 335 336 data = FlashcardUpdateSerializer(data=request.data)
data.is_valid(raise_exception=True) 336 337 data.is_valid(raise_exception=True)
new_flashcard = data.validated_data 337 338 new_flashcard = data.validated_data
new_flashcard = flashcard.edit(user, new_flashcard) 338 339 new_flashcard = flashcard.edit(user, new_flashcard)
return Response(FlashcardSerializer(new_flashcard, context={'user': request.user}).data, status=HTTP_200_OK) 339 340 return Response(FlashcardSerializer(new_flashcard, context={'user': request.user}).data, status=HTTP_200_OK)
340 341
341 342
class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): 342 343 class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin):
permission_classes = [IsAuthenticated, IsFlashcardReviewer] 343 344 permission_classes = [IsAuthenticatedAndConfirmed, IsFlashcardReviewer]
queryset = UserFlashcardQuiz.objects.all() 344 345 queryset = UserFlashcardQuiz.objects.all()
345 346
def get_serializer_class(self): 346 347 def get_serializer_class(self):
if self.request.method == 'POST': 347 348 if self.request.method == 'POST':
return QuizRequestSerializer 348 349 return QuizRequestSerializer
return QuizAnswerRequestSerializer 349 350 return QuizAnswerRequestSerializer
350 351
def create(self, request, *args, **kwargs): 351 352 def create(self, request, *args, **kwargs):
""" 352 353 """
Return a card based on the request params. 353 354 Return a card based on the request params.
:param request: A request object. 354 355 :param request: A request object.