Commit 67b19d6d9484b0dce4183e406f7c4ce04119e5b8
1 parent
2f1be78a16
Exists in
master
Subclasses set to create a FlashcardMask class. Moved Mask validation logic from…
… model MaskField to the FlashcardMask class
Showing 4 changed files with 46 additions and 16 deletions Side-by-side Diff
flashcards/fields.py
View file @
67b19d6
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/tests/test_models.py
View file @
67b19d6
... | ... | @@ -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 @
67b19d6
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 @
67b19d6
... | ... | @@ -273,7 +273,7 @@ |
273 | 273 | :return: A 204 response upon success. |
274 | 274 | """ |
275 | 275 | user = request.user |
276 | - flashcard = Flashcard.objects.get(pk=pk) | |
276 | + flashcard = self.get_object() | |
277 | 277 | user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) |
278 | 278 | user_card.save() |
279 | 279 | return Response(status=HTTP_204_NO_CONTENT) |