Commit fe6a4ff639924bc30df4bf07fbb6af1ca4bff366

Authored by Rohan Rangray
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 """