Commit 7fd48b304d59b4fafbfe84acae4b2c7ea9671e2d
Exists in
master
Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend
Showing 5 changed files Side-by-side Diff
flashcards/fields.py
View file @
7fd48b3
1 | 1 | from django.db import models |
2 | +from validators import FlashcardMask, OverlapIntervalException | |
2 | 3 | |
3 | 4 | |
4 | 5 | class MaskField(models.Field): |
5 | 6 | |
6 | 7 | |
... | ... | @@ -37,16 +38,14 @@ |
37 | 38 | return ','.join(['-'.join(map(str, i)) for i in value]) |
38 | 39 | |
39 | 40 | def to_python(self, value): |
40 | - if value is None or isinstance(value, set): | |
41 | + if value is None: | |
41 | 42 | return value |
42 | - return MaskField._parse_mask(value) | |
43 | + return sorted(list(FlashcardMask(value))) | |
43 | 44 | |
44 | 45 | def get_prep_value(self, value): |
45 | 46 | if value is None: |
46 | 47 | return value |
47 | - if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]): | |
48 | - raise ValueError("Invalid value for MaskField attribute") | |
49 | - return self.__class__._parse_mask(sorted(value)) | |
48 | + return sorted(list(FlashcardMask(value))) | |
50 | 49 | |
51 | 50 | def get_prep_lookup(self, lookup_type, value): |
52 | 51 | raise TypeError("Lookup not supported for MaskField") |
53 | 52 | |
... | ... | @@ -65,16 +64,10 @@ |
65 | 64 | |
66 | 65 | @classmethod |
67 | 66 | def _varchar_parse_mask(cls, value): |
68 | - intervals = [] | |
69 | - ranges = value.split(',') | |
70 | - for interval in ranges: | |
71 | - _range = interval.split('-') | |
72 | - if len(_range) != 2 or not all(map(unicode.isdigit, _range)): | |
73 | - raise ValueError("Invalid range format.") | |
74 | - intervals.append(tuple(_range)) | |
75 | - return set([tuple(i) for i in cls._parse_mask(sorted(intervals))]) | |
67 | + mask = [tuple(map(int, i.split('-'))) for i in value.split(',')] | |
68 | + return FlashcardMask(mask) | |
76 | 69 | |
77 | 70 | @classmethod |
78 | 71 | def _psql_parse_mask(cls, value): |
79 | - return set([tuple(i) for i in cls._parse_mask(sorted(value))]) | |
72 | + return FlashcardMask(value) |
flashcards/serializers.py
View file @
7fd48b3
1 | 1 | from django.utils.datetime_safe import datetime |
2 | 2 | from flashcards.models import Section, LecturePeriod, User, Flashcard |
3 | +from flashcards.validators import FlashcardMask, OverlapIntervalException | |
3 | 4 | from rest_framework import serializers |
4 | 5 | from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField |
5 | 6 | from rest_framework.relations import HyperlinkedRelatedField |
6 | 7 | |
7 | 8 | |
8 | 9 | |
... | ... | @@ -90,19 +91,23 @@ |
90 | 91 | } |
91 | 92 | |
92 | 93 | 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)) | |
94 | + return dumps(list(self._make_mask(value))) | |
96 | 95 | |
97 | - def to_internal_value(self, data): | |
96 | + def to_internal_value(self, value): | |
97 | + return self._make_mask(value) | |
98 | + | |
99 | + def _make_mask(self, data): | |
98 | 100 | 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 | |
101 | + mask = FlashcardMask(loads(data)) | |
103 | 102 | except ValueError: |
104 | 103 | raise serializers.ValidationError("Invalid JSON for MaskField") |
105 | - return set([tuple(i) for i in intervals]) | |
104 | + except TypeError: | |
105 | + raise serializers.ValidationError("Invalid data for MaskField.") | |
106 | + except OverlapIntervalException: | |
107 | + raise serializers.ValidationError("Invalid intervals for MaskField data.") | |
108 | + if len(mask) > 32: | |
109 | + raise serializers.ValidationError("Too many intervals in the mask.") | |
110 | + return mask | |
106 | 111 | |
107 | 112 | |
108 | 113 | class FlashcardSerializer(ModelSerializer): |
flashcards/tests/test_models.py
View file @
7fd48b3
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 | |
3 | 3 | from django.test import TestCase |
4 | 4 | from flashcards.models import User, Section, Flashcard |
5 | +from flashcards.validators import OverlapIntervalException | |
5 | 6 | |
6 | 7 | |
7 | 8 | class RegistrationTests(TestCase): |
... | ... | @@ -68,6 +69,6 @@ |
68 | 69 | previous=None, |
69 | 70 | mask={(10,34), (0, 14)}) |
70 | 71 | self.fail() |
71 | - except ValueError: | |
72 | + except OverlapIntervalException: | |
72 | 73 | self.assertTrue(True) |
flashcards/validators.py
View file @
7fd48b3
1 | +__author__ = 'rray' | |
2 | + | |
3 | +from collections import Iterable | |
4 | + | |
5 | + | |
6 | +class FlashcardMask(set): | |
7 | + def __init__(self, *args, **kwargs): | |
8 | + super(FlashcardMask, self).__init__(*args, **kwargs) | |
9 | + self._iterable_check() | |
10 | + self._interval_check() | |
11 | + self._overlap_check() | |
12 | + | |
13 | + def _iterable_check(self): | |
14 | + if not all([isinstance(i, Iterable) for i in self]): | |
15 | + raise TypeError("Interval not a valid iterable") | |
16 | + | |
17 | + def _interval_check(self): | |
18 | + if not all([len(i) == 2 for i in self]): | |
19 | + raise TypeError("Intervals must have exactly 2 elements, begin and end") | |
20 | + | |
21 | + def _overlap_check(self): | |
22 | + p_beg, p_end = -1, -1 | |
23 | + for interval in sorted(self): | |
24 | + beg, end = map(int, interval) | |
25 | + if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): | |
26 | + raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask") | |
27 | + p_beg, p_end = beg, end | |
28 | + | |
29 | + | |
30 | +class OverlapIntervalException(Exception): | |
31 | + def __init__(self, interval, reason): | |
32 | + self.interval = interval | |
33 | + self.reason = reason | |
34 | + | |
35 | + def __str__(self): | |
36 | + return repr(self.reason) + ': ' + repr(self.interval) |
flashcards/views.py
View file @
7fd48b3
... | ... | @@ -284,7 +284,7 @@ |
284 | 284 | :return: A 204 response upon success. |
285 | 285 | """ |
286 | 286 | user = request.user |
287 | - flashcard = Flashcard.objects.get(pk=pk) | |
287 | + flashcard = self.get_object() | |
288 | 288 | user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) |
289 | 289 | user_card.save() |
290 | 290 | return Response(status=HTTP_204_NO_CONTENT) |