Commit fe6a4ff639924bc30df4bf07fbb6af1ca4bff366
1 parent
ec4e278d01
Exists in
master
Replaced FlashcardMask with MaskField
Showing 6 changed files with 153 additions and 9 deletions Side-by-side Diff
flashcards/admin.py
View file @
fe6a4ff
1 | 1 | from django.contrib import admin |
2 | 2 | from django.contrib.auth.admin import UserAdmin |
3 | -from flashcards.models import Flashcard, UserFlashcard, Section, FlashcardMask, \ | |
3 | +from flashcards.models import Flashcard, UserFlashcard, Section, \ | |
4 | 4 | LecturePeriod, User, UserFlashcardQuiz |
5 | 5 | |
6 | 6 | admin.site.register([ |
7 | 7 | Flashcard, |
8 | - FlashcardMask, | |
9 | 8 | UserFlashcard, |
10 | 9 | UserFlashcardQuiz, |
11 | 10 | Section, |
flashcards/fields.py
View file @
fe6a4ff
1 | +__author__ = 'rray' | |
2 | + | |
3 | +from django.db import models | |
4 | + | |
5 | + | |
6 | +class MaskField(models.Field): | |
7 | + def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): | |
8 | + self.blank_sep = blank_sep | |
9 | + self.range_sep = range_sep | |
10 | + super(MaskField, self).__init__(*args, **kwargs) | |
11 | + | |
12 | + def deconstruct(self): | |
13 | + name, path, args, kwargs = super(MaskField, self).deconstruct() | |
14 | + if self.blank_sep != ',': | |
15 | + kwargs['blank_sep'] = self.blank_sep | |
16 | + if self.range_sep != '-': | |
17 | + kwargs['range_sep'] = self.range_sep | |
18 | + return name, path, args, kwargs | |
19 | + | |
20 | + def db_type(self, connection): | |
21 | + if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': | |
22 | + return 'varchar' | |
23 | + if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': | |
24 | + return 'integer[2][]' | |
25 | + | |
26 | + def from_db_value(self, value, expression, connection, context): | |
27 | + if value is None: | |
28 | + return value | |
29 | + if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': | |
30 | + return MaskField._sqlite_parse_mask(value) | |
31 | + if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': | |
32 | + return MaskField._psql_parse_mask(value) | |
33 | + | |
34 | + def get_db_prep_value(self, value, connection, prepared=False): | |
35 | + if not prepared: | |
36 | + value = self.get_prep_value(value) | |
37 | + if value is None: | |
38 | + return value | |
39 | + if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': | |
40 | + return ','.join(['-'.join(map(str, i)) for i in value]) | |
41 | + if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': | |
42 | + return value | |
43 | + | |
44 | + def to_python(self, value): | |
45 | + if value is None or isinstance(value, set): | |
46 | + return value | |
47 | + return MaskField._parse_mask(value) | |
48 | + | |
49 | + def get_prep_value(self, value): | |
50 | + if value is None: | |
51 | + return value | |
52 | + if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]): | |
53 | + raise ValueError("Invalid value for MaskField attribute") | |
54 | + return sorted([list(interval) for interval in value]) | |
55 | + | |
56 | + def get_prep_lookup(self, lookup_type, value): | |
57 | + raise TypeError("Lookup not supported for MaskField") | |
58 | + | |
59 | + @staticmethod | |
60 | + def _parse_mask(intervals): | |
61 | + p_beg, p_end = -1, -1 | |
62 | + mask_list = [] | |
63 | + for interval in intervals: | |
64 | + beg, end = map(int, interval) | |
65 | + if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): | |
66 | + raise ValueError("Invalid range offsets in the mask") | |
67 | + mask_list.append((beg, end)) | |
68 | + p_beg, p_end = beg, end | |
69 | + return set(mask_list) | |
70 | + | |
71 | + @staticmethod | |
72 | + def _sqlite_parse_mask(value): | |
73 | + intervals = [] | |
74 | + ranges = value.split(',') | |
75 | + for interval in ranges: | |
76 | + _range = interval.split('-') | |
77 | + if len(_range) != 2 or not all(map(unicode.isdigit, _range)): | |
78 | + raise ValueError("Invalid range format.") | |
79 | + intervals.append(tuple(_range)) | |
80 | + return MaskField._parse_mask(sorted(intervals)) | |
81 | + | |
82 | + @staticmethod | |
83 | + def _psql_parse_mask(value): | |
84 | + return MaskField._parse_mask(sorted(value)) |
flashcards/models.py
View file @
fe6a4ff
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 | from django.db.models import * |
3 | 3 | from django.utils.timezone import now |
4 | 4 | from simple_email_confirmation import SimpleEmailConfirmationUserMixin |
5 | +from fields import MaskField | |
5 | 6 | |
6 | 7 | # Hack to fix AbstractUser before subclassing it |
7 | 8 | AbstractUser._meta.get_field('email')._unique = True |
... | ... | @@ -53,7 +54,8 @@ |
53 | 54 | 3. A user has a flashcard hidden from them |
54 | 55 | """ |
55 | 56 | user = ForeignKey('User') |
56 | - mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") | |
57 | + # mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") | |
58 | + mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card") | |
57 | 59 | pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") |
58 | 60 | flashcard = ForeignKey('Flashcard') |
59 | 61 | unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") |
60 | 62 | |
61 | 63 | |
62 | 64 | |
... | ... | @@ -79,12 +81,14 @@ |
79 | 81 | return self.pulled and not self.unpulled |
80 | 82 | |
81 | 83 | |
84 | + | |
85 | +""" | |
82 | 86 | class FlashcardMask(Model): |
83 | - """ | |
84 | 87 | A serialized list of character ranges that can be blanked out during a quiz. |
85 | - This is encoded as '13-145,150-195' | |
86 | - """ | |
88 | + This is encoded as '13-145,150-195'. The ranges are 0-indexed and inclusive. | |
89 | + | |
87 | 90 | ranges = CharField(max_length=255) |
91 | +""" | |
88 | 92 | |
89 | 93 | |
90 | 94 | class Flashcard(Model): |
... | ... | @@ -97,7 +101,8 @@ |
97 | 101 | author = ForeignKey(User) |
98 | 102 | is_hidden = BooleanField(default=False) |
99 | 103 | hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") |
100 | - mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") | |
104 | + # mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") | |
105 | + mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") | |
101 | 106 | |
102 | 107 | class Meta: |
103 | 108 | # By default, order by most recently pushed |
flashcards/serializers.py
View file @
fe6a4ff
... | ... | @@ -100,4 +100,25 @@ |
100 | 100 | class Meta: |
101 | 101 | model = Flashcard |
102 | 102 | exclude = 'author', 'mask', |
103 | + | |
104 | + | |
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") | |
116 | + return value | |
117 | + class Meta: | |
118 | + model = FlashcardMask | |
119 | + | |
120 | +class UserFlashcard(ModelSerializer): | |
121 | + class Meta: | |
122 | + model = UserFlashcard | |
123 | +""" |
flashcards/tests/test_models.py
View file @
fe6a4ff
1 | 1 | from django.test import TestCase |
2 | -from flashcards.models import User, Section | |
3 | -from simple_email_confirmation import EmailAddress | |
2 | +from flashcards.models import User, Section, Flashcard | |
3 | +from datetime import datetime | |
4 | 4 | |
5 | 5 | |
6 | 6 | class RegistrationTests(TestCase): |
... | ... | @@ -33,4 +33,29 @@ |
33 | 33 | self.assertEqual(user.sections.count(), 1) |
34 | 34 | user.sections.remove(section) |
35 | 35 | self.assertEqual(user.sections.count(), 0) |
36 | + | |
37 | + | |
38 | +class FlashcardTests(TestCase): | |
39 | + def setUp(self): | |
40 | + user = User.objects.create_user(email="none@none.com", password="1234") | |
41 | + section = Section.objects.create(department='dept', | |
42 | + course_num='101a', | |
43 | + course_title='how 2 test', | |
44 | + instructor='George Lucas', | |
45 | + quarter='SP15') | |
46 | + Flashcard.objects.create(text="This is the text of the Flashcard", | |
47 | + section=section, | |
48 | + author=user, | |
49 | + material_date=datetime.now(), | |
50 | + previous=None, | |
51 | + mask={(0,4), (24,34)}) | |
52 | + | |
53 | + def test_mask_field(self): | |
54 | + user = User.objects.get(email="none@none.com") | |
55 | + flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") | |
56 | + self.assertTrue(isinstance(flashcard.mask, set)) | |
57 | + self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) | |
58 | + blank1, blank2 = sorted(list(flashcard.mask)) | |
59 | + self.assertEqual(flashcard.text[slice(*blank1)], 'This') | |
60 | + self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') |
flashcards/views.py
View file @
fe6a4ff
... | ... | @@ -235,6 +235,16 @@ |
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 | + | |
238 | 248 | @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) |
239 | 249 | def report(self, request, pk): |
240 | 250 | """ |