Commit ee4104aa2338e1dcdf6a2063a521aad923318262

Authored by Rohan Rangray
1 parent 8e66c8186f
Exists in master

Added a Study viewset and the associated serializers.

Showing 4 changed files with 176 additions and 21 deletions Side-by-side Diff

flashcards/models.py View file @ ee4104a
... ... @@ -248,7 +248,7 @@
248 248 user_flashcard = ForeignKey(UserFlashcard)
249 249 when = DateTimeField(auto_now=True)
250 250 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
251   - response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
  251 + response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response")
252 252 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
253 253  
254 254 def status(self):
flashcards/serializers.py View file @ ee4104a
... ... @@ -3,12 +3,14 @@
3 3 from django.utils.datetime_safe import datetime
4 4 from django.utils.timezone import now
5 5 import pytz
6   -from flashcards.models import Section, LecturePeriod, User, Flashcard
  6 +from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
7 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
8 8 from rest_framework import serializers
9 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField
10   -from rest_framework.serializers import ModelSerializer, Serializer
  10 +from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
11 11 from rest_framework.validators import UniqueValidator
  12 +from flashy.settings import QUARTER_END, QUARTER_START
  13 +from random import sample
12 14  
13 15  
14 16 class EmailSerializer(Serializer):
15 17  
... ... @@ -122,12 +124,8 @@
122 124 mask = MaskFieldSerializer(allow_null=True)
123 125  
124 126 def validate_material_date(self, value):
125   - utc = pytz.UTC
126 127 # TODO: make this dynamic
127   - quarter_start = utc.localize(datetime(2015, 3, 15))
128   - quarter_end = utc.localize(datetime(2015, 6, 15))
129   -
130   - if quarter_start <= value <= quarter_end:
  128 + if QUARTER_START <= value <= QUARTER_END:
131 129 return value
132 130 else:
133 131 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
... ... @@ -155,8 +153,7 @@
155 153 mask = MaskFieldSerializer(required=False)
156 154  
157 155 def validate_material_date(self, date):
158   - quarter_end = pytz.UTC.localize(datetime(2015, 6, 15))
159   - if date > quarter_end:
  156 + if date > QUARTER_END:
160 157 raise serializers.ValidationError("Invalid material_date for the flashcard")
161 158 return date
162 159  
... ... @@ -165,4 +162,122 @@
165 162 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
166 163 raise serializers.ValidationError("No new value passed in")
167 164 return attrs
  165 +
  166 +
  167 +class QuizRequestSerializer(serializers.Serializer):
  168 + sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True)
  169 + material_date_begin = DateTimeField(default=QUARTER_START)
  170 + material_date_end = DateTimeField(default=QUARTER_END)
  171 +
  172 + def __init__(self, user, *args, **kwargs):
  173 + super(QuizRequestSerializer, self).__init__(*args, **kwargs)
  174 + self.user = user
  175 + self.user_flashcard = None
  176 +
  177 + def create(self, validated_data):
  178 + return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard)
  179 +
  180 + def update(self, instance, validated_data):
  181 + for attr in validated_data:
  182 + setattr(instance, attr, validated_data[attr])
  183 + instance.save()
  184 + return instance
  185 +
  186 + def _get_user_flashcard(self, attrs):
  187 + user_flashcard_filter = UserFlashcard.objects.filter(
  188 + user=self.user, flashcard__section__in=attrs['sections'],
  189 + flashcard__material_date__gte=attrs['material_date_begin'],
  190 + flashcard__material_date__lte=attrs['material_date_end']
  191 + )
  192 + if not user_flashcard_filter.exists():
  193 + raise serializers.ValidationError("Your deck for that section is empty")
  194 + self.user_flashcard = user_flashcard_filter.order_by('?').first()
  195 +
  196 + def validate_material_date_begin(self, value):
  197 + if QUARTER_START <= value <= QUARTER_END:
  198 + return value
  199 + raise serializers.ValidationError("Invalid begin date for the flashcard range")
  200 +
  201 + def validate_material_date_end(self, value):
  202 + if QUARTER_START <= value <= QUARTER_END:
  203 + return value
  204 + raise serializers.ValidationError("Invalid end date for the flashcard range")
  205 +
  206 + def validate_sections(self, value):
  207 + print "VALUE", type(value), value
  208 + if value is None:
  209 + return self.user.sections
  210 + section_filter = Section.objects.filter(pk__in=value)
  211 + if not section_filter.exists():
  212 + raise serializers.ValidationError("You aren't enrolled in those section(s)")
  213 + return section_filter
  214 +
  215 + def validate(self, attrs):
  216 + if attrs['material_date_begin'] > attrs['material_date_end']:
  217 + raise serializers.ValidationError("Invalid range")
  218 + if 'sections' not in attrs:
  219 + attrs['sections'] = self.validate_sections(None)
  220 + self._get_user_flashcard(attrs)
  221 + return attrs
  222 +
  223 +
  224 +class QuizResponseSerializer(ModelSerializer):
  225 + pk = PrimaryKeyRelatedField(queryset=UserFlashcardQuiz.objects.all(), many=True)
  226 + section = PrimaryKeyRelatedField(queryset=Section.objects.all())
  227 + text = CharField(max_length=255)
  228 + mask = ListField(child=IntegerField())
  229 +
  230 + def __init__(self, instance=None, mask=[], data=None, **kwargs):
  231 + super(QuizResponseSerializer, self).__init__(instance=instance, data=data, **kwargs)
  232 + self.mask = self._validate_mask(mask)
  233 +
  234 + def to_representation(self, instance):
  235 + return {
  236 + 'pk': instance.user_flashcard.pk,
  237 + 'section': instance.user_flashcard.flashcard.section.pk,
  238 + 'text': instance.user_flashcard.flashcard.text,
  239 + 'mask': self.mask
  240 + }
  241 +
  242 + def _validate_mask(self, value):
  243 + if not isinstance(value, tuple) and value is not None:
  244 + raise serializers.ValidationError("The selected mask has to be a list")
  245 + if value is None or len(value) == 0:
  246 + return []
  247 + if len(value) == 2 and (0 <= value[0] and len(self.initial_data['text']) < value[1]):
  248 + return value
  249 + raise serializers.ValidationError("Invalid mask for the flashcard")
  250 +
  251 + class Meta:
  252 + model = UserFlashcardQuiz
  253 +
  254 +
  255 +class QuizAnswerRequestSerializer(ModelSerializer):
  256 + response = CharField(required=False, max_length=255, help_text="The user's response")
  257 + correct = BooleanField(required=False, help_text="The user's self-evaluation of their response")
  258 +
  259 + def __init__(self, instance, data, **kwargs):
  260 + assert isinstance(instance, UserFlashcardQuiz)
  261 + super(QuizAnswerRequestSerializer, self).__init__(instance, data, kwargs)
  262 +
  263 + def validate_response(self, response):
  264 + if response is None:
  265 + return ""
  266 + return response
  267 +
  268 + def validate(self, attrs):
  269 + if not any(i in attrs for i in ('correct', 'response')):
  270 + raise serializers.ValidationError("No data passed in")
  271 + if 'response' in attrs and self.instance.response is not None:
  272 + raise serializers.ValidationError("You have already sent in a response for this quiz")
  273 + if 'correct' in attrs:
  274 + if 'response' not in attrs and self.instance.response is None:
  275 + raise serializers.ValidationError("You haven't sent in a response yet")
  276 + if self.instance.correct is not None:
  277 + raise serializers.ValidationError("You have already sent in the user's evaluation")
  278 + return attrs
  279 +
  280 + class Meta:
  281 + model = UserFlashcardQuiz
  282 + exclude = 'blanked_word', 'user_flashcard', 'when'
flashcards/views.py View file @ ee4104a
... ... @@ -2,14 +2,14 @@
2 2  
3 3 from django.contrib import auth
4 4 from django.shortcuts import get_object_or_404
5   -from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection
6   -from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard
  5 +from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
  6 +from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
7 7 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
8 8 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
9   - FlashcardUpdateSerializer
  9 + FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer
10 10 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
11 11 from rest_framework.generics import ListAPIView, GenericAPIView
12   -from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
  12 +from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
13 13 from rest_framework.permissions import IsAuthenticated
14 14 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
15 15 from django.core.mail import send_mail
... ... @@ -19,6 +19,7 @@
19 19 from rest_framework.response import Response
20 20 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
21 21 from simple_email_confirmation import EmailAddress
  22 +from random import sample
22 23  
23 24  
24 25 class SectionViewSet(ReadOnlyModelViewSet):
... ... @@ -36,7 +37,7 @@
36 37 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
37 38 return Response(FlashcardSerializer(flashcards, many=True).data)
38 39  
39   - @detail_route(methods=['post'])
  40 + @detail_route(methods=['POST'])
40 41 def enroll(self, request, pk):
41 42 """
42 43 Add the current user to a specified section
... ... @@ -48,7 +49,7 @@
48 49 self.get_object().enroll(request.user)
49 50 return Response(status=HTTP_204_NO_CONTENT)
50 51  
51   - @detail_route(methods=['post'])
  52 + @detail_route(methods=['POST'])
52 53 def drop(self, request, pk):
53 54 """
54 55 Remove the current user from a specified section
... ... @@ -90,7 +91,7 @@
90 91 serializer = FlashcardSerializer(qs, many=True)
91 92 return Response(serializer.data)
92 93  
93   - @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
  94 + @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
94 95 def ordered_deck(self, request, pk):
95 96 """
96 97 Get a chronological order by material_date of flashcards for a section.
... ... @@ -271,7 +272,7 @@
271 272 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
272 273  
273 274  
274   - @detail_route(methods=['post'])
  275 + @detail_route(methods=['POST'])
275 276 def unhide(self, request, pk):
276 277 """
277 278 Unhide the given card
... ... @@ -282,7 +283,7 @@
282 283 hide.delete()
283 284 return Response(status=HTTP_204_NO_CONTENT)
284 285  
285   - @detail_route(methods=['post'])
  286 + @detail_route(methods=['POST'])
286 287 def report(self, request, pk):
287 288 """
288 289 Hide the given card
... ... @@ -331,4 +332,38 @@
331 332 new_flashcard = data.validated_data
332 333 new_flashcard = flashcard.edit(user, new_flashcard)
333 334 return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK)
  335 +
  336 +
  337 +class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin):
  338 + queryset = UserFlashcardQuiz.objects.all()
  339 + serializer_class = QuizAnswerRequestSerializer
  340 + permission_classes = [IsAuthenticated, IsFlashcardReviewer]
  341 +
  342 + def create(self, request, *args, **kwargs):
  343 + """
  344 + Return a card based on the request params.
  345 + :param request: A request object.
  346 + :param format: Format of the request.
  347 + :return: A response containing
  348 + """
  349 + serializer = QuizRequestSerializer(data=request.data)
  350 + serializer.is_valid(raise_exception=True)
  351 + user_flashcard_quiz = serializer.create()
  352 + mask = sample(user_flashcard_quiz.user_flashcard.mask.get_random_blank(), 1)
  353 + user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)]
  354 + user_flashcard_quiz.save()
  355 + return Response(QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data, status=HTTP_200_OK)
  356 +
  357 + def update(self, request, *args, **kwargs):
  358 + """
  359 + Receive the user's response to the quiz.
  360 + :param request: A request object.
  361 + :param format: Format of the request.
  362 + :return: A response containing
  363 + """
  364 + user_flashcard_quiz = self.get_object()
  365 + serializer = QuizAnswerRequestSerializer(instance=user_flashcard_quiz, data=request.data)
  366 + serializer.is_valid()
  367 + serializer.update(user_flashcard_quiz, serializer.validated_data)
  368 + return Response(status=HTTP_204_NO_CONTENT)
flashy/settings.py View file @ ee4104a
1 1 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
2 2 import os
  3 +from datetime import datetime
  4 +from pytz import UTC
3 5  
4 6 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 7  
... ... @@ -86,6 +88,9 @@
86 88 USE_I18N = True
87 89 USE_L10N = True
88 90 USE_TZ = True
  91 +
  92 +QUARTER_START = UTC.localize(datetime(2015, 3, 30))
  93 +QUARTER_END = UTC.localize(datetime(2015, 6, 12))
89 94  
90 95 STATIC_URL = '/static/'
91 96 STATIC_ROOT = 'static'