From ee4104aa2338e1dcdf6a2063a521aad923318262 Mon Sep 17 00:00:00 2001 From: Rohan Rangray Date: Tue, 19 May 2015 22:50:23 -0700 Subject: [PATCH] Added a Study viewset and the associated serializers. --- flashcards/models.py | 2 +- flashcards/serializers.py | 135 ++++++++++++++++++++++++++++++++++++++++++---- flashcards/views.py | 55 +++++++++++++++---- flashy/settings.py | 5 ++ 4 files changed, 176 insertions(+), 21 deletions(-) diff --git a/flashcards/models.py b/flashcards/models.py index 017c770..b6fc5aa 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -248,7 +248,7 @@ class UserFlashcardQuiz(Model): user_flashcard = ForeignKey(UserFlashcard) when = DateTimeField(auto_now=True) blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") - response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") + response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response") correct = NullBooleanField(help_text="The user's self-evaluation of their response") def status(self): diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 1a57f60..1479166 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -3,12 +3,14 @@ from json import dumps, loads from django.utils.datetime_safe import datetime from django.utils.timezone import now import pytz -from flashcards.models import Section, LecturePeriod, User, Flashcard +from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz from flashcards.validators import FlashcardMask, OverlapIntervalException from rest_framework import serializers from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField -from rest_framework.serializers import ModelSerializer, Serializer +from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField from rest_framework.validators import UniqueValidator +from flashy.settings import QUARTER_END, QUARTER_START +from random import sample class EmailSerializer(Serializer): @@ -122,12 +124,8 @@ class FlashcardSerializer(ModelSerializer): mask = MaskFieldSerializer(allow_null=True) def validate_material_date(self, value): - utc = pytz.UTC # TODO: make this dynamic - quarter_start = utc.localize(datetime(2015, 3, 15)) - quarter_end = utc.localize(datetime(2015, 6, 15)) - - if quarter_start <= value <= quarter_end: + if QUARTER_START <= value <= QUARTER_END: return value else: raise serializers.ValidationError("Material date is outside allowed range for this quarter") @@ -155,8 +153,7 @@ class FlashcardUpdateSerializer(serializers.Serializer): mask = MaskFieldSerializer(required=False) def validate_material_date(self, date): - quarter_end = pytz.UTC.localize(datetime(2015, 6, 15)) - if date > quarter_end: + if date > QUARTER_END: raise serializers.ValidationError("Invalid material_date for the flashcard") return date @@ -164,4 +161,122 @@ class FlashcardUpdateSerializer(serializers.Serializer): # Make sure that at least one of the attributes was passed in if not any(i in attrs for i in ['material_date', 'text', 'mask']): raise serializers.ValidationError("No new value passed in") - return attrs \ No newline at end of file + return attrs + + +class QuizRequestSerializer(serializers.Serializer): + sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True) + material_date_begin = DateTimeField(default=QUARTER_START) + material_date_end = DateTimeField(default=QUARTER_END) + + def __init__(self, user, *args, **kwargs): + super(QuizRequestSerializer, self).__init__(*args, **kwargs) + self.user = user + self.user_flashcard = None + + def create(self, validated_data): + return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard) + + def update(self, instance, validated_data): + for attr in validated_data: + setattr(instance, attr, validated_data[attr]) + instance.save() + return instance + + def _get_user_flashcard(self, attrs): + user_flashcard_filter = UserFlashcard.objects.filter( + user=self.user, flashcard__section__in=attrs['sections'], + flashcard__material_date__gte=attrs['material_date_begin'], + flashcard__material_date__lte=attrs['material_date_end'] + ) + if not user_flashcard_filter.exists(): + raise serializers.ValidationError("Your deck for that section is empty") + self.user_flashcard = user_flashcard_filter.order_by('?').first() + + def validate_material_date_begin(self, value): + if QUARTER_START <= value <= QUARTER_END: + return value + raise serializers.ValidationError("Invalid begin date for the flashcard range") + + def validate_material_date_end(self, value): + if QUARTER_START <= value <= QUARTER_END: + return value + raise serializers.ValidationError("Invalid end date for the flashcard range") + + def validate_sections(self, value): + print "VALUE", type(value), value + if value is None: + return self.user.sections + section_filter = Section.objects.filter(pk__in=value) + if not section_filter.exists(): + raise serializers.ValidationError("You aren't enrolled in those section(s)") + return section_filter + + def validate(self, attrs): + if attrs['material_date_begin'] > attrs['material_date_end']: + raise serializers.ValidationError("Invalid range") + if 'sections' not in attrs: + attrs['sections'] = self.validate_sections(None) + self._get_user_flashcard(attrs) + return attrs + + +class QuizResponseSerializer(ModelSerializer): + pk = PrimaryKeyRelatedField(queryset=UserFlashcardQuiz.objects.all(), many=True) + section = PrimaryKeyRelatedField(queryset=Section.objects.all()) + text = CharField(max_length=255) + mask = ListField(child=IntegerField()) + + def __init__(self, instance=None, mask=[], data=None, **kwargs): + super(QuizResponseSerializer, self).__init__(instance=instance, data=data, **kwargs) + self.mask = self._validate_mask(mask) + + def to_representation(self, instance): + return { + 'pk': instance.user_flashcard.pk, + 'section': instance.user_flashcard.flashcard.section.pk, + 'text': instance.user_flashcard.flashcard.text, + 'mask': self.mask + } + + def _validate_mask(self, value): + if not isinstance(value, tuple) and value is not None: + raise serializers.ValidationError("The selected mask has to be a list") + if value is None or len(value) == 0: + return [] + if len(value) == 2 and (0 <= value[0] and len(self.initial_data['text']) < value[1]): + return value + raise serializers.ValidationError("Invalid mask for the flashcard") + + class Meta: + model = UserFlashcardQuiz + + +class QuizAnswerRequestSerializer(ModelSerializer): + response = CharField(required=False, max_length=255, help_text="The user's response") + correct = BooleanField(required=False, help_text="The user's self-evaluation of their response") + + def __init__(self, instance, data, **kwargs): + assert isinstance(instance, UserFlashcardQuiz) + super(QuizAnswerRequestSerializer, self).__init__(instance, data, kwargs) + + def validate_response(self, response): + if response is None: + return "" + return response + + def validate(self, attrs): + if not any(i in attrs for i in ('correct', 'response')): + raise serializers.ValidationError("No data passed in") + if 'response' in attrs and self.instance.response is not None: + raise serializers.ValidationError("You have already sent in a response for this quiz") + if 'correct' in attrs: + if 'response' not in attrs and self.instance.response is None: + raise serializers.ValidationError("You haven't sent in a response yet") + if self.instance.correct is not None: + raise serializers.ValidationError("You have already sent in the user's evaluation") + return attrs + + class Meta: + model = UserFlashcardQuiz + exclude = 'blanked_word', 'user_flashcard', 'when' diff --git a/flashcards/views.py b/flashcards/views.py index bca6be4..0850851 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -2,14 +2,14 @@ import django from django.contrib import auth from django.shortcuts import get_object_or_404 -from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection -from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard +from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer +from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ - FlashcardUpdateSerializer + FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer 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 +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 @@ -19,6 +19,7 @@ from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_20 from rest_framework.response import Response from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied from simple_email_confirmation import EmailAddress +from random import sample class SectionViewSet(ReadOnlyModelViewSet): @@ -36,7 +37,7 @@ class SectionViewSet(ReadOnlyModelViewSet): flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() return Response(FlashcardSerializer(flashcards, many=True).data) - @detail_route(methods=['post']) + @detail_route(methods=['POST']) def enroll(self, request, pk): """ Add the current user to a specified section @@ -48,7 +49,7 @@ class SectionViewSet(ReadOnlyModelViewSet): self.get_object().enroll(request.user) return Response(status=HTTP_204_NO_CONTENT) - @detail_route(methods=['post']) + @detail_route(methods=['POST']) def drop(self, request, pk): """ Remove the current user from a specified section @@ -90,7 +91,7 @@ class SectionViewSet(ReadOnlyModelViewSet): serializer = FlashcardSerializer(qs, many=True) return Response(serializer.data) - @detail_route(methods=['get'], permission_classes=[IsAuthenticated]) + @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. @@ -271,7 +272,7 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) - @detail_route(methods=['post']) + @detail_route(methods=['POST']) def unhide(self, request, pk): """ Unhide the given card @@ -282,7 +283,7 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): hide.delete() return Response(status=HTTP_204_NO_CONTENT) - @detail_route(methods=['post']) + @detail_route(methods=['POST']) def report(self, request, pk): """ Hide the given card @@ -330,4 +331,38 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 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) \ No newline at end of file + return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK) + + +class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): + queryset = UserFlashcardQuiz.objects.all() + serializer_class = QuizAnswerRequestSerializer + permission_classes = [IsAuthenticated, IsFlashcardReviewer] + + 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 + """ + serializer = QuizRequestSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user_flashcard_quiz = serializer.create() + mask = sample(user_flashcard_quiz.user_flashcard.mask.get_random_blank(), 1) + user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)] + user_flashcard_quiz.save() + return Response(QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data, status=HTTP_200_OK) + + def 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 + """ + user_flashcard_quiz = self.get_object() + serializer = QuizAnswerRequestSerializer(instance=user_flashcard_quiz, data=request.data) + serializer.is_valid() + serializer.update(user_flashcard_quiz, serializer.validated_data) + return Response(status=HTTP_204_NO_CONTENT) diff --git a/flashy/settings.py b/flashy/settings.py index 8b0dae3..2fc59df 100644 --- a/flashy/settings.py +++ b/flashy/settings.py @@ -1,5 +1,7 @@ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os +from datetime import datetime +from pytz import UTC BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -87,6 +89,9 @@ USE_I18N = True USE_L10N = True USE_TZ = True +QUARTER_START = UTC.localize(datetime(2015, 3, 30)) +QUARTER_END = UTC.localize(datetime(2015, 6, 12)) + STATIC_URL = '/static/' STATIC_ROOT = 'static' -- 1.9.1