Commit 6b5090fca9af204894bdb1f281a4b7960f1b166a

Authored by Andrew Buss
1 parent 7dbe162dc5
Exists in master

things

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

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