Commit 22a482a65c6eead39850f9a513b3ce4fa9a1c616

Authored by Andrew Buss
1 parent a222348da7
Exists in master

pull a card that a user pushes

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

flashcards/views.py View file @ 22a482a
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 """
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)
279 request.user.pull(flashcard)
response_data = FlashcardSerializer(flashcard).data 279 280 response_data = FlashcardSerializer(flashcard).data
280 281
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 281 282 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
282 283
@detail_route(methods=['POST']) 283 284 @detail_route(methods=['POST'])
def unhide(self, request, pk): 284 285 def unhide(self, request, pk):
""" 285 286 """
Unhide the given card 286 287 Unhide the given card
--- 287 288 ---
view_mocker: flashcards.api.mock_no_params 288 289 view_mocker: flashcards.api.mock_no_params
""" 289 290 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 290 291 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 291 292 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 292 293 return Response(status=HTTP_204_NO_CONTENT)
293 294
@detail_route(methods=['POST']) 294 295 @detail_route(methods=['POST'])
def report(self, request, pk): 295 296 def report(self, request, pk):
""" 296 297 """
Hide the given card 297 298 Hide the given card
--- 298 299 ---
view_mocker: flashcards.api.mock_no_params 299 300 view_mocker: flashcards.api.mock_no_params
""" 300 301 """
self.get_object().report(request.user) 301 302 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 302 303 return Response(status=HTTP_204_NO_CONTENT)
303 304
hide = report 304 305 hide = report
305 306
@detail_route(methods=['POST']) 306 307 @detail_route(methods=['POST'])
def pull(self, request, pk): 307 308 def pull(self, request, pk):
""" 308 309 """
Pull a card from the live feed into the user's deck. 309 310 Pull a card from the live feed into the user's deck.
--- 310 311 ---
view_mocker: flashcards.api.mock_no_params 311 312 view_mocker: flashcards.api.mock_no_params
""" 312 313 """
313 314
request.user.pull(self.get_object()) 314 315 request.user.pull(self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 315 316 return Response(status=HTTP_204_NO_CONTENT)
316 317
317 318
@detail_route(methods=['POST']) 318 319 @detail_route(methods=['POST'])
def unpull(self, request, pk): 319 320 def unpull(self, request, pk):
""" 320 321 """
Unpull a card from the user's deck 321 322 Unpull a card from the user's deck