Commit ee4104aa2338e1dcdf6a2063a521aad923318262
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' |