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