views.py 14.3 KB
from random import sample
import django
from django.contrib import auth
from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail
from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress
class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all()
serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated]
@detail_route(methods=['GET'])
def flashcards(self, request, pk):
"""
Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date).
"""
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data)
@detail_route(methods=['POST'])
def enroll(self, request, pk):
"""
Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail.
---
view_mocker: flashcards.api.mock_no_params
"""
try:
self.get_object().enroll(request.user)
except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e:
raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT)
@detail_route(methods=['POST'])
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.
---
view_mocker: flashcards.api.mock_no_params
"""
try:
self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e:
raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT)
@list_route(methods=['GET'])
def search(self, request):
"""
Returns a list of sections which match a user's query
---
parameters:
- name: q
description: space-separated list of terms
required: true
type: form
response_serializer: SectionSerializer
"""
query = request.GET.get('q', None)
if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data
return Response(data)
@detail_route(methods=['GET'])
def deck(self, request, pk):
"""
Gets the contents of a user's deck for a given section.
"""
qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data)
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk):
"""
Get a chronological order by material_date of flashcards for a section.
This excludes hidden card.
"""
qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data)
@detail_route(methods=['GET'])
def feed(self, request, pk):
"""
Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck
"""
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data)
class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return self.request.user.sections.all()
def paginate_queryset(self, queryset): return None
class UserDetail(GenericAPIView):
serializer_class = UserSerializer
permission_classes = [IsAuthenticated]
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)
data = data.validated_data
if 'new_password' in data:
if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password'])
request.user.save()
if 'confirmation_key' in data:
try:
request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid')
return Response(UserSerializer(request.user).data)
def get(self, request, format=None):
"""
Return data about the user
---
response_serializer: UserSerializer
"""
serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data)
def delete(self, request):
"""
Irrevocably delete the user and their data
Yes, really
"""
request.user.delete()
return Response(status=HTTP_204_NO_CONTENT)
@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)
User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data)
auth.login(request, user)
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
@api_view(['POST'])
def login(request):
"""
Authenticates user and returns user data if valid.
---
request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer
"""
data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data)
if user is None:
raise AuthenticationFailed('Invalid email or password')
if not user.is_active:
raise NotAuthenticated('Account is disabled')
auth.login(request, user)
return Response(UserSerializer(request.user).data)
@api_view(['POST'])
@permission_classes((IsAuthenticated,))
def logout(request, format=None):
"""
Logs the authenticated user out.
"""
auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT)
@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)
get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT)
@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)
user = User.objects.get(id=data['uid'].value)
# Check token validity.
if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value)
user.save()
else:
raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT)
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
# Override create in CreateModelMixin
def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user
flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard)
notify_new_card(flashcard)
headers = self.get_success_headers(data)
request.user.pull(flashcard)
response_data = FlashcardSerializer(flashcard).data
return Response(response_data, status=HTTP_201_CREATED, headers=headers)
@detail_route(methods=['POST'])
def unhide(self, request, pk):
"""
Unhide the given card
---
view_mocker: flashcards.api.mock_no_params
"""
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete()
return Response(status=HTTP_204_NO_CONTENT)
@detail_route(methods=['POST'])
def report(self, request, pk):
"""
Hide the given card
---
view_mocker: flashcards.api.mock_no_params
"""
self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT)
hide = report
@detail_route(methods=['POST'])
def pull(self, request, pk):
"""
Pull a card from the live feed into the user's deck.
---
view_mocker: flashcards.api.mock_no_params
"""
request.user.pull(self.get_object())
return Response(status=HTTP_204_NO_CONTENT)
@detail_route(methods=['POST'])
def unpull(self, request, pk):
"""
Unpull a card from the user's deck
---
view_mocker: flashcards.api.mock_no_params
"""
user = request.user
flashcard = self.get_object()
user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT)
def partial_update(self, request, *args, **kwargs):
"""
Edit settings related to a card for the user.
---
request_serializer: FlashcardUpdateSerializer
"""
user = request.user
flashcard = self.get_object()
data = FlashcardUpdateSerializer(data=request.data)
data.is_valid(raise_exception=True)
new_flashcard = data.validated_data
new_flashcard = flashcard.edit(user, new_flashcard)
return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK)
class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin):
permission_classes = [IsAuthenticated, IsFlashcardReviewer]
queryset = UserFlashcardQuiz.objects.all()
def get_serializer_class(self):
if self.request.method == 'POST':
return QuizRequestSerializer
return QuizAnswerRequestSerializer
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
request_serializer: serializers.QuizRequestSerializer
response_serializer: serializers.QuizResponseSerializer
"""
serializer = QuizRequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
data = serializer.validated_data
user_flashcard_filter = UserFlashcard.objects.filter(
user=request.user, flashcard__section__pk__in=data['sections'],
flashcard__material_date__gte=data['material_date_begin'],
flashcard__material_date__lte=data['material_date_end']
)
if not user_flashcard_filter.exists():
raise ValidationError("No matching flashcard found in your decks")
user_flashcard = user_flashcard_filter.order_by('?').first()
mask = user_flashcard.get_mask().get_random_blank()
user_flashcard_quiz = UserFlashcardQuiz(user_flashcard=user_flashcard,
blanked_word=user_flashcard.flashcard.text[slice(*mask)])
user_flashcard_quiz.save()
response = QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask)
return Response(response.data, status=HTTP_200_OK)
def partial_update(self, request, *args, **kwargs):
"""
Receive the user's response to the quiz.
:param request: A request object.
:param format: Format of the request.
:return: A response containing
request_serializer: serializers.QuizAnswerRequestSerializer
"""
user_flashcard_quiz = self.get_object()
serializer = QuizAnswerRequestSerializer(instance=user_flashcard_quiz, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.update(user_flashcard_quiz, serializer.validated_data)
return Response(status=HTTP_204_NO_CONTENT)