Commit 70313758819072ed0109acebed0332896c91dd19

Authored by Andrew Buss
1 parent 1d468f661b
Exists in master

how to improve coverage: remove code which isn't covered by tests!

Showing 1 changed file with 0 additions and 5 deletions Inline Diff

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