Commit 2f49f8258704c96bd02d13a3a0af1a947aff981f
Exists in
master
Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend
Conflicts: flashcards/views.py
Showing 12 changed files Side-by-side Diff
flashcards/api.py
View file @
2f49f82
1 | -from flashcards.models import Flashcard | |
1 | +from flashcards.models import Flashcard, UserFlashcardQuiz | |
2 | 2 | from rest_framework.pagination import PageNumberPagination |
3 | 3 | from rest_framework.permissions import BasePermission |
4 | 4 | |
5 | 5 | |
... | ... | @@ -24,6 +24,16 @@ |
24 | 24 | |
25 | 25 | class IsEnrolledInAssociatedSection(BasePermission): |
26 | 26 | def has_object_permission(self, request, view, obj): |
27 | + if obj is None: | |
28 | + return True | |
27 | 29 | assert type(obj) is Flashcard |
28 | 30 | return request.user.is_in_section(obj.section) |
31 | + | |
32 | + | |
33 | +class IsFlashcardReviewer(BasePermission): | |
34 | + def has_object_permission(self, request, view, obj): | |
35 | + if obj is None: | |
36 | + return True | |
37 | + assert type(obj) is UserFlashcardQuiz | |
38 | + return request.user == obj.user_flashcard.user |
flashcards/fields.py
View file @
2f49f82
... | ... | @@ -40,7 +40,7 @@ |
40 | 40 | def to_python(self, value): |
41 | 41 | if value is None: |
42 | 42 | return value |
43 | - return sorted(list(FlashcardMask(value))) | |
43 | + return FlashcardMask(value) | |
44 | 44 | |
45 | 45 | def get_prep_value(self, value): |
46 | 46 | if value is None: |
... | ... | @@ -65,7 +65,7 @@ |
65 | 65 | @classmethod |
66 | 66 | def _varchar_parse_mask(cls, value): |
67 | 67 | if not value: |
68 | - return FlashcardMask([]) | |
68 | + return FlashcardMask([]) | |
69 | 69 | |
70 | 70 | mask = [tuple(map(int, i.split('-'))) for i in value.split(',')] |
71 | 71 | return FlashcardMask(mask) |
flashcards/models.py
View file @
2f49f82
... | ... | @@ -247,7 +247,7 @@ |
247 | 247 | user_flashcard = ForeignKey(UserFlashcard) |
248 | 248 | when = DateTimeField(auto_now=True) |
249 | 249 | blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") |
250 | - response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") | |
250 | + response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response") | |
251 | 251 | correct = NullBooleanField(help_text="The user's self-evaluation of their response") |
252 | 252 | |
253 | 253 | def status(self): |
... | ... | @@ -313,7 +313,6 @@ |
313 | 313 | """ |
314 | 314 | return self.whitelist.filter(email=user.email).exists() |
315 | 315 | |
316 | - | |
317 | 316 | def enroll(self, user): |
318 | 317 | if user.sections.filter(pk=self.pk).exists(): |
319 | 318 | raise ValidationError('User is already enrolled in this section') |
... | ... | @@ -327,7 +326,7 @@ |
327 | 326 | self.user_set.remove(user) |
328 | 327 | |
329 | 328 | class Meta: |
330 | - ordering = ['-course_title'] | |
329 | + ordering = ['department_abbreviation', 'course_num'] | |
331 | 330 | |
332 | 331 | @property |
333 | 332 | def lecture_times(self): |
334 | 333 | |
... | ... | @@ -339,10 +338,9 @@ |
339 | 338 | 0].short_start_time |
340 | 339 | else: |
341 | 340 | data = '' |
342 | - cache.set("section_%d_lecture_times" % self.pk, data, 24*60*60) | |
341 | + cache.set("section_%d_lecture_times" % self.pk, data, 24 * 60 * 60) | |
343 | 342 | return data |
344 | 343 | |
345 | - | |
346 | 344 | @property |
347 | 345 | def long_name(self): |
348 | 346 | return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor) |
... | ... | @@ -352,7 +350,8 @@ |
352 | 350 | return '%s %s' % (self.department_abbreviation, self.course_num) |
353 | 351 | |
354 | 352 | def get_feed_for_user(self, user): |
355 | - qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed') | |
353 | + qs = Flashcard.objects.filter(section=self).exclude(flashcardhide__user=user).exclude( | |
354 | + userflashcard__user=user).order_by('pushed') | |
356 | 355 | return qs |
357 | 356 | |
358 | 357 | def get_cards_for_user(self, user): |
flashcards/serializers.py
View file @
2f49f82
... | ... | @@ -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 | -from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField | |
10 | -from rest_framework.serializers import ModelSerializer, Serializer | |
9 | +from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty | |
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 | |
... | ... | @@ -125,12 +127,8 @@ |
125 | 127 | mask = MaskFieldSerializer(allow_null=True) |
126 | 128 | |
127 | 129 | def validate_material_date(self, value): |
128 | - utc = pytz.UTC | |
129 | 130 | # TODO: make this dynamic |
130 | - quarter_start = utc.localize(datetime(2015, 3, 15)) | |
131 | - quarter_end = utc.localize(datetime(2015, 6, 15)) | |
132 | - | |
133 | - if quarter_start <= value <= quarter_end: | |
131 | + if QUARTER_START <= value <= QUARTER_END: | |
134 | 132 | return value |
135 | 133 | else: |
136 | 134 | raise serializers.ValidationError("Material date is outside allowed range for this quarter") |
... | ... | @@ -158,8 +156,7 @@ |
158 | 156 | mask = MaskFieldSerializer(required=False) |
159 | 157 | |
160 | 158 | def validate_material_date(self, date): |
161 | - quarter_end = pytz.UTC.localize(datetime(2015, 6, 15)) | |
162 | - if date > quarter_end: | |
159 | + if date > QUARTER_END: | |
163 | 160 | raise serializers.ValidationError("Invalid material_date for the flashcard") |
164 | 161 | return date |
165 | 162 | |
... | ... | @@ -168,4 +165,122 @@ |
168 | 165 | if not any(i in attrs for i in ['material_date', 'text', 'mask']): |
169 | 166 | raise serializers.ValidationError("No new value passed in") |
170 | 167 | return attrs |
168 | + | |
169 | + | |
170 | +class QuizRequestSerializer(serializers.Serializer): | |
171 | + # sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True) | |
172 | + sections = ListField(child=IntegerField(min_value=1), required=False) | |
173 | + material_date_begin = DateTimeField(default=QUARTER_START) | |
174 | + material_date_end = DateTimeField(default=QUARTER_END) | |
175 | + | |
176 | + def __init__(self, user, *args, **kwargs): | |
177 | + super(QuizRequestSerializer, self).__init__(*args, **kwargs) | |
178 | + self.user = user | |
179 | + self.user_flashcard = None | |
180 | + | |
181 | + def create(self, validated_data): | |
182 | + return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard) | |
183 | + | |
184 | + def update(self, instance, validated_data): | |
185 | + for attr in validated_data: | |
186 | + setattr(instance, attr, validated_data[attr]) | |
187 | + instance.save() | |
188 | + return instance | |
189 | + | |
190 | + def _get_user_flashcard(self, attrs): | |
191 | + user_flashcard_filter = UserFlashcard.objects.filter( | |
192 | + user=self.user, flashcard__section__in=attrs['sections'], | |
193 | + flashcard__material_date__gte=attrs['material_date_begin'], | |
194 | + flashcard__material_date__lte=attrs['material_date_end'] | |
195 | + ) | |
196 | + if not user_flashcard_filter.exists(): | |
197 | + raise serializers.ValidationError("Your deck for that section is empty") | |
198 | + self.user_flashcard = user_flashcard_filter.order_by('?').first() | |
199 | + | |
200 | + def validate_material_date_begin(self, value): | |
201 | + if QUARTER_START <= value <= QUARTER_END: | |
202 | + return value | |
203 | + raise serializers.ValidationError("Invalid begin date for the flashcard range") | |
204 | + | |
205 | + def validate_material_date_end(self, value): | |
206 | + if QUARTER_START <= value <= QUARTER_END: | |
207 | + return value | |
208 | + raise serializers.ValidationError("Invalid end date for the flashcard range") | |
209 | + | |
210 | + def validate_sections(self, value): | |
211 | + if value is None: | |
212 | + return self.user.sections | |
213 | + section_filter = Section.objects.filter(pk__in=value) | |
214 | + if not section_filter.exists(): | |
215 | + raise serializers.ValidationError("You aren't enrolled in those section(s)") | |
216 | + return section_filter | |
217 | + | |
218 | + def validate(self, attrs): | |
219 | + if attrs['material_date_begin'] > attrs['material_date_end']: | |
220 | + raise serializers.ValidationError("Invalid range") | |
221 | + if 'sections' not in attrs: | |
222 | + attrs['sections'] = self.validate_sections(None) | |
223 | + self._get_user_flashcard(attrs) | |
224 | + return attrs | |
225 | + | |
226 | + | |
227 | +class QuizResponseSerializer(ModelSerializer): | |
228 | + pk = PrimaryKeyRelatedField(queryset=UserFlashcardQuiz.objects.all(), many=True) | |
229 | + section = PrimaryKeyRelatedField(queryset=Section.objects.all()) | |
230 | + text = CharField(max_length=255) | |
231 | + mask = ListField(child=IntegerField()) | |
232 | + | |
233 | + def __init__(self, instance=None, mask=[], data=empty, **kwargs): | |
234 | + super(QuizResponseSerializer, self).__init__(instance=instance, data=data, **kwargs) | |
235 | + self.mask = self._validate_mask(mask) | |
236 | + | |
237 | + def to_representation(self, instance): | |
238 | + return { | |
239 | + 'pk': instance.user_flashcard.pk, | |
240 | + 'section': instance.user_flashcard.flashcard.section.pk, | |
241 | + 'text': instance.user_flashcard.flashcard.text, | |
242 | + 'mask': self.mask | |
243 | + } | |
244 | + | |
245 | + def _validate_mask(self, value): | |
246 | + if not isinstance(value, tuple) and value is not None: | |
247 | + raise serializers.ValidationError("The selected mask has to be a list") | |
248 | + if value is None or len(value) == 0: | |
249 | + return [] | |
250 | + if len(value) == 2 and (0 <= value[0] and value[1] <= len(self.instance.user_flashcard.flashcard.text)): | |
251 | + return value | |
252 | + raise serializers.ValidationError("Invalid mask for the flashcard") | |
253 | + | |
254 | + class Meta: | |
255 | + model = UserFlashcardQuiz | |
256 | + | |
257 | + | |
258 | +class QuizAnswerRequestSerializer(ModelSerializer): | |
259 | + response = CharField(required=False, max_length=255, help_text="The user's response") | |
260 | + correct = BooleanField(required=False, help_text="The user's self-evaluation of their response") | |
261 | + | |
262 | + def __init__(self, instance, data, **kwargs): | |
263 | + assert isinstance(instance, UserFlashcardQuiz) | |
264 | + super(QuizAnswerRequestSerializer, self).__init__(instance, data, **kwargs) | |
265 | + | |
266 | + def validate_response(self, response): | |
267 | + if response is None: | |
268 | + return "" | |
269 | + return response | |
270 | + | |
271 | + def validate(self, attrs): | |
272 | + if not any(i in attrs for i in ('correct', 'response')): | |
273 | + raise serializers.ValidationError("No data passed in") | |
274 | + if 'response' in attrs and self.instance.response is not None: | |
275 | + raise serializers.ValidationError("You have already sent in a response for this quiz") | |
276 | + if 'correct' in attrs: | |
277 | + if 'response' not in attrs and self.instance.response is None: | |
278 | + raise serializers.ValidationError("You haven't sent in a response yet") | |
279 | + if self.instance.correct is not None: | |
280 | + raise serializers.ValidationError("You have already sent in the user's evaluation") | |
281 | + return attrs | |
282 | + | |
283 | + class Meta: | |
284 | + model = UserFlashcardQuiz | |
285 | + exclude = 'blanked_word', 'user_flashcard', 'when' |
flashcards/tests/test_api.py
View file @
2f49f82
... | ... | @@ -358,10 +358,30 @@ |
358 | 358 | response = self.client.get('/api/sections/{}/feed/'.format(self.section.pk)) |
359 | 359 | self.assertEqual(response.status_code, HTTP_200_OK) |
360 | 360 | self.assertEqual(response.data[0]['id'], 1) |
361 | + self.flashcard.hide_from(self.user) | |
362 | + response = self.client.get('/api/sections/{}/feed/'.format(self.section.pk)) | |
363 | + self.assertEqual(response.status_code, HTTP_200_OK) | |
364 | + self.assertNotEqual(response.data[0]['id'], 1) | |
361 | 365 | |
362 | 366 | def test_section_ordered_deck(self): |
363 | 367 | self.user.sections.add(self.section) |
364 | 368 | self.user.save() |
365 | 369 | response = self.client.get('/api/sections/1/ordered_deck/') |
366 | 370 | self.assertEqual(response.status_code, HTTP_200_OK) |
371 | + | |
372 | + | |
373 | +class UserFlashcardQuizTests(APITestCase): | |
374 | + fixtures = ['testusers', 'testsections'] | |
375 | + | |
376 | + def setUp(self): | |
377 | + self.client.login(email='none@none.com', password='1234') | |
378 | + self.user = User.objects.get(email='none@none.com') | |
379 | + self.section = Section.objects.get(pk=1) | |
380 | + self.flashcard = Flashcard(text="This is a flashcard", section=self.section, material_date=now(), | |
381 | + author=self.user, mask={(0,4), (5,7)}) | |
382 | + self.flashcard.save() | |
383 | + self.flashcard.refresh_from_db() | |
384 | + self.user_flashcard = UserFlashcard(flashcard=self.flashcard, user=self.user, | |
385 | + mask=self.flashcard.mask, pulled=datetime.now()) | |
386 | + self.user_flashcard.save() |
flashcards/tests/test_models.py
View file @
2f49f82
1 | 1 | from datetime import datetime |
2 | 2 | |
3 | 3 | from django.test import TestCase |
4 | -from flashcards.models import User, Section, Flashcard, UserFlashcard | |
4 | +from flashcards.models import User, Section, Flashcard, UserFlashcard, UserFlashcardQuiz | |
5 | 5 | from flashcards.validators import FlashcardMask, OverlapIntervalException |
6 | +from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer | |
7 | +from flashy.settings import QUARTER_START, QUARTER_END | |
6 | 8 | |
7 | 9 | |
8 | 10 | class RegistrationTests(TestCase): |
... | ... | @@ -98,7 +100,6 @@ |
98 | 100 | self.assertEqual(oie.message, "Invalid interval offsets in the mask") |
99 | 101 | |
100 | 102 | |
101 | - | |
102 | 103 | class FlashcardTests(TestCase): |
103 | 104 | def setUp(self): |
104 | 105 | section = Section.objects.create(department='dept', |
... | ... | @@ -113,7 +114,7 @@ |
113 | 114 | author=user, |
114 | 115 | material_date=datetime.now(), |
115 | 116 | previous=None, |
116 | - mask={(24,34), (0, 4)}) | |
117 | + mask={(24, 34), (0, 4)}) | |
117 | 118 | user.save() |
118 | 119 | section.save() |
119 | 120 | flashcard.save() |
... | ... | @@ -148,4 +149,64 @@ |
148 | 149 | self.fail() |
149 | 150 | except OverlapIntervalException: |
150 | 151 | self.assertTrue(True) |
152 | + | |
153 | + | |
154 | +class UserFlashcardQuizTests(TestCase): | |
155 | + def setUp(self): | |
156 | + self.section = Section.objects.create(department='dept', | |
157 | + course_num='101a', | |
158 | + course_title='how 2 test', | |
159 | + instructor='George Lucas', | |
160 | + quarter='SP15') | |
161 | + self.user = User.objects.create_user(email="none@none.com", password="1234") | |
162 | + self.user.sections.add(self.section) | |
163 | + self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", | |
164 | + section=self.section, | |
165 | + author=self.user, | |
166 | + material_date=datetime.now(), | |
167 | + previous=None, | |
168 | + mask=[(24, 33), (0, 4)]) | |
169 | + self.user.save() | |
170 | + self.section.save() | |
171 | + self.flashcard.save() | |
172 | + self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard, | |
173 | + user=self.user, | |
174 | + mask=self.flashcard.mask, | |
175 | + pulled=datetime.now()) | |
176 | + self.user_flashcard.save() | |
177 | + self.user_flashcard.refresh_from_db() | |
178 | + self.flashcard.refresh_from_db() | |
179 | + | |
180 | + def test_quiz_request(self): | |
181 | + data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END} | |
182 | + serializer = QuizRequestSerializer(user=self.user, data=data) | |
183 | + serializer.is_valid(raise_exception=True) | |
184 | + user_flashcard_quiz = serializer.create(serializer.validated_data) | |
185 | + self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz)) | |
186 | + mask = user_flashcard_quiz.user_flashcard.mask.get_random_blank() | |
187 | + self.assertIn(mask, [(24, 33), (0, 4)]) | |
188 | + user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)] | |
189 | + self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"]) | |
190 | + user_flashcard_quiz.save() | |
191 | + response = QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data | |
192 | + self.assertEqual(response['pk'], 1) | |
193 | + self.assertEqual(response['section'], 1) | |
194 | + self.assertEqual(response['text'], user_flashcard_quiz.user_flashcard.flashcard.text) | |
195 | + self.assertEqual(response['mask'], mask) | |
196 | + | |
197 | + def test_quiz_answer(self): | |
198 | + data = {'response': 'Flashcard'} | |
199 | + mask = self.user_flashcard.mask.get_random_blank() | |
200 | + word = self.flashcard.text[slice(*mask)] | |
201 | + user_flashcard_quiz = UserFlashcardQuiz(user_flashcard=self.user_flashcard, blanked_word=word) | |
202 | + user_flashcard_quiz.save() | |
203 | + serializer = QuizAnswerRequestSerializer(instance=user_flashcard_quiz, data=data) | |
204 | + serializer.is_valid() | |
205 | + serializer.update(user_flashcard_quiz, serializer.validated_data) | |
206 | + self.assertEqual(user_flashcard_quiz.response, data['response']) | |
207 | + data = {'correct': True} | |
208 | + serializer = QuizAnswerRequestSerializer(instance=user_flashcard_quiz, data=data) | |
209 | + serializer.is_valid() | |
210 | + serializer.update(user_flashcard_quiz, serializer.validated_data) | |
211 | + self.assertTrue(user_flashcard_quiz.correct) |
flashcards/validators.py
View file @
2f49f82
1 | 1 | from collections import Iterable |
2 | +from random import sample | |
2 | 3 | |
3 | 4 | |
4 | 5 | class FlashcardMask(set): |
... | ... | @@ -13,6 +14,11 @@ |
13 | 14 | |
14 | 15 | def max_offset(self): |
15 | 16 | return self._end |
17 | + | |
18 | + def get_random_blank(self): | |
19 | + if self.max_offset() > 0: | |
20 | + return sample(self, 1)[0] | |
21 | + return () | |
16 | 22 | |
17 | 23 | def _iterable_check(self, iterable): |
18 | 24 | if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): |
flashcards/views.py
View file @
2f49f82
... | ... | @@ -3,14 +3,15 @@ |
3 | 3 | from django.contrib import auth |
4 | 4 | from django.core.cache import cache |
5 | 5 | from django.shortcuts import get_object_or_404 |
6 | -from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection | |
7 | -from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard | |
6 | +from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer | |
7 | +from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz | |
8 | 8 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ |
9 | 9 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ |
10 | - FlashcardUpdateSerializer, DeepSectionSerializer | |
10 | + FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ | |
11 | + QuizAnswerRequestSerializer, DeepSectionSerializer | |
11 | 12 | from rest_framework.decorators import detail_route, permission_classes, api_view, list_route |
12 | 13 | from rest_framework.generics import ListAPIView, GenericAPIView |
13 | -from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin | |
14 | +from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin | |
14 | 15 | from rest_framework.permissions import IsAuthenticated |
15 | 16 | from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet |
16 | 17 | from django.core.mail import send_mail |
... | ... | @@ -20,6 +21,7 @@ |
20 | 21 | from rest_framework.response import Response |
21 | 22 | from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied |
22 | 23 | from simple_email_confirmation import EmailAddress |
24 | +from random import sample | |
23 | 25 | |
24 | 26 | |
25 | 27 | class SectionViewSet(ReadOnlyModelViewSet): |
... | ... | @@ -37,7 +39,7 @@ |
37 | 39 | flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() |
38 | 40 | return Response(FlashcardSerializer(flashcards, many=True).data) |
39 | 41 | |
40 | - @detail_route(methods=['post']) | |
42 | + @detail_route(methods=['POST']) | |
41 | 43 | def enroll(self, request, pk): |
42 | 44 | """ |
43 | 45 | Add the current user to a specified section |
... | ... | @@ -49,7 +51,7 @@ |
49 | 51 | self.get_object().enroll(request.user) |
50 | 52 | return Response(status=HTTP_204_NO_CONTENT) |
51 | 53 | |
52 | - @detail_route(methods=['post']) | |
54 | + @detail_route(methods=['POST']) | |
53 | 55 | def drop(self, request, pk): |
54 | 56 | """ |
55 | 57 | Remove the current user from a specified section |
... | ... | @@ -92,7 +94,7 @@ |
92 | 94 | serializer = FlashcardSerializer(qs, many=True) |
93 | 95 | return Response(serializer.data) |
94 | 96 | |
95 | - @detail_route(methods=['get'], permission_classes=[IsAuthenticated]) | |
97 | + @detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) | |
96 | 98 | def ordered_deck(self, request, pk): |
97 | 99 | """ |
98 | 100 | Get a chronological order by material_date of flashcards for a section. |
... | ... | @@ -273,7 +275,7 @@ |
273 | 275 | return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) |
274 | 276 | |
275 | 277 | |
276 | - @detail_route(methods=['post']) | |
278 | + @detail_route(methods=['POST']) | |
277 | 279 | def unhide(self, request, pk): |
278 | 280 | """ |
279 | 281 | Unhide the given card |
... | ... | @@ -284,7 +286,7 @@ |
284 | 286 | hide.delete() |
285 | 287 | return Response(status=HTTP_204_NO_CONTENT) |
286 | 288 | |
287 | - @detail_route(methods=['post']) | |
289 | + @detail_route(methods=['POST']) | |
288 | 290 | def report(self, request, pk): |
289 | 291 | """ |
290 | 292 | Hide the given card |
... | ... | @@ -333,4 +335,38 @@ |
333 | 335 | new_flashcard = data.validated_data |
334 | 336 | new_flashcard = flashcard.edit(user, new_flashcard) |
335 | 337 | return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK) |
338 | + | |
339 | +class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): | |
340 | + queryset = UserFlashcardQuiz.objects.all() | |
341 | + serializer_class = QuizAnswerRequestSerializer | |
342 | + permission_classes = [IsAuthenticated, IsFlashcardReviewer] | |
343 | + | |
344 | + def create(self, request, *args, **kwargs): | |
345 | + """ | |
346 | + Return a card based on the request params. | |
347 | + :param request: A request object. | |
348 | + :param format: Format of the request. | |
349 | + :return: A response containing | |
350 | + """ | |
351 | + serializer = QuizRequestSerializer(data=request.data) | |
352 | + serializer.is_valid(raise_exception=True) | |
353 | + user_flashcard_quiz = serializer.create(serializer.validated_data) | |
354 | + mask = sample(user_flashcard_quiz.user_flashcard.mask.get_random_blank(), 1) | |
355 | + user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)] | |
356 | + user_flashcard_quiz.save() | |
357 | + return Response(QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data, status=HTTP_200_OK) | |
358 | + | |
359 | + def update(self, request, *args, **kwargs): | |
360 | + """ | |
361 | + Receive the user's response to the quiz. | |
362 | + :param request: A request object. | |
363 | + :param format: Format of the request. | |
364 | + :return: A response containing | |
365 | + """ | |
366 | + user_flashcard_quiz = self.get_object() | |
367 | + serializer = QuizAnswerRequestSerializer(instance=user_flashcard_quiz, data=request.data) | |
368 | + serializer.is_valid() | |
369 | + serializer.update(user_flashcard_quiz, serializer.validated_data) | |
370 | + return Response(status=HTTP_204_NO_CONTENT) | |
371 | +>>>>>>> 6713d6dc9f0ddfc99cc24a9b57194c0d5b032c91 |
flashy/settings.py
View file @
2f49f82
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 | |
6 | 8 | |
... | ... | @@ -24,11 +26,14 @@ |
24 | 26 | 'django.contrib.sessions', |
25 | 27 | 'django.contrib.messages', |
26 | 28 | 'django.contrib.staticfiles', |
27 | - | |
29 | + 'ws4redis', | |
28 | 30 | 'rest_framework_swagger', |
29 | 31 | 'rest_framework', |
30 | 32 | ] |
31 | 33 | |
34 | +WEBSOCKET_URL = '/ws/' | |
35 | + | |
36 | + | |
32 | 37 | MIDDLEWARE_CLASSES = ( |
33 | 38 | 'django.contrib.sessions.middleware.SessionMiddleware', |
34 | 39 | 'django.middleware.common.CommonMiddleware', |
35 | 40 | |
... | ... | @@ -57,12 +62,14 @@ |
57 | 62 | 'django.template.context_processors.request', |
58 | 63 | 'django.contrib.auth.context_processors.auth', |
59 | 64 | 'django.contrib.messages.context_processors.messages', |
65 | + 'django.core.context_processors.static', | |
66 | + 'ws4redis.context_processors.default', | |
60 | 67 | ], |
61 | 68 | }, |
62 | 69 | }, |
63 | 70 | ] |
64 | 71 | |
65 | -WSGI_APPLICATION = 'flashy.wsgi.application' | |
72 | +WSGI_APPLICATION = 'ws4redis.django_runserver.application' | |
66 | 73 | |
67 | 74 | DATABASES = { |
68 | 75 | 'default': { |
... | ... | @@ -86,6 +93,9 @@ |
86 | 93 | USE_I18N = True |
87 | 94 | USE_L10N = True |
88 | 95 | USE_TZ = True |
96 | + | |
97 | +QUARTER_START = UTC.localize(datetime(2015, 3, 30)) | |
98 | +QUARTER_END = UTC.localize(datetime(2015, 6, 12)) | |
89 | 99 | |
90 | 100 | STATIC_URL = '/static/' |
91 | 101 | STATIC_ROOT = 'static' |
flashy/urls.py
View file @
2f49f82
1 | 1 | from django.conf.urls import include, url |
2 | 2 | from django.contrib import admin |
3 | 3 | from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \ |
4 | - reset_password, logout, login, register | |
4 | + reset_password, logout, login, register, UserFlashcardQuizViewSet | |
5 | 5 | from flashy.frontend_serve import serve_with_default |
6 | 6 | from flashy.settings import DEBUG, IN_PRODUCTION |
7 | 7 | from rest_framework.routers import DefaultRouter |
... | ... | @@ -10,6 +10,7 @@ |
10 | 10 | router = DefaultRouter() |
11 | 11 | router.register(r'sections', SectionViewSet) |
12 | 12 | router.register(r'flashcards', FlashcardViewSet) |
13 | +router.register(r'study', UserFlashcardQuizViewSet) | |
13 | 14 | |
14 | 15 | urlpatterns = [ |
15 | 16 | url(r'^api/docs/', include('rest_framework_swagger.urls')), |
requirements.txt
View file @
2f49f82
scripts/run_production.sh
View file @
2f49f82
1 | 1 | #!/bin/bash -xe |
2 | 2 | source secrets.sh |
3 | 3 | source venv/bin/activate |
4 | -newrelic-admin run-program /srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi | |
4 | +# newrelic-admin run-program /srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi | |
5 | +newrelic-admin run-program uwsgi /etc/uwsgi/flashy.ini --touch-reload=/etc/uwsgi/flashy.ini |