From fe6a4ff639924bc30df4bf07fbb6af1ca4bff366 Mon Sep 17 00:00:00 2001 From: Rohan Rangray Date: Tue, 12 May 2015 00:27:56 -0700 Subject: [PATCH] Replaced FlashcardMask with MaskField --- flashcards/admin.py | 3 +- flashcards/fields.py | 84 +++++++++++++++++++++++++++++++++++++++++ flashcards/models.py | 15 +++++--- flashcards/serializers.py | 20 ++++++++++ flashcards/tests/test_models.py | 29 +++++++++++++- flashcards/views.py | 11 ++++++ 6 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 flashcards/fields.py diff --git a/flashcards/admin.py b/flashcards/admin.py index a6a1977..66ec113 100644 --- a/flashcards/admin.py +++ b/flashcards/admin.py @@ -1,11 +1,10 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin -from flashcards.models import Flashcard, UserFlashcard, Section, FlashcardMask, \ +from flashcards.models import Flashcard, UserFlashcard, Section, \ LecturePeriod, User, UserFlashcardQuiz admin.site.register([ Flashcard, - FlashcardMask, UserFlashcard, UserFlashcardQuiz, Section, diff --git a/flashcards/fields.py b/flashcards/fields.py new file mode 100644 index 0000000..532d297 --- /dev/null +++ b/flashcards/fields.py @@ -0,0 +1,84 @@ +__author__ = 'rray' + +from django.db import models + + +class MaskField(models.Field): + def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): + self.blank_sep = blank_sep + self.range_sep = range_sep + super(MaskField, self).__init__(*args, **kwargs) + + def deconstruct(self): + name, path, args, kwargs = super(MaskField, self).deconstruct() + if self.blank_sep != ',': + kwargs['blank_sep'] = self.blank_sep + if self.range_sep != '-': + kwargs['range_sep'] = self.range_sep + return name, path, args, kwargs + + def db_type(self, connection): + if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': + return 'varchar' + if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': + return 'integer[2][]' + + def from_db_value(self, value, expression, connection, context): + if value is None: + return value + if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': + return MaskField._sqlite_parse_mask(value) + if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': + return MaskField._psql_parse_mask(value) + + def get_db_prep_value(self, value, connection, prepared=False): + if not prepared: + value = self.get_prep_value(value) + if value is None: + return value + if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': + return ','.join(['-'.join(map(str, i)) for i in value]) + if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': + return value + + def to_python(self, value): + if value is None or isinstance(value, set): + return value + return MaskField._parse_mask(value) + + def get_prep_value(self, value): + if value is None: + 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]) + + def get_prep_lookup(self, lookup_type, value): + raise TypeError("Lookup not supported for MaskField") + + @staticmethod + def _parse_mask(intervals): + p_beg, p_end = -1, -1 + mask_list = [] + for interval in intervals: + 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)) + p_beg, p_end = beg, end + return set(mask_list) + + @staticmethod + def _sqlite_parse_mask(value): + intervals = [] + ranges = value.split(',') + for interval in ranges: + _range = interval.split('-') + 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)) + + @staticmethod + def _psql_parse_mask(value): + return MaskField._parse_mask(sorted(value)) diff --git a/flashcards/models.py b/flashcards/models.py index 89ef439..4fe4146 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, Abstr from django.db.models import * from django.utils.timezone import now from simple_email_confirmation import SimpleEmailConfirmationUserMixin +from fields import MaskField # Hack to fix AbstractUser before subclassing it AbstractUser._meta.get_field('email')._unique = True @@ -53,7 +54,8 @@ 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 = 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') unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") @@ -79,12 +81,14 @@ class UserFlashcard(Model): return self.pulled and not self.unpulled + +""" class FlashcardMask(Model): - """ A serialized list of character ranges that can be blanked out during a quiz. - This is encoded as '13-145,150-195' - """ + This is encoded as '13-145,150-195'. The ranges are 0-indexed and inclusive. + ranges = CharField(max_length=255) +""" class Flashcard(Model): @@ -97,7 +101,8 @@ 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 = 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: # By default, order by most recently pushed diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 9c100b4..574c1f6 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -102,3 +102,23 @@ class FlashcardSerializer(ModelSerializer): exclude = 'author', 'mask', +""" +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") + return value + class Meta: + model = FlashcardMask + +class UserFlashcard(ModelSerializer): + class Meta: + model = UserFlashcard +""" + diff --git a/flashcards/tests/test_models.py b/flashcards/tests/test_models.py index b7548c1..736f079 100644 --- a/flashcards/tests/test_models.py +++ b/flashcards/tests/test_models.py @@ -1,6 +1,6 @@ from django.test import TestCase -from flashcards.models import User, Section -from simple_email_confirmation import EmailAddress +from flashcards.models import User, Section, Flashcard +from datetime import datetime class RegistrationTests(TestCase): @@ -33,3 +33,28 @@ class UserTests(TestCase): self.assertEqual(user.sections.count(), 1) user.sections.remove(section) self.assertEqual(user.sections.count(), 0) + + +class FlashcardTests(TestCase): + def setUp(self): + user = User.objects.create_user(email="none@none.com", password="1234") + section = Section.objects.create(department='dept', + course_num='101a', + course_title='how 2 test', + instructor='George Lucas', + quarter='SP15') + Flashcard.objects.create(text="This is the text of the Flashcard", + section=section, + author=user, + material_date=datetime.now(), + previous=None, + mask={(0,4), (24,34)}) + + def test_mask_field(self): + user = User.objects.get(email="none@none.com") + 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') diff --git a/flashcards/views.py b/flashcards/views.py index 1c8fe0c..691164c 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -235,6 +235,16 @@ 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): """ @@ -250,3 +260,4 @@ class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): obj.reason = request.data['reason'] obj.save() return Response(status=HTTP_204_NO_CONTENT) + -- 1.9.1