Commit a9cb7ae511762a454b7d78550afbd107dc86deb7
1 parent
c95e5d254d
Exists in
master
log serializer data rather than the serializer itself
Showing 2 changed files with 3 additions and 4 deletions Inline Diff
flashcards/api.py
View file @
a9cb7ae
from django.utils.timezone import now | 1 | 1 | from django.utils.timezone import now | |
from flashcards.models import Flashcard, UserFlashcardQuiz | 2 | 2 | from flashcards.models import Flashcard, UserFlashcardQuiz | |
from rest_framework.exceptions import PermissionDenied | 3 | 3 | from rest_framework.exceptions import PermissionDenied | |
from rest_framework.pagination import PageNumberPagination | 4 | 4 | from rest_framework.pagination import PageNumberPagination | |
from rest_framework.permissions import BasePermission | 5 | 5 | from rest_framework.permissions import BasePermission | |
6 | 6 | |||
mock_no_params = lambda x: None | 7 | 7 | mock_no_params = lambda x: None | |
8 | 8 | |||
9 | 9 | |||
class StandardResultsSetPagination(PageNumberPagination): | 10 | 10 | class StandardResultsSetPagination(PageNumberPagination): | |
page_size = 40 | 11 | 11 | page_size = 40 | |
page_size_query_param = 'page_size' | 12 | 12 | page_size_query_param = 'page_size' | |
max_page_size = 1000 | 13 | 13 | max_page_size = 1000 | |
14 | 14 | |||
15 | 15 | |||
class UserDetailPermissions(BasePermission): | 16 | 16 | class UserDetailPermissions(BasePermission): | |
""" | 17 | 17 | """ | |
Permissions for the user detail view. Anonymous users may only POST. | 18 | 18 | Permissions for the user detail view. Anonymous users may only POST. | |
""" | 19 | 19 | """ | |
20 | 20 | |||
def has_object_permission(self, request, view, obj): | 21 | 21 | def has_object_permission(self, request, view, obj): | |
if request.method == 'POST': | 22 | 22 | if request.method == 'POST': | |
return True | 23 | 23 | return True | |
return request.user.is_authenticated() | 24 | 24 | return request.user.is_authenticated() | |
25 | 25 | |||
26 | 26 | |||
class IsEnrolledInAssociatedSection(BasePermission): | 27 | 27 | class IsEnrolledInAssociatedSection(BasePermission): | |
def has_object_permission(self, request, view, obj): | 28 | 28 | def has_object_permission(self, request, view, obj): | |
if obj is None: | 29 | 29 | if obj is None: | |
return True | 30 | 30 | return True | |
assert type(obj) is Flashcard | 31 | 31 | assert type(obj) is Flashcard | |
return request.user.is_in_section(obj.section) | 32 | 32 | return request.user.is_in_section(obj.section) | |
33 | 33 | |||
34 | 34 | |||
class IsFlashcardReviewer(BasePermission): | 35 | 35 | class IsFlashcardReviewer(BasePermission): | |
def has_object_permission(self, request, view, obj): | 36 | 36 | def has_object_permission(self, request, view, obj): | |
if obj is None: | 37 | 37 | if obj is None: | |
return True | 38 | 38 | return True | |
assert type(obj) is UserFlashcardQuiz | 39 | 39 | assert type(obj) is UserFlashcardQuiz | |
return request.user == obj.user_flashcard.user | 40 | 40 | return request.user == obj.user_flashcard.user | |
41 | 41 | |||
42 | 42 | |||
class IsAuthenticatedAndConfirmed(BasePermission): | 43 | 43 | class IsAuthenticatedAndConfirmed(BasePermission): |
flashcards/views.py
View file @
a9cb7ae
import django | 1 | 1 | import django | |
from django.contrib import auth | 2 | 2 | from django.contrib import auth | |
from django.shortcuts import get_object_or_404 | 3 | 3 | from django.shortcuts import get_object_or_404 | |
from django.utils.log import getLogger | 4 | 4 | from django.utils.log import getLogger | |
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \ | 5 | 5 | from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \ | |
IsAuthenticatedAndConfirmed | 6 | 6 | IsAuthenticatedAndConfirmed | |
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz, \ | 7 | 7 | from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz, \ | |
FlashcardAlreadyPulledException, FlashcardNotInDeckException, Now, interval_days | 8 | 8 | FlashcardAlreadyPulledException, FlashcardNotInDeckException, Now, interval_days | |
from flashcards.notifications import notify_new_card | 9 | 9 | from flashcards.notifications import notify_new_card | |
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ | 10 | 10 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ | |
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ | 11 | 11 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ | |
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ | 12 | 12 | FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ | |
QuizAnswerRequestSerializer, DeepSectionSerializer | 13 | 13 | QuizAnswerRequestSerializer, DeepSectionSerializer | |
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route | 14 | 14 | from rest_framework.decorators import detail_route, permission_classes, api_view, list_route | |
from rest_framework.generics import ListAPIView, GenericAPIView | 15 | 15 | from rest_framework.generics import ListAPIView, GenericAPIView | |
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin | 16 | 16 | from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin | |
from rest_framework.permissions import IsAuthenticated | 17 | 17 | from rest_framework.permissions import IsAuthenticated | |
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet | 18 | 18 | from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet | |
from django.core.mail import send_mail | 19 | 19 | from django.core.mail import send_mail | |
from django.contrib.auth import authenticate | 20 | 20 | from django.contrib.auth import authenticate | |
from django.contrib.auth.tokens import default_token_generator | 21 | 21 | from django.contrib.auth.tokens import default_token_generator | |
from django.db.models import Count, Max, F, Value, When, Case, DateTimeField, FloatField | 22 | 22 | from django.db.models import Count, Max, F, Value, When, Case, DateTimeField, FloatField | |
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK | 23 | 23 | from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK | |
from rest_framework.response import Response | 24 | 24 | from rest_framework.response import Response | |
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied | 25 | 25 | from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied | |
from simple_email_confirmation import EmailAddress | 26 | 26 | from simple_email_confirmation import EmailAddress | |
from math import e | 27 | 27 | from math import e | |
from django.utils.timezone import now | 28 | 28 | from django.utils.timezone import now | |
29 | 29 | |||
30 | 30 | |||
def log_event(request, event=''): | 31 | 31 | def log_event(request, event=''): | |
getLogger('flashy.events').info( | 32 | 32 | getLogger('flashy.events').info( | |
'%s %s %s %s' % (request.META['REMOTE_ADDR'], str(request.user), request.path, event)) | 33 | 33 | '%s %s %s %s' % (request.META['REMOTE_ADDR'], str(request.user), request.path, event)) | |
34 | 34 | |||
35 | 35 | |||
class SectionViewSet(ReadOnlyModelViewSet): | 36 | 36 | class SectionViewSet(ReadOnlyModelViewSet): | |
queryset = Section.objects.all() | 37 | 37 | queryset = Section.objects.all() | |
serializer_class = DeepSectionSerializer | 38 | 38 | serializer_class = DeepSectionSerializer | |
pagination_class = StandardResultsSetPagination | 39 | 39 | pagination_class = StandardResultsSetPagination | |
permission_classes = [IsAuthenticatedAndConfirmed] | 40 | 40 | permission_classes = [IsAuthenticatedAndConfirmed] | |
41 | 41 | |||
@detail_route(methods=['GET']) | 42 | 42 | @detail_route(methods=['GET']) | |
def flashcards(self, request, pk): | 43 | 43 | def flashcards(self, request, pk): | |
""" | 44 | 44 | """ | |
Gets flashcards for a section, excluding hidden cards. | 45 | 45 | Gets flashcards for a section, excluding hidden cards. | |
Returned in strictly chronological order (material date). | 46 | 46 | Returned in strictly chronological order (material date). | |
""" | 47 | 47 | """ | |
flashcards = Flashcard.cards_visible_to(request.user) | 48 | 48 | flashcards = Flashcard.cards_visible_to(request.user) | |
if 'hidden' in request.GET: | 49 | 49 | if 'hidden' in request.GET: | |
if request.GET['hidden'] == 'only': | 50 | 50 | if request.GET['hidden'] == 'only': | |
flashcards = Flashcard.cards_hidden_by(request.user) | 51 | 51 | flashcards = Flashcard.cards_hidden_by(request.user) | |
else: | 52 | 52 | else: | |
flashcards |= Flashcard.cards_hidden_by(request.user) | 53 | 53 | flashcards |= Flashcard.cards_hidden_by(request.user) | |
flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all() | 54 | 54 | flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all() | |
log_event(request, str(self.get_object())) | 55 | 55 | log_event(request, str(self.get_object())) | |
return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data) | 56 | 56 | return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data) | |
57 | 57 | |||
@detail_route(methods=['POST']) | 58 | 58 | @detail_route(methods=['POST']) | |
def enroll(self, request, pk): | 59 | 59 | def enroll(self, request, pk): | |
60 | 60 | |||
""" | 61 | 61 | """ | |
Add the current user to a specified section | 62 | 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. | 63 | 63 | If the class has a whitelist, but the user is not on the whitelist, the request will fail. | |
--- | 64 | 64 | --- | |
view_mocker: flashcards.api.mock_no_params | 65 | 65 | view_mocker: flashcards.api.mock_no_params | |
""" | 66 | 66 | """ | |
try: | 67 | 67 | try: | |
self.get_object().enroll(request.user) | 68 | 68 | self.get_object().enroll(request.user) | |
log_event(request, str(self.get_object())) | 69 | 69 | log_event(request, str(self.get_object())) | |
except django.core.exceptions.PermissionDenied as e: | 70 | 70 | except django.core.exceptions.PermissionDenied as e: | |
raise PermissionDenied(e) | 71 | 71 | raise PermissionDenied(e) | |
except django.core.exceptions.ValidationError as e: | 72 | 72 | except django.core.exceptions.ValidationError as e: | |
raise ValidationError(e) | 73 | 73 | raise ValidationError(e) | |
return Response(status=HTTP_204_NO_CONTENT) | 74 | 74 | return Response(status=HTTP_204_NO_CONTENT) | |
75 | 75 | |||
@detail_route(methods=['POST']) | 76 | 76 | @detail_route(methods=['POST']) | |
def drop(self, request, pk): | 77 | 77 | def drop(self, request, pk): | |
""" | 78 | 78 | """ | |
Remove the current user from a specified section | 79 | 79 | Remove the current user from a specified section | |
If the user is not in the class, the request will fail. | 80 | 80 | If the user is not in the class, the request will fail. | |
--- | 81 | 81 | --- | |
view_mocker: flashcards.api.mock_no_params | 82 | 82 | view_mocker: flashcards.api.mock_no_params | |
""" | 83 | 83 | """ | |
try: | 84 | 84 | try: | |
self.get_object().drop(request.user) | 85 | 85 | self.get_object().drop(request.user) | |
log_event(request, str(self.get_object())) | 86 | 86 | log_event(request, str(self.get_object())) | |
except django.core.exceptions.PermissionDenied as e: | 87 | 87 | except django.core.exceptions.PermissionDenied as e: | |
raise PermissionDenied(e) | 88 | 88 | raise PermissionDenied(e) | |
except django.core.exceptions.ValidationError as e: | 89 | 89 | except django.core.exceptions.ValidationError as e: | |
raise ValidationError(e) | 90 | 90 | raise ValidationError(e) | |
return Response(status=HTTP_204_NO_CONTENT) | 91 | 91 | return Response(status=HTTP_204_NO_CONTENT) | |
92 | 92 | |||
@list_route(methods=['GET']) | 93 | 93 | @list_route(methods=['GET']) | |
def search(self, request): | 94 | 94 | def search(self, request): | |
""" | 95 | 95 | """ | |
Returns a list of sections which match a user's query | 96 | 96 | Returns a list of sections which match a user's query | |
--- | 97 | 97 | --- | |
parameters: | 98 | 98 | parameters: | |
- name: q | 99 | 99 | - name: q | |
description: space-separated list of terms | 100 | 100 | description: space-separated list of terms | |
required: true | 101 | 101 | required: true | |
type: form | 102 | 102 | type: form | |
response_serializer: SectionSerializer | 103 | 103 | response_serializer: SectionSerializer | |
""" | 104 | 104 | """ | |
query = request.GET.get('q', None) | 105 | 105 | query = request.GET.get('q', None) | |
if not query: return Response('[]') | 106 | 106 | if not query: return Response('[]') | |
qs = Section.search(query.split(' '))[:20] | 107 | 107 | qs = Section.search(query.split(' '))[:20] | |
data = SectionSerializer(qs, many=True).data | 108 | 108 | data = SectionSerializer(qs, many=True).data | |
log_event(request, query) | 109 | 109 | log_event(request, query) | |
return Response(data) | 110 | 110 | return Response(data) | |
111 | 111 | |||
@detail_route(methods=['GET']) | 112 | 112 | @detail_route(methods=['GET']) | |
def deck(self, request, pk): | 113 | 113 | def deck(self, request, pk): | |
""" | 114 | 114 | """ | |
Gets the contents of a user's deck for a given section. | 115 | 115 | Gets the contents of a user's deck for a given section. | |
""" | 116 | 116 | """ | |
qs = request.user.get_deck(self.get_object()) | 117 | 117 | qs = request.user.get_deck(self.get_object()) | |
serializer = FlashcardSerializer(qs, many=True) | 118 | 118 | serializer = FlashcardSerializer(qs, many=True) | |
log_event(request, str(self.get_object())) | 119 | 119 | log_event(request, str(self.get_object())) | |
return Response(serializer.data) | 120 | 120 | return Response(serializer.data) | |
121 | 121 | |||
@detail_route(methods=['GET']) | 122 | 122 | @detail_route(methods=['GET']) | |
def feed(self, request, pk): | 123 | 123 | def feed(self, request, pk): | |
""" | 124 | 124 | """ | |
Gets the contents of a user's feed for a section. | 125 | 125 | Gets the contents of a user's feed for a section. | |
Exclude cards that are already in the user's deck | 126 | 126 | Exclude cards that are already in the user's deck | |
""" | 127 | 127 | """ | |
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True, | 128 | 128 | serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True, | |
context={'user': request.user}) | 129 | 129 | context={'user': request.user}) | |
log_event(request, str(self.get_object())) | 130 | 130 | log_event(request, str(self.get_object())) | |
return Response(serializer.data) | 131 | 131 | return Response(serializer.data) | |
132 | 132 | |||
133 | 133 | |||
class UserSectionListView(ListAPIView): | 134 | 134 | class UserSectionListView(ListAPIView): | |
serializer_class = DeepSectionSerializer | 135 | 135 | serializer_class = DeepSectionSerializer | |
permission_classes = [IsAuthenticatedAndConfirmed] | 136 | 136 | permission_classes = [IsAuthenticatedAndConfirmed] | |
137 | 137 | |||
def get_queryset(self): | 138 | 138 | def get_queryset(self): | |
return self.request.user.sections.all() | 139 | 139 | return self.request.user.sections.all() | |
140 | 140 | |||
def paginate_queryset(self, queryset): return None | 141 | 141 | def paginate_queryset(self, queryset): return None | |
142 | 142 | |||
143 | 143 | |||
class UserDetail(GenericAPIView): | 144 | 144 | class UserDetail(GenericAPIView): | |
serializer_class = UserSerializer | 145 | 145 | serializer_class = UserSerializer | |
permission_classes = [IsAuthenticatedAndConfirmed] | 146 | 146 | permission_classes = [IsAuthenticatedAndConfirmed] | |
147 | 147 | |||
def patch(self, request, format=None): | 148 | 148 | def patch(self, request, format=None): | |
""" | 149 | 149 | """ | |
Updates the user's password, or verifies their email address | 150 | 150 | Updates the user's password, or verifies their email address | |
--- | 151 | 151 | --- | |
request_serializer: UserUpdateSerializer | 152 | 152 | request_serializer: UserUpdateSerializer | |
response_serializer: UserSerializer | 153 | 153 | response_serializer: UserSerializer | |
""" | 154 | 154 | """ | |
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) | 155 | 155 | data = UserUpdateSerializer(data=request.data, context={'user': request.user}) | |
data.is_valid(raise_exception=True) | 156 | 156 | data.is_valid(raise_exception=True) | |
data = data.validated_data | 157 | 157 | data = data.validated_data | |
158 | 158 | |||
if 'new_password' in data: | 159 | 159 | if 'new_password' in data: | |
if not request.user.check_password(data['old_password']): | 160 | 160 | if not request.user.check_password(data['old_password']): | |
raise ValidationError('old_password is incorrect') | 161 | 161 | raise ValidationError('old_password is incorrect') | |
request.user.set_password(data['new_password']) | 162 | 162 | request.user.set_password(data['new_password']) | |
request.user.save() | 163 | 163 | request.user.save() | |
log_event(request, 'change password') | 164 | 164 | log_event(request, 'change password') | |
165 | 165 | |||
if 'confirmation_key' in data: | 166 | 166 | if 'confirmation_key' in data: | |
try: | 167 | 167 | try: | |
request.user.confirm_email(data['confirmation_key']) | 168 | 168 | request.user.confirm_email(data['confirmation_key']) | |
log_event(request, 'confirm email') | 169 | 169 | log_event(request, 'confirm email') | |
except EmailAddress.DoesNotExist: | 170 | 170 | except EmailAddress.DoesNotExist: | |
raise ValidationError('confirmation_key is invalid') | 171 | 171 | raise ValidationError('confirmation_key is invalid') | |
172 | 172 | |||
return Response(UserSerializer(request.user).data) | 173 | 173 | return Response(UserSerializer(request.user).data) | |
174 | 174 | |||
def get(self, request, format=None): | 175 | 175 | def get(self, request, format=None): | |
""" | 176 | 176 | """ | |
Return data about the user | 177 | 177 | Return data about the user | |
--- | 178 | 178 | --- | |
response_serializer: UserSerializer | 179 | 179 | response_serializer: UserSerializer | |
""" | 180 | 180 | """ | |
serializer = UserSerializer(request.user, context={'request': request}) | 181 | 181 | serializer = UserSerializer(request.user, context={'request': request}) | |
return Response(serializer.data) | 182 | 182 | return Response(serializer.data) | |
183 | 183 | |||
def delete(self, request): | 184 | 184 | def delete(self, request): | |
""" | 185 | 185 | """ | |
Irrevocably delete the user and their data | 186 | 186 | Irrevocably delete the user and their data | |
187 | 187 | |||
Yes, really | 188 | 188 | Yes, really | |
""" | 189 | 189 | """ | |
request.user.delete() | 190 | 190 | request.user.delete() | |
log_event(request) | 191 | 191 | log_event(request) | |
return Response(status=HTTP_204_NO_CONTENT) | 192 | 192 | return Response(status=HTTP_204_NO_CONTENT) | |
193 | 193 | |||
194 | 194 | |||
@api_view(['POST']) | 195 | 195 | @api_view(['POST']) | |
def register(request, format=None): | 196 | 196 | def register(request, format=None): | |
""" | 197 | 197 | """ | |
Register a new user | 198 | 198 | Register a new user | |
--- | 199 | 199 | --- | |
request_serializer: EmailPasswordSerializer | 200 | 200 | request_serializer: EmailPasswordSerializer | |
response_serializer: UserSerializer | 201 | 201 | response_serializer: UserSerializer | |
""" | 202 | 202 | """ | |
data = RegistrationSerializer(data=request.data) | 203 | 203 | data = RegistrationSerializer(data=request.data) | |
data.is_valid(raise_exception=True) | 204 | 204 | data.is_valid(raise_exception=True) | |
205 | 205 | |||
User.objects.create_user(**data.validated_data) | 206 | 206 | User.objects.create_user(**data.validated_data) | |
user = authenticate(**data.validated_data) | 207 | 207 | user = authenticate(**data.validated_data) | |
auth.login(request, user) | 208 | 208 | auth.login(request, user) | |
log_event(request) | 209 | 209 | log_event(request) | |
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) | 210 | 210 | return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) | |
211 | 211 | |||
212 | 212 | |||
@api_view(['POST']) | 213 | 213 | @api_view(['POST']) | |
def login(request): | 214 | 214 | def login(request): | |
""" | 215 | 215 | """ | |
Authenticates user and returns user data if valid. | 216 | 216 | Authenticates user and returns user data if valid. | |
--- | 217 | 217 | --- | |
request_serializer: EmailPasswordSerializer | 218 | 218 | request_serializer: EmailPasswordSerializer | |
response_serializer: UserSerializer | 219 | 219 | response_serializer: UserSerializer | |
""" | 220 | 220 | """ | |
221 | 221 | |||
data = EmailPasswordSerializer(data=request.data) | 222 | 222 | data = EmailPasswordSerializer(data=request.data) | |
data.is_valid(raise_exception=True) | 223 | 223 | data.is_valid(raise_exception=True) | |
user = authenticate(**data.validated_data) | 224 | 224 | user = authenticate(**data.validated_data) | |
225 | 225 | |||
if user is None: | 226 | 226 | if user is None: | |
raise AuthenticationFailed('Invalid email or password') | 227 | 227 | raise AuthenticationFailed('Invalid email or password') | |
if not user.is_active: | 228 | 228 | if not user.is_active: | |
raise NotAuthenticated('Account is disabled') | 229 | 229 | raise NotAuthenticated('Account is disabled') | |
auth.login(request, user) | 230 | 230 | auth.login(request, user) | |
log_event(request) | 231 | 231 | log_event(request) | |
return Response(UserSerializer(request.user).data) | 232 | 232 | return Response(UserSerializer(request.user).data) | |
233 | 233 | |||
234 | 234 | |||
@api_view(['POST']) | 235 | 235 | @api_view(['POST']) | |
@permission_classes((IsAuthenticated,)) | 236 | 236 | @permission_classes((IsAuthenticated,)) | |
def logout(request, format=None): | 237 | 237 | def logout(request, format=None): | |
""" | 238 | 238 | """ | |
Logs the authenticated user out. | 239 | 239 | Logs the authenticated user out. | |
""" | 240 | 240 | """ | |
auth.logout(request) | 241 | 241 | auth.logout(request) | |
log_event(request) | 242 | 242 | log_event(request) | |
return Response(status=HTTP_204_NO_CONTENT) | 243 | 243 | return Response(status=HTTP_204_NO_CONTENT) | |
244 | 244 | |||
245 | 245 | |||
@api_view(['POST']) | 246 | 246 | @api_view(['POST']) | |
def request_password_reset(request, format=None): | 247 | 247 | def request_password_reset(request, format=None): | |
""" | 248 | 248 | """ | |
Send a password reset token/link to the provided email. | 249 | 249 | Send a password reset token/link to the provided email. | |
--- | 250 | 250 | --- | |
request_serializer: PasswordResetRequestSerializer | 251 | 251 | request_serializer: PasswordResetRequestSerializer | |
""" | 252 | 252 | """ | |
data = PasswordResetRequestSerializer(data=request.data) | 253 | 253 | data = PasswordResetRequestSerializer(data=request.data) | |
data.is_valid(raise_exception=True) | 254 | 254 | data.is_valid(raise_exception=True) | |
log_event(request, 'email: ' + str(data['email'])) | 255 | 255 | log_event(request, 'email: ' + str(data['email'])) | |
get_object_or_404(User, email=data['email'].value).request_password_reset() | 256 | 256 | get_object_or_404(User, email=data['email'].value).request_password_reset() | |
return Response(status=HTTP_204_NO_CONTENT) | 257 | 257 | return Response(status=HTTP_204_NO_CONTENT) | |
258 | 258 | |||
259 | 259 | |||
@api_view(['POST']) | 260 | 260 | @api_view(['POST']) | |
def reset_password(request, format=None): | 261 | 261 | def reset_password(request, format=None): | |
""" | 262 | 262 | """ | |
Updates user's password to new password if token is valid. | 263 | 263 | Updates user's password to new password if token is valid. | |
--- | 264 | 264 | --- | |
request_serializer: PasswordResetSerializer | 265 | 265 | request_serializer: PasswordResetSerializer | |
""" | 266 | 266 | """ | |
data = PasswordResetSerializer(data=request.data) | 267 | 267 | data = PasswordResetSerializer(data=request.data) | |
data.is_valid(raise_exception=True) | 268 | 268 | data.is_valid(raise_exception=True) | |
269 | 269 | |||
user = User.objects.get(id=data['uid'].value) | 270 | 270 | user = User.objects.get(id=data['uid'].value) | |
# Check token validity. | 271 | 271 | # Check token validity. | |
272 | 272 | |||
if default_token_generator.check_token(user, data['token'].value): | 273 | 273 | if default_token_generator.check_token(user, data['token'].value): | |
user.set_password(data['new_password'].value) | 274 | 274 | user.set_password(data['new_password'].value) | |
user.save() | 275 | 275 | user.save() | |
log_event(request) | 276 | 276 | log_event(request) | |
else: | 277 | 277 | else: | |
raise ValidationError('Could not verify reset token') | 278 | 278 | raise ValidationError('Could not verify reset token') | |
return Response(status=HTTP_204_NO_CONTENT) | 279 | 279 | return Response(status=HTTP_204_NO_CONTENT) | |
280 | 280 | |||
281 | 281 | |||
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): | 282 | 282 | class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): | |
queryset = Flashcard.objects.all() | 283 | 283 | queryset = Flashcard.objects.all() | |
serializer_class = FlashcardSerializer | 284 | 284 | serializer_class = FlashcardSerializer | |
permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection] | 285 | 285 | permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection] | |
286 | 286 | |||
# Override create in CreateModelMixin | 287 | 287 | # Override create in CreateModelMixin | |
def create(self, request, *args, **kwargs): | 288 | 288 | def create(self, request, *args, **kwargs): | |
serializer = FlashcardSerializer(data=request.data) | 289 | 289 | serializer = FlashcardSerializer(data=request.data) | |
serializer.is_valid(raise_exception=True) | 290 | 290 | serializer.is_valid(raise_exception=True) | |
data = serializer.validated_data | 291 | 291 | data = serializer.validated_data | |
if not request.user.is_in_section(data['section']): | 292 | 292 | if not request.user.is_in_section(data['section']): | |
raise PermissionDenied('The user is not enrolled in that section') | 293 | 293 | raise PermissionDenied('The user is not enrolled in that section') | |
data['author'] = request.user | 294 | 294 | data['author'] = request.user | |
flashcard = Flashcard.objects.create(**data) | 295 | 295 | flashcard = Flashcard.objects.create(**data) | |
self.perform_create(flashcard) | 296 | 296 | self.perform_create(flashcard) | |
notify_new_card(flashcard) | 297 | 297 | notify_new_card(flashcard) | |
headers = self.get_success_headers(data) | 298 | 298 | headers = self.get_success_headers(data) | |
request.user.pull(flashcard) | 299 | 299 | request.user.pull(flashcard) | |
response_data = FlashcardSerializer(flashcard).data | 300 | 300 | response_data = FlashcardSerializer(flashcard).data | |
log_event(request, response_data) | 301 | 301 | log_event(request, response_data) | |
return Response(response_data, status=HTTP_201_CREATED, headers=headers) | 302 | 302 | return Response(response_data, status=HTTP_201_CREATED, headers=headers) | |
303 | 303 | |||
@detail_route(methods=['POST']) | 304 | 304 | @detail_route(methods=['POST']) | |
def unhide(self, request, pk): | 305 | 305 | def unhide(self, request, pk): | |
""" | 306 | 306 | """ | |
Unhide the given card | 307 | 307 | Unhide the given card | |
--- | 308 | 308 | --- | |
view_mocker: flashcards.api.mock_no_params | 309 | 309 | view_mocker: flashcards.api.mock_no_params | |
""" | 310 | 310 | """ | |
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) | 311 | 311 | hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) | |
hide.delete() | 312 | 312 | hide.delete() | |
log_event(request, str(self.get_object())) | 313 | 313 | log_event(request, str(self.get_object())) | |
return Response(status=HTTP_204_NO_CONTENT) | 314 | 314 | return Response(status=HTTP_204_NO_CONTENT) | |
315 | 315 | |||
@detail_route(methods=['POST']) | 316 | 316 | @detail_route(methods=['POST']) | |
def report(self, request, pk): | 317 | 317 | def report(self, request, pk): | |
""" | 318 | 318 | """ | |
Hide the given card | 319 | 319 | Hide the given card | |
--- | 320 | 320 | --- | |
view_mocker: flashcards.api.mock_no_params | 321 | 321 | view_mocker: flashcards.api.mock_no_params | |
""" | 322 | 322 | """ | |
self.get_object().report(request.user) | 323 | 323 | self.get_object().report(request.user) | |
log_event(request, str(self.get_object())) | 324 | 324 | log_event(request, str(self.get_object())) | |
return Response(status=HTTP_204_NO_CONTENT) | 325 | 325 | return Response(status=HTTP_204_NO_CONTENT) | |
326 | 326 | |||
hide = report | 327 | 327 | hide = report | |
328 | 328 | |||
@detail_route(methods=['POST']) | 329 | 329 | @detail_route(methods=['POST']) | |
def pull(self, request, pk): | 330 | 330 | def pull(self, request, pk): | |
""" | 331 | 331 | """ | |
Pull a card from the live feed into the user's deck. | 332 | 332 | Pull a card from the live feed into the user's deck. | |
--- | 333 | 333 | --- | |
view_mocker: flashcards.api.mock_no_params | 334 | 334 | view_mocker: flashcards.api.mock_no_params | |
""" | 335 | 335 | """ | |
try: | 336 | 336 | try: | |
request.user.pull(self.get_object()) | 337 | 337 | request.user.pull(self.get_object()) | |
log_event(request, str(self.get_object())) | 338 | 338 | log_event(request, str(self.get_object())) | |
return Response(status=HTTP_204_NO_CONTENT) | 339 | 339 | return Response(status=HTTP_204_NO_CONTENT) | |
except FlashcardAlreadyPulledException: | 340 | 340 | except FlashcardAlreadyPulledException: | |
raise ValidationError('Cannot pull a card already in deck') | 341 | 341 | raise ValidationError('Cannot pull a card already in deck') | |
342 | 342 | |||
@detail_route(methods=['POST']) | 343 | 343 | @detail_route(methods=['POST']) | |
def unpull(self, request, pk): | 344 | 344 | def unpull(self, request, pk): | |
""" | 345 | 345 | """ | |
Unpull a card from the user's deck | 346 | 346 | Unpull a card from the user's deck | |
--- | 347 | 347 | --- | |
view_mocker: flashcards.api.mock_no_params | 348 | 348 | view_mocker: flashcards.api.mock_no_params | |
""" | 349 | 349 | """ | |
user = request.user | 350 | 350 | user = request.user | |
flashcard = self.get_object() | 351 | 351 | flashcard = self.get_object() | |
try: | 352 | 352 | try: | |
user.unpull(flashcard) | 353 | 353 | user.unpull(flashcard) | |
log_event(request, str(self.get_object())) | 354 | 354 | log_event(request, str(self.get_object())) | |
return Response(status=HTTP_204_NO_CONTENT) | 355 | 355 | return Response(status=HTTP_204_NO_CONTENT) | |
except FlashcardNotInDeckException: | 356 | 356 | except FlashcardNotInDeckException: | |
raise ValidationError('Cannot unpull a card not in deck') | 357 | 357 | raise ValidationError('Cannot unpull a card not in deck') | |
358 | 358 | |||
359 | 359 | |||
def partial_update(self, request, *args, **kwargs): | 360 | 360 | def partial_update(self, request, *args, **kwargs): | |
""" | 361 | 361 | """ | |
Edit settings related to a card for the user. | 362 | 362 | Edit settings related to a card for the user. | |
--- | 363 | 363 | --- | |
request_serializer: FlashcardUpdateSerializer | 364 | 364 | request_serializer: FlashcardUpdateSerializer | |
""" | 365 | 365 | """ | |
user = request.user | 366 | 366 | user = request.user | |
flashcard = self.get_object() | 367 | 367 | flashcard = self.get_object() | |
data = FlashcardUpdateSerializer(data=request.data) | 368 | 368 | data = FlashcardUpdateSerializer(data=request.data) | |
data.is_valid(raise_exception=True) | 369 | 369 | data.is_valid(raise_exception=True) | |
new_flashcard = data.validated_data | 370 | 370 | new_flashcard = data.validated_data | |
new_flashcard = flashcard.edit(user, new_flashcard) | 371 | 371 | new_flashcard = flashcard.edit(user, new_flashcard) | |
log_event(request, str(new_flashcard)) | 372 | 372 | log_event(request, str(new_flashcard)) | |
return Response(FlashcardSerializer(new_flashcard, context={'user': request.user}).data, status=HTTP_200_OK) | 373 | 373 | return Response(FlashcardSerializer(new_flashcard, context={'user': request.user}).data, status=HTTP_200_OK) | |
374 | 374 | |||
375 | 375 | |||
class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): | 376 | 376 | class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): | |
permission_classes = [IsAuthenticatedAndConfirmed, IsFlashcardReviewer] | 377 | 377 | permission_classes = [IsAuthenticatedAndConfirmed, IsFlashcardReviewer] | |
queryset = UserFlashcardQuiz.objects.all() | 378 | 378 | queryset = UserFlashcardQuiz.objects.all() | |
379 | 379 | |||
def get_serializer_class(self): | 380 | 380 | def get_serializer_class(self): | |
if self.request.method == 'POST': | 381 | 381 | if self.request.method == 'POST': | |
return QuizRequestSerializer | 382 | 382 | return QuizRequestSerializer | |
return QuizAnswerRequestSerializer | 383 | 383 | return QuizAnswerRequestSerializer | |
384 | 384 | |||
def create(self, request, *args, **kwargs): | 385 | 385 | def create(self, request, *args, **kwargs): | |
""" | 386 | 386 | """ | |
Return a card based on the request params. | 387 | 387 | Return a card based on the request params. | |
:param request: A request object. | 388 | 388 | :param request: A request object. | |
:param format: Format of the request. | 389 | 389 | :param format: Format of the request. | |
:return: A response containing | 390 | 390 | :return: A response containing |