Commit b8dcac27e19b4d93ebd510e5d049dd06e906fd35

Authored by Andrew Buss
1 parent d5f70a1583
Exists in master

friendlier error on re-enroll

Showing 1 changed file with 6 additions and 2 deletions Inline Diff

flashcards/views.py View file @ b8dcac2
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, UserFlashcardQuiz 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, 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 """
50 50 try:
self.get_object().enroll(request.user) 51 51 self.get_object().enroll(request.user)
52 except django.core.exceptions.PermissionDenied as e:
53 raise PermissionDenied(e)
54 except django.core.exceptions.ValidationError as e:
55 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 52 56 return Response(status=HTTP_204_NO_CONTENT)
53 57
@detail_route(methods=['POST']) 54 58 @detail_route(methods=['POST'])
def drop(self, request, pk): 55 59 def drop(self, request, pk):
""" 56 60 """
Remove the current user from a specified section 57 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 58 62 If the user is not in the class, the request will fail.
--- 59 63 ---
view_mocker: flashcards.api.mock_no_params 60 64 view_mocker: flashcards.api.mock_no_params
""" 61 65 """
try: 62 66 try:
self.get_object().drop(request.user) 63 67 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 64 68 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 65 69 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 66 70 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 67 71 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 68 72 return Response(status=HTTP_204_NO_CONTENT)
69 73
@list_route(methods=['GET']) 70 74 @list_route(methods=['GET'])
def search(self, request): 71 75 def search(self, request):
""" 72 76 """
Returns a list of sections which match a user's query 73 77 Returns a list of sections which match a user's query
--- 74 78 ---
parameters: 75 79 parameters:
- name: q 76 80 - name: q
description: space-separated list of terms 77 81 description: space-separated list of terms
required: true 78 82 required: true
type: form 79 83 type: form
response_serializer: SectionSerializer 80 84 response_serializer: SectionSerializer
""" 81 85 """
query = request.GET.get('q', None) 82 86 query = request.GET.get('q', None)
if not query: return Response('[]') 83 87 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 84 88 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 85 89 data = SectionSerializer(qs, many=True).data
return Response(data) 86 90 return Response(data)
87 91
@detail_route(methods=['GET']) 88 92 @detail_route(methods=['GET'])
def deck(self, request, pk): 89 93 def deck(self, request, pk):
""" 90 94 """
Gets the contents of a user's deck for a given section. 91 95 Gets the contents of a user's deck for a given section.
""" 92 96 """
qs = request.user.get_deck(self.get_object()) 93 97 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 94 98 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 95 99 return Response(serializer.data)
96 100
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) 97 101 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 98 102 def ordered_deck(self, request, pk):
""" 99 103 """
Get a chronological order by material_date of flashcards for a section. 100 104 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 101 105 This excludes hidden card.
""" 102 106 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 103 107 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 104 108 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 105 109 return Response(serializer.data)
106 110
@detail_route(methods=['GET']) 107 111 @detail_route(methods=['GET'])
def feed(self, request, pk): 108 112 def feed(self, request, pk):
""" 109 113 """
Gets the contents of a user's feed for a section. 110 114 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 111 115 Exclude cards that are already in the user's deck
""" 112 116 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 113 117 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 114 118 return Response(serializer.data)
115 119
116 120
class UserSectionListView(ListAPIView): 117 121 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 118 122 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 119 123 permission_classes = [IsAuthenticated]
120 124
def get_queryset(self): 121 125 def get_queryset(self):
return self.request.user.sections.all() 122 126 return self.request.user.sections.all()
123 127
def paginate_queryset(self, queryset): return None 124 128 def paginate_queryset(self, queryset): return None
125 129
126 130
class UserDetail(GenericAPIView): 127 131 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 128 132 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 129 133 permission_classes = [IsAuthenticated]
130 134
def patch(self, request, format=None): 131 135 def patch(self, request, format=None):
""" 132 136 """
Updates the user's password, or verifies their email address 133 137 Updates the user's password, or verifies their email address
--- 134 138 ---
request_serializer: UserUpdateSerializer 135 139 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 136 140 response_serializer: UserSerializer
""" 137 141 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 138 142 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 139 143 data.is_valid(raise_exception=True)
data = data.validated_data 140 144 data = data.validated_data
141 145
if 'new_password' in data: 142 146 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 143 147 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 144 148 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 145 149 request.user.set_password(data['new_password'])
request.user.save() 146 150 request.user.save()
147 151
if 'confirmation_key' in data: 148 152 if 'confirmation_key' in data:
try: 149 153 try:
request.user.confirm_email(data['confirmation_key']) 150 154 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 151 155 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 152 156 raise ValidationError('confirmation_key is invalid')
153 157
return Response(UserSerializer(request.user).data) 154 158 return Response(UserSerializer(request.user).data)
155 159
def get(self, request, format=None): 156 160 def get(self, request, format=None):
""" 157 161 """
Return data about the user 158 162 Return data about the user
--- 159 163 ---
response_serializer: UserSerializer 160 164 response_serializer: UserSerializer
""" 161 165 """
serializer = UserSerializer(request.user, context={'request': request}) 162 166 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 163 167 return Response(serializer.data)
164 168
def delete(self, request): 165 169 def delete(self, request):
""" 166 170 """
Irrevocably delete the user and their data 167 171 Irrevocably delete the user and their data
168 172
Yes, really 169 173 Yes, really
""" 170 174 """
request.user.delete() 171 175 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 172 176 return Response(status=HTTP_204_NO_CONTENT)
173 177
174 178
@api_view(['POST']) 175 179 @api_view(['POST'])
def register(request, format=None): 176 180 def register(request, format=None):
""" 177 181 """
Register a new user 178 182 Register a new user
--- 179 183 ---
request_serializer: EmailPasswordSerializer 180 184 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 181 185 response_serializer: UserSerializer
""" 182 186 """
data = RegistrationSerializer(data=request.data) 183 187 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 184 188 data.is_valid(raise_exception=True)
185 189
User.objects.create_user(**data.validated_data) 186 190 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 187 191 user = authenticate(**data.validated_data)
auth.login(request, user) 188 192 auth.login(request, user)
189 193
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 190 194 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
191 195
192 196
@api_view(['POST']) 193 197 @api_view(['POST'])
def login(request): 194 198 def login(request):
""" 195 199 """
Authenticates user and returns user data if valid. 196 200 Authenticates user and returns user data if valid.
--- 197 201 ---
request_serializer: EmailPasswordSerializer 198 202 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 199 203 response_serializer: UserSerializer
""" 200 204 """
201 205
data = EmailPasswordSerializer(data=request.data) 202 206 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 203 207 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 204 208 user = authenticate(**data.validated_data)
205 209
if user is None: 206 210 if user is None:
raise AuthenticationFailed('Invalid email or password') 207 211 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 208 212 if not user.is_active:
raise NotAuthenticated('Account is disabled') 209 213 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 210 214 auth.login(request, user)
return Response(UserSerializer(request.user).data) 211 215 return Response(UserSerializer(request.user).data)
212 216
213 217
@api_view(['POST']) 214 218 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 215 219 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 216 220 def logout(request, format=None):
""" 217 221 """
Logs the authenticated user out. 218 222 Logs the authenticated user out.
""" 219 223 """
auth.logout(request) 220 224 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 221 225 return Response(status=HTTP_204_NO_CONTENT)
222 226
223 227
@api_view(['POST']) 224 228 @api_view(['POST'])
def request_password_reset(request, format=None): 225 229 def request_password_reset(request, format=None):
""" 226 230 """
Send a password reset token/link to the provided email. 227 231 Send a password reset token/link to the provided email.
--- 228 232 ---
request_serializer: PasswordResetRequestSerializer 229 233 request_serializer: PasswordResetRequestSerializer
""" 230 234 """
data = PasswordResetRequestSerializer(data=request.data) 231 235 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 232 236 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 233 237 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 234 238 return Response(status=HTTP_204_NO_CONTENT)
235 239
236 240
@api_view(['POST']) 237 241 @api_view(['POST'])
def reset_password(request, format=None): 238 242 def reset_password(request, format=None):
""" 239 243 """
Updates user's password to new password if token is valid. 240 244 Updates user's password to new password if token is valid.
--- 241 245 ---
request_serializer: PasswordResetSerializer 242 246 request_serializer: PasswordResetSerializer
""" 243 247 """
data = PasswordResetSerializer(data=request.data) 244 248 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 249 data.is_valid(raise_exception=True)
246 250
user = User.objects.get(id=data['uid'].value) 247 251 user = User.objects.get(id=data['uid'].value)
# Check token validity. 248 252 # Check token validity.
249 253
if default_token_generator.check_token(user, data['token'].value): 250 254 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 251 255 user.set_password(data['new_password'].value)
user.save() 252 256 user.save()
else: 253 257 else:
raise ValidationError('Could not verify reset token') 254 258 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 255 259 return Response(status=HTTP_204_NO_CONTENT)
256 260
257 261
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 258 262 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 259 263 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 260 264 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 261 265 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
262 266
# Override create in CreateModelMixin 263 267 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 264 268 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 265 269 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 266 270 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 267 271 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 268 272 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 269 273 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 270 274 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 271 275 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 272 276 self.perform_create(flashcard)
notify_new_card(flashcard) 273 277 notify_new_card(flashcard)
headers = self.get_success_headers(data) 274 278 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard).data 275 279 response_data = FlashcardSerializer(flashcard).data
276 280
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 277 281 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
278 282
@detail_route(methods=['POST']) 279 283 @detail_route(methods=['POST'])
def unhide(self, request, pk): 280 284 def unhide(self, request, pk):
""" 281 285 """
Unhide the given card 282 286 Unhide the given card
--- 283 287 ---
view_mocker: flashcards.api.mock_no_params 284 288 view_mocker: flashcards.api.mock_no_params
""" 285 289 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 286 290 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 287 291 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 288 292 return Response(status=HTTP_204_NO_CONTENT)
289 293
@detail_route(methods=['POST']) 290 294 @detail_route(methods=['POST'])
def report(self, request, pk): 291 295 def report(self, request, pk):
""" 292 296 """
Hide the given card 293 297 Hide the given card
--- 294 298 ---
view_mocker: flashcards.api.mock_no_params 295 299 view_mocker: flashcards.api.mock_no_params
""" 296 300 """
self.get_object().report(request.user) 297 301 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 298 302 return Response(status=HTTP_204_NO_CONTENT)
299 303
hide = report 300 304 hide = report
301 305
@detail_route(methods=['POST']) 302 306 @detail_route(methods=['POST'])
def pull(self, request, pk): 303 307 def pull(self, request, pk):
""" 304 308 """
Pull a card from the live feed into the user's deck. 305 309 Pull a card from the live feed into the user's deck.
--- 306 310 ---
view_mocker: flashcards.api.mock_no_params 307 311 view_mocker: flashcards.api.mock_no_params
""" 308 312 """
user = request.user 309 313 user = request.user
flashcard = self.get_object() 310 314 flashcard = self.get_object()
user.pull(flashcard) 311 315 user.pull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 312 316 return Response(status=HTTP_204_NO_CONTENT)
313 317
@detail_route(methods=['POST']) 314 318 @detail_route(methods=['POST'])
def unpull(self, request, pk): 315 319 def unpull(self, request, pk):
""" 316 320 """
Unpull a card from the user's deck 317 321 Unpull a card from the user's deck
--- 318 322 ---
view_mocker: flashcards.api.mock_no_params 319 323 view_mocker: flashcards.api.mock_no_params
""" 320 324 """