diff --git a/flashcards/fields.py b/flashcards/fields.py index 532d297..347573c 100644 --- a/flashcards/fields.py +++ b/flashcards/fields.py @@ -51,7 +51,7 @@ class MaskField(models.Field): return value if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]): raise ValueError("Invalid value for MaskField attribute") - return sorted([list(interval) for interval in value]) + return MaskField._parse_mask(sorted(value)) def get_prep_lookup(self, lookup_type, value): raise TypeError("Lookup not supported for MaskField") @@ -64,9 +64,9 @@ class MaskField(models.Field): beg, end = map(int, interval) if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): raise ValueError("Invalid range offsets in the mask") - mask_list.append((beg, end)) + mask_list.append([beg, end]) p_beg, p_end = beg, end - return set(mask_list) + return mask_list @staticmethod def _sqlite_parse_mask(value): @@ -77,8 +77,8 @@ class MaskField(models.Field): if len(_range) != 2 or not all(map(unicode.isdigit, _range)): raise ValueError("Invalid range format.") intervals.append(tuple(_range)) - return MaskField._parse_mask(sorted(intervals)) + return set([tuple(i) for i in MaskField._parse_mask(sorted(intervals))]) @staticmethod def _psql_parse_mask(value): - return MaskField._parse_mask(sorted(value)) + return set([tuple(i) for i in MaskField._parse_mask(sorted(value))]) diff --git a/flashcards/models.py b/flashcards/models.py index 4fe4146..cec2f82 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -54,7 +54,6 @@ class UserFlashcard(Model): 3. A user has a flashcard hidden from them """ user = ForeignKey('User') - # mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card") pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") flashcard = ForeignKey('Flashcard') @@ -101,7 +100,6 @@ class Flashcard(Model): author = ForeignKey(User) is_hidden = BooleanField(default=False) hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") - # mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") class Meta: diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 574c1f6..4e5418e 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -5,6 +5,7 @@ from rest_framework.fields import EmailField, BooleanField, CharField, IntegerFi from rest_framework.relations import HyperlinkedRelatedField from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.validators import UniqueValidator +from json import dumps, loads class EmailSerializer(Serializer): @@ -81,44 +82,70 @@ class UserSerializer(ModelSerializer): fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") +class MaskFieldSerializer(serializers.Field): + default_error_messages = { + 'max_length': 'Ensure this field has no more than {max_length} characters.', + 'interval': 'Ensure this field has valid intervals.', + 'overlap': 'Ensure this field does not have overlapping intervals.' + } + + def to_representation(self, value): + if not isinstance(value, set) or not all([isinstance(i, tuple) for i in value]): + raise serializers.ValidationError("Invalid MaskField.") + return dumps(list(value)) + + def to_internal_value(self, data): + try: + intervals = loads(data) + if not isinstance(intervals, list) or len(intervals) > 32 \ + or not all([isinstance(i, list) and len(i) == 2 for i in intervals]): + raise ValueError + except ValueError: + raise serializers.ValidationError("Invalid JSON for MaskField") + return set([tuple(i) for i in intervals]) + + class FlashcardSerializer(ModelSerializer): is_hidden = BooleanField(read_only=True) hide_reason = CharField(read_only=True) + mask = MaskFieldSerializer() def validate_material_date(self, value): # TODO: make this dynamic quarter_start = datetime(2015, 3, 15) quarter_end = datetime(2015, 6, 15) - if quarter_start <= value <= quarter_end: return value + if quarter_start <= value <= quarter_end: + return value else: raise serializers.ValidationError("Material date is outside allowed range for this quarter") - # - # def validate(self, data): - # if + def validate_previous(self, value): + if value is None: + return value + if Flashcard.objects.filter(pk=value).count() > 0: + return value + raise serializers.ValidationError("Invalid previous Flashcard object") - class Meta: - model = Flashcard - exclude = 'author', 'mask', + def validate_pushed(self, value): + if value > datetime.now(): + raise serializers.ValidationError("Invalid creation date for the Flashcard") + return value + def validate_section(self, value): + if Section.objects.filter(pk=value).count() > 0: + return value + raise serializers.ValidationError("Invalid section for the flashcard") -""" -class FlashcardMaskSerializer(ModelSerializer): - def validate_ranges(self, value): - try: - intervals = value.split(',') - for interval in intervals: - beg, end = interval.split('-') - if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end): - raise Exception # just a local exception - except Exception: - raise serializers.ValidationError("The mask is invalid") + def validate_text(self, value): + if len(value) > 255: + raise serializers.ValidationError("Flashcard text limit exceeded") return value - class Meta: - model = FlashcardMask -class UserFlashcard(ModelSerializer): - class Meta: - model = UserFlashcard -""" + def validate_hide_reason(self, value): + if len(value) > 255: + raise serializers.ValidationError("Hide reason limit exceeded") + return value + class Meta: + model = Flashcard + exclude = 'author', 'mask', diff --git a/flashcards/tests/test_models.py b/flashcards/tests/test_models.py index 736f079..f79eff1 100644 --- a/flashcards/tests/test_models.py +++ b/flashcards/tests/test_models.py @@ -48,13 +48,24 @@ class FlashcardTests(TestCase): author=user, material_date=datetime.now(), previous=None, - mask={(0,4), (24,34)}) + mask={(24,34), (0, 4)}) def test_mask_field(self): user = User.objects.get(email="none@none.com") + section = Section.objects.get(course_title='how 2 test') flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") self.assertTrue(isinstance(flashcard.mask, set)) self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) blank1, blank2 = sorted(list(flashcard.mask)) self.assertEqual(flashcard.text[slice(*blank1)], 'This') self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') + try: + Flashcard.objects.create(text="This is the text of the Flashcard", + section=section, + author=user, + material_date=datetime.now(), + previous=None, + mask={(10,34), (0, 14)}) + self.fail() + except ValueError: + self.assertTrue(True) diff --git a/flashcards/views.py b/flashcards/views.py index 691164c..f743e5e 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -1,6 +1,6 @@ from django.contrib import auth from flashcards.api import StandardResultsSetPagination -from flashcards.models import Section, User, Flashcard, FlashcardReport +from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer from rest_framework.decorators import detail_route, permission_classes, api_view @@ -235,16 +235,6 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): serializer_class = FlashcardSerializer permission_classes = [IsAuthenticated] - def get(self, request, pk): - """ - Return the requested flashcard. - :param request: The request object. - :param pk: The primary key of the card to be retrieved - :return: A 200 OK request along with the card data. - """ - obj = self.queryset.get(pk=pk) - return Response(FlashcardSerializer(obj).data) - @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) def report(self, request, pk): """ @@ -261,3 +251,16 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): obj.save() return Response(status=HTTP_204_NO_CONTENT) + @detail_route(methods=['POST'], permission_classes=[IsAuthenticated]) + def pull(self, request, pk): + """ + Pull a card from the live feed into the user's deck. + :param request: The request object + :param pk: The primary key + :return: A 204 response upon success. + """ + user = request.user + flashcard = Flashcard.objects.get(pk=pk) + user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) + user_card.save() + return Response(status=HTTP_204_NO_CONTENT)