Commit be6cc9169b8e3a3dd1b54ede99939a45b43ec207

Authored by Rohan Rangray
1 parent 5354852580
Exists in master

Fixed a bug in the MaskField modelfield implementation. Added MaskField serializer field.

Showing 5 changed files with 82 additions and 43 deletions Inline Diff

flashcards/fields.py View file @ be6cc91
__author__ = 'rray' 1 1 __author__ = 'rray'
2 2
from django.db import models 3 3 from django.db import models
4 4
5 5
class MaskField(models.Field): 6 6 class MaskField(models.Field):
def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): 7 7 def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs):
self.blank_sep = blank_sep 8 8 self.blank_sep = blank_sep
self.range_sep = range_sep 9 9 self.range_sep = range_sep
super(MaskField, self).__init__(*args, **kwargs) 10 10 super(MaskField, self).__init__(*args, **kwargs)
11 11
def deconstruct(self): 12 12 def deconstruct(self):
name, path, args, kwargs = super(MaskField, self).deconstruct() 13 13 name, path, args, kwargs = super(MaskField, self).deconstruct()
if self.blank_sep != ',': 14 14 if self.blank_sep != ',':
kwargs['blank_sep'] = self.blank_sep 15 15 kwargs['blank_sep'] = self.blank_sep
if self.range_sep != '-': 16 16 if self.range_sep != '-':
kwargs['range_sep'] = self.range_sep 17 17 kwargs['range_sep'] = self.range_sep
return name, path, args, kwargs 18 18 return name, path, args, kwargs
19 19
def db_type(self, connection): 20 20 def db_type(self, connection):
if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': 21 21 if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
return 'varchar' 22 22 return 'varchar'
if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': 23 23 if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
return 'integer[2][]' 24 24 return 'integer[2][]'
25 25
def from_db_value(self, value, expression, connection, context): 26 26 def from_db_value(self, value, expression, connection, context):
if value is None: 27 27 if value is None:
return value 28 28 return value
if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': 29 29 if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
return MaskField._sqlite_parse_mask(value) 30 30 return MaskField._sqlite_parse_mask(value)
if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': 31 31 if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
return MaskField._psql_parse_mask(value) 32 32 return MaskField._psql_parse_mask(value)
33 33
def get_db_prep_value(self, value, connection, prepared=False): 34 34 def get_db_prep_value(self, value, connection, prepared=False):
if not prepared: 35 35 if not prepared:
value = self.get_prep_value(value) 36 36 value = self.get_prep_value(value)
if value is None: 37 37 if value is None:
return value 38 38 return value
if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3': 39 39 if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
return ','.join(['-'.join(map(str, i)) for i in value]) 40 40 return ','.join(['-'.join(map(str, i)) for i in value])
if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2': 41 41 if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
return value 42 42 return value
43 43
def to_python(self, value): 44 44 def to_python(self, value):
if value is None or isinstance(value, set): 45 45 if value is None or isinstance(value, set):
return value 46 46 return value
return MaskField._parse_mask(value) 47 47 return MaskField._parse_mask(value)
48 48
def get_prep_value(self, value): 49 49 def get_prep_value(self, value):
if value is None: 50 50 if value is None:
return value 51 51 return value
if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]): 52 52 if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]):
raise ValueError("Invalid value for MaskField attribute") 53 53 raise ValueError("Invalid value for MaskField attribute")
return sorted([list(interval) for interval in value]) 54 54 return MaskField._parse_mask(sorted(value))
55 55
def get_prep_lookup(self, lookup_type, value): 56 56 def get_prep_lookup(self, lookup_type, value):
raise TypeError("Lookup not supported for MaskField") 57 57 raise TypeError("Lookup not supported for MaskField")
58 58
@staticmethod 59 59 @staticmethod
def _parse_mask(intervals): 60 60 def _parse_mask(intervals):
p_beg, p_end = -1, -1 61 61 p_beg, p_end = -1, -1
mask_list = [] 62 62 mask_list = []
for interval in intervals: 63 63 for interval in intervals:
beg, end = map(int, interval) 64 64 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 65 65 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") 66 66 raise ValueError("Invalid range offsets in the mask")
mask_list.append((beg, end)) 67 67 mask_list.append([beg, end])
p_beg, p_end = beg, end 68 68 p_beg, p_end = beg, end
return set(mask_list) 69 69 return mask_list
70 70
@staticmethod 71 71 @staticmethod
def _sqlite_parse_mask(value): 72 72 def _sqlite_parse_mask(value):
intervals = [] 73 73 intervals = []
ranges = value.split(',') 74 74 ranges = value.split(',')
for interval in ranges: 75 75 for interval in ranges:
_range = interval.split('-') 76 76 _range = interval.split('-')
flashcards/models.py View file @ be6cc91
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, AbstractUser, UserManager
from django.db.models import * 2 2 from django.db.models import *
from django.utils.timezone import now 3 3 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 4 4 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 5 5 from fields import MaskField
6 6
# Hack to fix AbstractUser before subclassing it 7 7 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 8 8 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 9 9 AbstractUser._meta.get_field('username')._unique = False
10 10
11 11
class EmailOnlyUserManager(UserManager): 12 12 class EmailOnlyUserManager(UserManager):
""" 13 13 """
A tiny extension of Django's UserManager which correctly creates users 14 14 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 15 15 without usernames (using emails instead).
""" 16 16 """
17 17
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 18 18 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 19 19 """
Creates and saves a User with the given email and password. 20 20 Creates and saves a User with the given email and password.
""" 21 21 """
email = self.normalize_email(email) 22 22 email = self.normalize_email(email)
user = self.model(email=email, 23 23 user = self.model(email=email,
is_staff=is_staff, is_active=True, 24 24 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 25 25 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 26 26 date_joined=now(), **extra_fields)
user.set_password(password) 27 27 user.set_password(password)
user.save(using=self._db) 28 28 user.save(using=self._db)
return user 29 29 return user
30 30
def create_user(self, email, password=None, **extra_fields): 31 31 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 32 32 return self._create_user(email, password, False, False, **extra_fields)
33 33
def create_superuser(self, email, password, **extra_fields): 34 34 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 35 35 return self._create_user(email, password, True, True, **extra_fields)
36 36
37 37
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 38 38 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 39 39 """
An extension of Django's default user model. 40 40 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 41 41 We use email as the username field, and include enrolled sections here
""" 42 42 """
objects = EmailOnlyUserManager() 43 43 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 44 44 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 45 45 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 46 46 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
47 47
48 48
class UserFlashcard(Model): 49 49 class UserFlashcard(Model):
""" 50 50 """
Represents the relationship between a user and a flashcard by: 51 51 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 52 52 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 53 53 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 54 54 3. A user has a flashcard hidden from them
""" 55 55 """
user = ForeignKey('User') 56 56 user = ForeignKey('User')
# mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") 57
mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card") 58 57 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") 59 58 pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 60 59 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 61 60 unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card")
62 61
class Meta: 63 62 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 64 63 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 65 64 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 66 65 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 67 66 # By default, order by most recently pulled
ordering = ['-pulled'] 68 67 ordering = ['-pulled']
69 68
def is_hidden(self): 70 69 def is_hidden(self):
""" 71 70 """
A card is hidden only if a user has not ever added it to their deck. 72 71 A card is hidden only if a user has not ever added it to their deck.
:return: Whether the flashcard is hidden from the user 73 72 :return: Whether the flashcard is hidden from the user
""" 74 73 """
return not self.pulled 75 74 return not self.pulled
76 75
def is_in_deck(self): 77 76 def is_in_deck(self):
""" 78 77 """
:return:Whether the flashcard is in the user's deck 79 78 :return:Whether the flashcard is in the user's deck
""" 80 79 """
return self.pulled and not self.unpulled 81 80 return self.pulled and not self.unpulled
82 81
83 82
84 83
""" 85 84 """
class FlashcardMask(Model): 86 85 class FlashcardMask(Model):
A serialized list of character ranges that can be blanked out during a quiz. 87 86 A serialized list of character ranges that can be blanked out during a quiz.
This is encoded as '13-145,150-195'. The ranges are 0-indexed and inclusive. 88 87 This is encoded as '13-145,150-195'. The ranges are 0-indexed and inclusive.
89 88
ranges = CharField(max_length=255) 90 89 ranges = CharField(max_length=255)
""" 91 90 """
92 91
93 92
class Flashcard(Model): 94 93 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 95 94 text = CharField(max_length=255, help_text='The text on the card')
section = ForeignKey('Section', help_text='The section with which the card is associated') 96 95 section = ForeignKey('Section', help_text='The section with which the card is associated')
pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") 97 96 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(help_text="The date with which the card is associated") 98 97 material_date = DateTimeField(help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 99 98 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 100 99 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 101 100 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 102 101 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 103 102 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") 104
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 105 103 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
106 104
class Meta: 107 105 class Meta:
# By default, order by most recently pushed 108 106 # By default, order by most recently pushed
ordering = ['-pushed'] 109 107 ordering = ['-pushed']
110 108
def is_hidden_from(self, user): 111 109 def is_hidden_from(self, user):
""" 112 110 """
A card can be hidden globally, but if a user has the card in their deck, 113 111 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 114 112 this visibility overrides a global hide.
:param user: 115 113 :param user:
:return: Whether the card is hidden from the user. 116 114 :return: Whether the card is hidden from the user.
""" 117 115 """
result = user.userflashcard_set.filter(flashcard=self) 118 116 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 119 117 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 120 118 return result[0].is_hidden()
121 119
122 120
@classmethod 123 121 @classmethod
def cards_visible_to(cls, user): 124 122 def cards_visible_to(cls, user):
""" 125 123 """
:param user: 126 124 :param user:
:return: A queryset with all cards that should be visible to a user. 127 125 :return: A queryset with all cards that should be visible to a user.
""" 128 126 """
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) 129 127 return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None)
130 128
131 129
class UserFlashcardQuiz(Model): 132 130 class UserFlashcardQuiz(Model):
""" 133 131 """
An event of a user being quizzed on a flashcard. 134 132 An event of a user being quizzed on a flashcard.
""" 135 133 """
user_flashcard = ForeignKey(UserFlashcard) 136 134 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField() 137 135 when = DateTimeField()
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 138 136 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") 139 137 response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 140 138 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
141 139
def status(self): 142 140 def status(self):
""" 143 141 """
There are three stages of a quiz object: 144 142 There are three stages of a quiz object:
1. the user has been shown the card 145 143 1. the user has been shown the card
2. the user has answered the card 146 144 2. the user has answered the card
3. the user has self-evaluated their response's correctness 147 145 3. the user has self-evaluated their response's correctness
148 146
:return: string (evaluated, answered, viewed) 149 147 :return: string (evaluated, answered, viewed)
""" 150 148 """
if self.correct is not None: return "evaluated" 151 149 if self.correct is not None: return "evaluated"
if self.response: return "answered" 152 150 if self.response: return "answered"
return "viewed" 153 151 return "viewed"
154 152
155 153
class Section(Model): 156 154 class Section(Model):
""" 157 155 """
A UCSD course taught by an instructor during a quarter. 158 156 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 159 157 We use the term "section" to avoid collision with the builtin keyword "class"
""" 160 158 """
department = CharField(max_length=50) 161 159 department = CharField(max_length=50)
course_num = CharField(max_length=6) 162 160 course_num = CharField(max_length=6)
course_title = CharField(max_length=50) 163 161 course_title = CharField(max_length=50)
instructor = CharField(max_length=100) 164 162 instructor = CharField(max_length=100)
quarter = CharField(max_length=4) 165 163 quarter = CharField(max_length=4)
166 164
def is_whitelisted(self): 167 165 def is_whitelisted(self):
""" 168 166 """
:return: whether a whitelist exists for this section 169 167 :return: whether a whitelist exists for this section
""" 170 168 """
return self.whitelist.exists() 171 169 return self.whitelist.exists()
172 170
def is_user_on_whitelist(self, user): 173 171 def is_user_on_whitelist(self, user):
flashcards/serializers.py View file @ be6cc91
from django.utils.datetime_safe import datetime 1 1 from django.utils.datetime_safe import datetime
from flashcards.models import Section, LecturePeriod, User, Flashcard 2 2 from flashcards.models import Section, LecturePeriod, User, Flashcard
from rest_framework import serializers 3 3 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField 4 4 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField
from rest_framework.relations import HyperlinkedRelatedField 5 5 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 6 6 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 7 7 from rest_framework.validators import UniqueValidator
8 from json import dumps, loads
8 9
9 10
class EmailSerializer(Serializer): 10 11 class EmailSerializer(Serializer):
email = EmailField(required=True) 11 12 email = EmailField(required=True)
12 13
13 14
class EmailPasswordSerializer(EmailSerializer): 14 15 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 15 16 password = CharField(required=True)
16 17
17 18
class RegistrationSerializer(EmailPasswordSerializer): 18 19 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 19 20 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
20 21
21 22
class PasswordResetRequestSerializer(EmailSerializer): 22 23 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 23 24 def validate_email(self, value):
try: 24 25 try:
User.objects.get(email=value) 25 26 User.objects.get(email=value)
return value 26 27 return value
except User.DoesNotExist: 27 28 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 28 29 raise serializers.ValidationError('No user exists with that email')
29 30
30 31
class PasswordResetSerializer(Serializer): 31 32 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 32 33 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 33 34 uid = IntegerField(required=True)
token = CharField(required=True) 34 35 token = CharField(required=True)
35 36
def validate_uid(self, value): 36 37 def validate_uid(self, value):
try: 37 38 try:
User.objects.get(id=value) 38 39 User.objects.get(id=value)
return value 39 40 return value
except User.DoesNotExist: 40 41 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 41 42 raise serializers.ValidationError('Could not verify reset token')
42 43
43 44
class UserUpdateSerializer(Serializer): 44 45 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 45 46 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 46 47 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 47 48 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 48 49 # reset_token = CharField(required=False)
49 50
def validate(self, data): 50 51 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 51 52 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 52 53 raise serializers.ValidationError('old_password is required to set a new_password')
return data 53 54 return data
54 55
55 56
class Password(Serializer): 56 57 class Password(Serializer):
email = EmailField(required=True) 57 58 email = EmailField(required=True)
password = CharField(required=True) 58 59 password = CharField(required=True)
59 60
60 61
class LecturePeriodSerializer(ModelSerializer): 61 62 class LecturePeriodSerializer(ModelSerializer):
class Meta: 62 63 class Meta:
model = LecturePeriod 63 64 model = LecturePeriod
exclude = 'id', 'section' 64 65 exclude = 'id', 'section'
65 66
66 67
class SectionSerializer(ModelSerializer): 67 68 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 68 69 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
69 70
class Meta: 70 71 class Meta:
model = Section 71 72 model = Section
72 73
73 74
class UserSerializer(ModelSerializer): 74 75 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 75 76 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 76 77 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 77 78 is_confirmed = BooleanField()
78 79
class Meta: 79 80 class Meta:
model = User 80 81 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 81 82 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
82 83
83 84
85 class MaskFieldSerializer(serializers.Field):
86 default_error_messages = {
87 'max_length': 'Ensure this field has no more than {max_length} characters.',
88 'interval': 'Ensure this field has valid intervals.',
89 'overlap': 'Ensure this field does not have overlapping intervals.'
90 }
91
92 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))
96
97 def to_internal_value(self, data):
98 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
103 except ValueError:
104 raise serializers.ValidationError("Invalid JSON for MaskField")
105 return set([tuple(i) for i in intervals])
106
107
class FlashcardSerializer(ModelSerializer): 84 108 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 85 109 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 86 110 hide_reason = CharField(read_only=True)
111 mask = MaskFieldSerializer()
87 112
def validate_material_date(self, value): 88 113 def validate_material_date(self, value):
# TODO: make this dynamic 89 114 # TODO: make this dynamic
quarter_start = datetime(2015, 3, 15) 90 115 quarter_start = datetime(2015, 3, 15)
quarter_end = datetime(2015, 6, 15) 91 116 quarter_end = datetime(2015, 6, 15)
if quarter_start <= value <= quarter_end: return value 92 117 if quarter_start <= value <= quarter_end:
118 return value
else: 93 119 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 94 120 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
95 121
# 96 122 def validate_previous(self, value):
# def validate(self, data): 97 123 if value is None:
# if 98 124 return value
125 if Flashcard.objects.filter(pk=value).count() > 0:
126 return value
127 raise serializers.ValidationError("Invalid previous Flashcard object")
99 128
class Meta: 100 129 def validate_pushed(self, value):
model = Flashcard 101 130 if value > datetime.now():
exclude = 'author', 'mask', 102 131 raise serializers.ValidationError("Invalid creation date for the Flashcard")
132 return value
103 133
134 def validate_section(self, value):
135 if Section.objects.filter(pk=value).count() > 0:
136 return value
137 raise serializers.ValidationError("Invalid section for the flashcard")
104 138
""" 105 139 def validate_text(self, value):
class FlashcardMaskSerializer(ModelSerializer): 106 140 if len(value) > 255:
def validate_ranges(self, value): 107 141 raise serializers.ValidationError("Flashcard text limit exceeded")
try: 108
flashcards/tests/test_models.py View file @ be6cc91
from django.test import TestCase 1 1 from django.test import TestCase
from flashcards.models import User, Section, Flashcard 2 2 from flashcards.models import User, Section, Flashcard
from datetime import datetime 3 3 from datetime import datetime
4 4
5 5
class RegistrationTests(TestCase): 6 6 class RegistrationTests(TestCase):
def setUp(self): 7 7 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 8 8 User.objects.create_user(email="none@none.com", password="1234")
9 9
def test_email_confirmation(self): 10 10 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 11 11 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 12 12 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 13 13 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 14 14 self.assertTrue(user.is_confirmed)
15 15
16 16
class UserTests(TestCase): 17 17 class UserTests(TestCase):
def setUp(self): 18 18 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 19 19 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 20 20 Section.objects.create(department='dept',
course_num='101a', 21 21 course_num='101a',
course_title='how 2 test', 22 22 course_title='how 2 test',
instructor='George Lucas', 23 23 instructor='George Lucas',
quarter='SP15') 24 24 quarter='SP15')
25 25
def test_section_list(self): 26 26 def test_section_list(self):
section = Section.objects.get(course_num='101a') 27 27 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 28 28 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 29 29 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 30 30 user.sections.add(section)
self.assertIn(section, user.sections.all()) 31 31 self.assertIn(section, user.sections.all())
user.sections.add(section) 32 32 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 33 33 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 34 34 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 35 35 self.assertEqual(user.sections.count(), 0)
36 36
37 37
class FlashcardTests(TestCase): 38 38 class FlashcardTests(TestCase):
def setUp(self): 39 39 def setUp(self):
user = User.objects.create_user(email="none@none.com", password="1234") 40 40 user = User.objects.create_user(email="none@none.com", password="1234")
section = Section.objects.create(department='dept', 41 41 section = Section.objects.create(department='dept',
course_num='101a', 42 42 course_num='101a',
course_title='how 2 test', 43 43 course_title='how 2 test',
instructor='George Lucas', 44 44 instructor='George Lucas',
quarter='SP15') 45 45 quarter='SP15')
Flashcard.objects.create(text="This is the text of the Flashcard", 46 46 Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 47 47 section=section,
author=user, 48 48 author=user,
material_date=datetime.now(), 49 49 material_date=datetime.now(),
previous=None, 50 50 previous=None,
mask={(0,4), (24,34)}) 51 51 mask={(24,34), (0, 4)})
52 52
def test_mask_field(self): 53 53 def test_mask_field(self):
user = User.objects.get(email="none@none.com") 54 54 user = User.objects.get(email="none@none.com")
55 section = Section.objects.get(course_title='how 2 test')
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 55 56 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
self.assertTrue(isinstance(flashcard.mask, set)) 56 57 self.assertTrue(isinstance(flashcard.mask, set))
self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) 57 58 self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask]))
blank1, blank2 = sorted(list(flashcard.mask)) 58 59 blank1, blank2 = sorted(list(flashcard.mask))
self.assertEqual(flashcard.text[slice(*blank1)], 'This') 59 60 self.assertEqual(flashcard.text[slice(*blank1)], 'This')
self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') 60 61 self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard')
62 try:
63 Flashcard.objects.create(text="This is the text of the Flashcard",
flashcards/views.py View file @ be6cc91
from django.contrib import auth 1 1 from django.contrib import auth
from flashcards.api import StandardResultsSetPagination 2 2 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport 3 3 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 4 4 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer 5 5 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view 6 6 from rest_framework.decorators import detail_route, permission_classes, api_view
from rest_framework.generics import ListAPIView, DestroyAPIView, GenericAPIView 7 7 from rest_framework.generics import ListAPIView, DestroyAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 8 8 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 9 9 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 10 10 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 11 11 from django.core.mail import send_mail
from django.contrib.auth import authenticate 12 12 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 13 13 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 14 14 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 15 15 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 16 16 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 17 17 from simple_email_confirmation import EmailAddress
18 18
19 19
class SectionViewSet(ReadOnlyModelViewSet): 20 20 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 21 21 queryset = Section.objects.all()
serializer_class = SectionSerializer 22 22 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 23 23 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 24 24 permission_classes = [IsAuthenticated]
25 25
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 26 26 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 27 27 def enroll(self, request, pk):
""" 28 28 """
Add the current user to a specified section 29 29 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 30 30 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 31 31 ---
omit_serializer: true 32 32 omit_serializer: true
parameters: 33 33 parameters:
- fake: None 34 34 - fake: None
parameters_strategy: 35 35 parameters_strategy:
form: replace 36 36 form: replace
""" 37 37 """
section = self.get_object() 38 38 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 39 39 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 40 40 raise ValidationError("You are already in this section.")
if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): 41 41 if section.is_whitelisted() and not section.is_user_on_whitelist(request.user):
raise PermissionDenied("You must be on the whitelist to add this section.") 42 42 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 43 43 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 44 44 return Response(status=HTTP_204_NO_CONTENT)
45 45
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 46 46 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 47 47 def drop(self, request, pk):
""" 48 48 """
Remove the current user from a specified section 49 49 Remove the current user from a specified section
If the user is not in the class, the request will fail. 50 50 If the user is not in the class, the request will fail.
--- 51 51 ---
omit_serializer: true 52 52 omit_serializer: true
parameters: 53 53 parameters:
- fake: None 54 54 - fake: None
parameters_strategy: 55 55 parameters_strategy:
form: replace 56 56 form: replace
""" 57 57 """
section = self.get_object() 58 58 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 59 59 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 60 60 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 61 61 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 62 62 return Response(status=HTTP_204_NO_CONTENT)
63 63
64 64
class UserSectionListView(ListAPIView): 65 65 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 66 66 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 67 67 permission_classes = [IsAuthenticated]
68 68
def get_queryset(self): 69 69 def get_queryset(self):
return self.request.user.sections.all() 70 70 return self.request.user.sections.all()
71 71
def paginate_queryset(self, queryset): return None 72 72 def paginate_queryset(self, queryset): return None
73 73
74 74
class UserDetail(GenericAPIView): 75 75 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 76 76 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 77 77 permission_classes = [IsAuthenticated]
78 78
def get_queryset(self): 79 79 def get_queryset(self):
return User.objects.all() 80 80 return User.objects.all()
81 81
def patch(self, request, format=None): 82 82 def patch(self, request, format=None):
""" 83 83 """
Updates the user's password, or verifies their email address 84 84 Updates the user's password, or verifies their email address
--- 85 85 ---
request_serializer: UserUpdateSerializer 86 86 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 87 87 response_serializer: UserSerializer
""" 88 88 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 89 89 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 90 90 data.is_valid(raise_exception=True)
data = data.validated_data 91 91 data = data.validated_data
92 92
if 'new_password' in data: 93 93 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 94 94 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 95 95 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 96 96 request.user.set_password(data['new_password'])
request.user.save() 97 97 request.user.save()
98 98
if 'confirmation_key' in data: 99 99 if 'confirmation_key' in data:
try: 100 100 try:
request.user.confirm_email(data['confirmation_key']) 101 101 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 102 102 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 103 103 raise ValidationError('confirmation_key is invalid')
104 104
return Response(UserSerializer(request.user).data) 105 105 return Response(UserSerializer(request.user).data)
106 106
def get(self, request, format=None): 107 107 def get(self, request, format=None):
""" 108 108 """
Return data about the user 109 109 Return data about the user
--- 110 110 ---
response_serializer: UserSerializer 111 111 response_serializer: UserSerializer
""" 112 112 """
serializer = UserSerializer(request.user, context={'request': request}) 113 113 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 114 114 return Response(serializer.data)
115 115
def delete(self, request): 116 116 def delete(self, request):
""" 117 117 """
Irrevocably delete the user and their data 118 118 Irrevocably delete the user and their data
119 119
Yes, really 120 120 Yes, really
""" 121 121 """
request.user.delete() 122 122 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 123 123 return Response(status=HTTP_204_NO_CONTENT)
124 124
@api_view(['POST']) 125 125 @api_view(['POST'])
def register(request, format=None): 126 126 def register(request, format=None):
""" 127 127 """
Register a new user 128 128 Register a new user
--- 129 129 ---
request_serializer: EmailPasswordSerializer 130 130 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 131 131 response_serializer: UserSerializer
""" 132 132 """
data = RegistrationSerializer(data=request.data) 133 133 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 134 134 data.is_valid(raise_exception=True)
135 135
User.objects.create_user(**data.validated_data) 136 136 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 137 137 user = authenticate(**data.validated_data)
auth.login(request, user) 138 138 auth.login(request, user)
139 139
body = ''' 140 140 body = '''
Visit the following link to confirm your email address: 141 141 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 142 142 https://flashy.cards/app/verify_email/%s
143 143
If you did not register for Flashy, no action is required. 144 144 If you did not register for Flashy, no action is required.
''' 145 145 '''
146 146
assert send_mail("Flashy email verification", 147 147 assert send_mail("Flashy email verification",
body % user.confirmation_key, 148 148 body % user.confirmation_key,
"noreply@flashy.cards", 149 149 "noreply@flashy.cards",
[user.email]) 150 150 [user.email])
151 151
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 152 152 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
153 153
@api_view(['POST']) 154 154 @api_view(['POST'])
def login(request): 155 155 def login(request):
""" 156 156 """
Authenticates user and returns user data if valid. 157 157 Authenticates user and returns user data if valid.
--- 158 158 ---
request_serializer: EmailPasswordSerializer 159 159 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 160 160 response_serializer: UserSerializer
""" 161 161 """
162 162
data = EmailPasswordSerializer(data=request.data) 163 163 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 164 164 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 165 165 user = authenticate(**data.validated_data)
166 166
if user is None: 167 167 if user is None:
raise AuthenticationFailed('Invalid email or password') 168 168 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 169 169 if not user.is_active:
raise NotAuthenticated('Account is disabled') 170 170 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 171 171 auth.login(request, user)
return Response(UserSerializer(request.user).data) 172 172 return Response(UserSerializer(request.user).data)
173 173
174 174
@api_view(['POST']) 175 175 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 176 176 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 177 177 def logout(request, format=None):
""" 178 178 """
Logs the authenticated user out. 179 179 Logs the authenticated user out.
""" 180 180 """
auth.logout(request) 181 181 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 182 182 return Response(status=HTTP_204_NO_CONTENT)
183 183
184 184
@api_view(['POST']) 185 185 @api_view(['POST'])
def request_password_reset(request, format=None): 186 186 def request_password_reset(request, format=None):
""" 187 187 """
Send a password reset token/link to the provided email. 188 188 Send a password reset token/link to the provided email.
--- 189 189 ---
request_serializer: PasswordResetRequestSerializer 190 190 request_serializer: PasswordResetRequestSerializer
""" 191 191 """
data = PasswordResetRequestSerializer(data=request.data) 192 192 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 193 193 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 194 194 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 195 195 token = default_token_generator.make_token(user)
196 196
body = ''' 197 197 body = '''
Visit the following link to reset your password: 198 198 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 199 199 https://flashy.cards/app/reset_password/%d/%s
200 200
If you did not request a password reset, no action is required. 201 201 If you did not request a password reset, no action is required.
''' 202 202 '''
203 203
send_mail("Flashy password reset", 204 204 send_mail("Flashy password reset",
body % (user.pk, token), 205 205 body % (user.pk, token),
"noreply@flashy.cards", 206 206 "noreply@flashy.cards",
[user.email]) 207 207 [user.email])
208 208
return Response(status=HTTP_204_NO_CONTENT) 209 209 return Response(status=HTTP_204_NO_CONTENT)
210 210
211 211
@api_view(['POST']) 212 212 @api_view(['POST'])
def reset_password(request, format=None): 213 213 def reset_password(request, format=None):
""" 214 214 """
Updates user's password to new password if token is valid. 215 215 Updates user's password to new password if token is valid.
--- 216 216 ---
request_serializer: PasswordResetSerializer 217 217 request_serializer: PasswordResetSerializer
""" 218 218 """
data = PasswordResetSerializer(data=request.data) 219 219 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 220 220 data.is_valid(raise_exception=True)
221 221
user = User.objects.get(id=data['uid'].value) 222 222 user = User.objects.get(id=data['uid'].value)
# Check token validity. 223 223 # Check token validity.
224 224
if default_token_generator.check_token(user, data['token'].value): 225 225 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 226 226 user.set_password(data['new_password'].value)
user.save() 227 227 user.save()
else: 228 228 else:
raise ValidationError('Could not verify reset token') 229 229 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 230 230 return Response(status=HTTP_204_NO_CONTENT)
231 231
232 232
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 233 233 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 234 234 queryset = Flashcard.objects.all()