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