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