Commit be6cc9169b8e3a3dd1b54ede99939a45b43ec207
1 parent
5354852580
Exists in
master
Fixed a bug in the MaskField modelfield implementation. Added MaskField serializer field.
Showing 5 changed files with 82 additions and 43 deletions Side-by-side Diff
flashcards/fields.py
View file @
be6cc91
... | ... | @@ -51,7 +51,7 @@ |
51 | 51 | return value |
52 | 52 | if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]): |
53 | 53 | raise ValueError("Invalid value for MaskField attribute") |
54 | - return sorted([list(interval) for interval in value]) | |
54 | + return MaskField._parse_mask(sorted(value)) | |
55 | 55 | |
56 | 56 | def get_prep_lookup(self, lookup_type, value): |
57 | 57 | raise TypeError("Lookup not supported for MaskField") |
58 | 58 | |
... | ... | @@ -64,9 +64,9 @@ |
64 | 64 | beg, end = map(int, interval) |
65 | 65 | if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): |
66 | 66 | raise ValueError("Invalid range offsets in the mask") |
67 | - mask_list.append((beg, end)) | |
67 | + mask_list.append([beg, end]) | |
68 | 68 | p_beg, p_end = beg, end |
69 | - return set(mask_list) | |
69 | + return mask_list | |
70 | 70 | |
71 | 71 | @staticmethod |
72 | 72 | def _sqlite_parse_mask(value): |
73 | 73 | |
... | ... | @@ -77,9 +77,9 @@ |
77 | 77 | if len(_range) != 2 or not all(map(unicode.isdigit, _range)): |
78 | 78 | raise ValueError("Invalid range format.") |
79 | 79 | intervals.append(tuple(_range)) |
80 | - return MaskField._parse_mask(sorted(intervals)) | |
80 | + return set([tuple(i) for i in MaskField._parse_mask(sorted(intervals))]) | |
81 | 81 | |
82 | 82 | @staticmethod |
83 | 83 | def _psql_parse_mask(value): |
84 | - return MaskField._parse_mask(sorted(value)) | |
84 | + return set([tuple(i) for i in MaskField._parse_mask(sorted(value))]) |
flashcards/models.py
View file @
be6cc91
... | ... | @@ -54,7 +54,6 @@ |
54 | 54 | 3. A user has a flashcard hidden from them |
55 | 55 | """ |
56 | 56 | user = ForeignKey('User') |
57 | - # mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") | |
58 | 57 | mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card") |
59 | 58 | pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") |
60 | 59 | flashcard = ForeignKey('Flashcard') |
... | ... | @@ -101,7 +100,6 @@ |
101 | 100 | author = ForeignKey(User) |
102 | 101 | is_hidden = BooleanField(default=False) |
103 | 102 | hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") |
104 | - # mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") | |
105 | 103 | mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") |
106 | 104 | |
107 | 105 | class Meta: |
flashcards/serializers.py
View file @
be6cc91
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 | from rest_framework.relations import HyperlinkedRelatedField |
6 | 6 | from rest_framework.serializers import ModelSerializer, Serializer |
7 | 7 | from rest_framework.validators import UniqueValidator |
8 | +from json import dumps, loads | |
8 | 9 | |
9 | 10 | |
10 | 11 | class EmailSerializer(Serializer): |
11 | 12 | |
12 | 13 | |
13 | 14 | |
14 | 15 | |
15 | 16 | |
16 | 17 | |
17 | 18 | |
18 | 19 | |
19 | 20 | |
... | ... | @@ -81,44 +82,71 @@ |
81 | 82 | fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") |
82 | 83 | |
83 | 84 | |
85 | +class MaskFieldSerializer(serializers.Field): | |
86 | + default_error_messages = { | |
87 | + 'max_length': 'Ensure this field has no more than {max_length} characters.', | |
88 | + 'interval': 'Ensure this field has valid intervals.', | |
89 | + 'overlap': 'Ensure this field does not have overlapping intervals.' | |
90 | + } | |
91 | + | |
92 | + def to_representation(self, value): | |
93 | + if not isinstance(value, set) or not all([isinstance(i, tuple) for i in value]): | |
94 | + raise serializers.ValidationError("Invalid MaskField.") | |
95 | + return dumps(list(value)) | |
96 | + | |
97 | + def to_internal_value(self, data): | |
98 | + try: | |
99 | + intervals = loads(data) | |
100 | + if not isinstance(intervals, list) or len(intervals) > 32 \ | |
101 | + or not all([isinstance(i, list) and len(i) == 2 for i in intervals]): | |
102 | + raise ValueError | |
103 | + except ValueError: | |
104 | + raise serializers.ValidationError("Invalid JSON for MaskField") | |
105 | + return set([tuple(i) for i in intervals]) | |
106 | + | |
107 | + | |
84 | 108 | class FlashcardSerializer(ModelSerializer): |
85 | 109 | is_hidden = BooleanField(read_only=True) |
86 | 110 | hide_reason = CharField(read_only=True) |
111 | + mask = MaskFieldSerializer() | |
87 | 112 | |
88 | 113 | def validate_material_date(self, value): |
89 | 114 | # TODO: make this dynamic |
90 | 115 | quarter_start = datetime(2015, 3, 15) |
91 | 116 | quarter_end = datetime(2015, 6, 15) |
92 | - if quarter_start <= value <= quarter_end: return value | |
117 | + if quarter_start <= value <= quarter_end: | |
118 | + return value | |
93 | 119 | else: |
94 | 120 | raise serializers.ValidationError("Material date is outside allowed range for this quarter") |
95 | 121 | |
96 | - # | |
97 | - # def validate(self, data): | |
98 | - # if | |
122 | + def validate_previous(self, value): | |
123 | + if value is None: | |
124 | + return value | |
125 | + if Flashcard.objects.filter(pk=value).count() > 0: | |
126 | + return value | |
127 | + raise serializers.ValidationError("Invalid previous Flashcard object") | |
99 | 128 | |
100 | - class Meta: | |
101 | - model = Flashcard | |
102 | - exclude = 'author', 'mask', | |
129 | + def validate_pushed(self, value): | |
130 | + if value > datetime.now(): | |
131 | + raise serializers.ValidationError("Invalid creation date for the Flashcard") | |
132 | + return value | |
103 | 133 | |
134 | + def validate_section(self, value): | |
135 | + if Section.objects.filter(pk=value).count() > 0: | |
136 | + return value | |
137 | + raise serializers.ValidationError("Invalid section for the flashcard") | |
104 | 138 | |
105 | -""" | |
106 | -class FlashcardMaskSerializer(ModelSerializer): | |
107 | - def validate_ranges(self, value): | |
108 | - try: | |
109 | - intervals = value.split(',') | |
110 | - for interval in intervals: | |
111 | - beg, end = interval.split('-') | |
112 | - if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end): | |
113 | - raise Exception # just a local exception | |
114 | - except Exception: | |
115 | - raise serializers.ValidationError("The mask is invalid") | |
139 | + def validate_text(self, value): | |
140 | + if len(value) > 255: | |
141 | + raise serializers.ValidationError("Flashcard text limit exceeded") | |
116 | 142 | return value |
117 | - class Meta: | |
118 | - model = FlashcardMask | |
119 | 143 | |
120 | -class UserFlashcard(ModelSerializer): | |
144 | + def validate_hide_reason(self, value): | |
145 | + if len(value) > 255: | |
146 | + raise serializers.ValidationError("Hide reason limit exceeded") | |
147 | + return value | |
148 | + | |
121 | 149 | class Meta: |
122 | - model = UserFlashcard | |
123 | -""" | |
150 | + model = Flashcard | |
151 | + exclude = 'author', 'mask', |
flashcards/tests/test_models.py
View file @
be6cc91
... | ... | @@ -48,14 +48,25 @@ |
48 | 48 | author=user, |
49 | 49 | material_date=datetime.now(), |
50 | 50 | previous=None, |
51 | - mask={(0,4), (24,34)}) | |
51 | + mask={(24,34), (0, 4)}) | |
52 | 52 | |
53 | 53 | def test_mask_field(self): |
54 | 54 | user = User.objects.get(email="none@none.com") |
55 | + section = Section.objects.get(course_title='how 2 test') | |
55 | 56 | flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") |
56 | 57 | self.assertTrue(isinstance(flashcard.mask, set)) |
57 | 58 | self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) |
58 | 59 | blank1, blank2 = sorted(list(flashcard.mask)) |
59 | 60 | self.assertEqual(flashcard.text[slice(*blank1)], 'This') |
60 | 61 | self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') |
62 | + try: | |
63 | + Flashcard.objects.create(text="This is the text of the Flashcard", | |
64 | + section=section, | |
65 | + author=user, | |
66 | + material_date=datetime.now(), | |
67 | + previous=None, | |
68 | + mask={(10,34), (0, 14)}) | |
69 | + self.fail() | |
70 | + except ValueError: | |
71 | + self.assertTrue(True) |
flashcards/views.py
View file @
be6cc91
1 | 1 | from django.contrib import auth |
2 | 2 | from flashcards.api import StandardResultsSetPagination |
3 | -from flashcards.models import Section, User, Flashcard, FlashcardReport | |
3 | +from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard | |
4 | 4 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ |
5 | 5 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer |
6 | 6 | from rest_framework.decorators import detail_route, permission_classes, api_view |
... | ... | @@ -235,16 +235,6 @@ |
235 | 235 | serializer_class = FlashcardSerializer |
236 | 236 | permission_classes = [IsAuthenticated] |
237 | 237 | |
238 | - def get(self, request, pk): | |
239 | - """ | |
240 | - Return the requested flashcard. | |
241 | - :param request: The request object. | |
242 | - :param pk: The primary key of the card to be retrieved | |
243 | - :return: A 200 OK request along with the card data. | |
244 | - """ | |
245 | - obj = self.queryset.get(pk=pk) | |
246 | - return Response(FlashcardSerializer(obj).data) | |
247 | - | |
248 | 238 | @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) |
249 | 239 | def report(self, request, pk): |
250 | 240 | """ |
... | ... | @@ -259,5 +249,19 @@ |
259 | 249 | obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) |
260 | 250 | obj.reason = request.data['reason'] |
261 | 251 | obj.save() |
252 | + return Response(status=HTTP_204_NO_CONTENT) | |
253 | + | |
254 | + @detail_route(methods=['POST'], permission_classes=[IsAuthenticated]) | |
255 | + def pull(self, request, pk): | |
256 | + """ | |
257 | + Pull a card from the live feed into the user's deck. | |
258 | + :param request: The request object | |
259 | + :param pk: The primary key | |
260 | + :return: A 204 response upon success. | |
261 | + """ | |
262 | + user = request.user | |
263 | + flashcard = Flashcard.objects.get(pk=pk) | |
264 | + user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) | |
265 | + user_card.save() | |
262 | 266 | return Response(status=HTTP_204_NO_CONTENT) |