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