Commit 17768114904009e2b6776400c739d2e3e6aad62b

Authored by Rohan Rangray
1 parent d818aecbc5
Exists in master

Fixed the Study-related views. Wrote tests for them.

Showing 5 changed files with 74 additions and 13 deletions Inline Diff

flashcards/fields.py View file @ 1776811
from django.db import models 1 1 from django.db import models
from validators import FlashcardMask, OverlapIntervalException 2 2 from validators import FlashcardMask, OverlapIntervalException
3 3
4 4
class MaskField(models.Field): 5 5 class MaskField(models.Field):
def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): 6 6 def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs):
self.blank_sep = blank_sep 7 7 self.blank_sep = blank_sep
self.range_sep = range_sep 8 8 self.range_sep = range_sep
super(MaskField, self).__init__(*args, **kwargs) 9 9 super(MaskField, self).__init__(*args, **kwargs)
10 10
@staticmethod 11 11 @staticmethod
def _using_array(connection): 12 12 def _using_array(connection):
return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2' 13 13 return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
14 14
def deconstruct(self): 15 15 def deconstruct(self):
name, path, args, kwargs = super(MaskField, self).deconstruct() 16 16 name, path, args, kwargs = super(MaskField, self).deconstruct()
kwargs['blank_sep'] = self.blank_sep 17 17 kwargs['blank_sep'] = self.blank_sep
kwargs['range_sep'] = self.range_sep 18 18 kwargs['range_sep'] = self.range_sep
return name, path, args, kwargs 19 19 return name, path, args, kwargs
20 20
def db_type(self, connection): 21 21 def db_type(self, connection):
return 'integer[2][]' if self._using_array(connection) else 'varchar' 22 22 return 'integer[2][]' if self._using_array(connection) else 'varchar'
23 23
def from_db_value(self, value, expression, connection, context): 24 24 def from_db_value(self, value, expression, connection, context):
if value is None: 25 25 if value is None:
return value 26 26 return value
if self._using_array(connection): 27 27 if self._using_array(connection):
return MaskField._psql_parse_mask(value) 28 28 return MaskField._psql_parse_mask(value)
return MaskField._varchar_parse_mask(value) 29 29 return MaskField._varchar_parse_mask(value)
30 30
def get_db_prep_value(self, value, connection, prepared=False): 31 31 def get_db_prep_value(self, value, connection, prepared=False):
if not prepared: 32 32 if not prepared:
value = self.get_prep_value(value) 33 33 value = self.get_prep_value(value)
if value is None: 34 34 if value is None:
return value 35 35 return value
if self._using_array(connection): 36 36 if self._using_array(connection):
return value 37 37 return value
return ','.join(['-'.join(map(str, i)) for i in value]) 38 38 return ','.join(['-'.join(map(str, i)) for i in value])
39 39
def to_python(self, value): 40 40 def to_python(self, value):
if value is None: 41 41 if value is None:
return value 42 42 return value
return sorted(list(FlashcardMask(value))) 43 43 return FlashcardMask(value)
44 44
def get_prep_value(self, value): 45 45 def get_prep_value(self, value):
if value is None: 46 46 if value is None:
return value 47 47 return value
return sorted(map(list, FlashcardMask(value))) 48 48 return sorted(map(list, FlashcardMask(value)))
49 49
def get_prep_lookup(self, lookup_type, value): 50 50 def get_prep_lookup(self, lookup_type, value):
raise TypeError("Lookup not supported for MaskField") 51 51 raise TypeError("Lookup not supported for MaskField")
52 52
@staticmethod 53 53 @staticmethod
def _parse_mask(intervals): 54 54 def _parse_mask(intervals):
p_beg, p_end = -1, -1 55 55 p_beg, p_end = -1, -1
mask_list = [] 56 56 mask_list = []
for interval in intervals: 57 57 for interval in intervals:
beg, end = map(int, interval) 58 58 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 59 59 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") 60 60 raise ValueError("Invalid range offsets in the mask")
mask_list.append([beg, end]) 61 61 mask_list.append([beg, end])
p_beg, p_end = beg, end 62 62 p_beg, p_end = beg, end
return mask_list 63 63 return mask_list
flashcards/serializers.py View file @ 1776811
from json import dumps, loads 1 1 from json import dumps, loads
2 2
from django.utils.datetime_safe import datetime 3 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 4 4 from django.utils.timezone import now
import pytz 5 5 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz 6 6 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 7 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 8 8 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 9 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 10 10 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 11 11 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 12 12 from flashy.settings import QUARTER_END, QUARTER_START
from random import sample 13 13 from random import sample
14 14
15 15
class EmailSerializer(Serializer): 16 16 class EmailSerializer(Serializer):
email = EmailField(required=True) 17 17 email = EmailField(required=True)
18 18
19 19
class EmailPasswordSerializer(EmailSerializer): 20 20 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 21 21 password = CharField(required=True)
22 22
23 23
class RegistrationSerializer(EmailPasswordSerializer): 24 24 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 25 25 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
26 26
27 27
class PasswordResetRequestSerializer(EmailSerializer): 28 28 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 29 29 def validate_email(self, value):
try: 30 30 try:
User.objects.get(email=value) 31 31 User.objects.get(email=value)
return value 32 32 return value
except User.DoesNotExist: 33 33 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 34 34 raise serializers.ValidationError('No user exists with that email')
35 35
36 36
class PasswordResetSerializer(Serializer): 37 37 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 38 38 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 39 39 uid = IntegerField(required=True)
token = CharField(required=True) 40 40 token = CharField(required=True)
41 41
def validate_uid(self, value): 42 42 def validate_uid(self, value):
try: 43 43 try:
User.objects.get(id=value) 44 44 User.objects.get(id=value)
return value 45 45 return value
except User.DoesNotExist: 46 46 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 47 47 raise serializers.ValidationError('Could not verify reset token')
48 48
49 49
class UserUpdateSerializer(Serializer): 50 50 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 51 51 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 52 52 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 53 53 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 54 54 # reset_token = CharField(required=False)
55 55
def validate(self, data): 56 56 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 57 57 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 58 58 raise serializers.ValidationError('old_password is required to set a new_password')
return data 59 59 return data
60 60
61 61
class Password(Serializer): 62 62 class Password(Serializer):
email = EmailField(required=True) 63 63 email = EmailField(required=True)
password = CharField(required=True) 64 64 password = CharField(required=True)
65 65
66 66
class LecturePeriodSerializer(ModelSerializer): 67 67 class LecturePeriodSerializer(ModelSerializer):
class Meta: 68 68 class Meta:
model = LecturePeriod 69 69 model = LecturePeriod
exclude = 'id', 'section' 70 70 exclude = 'id', 'section'
71 71
72 72
class SectionSerializer(ModelSerializer): 73 73 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 74 74 lecture_times = CharField()
short_name = CharField() 75 75 short_name = CharField()
long_name = CharField() 76 76 long_name = CharField()
77 77
class Meta: 78 78 class Meta:
model = Section 79 79 model = Section
80 80
class DeepSectionSerializer(SectionSerializer): 81 81 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 82 82 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
83 83
84 84
85 85
class UserSerializer(ModelSerializer): 86 86 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 87 87 email = EmailField(required=False)
sections = SectionSerializer(many=True) 88 88 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 89 89 is_confirmed = BooleanField()
90 90
class Meta: 91 91 class Meta:
model = User 92 92 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 93 93 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
94 94
95 95
class MaskFieldSerializer(serializers.Field): 96 96 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 97 97 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 98 98 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 99 99 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 100 100 'overlap': 'Ensure this field does not have overlapping intervals.'
} 101 101 }
102 102
def to_representation(self, value): 103 103 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 104 104 return dumps(list(self._make_mask(value)))
105 105
def to_internal_value(self, value): 106 106 def to_internal_value(self, value):
return self._make_mask(loads(value)) 107 107 return self._make_mask(loads(value))
108 108
def _make_mask(self, data): 109 109 def _make_mask(self, data):
try: 110 110 try:
mask = FlashcardMask(data) 111 111 mask = FlashcardMask(data)
except ValueError: 112 112 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 113 113 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 114 114 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 115 115 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 116 116 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 117 117 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 118 118 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 119 119 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 120 120 return mask
121 121
122 122
class FlashcardSerializer(ModelSerializer): 123 123 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 124 124 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 125 125 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 126 126 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 127 127 mask = MaskFieldSerializer(allow_null=True)
128 128
def validate_material_date(self, value): 129 129 def validate_material_date(self, value):
# TODO: make this dynamic 130 130 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 131 131 if QUARTER_START <= value <= QUARTER_END:
return value 132 132 return value
else: 133 133 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 134 134 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
135 135
def validate_pushed(self, value): 136 136 def validate_pushed(self, value):
if value > datetime.now(): 137 137 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 138 138 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 139 139 return value
140 140
def validate_mask(self, value): 141 141 def validate_mask(self, value):
if value is None: 142 142 if value is None:
return None 143 143 return None
if len(self.initial_data['text']) < value.max_offset(): 144 144 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 145 145 raise serializers.ValidationError("Mask out of bounds")
return value 146 146 return value
147 147
class Meta: 148 148 class Meta:
model = Flashcard 149 149 model = Flashcard
exclude = 'author', 'previous' 150 150 exclude = 'author', 'previous'
151 151
152 152
class FlashcardUpdateSerializer(serializers.Serializer): 153 153 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 154 154 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 155 155 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 156 156 mask = MaskFieldSerializer(required=False)
157 157
def validate_material_date(self, date): 158 158 def validate_material_date(self, date):
if date > QUARTER_END: 159 159 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 160 160 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 161 161 return date
162 162
def validate(self, attrs): 163 163 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 164 164 # Make sure that at least one of the attributes was passed in
if not any(i in attrs for i in ['material_date', 'text', 'mask']): 165 165 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 166 166 raise serializers.ValidationError("No new value passed in")
return attrs 167 167 return attrs
168 168
169 169
class QuizRequestSerializer(serializers.Serializer): 170 170 class QuizRequestSerializer(serializers.Serializer):
sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True) 171 171 # sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True)
172 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 172 173 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 173 174 material_date_end = DateTimeField(default=QUARTER_END)
174 175
def __init__(self, user, *args, **kwargs): 175 176 def __init__(self, user, *args, **kwargs):
super(QuizRequestSerializer, self).__init__(*args, **kwargs) 176 177 super(QuizRequestSerializer, self).__init__(*args, **kwargs)
self.user = user 177 178 self.user = user
self.user_flashcard = None 178 179 self.user_flashcard = None
179 180
def create(self, validated_data): 180 181 def create(self, validated_data):
return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard) 181 182 return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard)
182 183
def update(self, instance, validated_data): 183 184 def update(self, instance, validated_data):
for attr in validated_data: 184 185 for attr in validated_data:
setattr(instance, attr, validated_data[attr]) 185 186 setattr(instance, attr, validated_data[attr])
instance.save() 186 187 instance.save()
return instance 187 188 return instance
188 189
def _get_user_flashcard(self, attrs): 189 190 def _get_user_flashcard(self, attrs):
user_flashcard_filter = UserFlashcard.objects.filter( 190 191 user_flashcard_filter = UserFlashcard.objects.filter(
user=self.user, flashcard__section__in=attrs['sections'], 191 192 user=self.user, flashcard__section__in=attrs['sections'],
flashcard__material_date__gte=attrs['material_date_begin'], 192 193 flashcard__material_date__gte=attrs['material_date_begin'],
flashcard__material_date__lte=attrs['material_date_end'] 193 194 flashcard__material_date__lte=attrs['material_date_end']
) 194 195 )
if not user_flashcard_filter.exists(): 195 196 if not user_flashcard_filter.exists():
raise serializers.ValidationError("Your deck for that section is empty") 196 197 raise serializers.ValidationError("Your deck for that section is empty")
self.user_flashcard = user_flashcard_filter.order_by('?').first() 197 198 self.user_flashcard = user_flashcard_filter.order_by('?').first()
198 199
def validate_material_date_begin(self, value): 199 200 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 200 201 if QUARTER_START <= value <= QUARTER_END:
return value 201 202 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 202 203 raise serializers.ValidationError("Invalid begin date for the flashcard range")
203 204
def validate_material_date_end(self, value): 204 205 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 205 206 if QUARTER_START <= value <= QUARTER_END:
return value 206 207 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 207 208 raise serializers.ValidationError("Invalid end date for the flashcard range")
208 209
def validate_sections(self, value): 209 210 def validate_sections(self, value):
print "VALUE", type(value), value 210
if value is None: 211 211 if value is None:
return self.user.sections 212 212 return self.user.sections
section_filter = Section.objects.filter(pk__in=value) 213 213 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 214 214 if not section_filter.exists():
raise serializers.ValidationError("You aren't enrolled in those section(s)") 215 215 raise serializers.ValidationError("You aren't enrolled in those section(s)")
return section_filter 216 216 return section_filter
217 217
def validate(self, attrs): 218 218 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 219 219 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 220 220 raise serializers.ValidationError("Invalid range")
if 'sections' not in attrs: 221 221 if 'sections' not in attrs:
attrs['sections'] = self.validate_sections(None) 222 222 attrs['sections'] = self.validate_sections(None)
self._get_user_flashcard(attrs) 223 223 self._get_user_flashcard(attrs)
return attrs 224 224 return attrs
225 225
226 226
flashcards/tests/test_models.py View file @ 1776811
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.test import TestCase 3 3 from django.test import TestCase
from flashcards.models import User, Section, Flashcard, UserFlashcard 4 4 from flashcards.models import User, Section, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
6 from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer
7 from flashy.settings import QUARTER_START, QUARTER_END
6 8
7 9
class RegistrationTests(TestCase): 8 10 class RegistrationTests(TestCase):
def setUp(self): 9 11 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 10 12 User.objects.create_user(email="none@none.com", password="1234")
11 13
def test_email_confirmation(self): 12 14 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 13 15 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 14 16 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 15 17 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 16 18 self.assertTrue(user.is_confirmed)
17 19
18 20
class UserTests(TestCase): 19 21 class UserTests(TestCase):
def setUp(self): 20 22 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 21 23 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 22 24 Section.objects.create(department='dept',
course_num='101a', 23 25 course_num='101a',
course_title='how 2 test', 24 26 course_title='how 2 test',
instructor='George Lucas', 25 27 instructor='George Lucas',
quarter='SP15') 26 28 quarter='SP15')
27 29
def test_section_list(self): 28 30 def test_section_list(self):
section = Section.objects.get(course_num='101a') 29 31 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 30 32 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 31 33 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 32 34 user.sections.add(section)
self.assertIn(section, user.sections.all()) 33 35 self.assertIn(section, user.sections.all())
user.sections.add(section) 34 36 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 35 37 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 36 38 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 37 39 self.assertEqual(user.sections.count(), 0)
38 40
39 41
class FlashcardMaskTest(TestCase): 40 42 class FlashcardMaskTest(TestCase):
def test_empty(self): 41 43 def test_empty(self):
try: 42 44 try:
fm = FlashcardMask([]) 43 45 fm = FlashcardMask([])
self.assertEqual(fm.max_offset(), -1) 44 46 self.assertEqual(fm.max_offset(), -1)
except TypeError: 45 47 except TypeError:
self.fail() 46 48 self.fail()
try: 47 49 try:
fm = FlashcardMask('') 48 50 fm = FlashcardMask('')
self.assertEqual(fm.max_offset(), -1) 49 51 self.assertEqual(fm.max_offset(), -1)
except TypeError: 50 52 except TypeError:
self.fail() 51 53 self.fail()
try: 52 54 try:
fm = FlashcardMask(None) 53 55 fm = FlashcardMask(None)
self.assertEqual(fm.max_offset(), -1) 54 56 self.assertEqual(fm.max_offset(), -1)
except TypeError: 55 57 except TypeError:
self.fail() 56 58 self.fail()
57 59
def test_iterable(self): 58 60 def test_iterable(self):
try: 59 61 try:
FlashcardMask(1) 60 62 FlashcardMask(1)
except TypeError as te: 61 63 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 62 64 self.assertEqual(te.message, "Interval not a valid iterable")
try: 63 65 try:
FlashcardMask([1, 2, 4]) 64 66 FlashcardMask([1, 2, 4])
except TypeError as te: 65 67 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 66 68 self.assertEqual(te.message, "Interval not a valid iterable")
67 69
def test_interval(self): 68 70 def test_interval(self):
try: 69 71 try:
FlashcardMask([[1, 2, 3], [1]]) 70 72 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 71 73 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 72 74 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 73 75 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 74 76 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 75 77 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 76 78 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 77 79 try:
FlashcardMask(([1, 2], [1])) 78 80 FlashcardMask(([1, 2], [1]))
except TypeError as te: 79 81 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 80 82 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 81 83 try:
FlashcardMask("[1,2,3]") 82 84 FlashcardMask("[1,2,3]")
except TypeError as te: 83 85 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 84 86 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
85 87
def test_overlap(self): 86 88 def test_overlap(self):
try: 87 89 try:
FlashcardMask({(1, 2), (2, 5)}) 88 90 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 89 91 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 90 92 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 91 93 try:
FlashcardMask({(1, 20), (12, 15)}) 92 94 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 93 95 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 94 96 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 95 97 try:
FlashcardMask({(2, 1), (5, 2)}) 96 98 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 97 99 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 98 100 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
99 101
100 102
101
class FlashcardTests(TestCase): 102 103 class FlashcardTests(TestCase):
def setUp(self): 103 104 def setUp(self):
section = Section.objects.create(department='dept', 104 105 section = Section.objects.create(department='dept',
course_num='101a', 105 106 course_num='101a',
course_title='how 2 test', 106 107 course_title='how 2 test',
instructor='George Lucas', 107 108 instructor='George Lucas',
quarter='SP15') 108 109 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 109 110 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 110 111 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 111 112 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 112 113 section=section,
author=user, 113 114 author=user,
material_date=datetime.now(), 114 115 material_date=datetime.now(),
previous=None, 115 116 previous=None,
mask={(24,34), (0, 4)}) 116 117 mask={(24, 34), (0, 4)})
user.save() 117 118 user.save()
section.save() 118 119 section.save()
flashcard.save() 119 120 flashcard.save()
120 121
def test_flashcard_edit(self): 121 122 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 122 123 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 123 124 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 124 125 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 125 126 user2.sections.add(section)
user2.save() 126 127 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 127 128 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 128 129 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 129 130 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 130 131 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 131 132 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 132 133 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 133 134 flashcard.edit(user2, {'text': 'This is the new text'})
self.assertNotEqual(flashcard.pk, pk_backup) 134 135 self.assertNotEqual(flashcard.pk, pk_backup)
self.assertEqual(flashcard.text, 'This is the new text') 135 136 self.assertEqual(flashcard.text, 'This is the new text')
136 137
def test_mask_field(self): 137 138 def test_mask_field(self):
user = User.objects.get(email="none@none.com") 138 139 user = User.objects.get(email="none@none.com")
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 139 140 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
self.assertTrue(isinstance(flashcard.mask, set)) 140 141 self.assertTrue(isinstance(flashcard.mask, set))
self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) 141 142 self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask]))
blank1, blank2 = sorted(list(flashcard.mask)) 142 143 blank1, blank2 = sorted(list(flashcard.mask))
self.assertEqual(flashcard.text[slice(*blank1)], 'This') 143 144 self.assertEqual(flashcard.text[slice(*blank1)], 'This')
self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') 144 145 self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard')
try: 145 146 try:
flashcard.mask = {(10, 34), (0, 14)} 146 147 flashcard.mask = {(10, 34), (0, 14)}
flashcard.save() 147 148 flashcard.save()
self.fail() 148 149 self.fail()
except OverlapIntervalException: 149 150 except OverlapIntervalException:
self.assertTrue(True) 150 151 self.assertTrue(True)
152
153
154 class UserFlashcardQuizTests(TestCase):
155 def setUp(self):
156 self.section = Section.objects.create(department='dept',
157 course_num='101a',
158 course_title='how 2 test',
159 instructor='George Lucas',
160 quarter='SP15')
161 self.user = User.objects.create_user(email="none@none.com", password="1234")
162 self.user.sections.add(self.section)
163 self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
164 section=self.section,
165 author=self.user,
166 material_date=datetime.now(),
167 previous=None,
168 mask=[(24, 33), (0, 4)])
169 self.user.save()
170 self.section.save()
171 self.flashcard.save()
172 self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard,
173 user=self.user,
174 mask=self.flashcard.mask,
175 pulled=datetime.now())
176 self.user_flashcard.save()
177 self.user_flashcard.refresh_from_db()
178 self.flashcard.refresh_from_db()
179
180 def test_quiz_request(self):
181 data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END}
182 serializer = QuizRequestSerializer(user=self.user, data=data)
183 serializer.is_valid(raise_exception=True)
184 user_flashcard_quiz = serializer.create(serializer.validated_data)
185 self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz))
186 mask = user_flashcard_quiz.user_flashcard.mask.get_random_blank()
187 self.assertIn(mask, [(24, 33), (0, 4)])
188 user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)]
189 self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"])
190 user_flashcard_quiz.save()
191 response = QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data
192 self.assertEqual(response['pk'], 1)
193 self.assertEqual(response['section'], 1)
194 self.assertEqual(response['text'], user_flashcard_quiz.user_flashcard.flashcard.text)
195 self.assertEqual(response['mask'], mask)
196
flashcards/validators.py View file @ 1776811
from collections import Iterable 1 1 from collections import Iterable
from random import sample 2 2 from random import sample
3 3
4 4
class FlashcardMask(set): 5 5 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 6 6 def __init__(self, iterable, *args, **kwargs):
if iterable is None or iterable == '': 7 7 if iterable is None or iterable == '':
iterable = [] 8 8 iterable = []
self._iterable_check(iterable) 9 9 self._iterable_check(iterable)
iterable = map(tuple, iterable) 10 10 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 11 11 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._interval_check() 12 12 self._interval_check()
self._overlap_check() 13 13 self._overlap_check()
14 14
def max_offset(self): 15 15 def max_offset(self):
return self._end 16 16 return self._end
17 17
def get_random_blank(self): 18 18 def get_random_blank(self):
if self.max_offset() > 0: 19 19 if self.max_offset() > 0:
return sample(self, 1) 20 20 return sample(self, 1)[0]
return () 21 21 return ()
22 22
def _iterable_check(self, iterable): 23 23 def _iterable_check(self, iterable):
if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): 24 24 if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]):
raise TypeError("Interval not a valid iterable") 25 25 raise TypeError("Interval not a valid iterable")
26 26
def _interval_check(self): 27 27 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 28 28 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 29 29 raise TypeError("Intervals must have exactly 2 elements, begin and end")
30 30
def _overlap_check(self): 31 31 def _overlap_check(self):
p_beg, p_end = -1, -1 32 32 p_beg, p_end = -1, -1
for interval in sorted(self): 33 33 for interval in sorted(self):
beg, end = map(int, interval) 34 34 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 35 35 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask") 36 36 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 37 37 p_beg, p_end = beg, end
self._end = p_end 38 38 self._end = p_end
flashcards/views.py View file @ 1776811
import django 1 1 import django
2 2
from django.contrib import auth 3 3 from django.contrib import auth
from django.core.cache import cache 4 4 from django.core.cache import cache
from django.shortcuts import get_object_or_404 5 5 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer 6 6 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 8 8 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 9 9 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 10 10 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer 11 11 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 12 12 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 13 13 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 14 14 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 15 15 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 16 16 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 17 17 from django.core.mail import send_mail
from django.contrib.auth import authenticate 18 18 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 19 19 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 20 20 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 21 21 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 22 22 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 23 23 from simple_email_confirmation import EmailAddress
from random import sample 24 24 from random import sample
25 25
26 26
class SectionViewSet(ReadOnlyModelViewSet): 27 27 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 28 28 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 29 29 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 30 30 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 31 31 permission_classes = [IsAuthenticated]
32 32
@detail_route(methods=['GET']) 33 33 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 34 34 def flashcards(self, request, pk):
""" 35 35 """
Gets flashcards for a section, excluding hidden cards. 36 36 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 37 37 Returned in strictly chronological order (material date).
""" 38 38 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() 39 39 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 40 40 return Response(FlashcardSerializer(flashcards, many=True).data)
41 41
@detail_route(methods=['POST']) 42 42 @detail_route(methods=['POST'])
def enroll(self, request, pk): 43 43 def enroll(self, request, pk):
""" 44 44 """
Add the current user to a specified section 45 45 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. 46 46 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 47 47 ---
view_mocker: flashcards.api.mock_no_params 48 48 view_mocker: flashcards.api.mock_no_params
""" 49 49 """
50 50
self.get_object().enroll(request.user) 51 51 self.get_object().enroll(request.user)
return Response(status=HTTP_204_NO_CONTENT) 52 52 return Response(status=HTTP_204_NO_CONTENT)
53 53
@detail_route(methods=['POST']) 54 54 @detail_route(methods=['POST'])
def drop(self, request, pk): 55 55 def drop(self, request, pk):
""" 56 56 """
Remove the current user from a specified section 57 57 Remove the current user from a specified section
If the user is not in the class, the request will fail. 58 58 If the user is not in the class, the request will fail.
--- 59 59 ---
view_mocker: flashcards.api.mock_no_params 60 60 view_mocker: flashcards.api.mock_no_params
""" 61 61 """
try: 62 62 try:
self.get_object().drop(request.user) 63 63 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 64 64 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 65 65 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 66 66 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 67 67 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 68 68 return Response(status=HTTP_204_NO_CONTENT)
69 69
@list_route(methods=['GET']) 70 70 @list_route(methods=['GET'])
def search(self, request): 71 71 def search(self, request):
""" 72 72 """
Returns a list of sections which match a user's query 73 73 Returns a list of sections which match a user's query
--- 74 74 ---
parameters: 75 75 parameters:
- name: q 76 76 - name: q
description: space-separated list of terms 77 77 description: space-separated list of terms
required: true 78 78 required: true
type: form 79 79 type: form
response_serializer: SectionSerializer 80 80 response_serializer: SectionSerializer
""" 81 81 """
query = request.GET.get('q', None) 82 82 query = request.GET.get('q', None)
if not query: return Response('[]') 83 83 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 84 84 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 85 85 data = SectionSerializer(qs, many=True).data
return Response(data) 86 86 return Response(data)
87 87
@detail_route(methods=['GET']) 88 88 @detail_route(methods=['GET'])
def deck(self, request, pk): 89 89 def deck(self, request, pk):
""" 90 90 """
Gets the contents of a user's deck for a given section. 91 91 Gets the contents of a user's deck for a given section.
""" 92 92 """
qs = request.user.get_deck(self.get_object()) 93 93 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 94 94 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 95 95 return Response(serializer.data)
96 96
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) 97 97 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 98 98 def ordered_deck(self, request, pk):
""" 99 99 """
Get a chronological order by material_date of flashcards for a section. 100 100 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 101 101 This excludes hidden card.
""" 102 102 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 103 103 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 104 104 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 105 105 return Response(serializer.data)
106 106
@detail_route(methods=['GET']) 107 107 @detail_route(methods=['GET'])
def feed(self, request, pk): 108 108 def feed(self, request, pk):
""" 109 109 """
Gets the contents of a user's feed for a section. 110 110 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 111 111 Exclude cards that are already in the user's deck
""" 112 112 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 113 113 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 114 114 return Response(serializer.data)
115 115
116 116
class UserSectionListView(ListAPIView): 117 117 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 118 118 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 119 119 permission_classes = [IsAuthenticated]
120 120
def get_queryset(self): 121 121 def get_queryset(self):
return self.request.user.sections.all() 122 122 return self.request.user.sections.all()
123 123
def paginate_queryset(self, queryset): return None 124 124 def paginate_queryset(self, queryset): return None
125 125
126 126
class UserDetail(GenericAPIView): 127 127 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 128 128 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 129 129 permission_classes = [IsAuthenticated]
130 130
def patch(self, request, format=None): 131 131 def patch(self, request, format=None):
""" 132 132 """
Updates the user's password, or verifies their email address 133 133 Updates the user's password, or verifies their email address
--- 134 134 ---
request_serializer: UserUpdateSerializer 135 135 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 136 136 response_serializer: UserSerializer
""" 137 137 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 138 138 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 139 139 data.is_valid(raise_exception=True)
data = data.validated_data 140 140 data = data.validated_data
141 141
if 'new_password' in data: 142 142 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 143 143 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 144 144 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 145 145 request.user.set_password(data['new_password'])
request.user.save() 146 146 request.user.save()
147 147
if 'confirmation_key' in data: 148 148 if 'confirmation_key' in data:
try: 149 149 try:
request.user.confirm_email(data['confirmation_key']) 150 150 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 151 151 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 152 152 raise ValidationError('confirmation_key is invalid')
153 153
return Response(UserSerializer(request.user).data) 154 154 return Response(UserSerializer(request.user).data)
155 155
def get(self, request, format=None): 156 156 def get(self, request, format=None):
""" 157 157 """
Return data about the user 158 158 Return data about the user
--- 159 159 ---
response_serializer: UserSerializer 160 160 response_serializer: UserSerializer
""" 161 161 """
serializer = UserSerializer(request.user, context={'request': request}) 162 162 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 163 163 return Response(serializer.data)
164 164
def delete(self, request): 165 165 def delete(self, request):
""" 166 166 """
Irrevocably delete the user and their data 167 167 Irrevocably delete the user and their data
168 168
Yes, really 169 169 Yes, really
""" 170 170 """
request.user.delete() 171 171 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 172 172 return Response(status=HTTP_204_NO_CONTENT)
173 173
174 174
@api_view(['POST']) 175 175 @api_view(['POST'])
def register(request, format=None): 176 176 def register(request, format=None):
""" 177 177 """
Register a new user 178 178 Register a new user
--- 179 179 ---
request_serializer: EmailPasswordSerializer 180 180 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 181 181 response_serializer: UserSerializer
""" 182 182 """
data = RegistrationSerializer(data=request.data) 183 183 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 184 184 data.is_valid(raise_exception=True)
185 185
User.objects.create_user(**data.validated_data) 186 186 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 187 187 user = authenticate(**data.validated_data)
auth.login(request, user) 188 188 auth.login(request, user)
189 189
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 190 190 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
191 191
192 192
@api_view(['POST']) 193 193 @api_view(['POST'])
def login(request): 194 194 def login(request):
""" 195 195 """
Authenticates user and returns user data if valid. 196 196 Authenticates user and returns user data if valid.
--- 197 197 ---
request_serializer: EmailPasswordSerializer 198 198 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 199 199 response_serializer: UserSerializer
""" 200 200 """
201 201
data = EmailPasswordSerializer(data=request.data) 202 202 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 203 203 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 204 204 user = authenticate(**data.validated_data)
205 205
if user is None: 206 206 if user is None:
raise AuthenticationFailed('Invalid email or password') 207 207 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 208 208 if not user.is_active:
raise NotAuthenticated('Account is disabled') 209 209 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 210 210 auth.login(request, user)
return Response(UserSerializer(request.user).data) 211 211 return Response(UserSerializer(request.user).data)
212 212
213 213
@api_view(['POST']) 214 214 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 215 215 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 216 216 def logout(request, format=None):
""" 217 217 """
Logs the authenticated user out. 218 218 Logs the authenticated user out.
""" 219 219 """
auth.logout(request) 220 220 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 221 221 return Response(status=HTTP_204_NO_CONTENT)
222 222
223 223
@api_view(['POST']) 224 224 @api_view(['POST'])
def request_password_reset(request, format=None): 225 225 def request_password_reset(request, format=None):
""" 226 226 """
Send a password reset token/link to the provided email. 227 227 Send a password reset token/link to the provided email.
--- 228 228 ---
request_serializer: PasswordResetRequestSerializer 229 229 request_serializer: PasswordResetRequestSerializer
""" 230 230 """
data = PasswordResetRequestSerializer(data=request.data) 231 231 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 232 232 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 233 233 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 234 234 return Response(status=HTTP_204_NO_CONTENT)
235 235
236 236
@api_view(['POST']) 237 237 @api_view(['POST'])
def reset_password(request, format=None): 238 238 def reset_password(request, format=None):
""" 239 239 """
Updates user's password to new password if token is valid. 240 240 Updates user's password to new password if token is valid.
--- 241 241 ---
request_serializer: PasswordResetSerializer 242 242 request_serializer: PasswordResetSerializer
""" 243 243 """
data = PasswordResetSerializer(data=request.data) 244 244 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 245 data.is_valid(raise_exception=True)
246 246
user = User.objects.get(id=data['uid'].value) 247 247 user = User.objects.get(id=data['uid'].value)
# Check token validity. 248 248 # Check token validity.
249 249
if default_token_generator.check_token(user, data['token'].value): 250 250 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 251 251 user.set_password(data['new_password'].value)
user.save() 252 252 user.save()
else: 253 253 else:
raise ValidationError('Could not verify reset token') 254 254 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 255 255 return Response(status=HTTP_204_NO_CONTENT)
256 256
257 257
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 258 258 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 259 259 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 260 260 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 261 261 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
262 262
# Override create in CreateModelMixin 263 263 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 264 264 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 265 265 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 266 266 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 267 267 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 268 268 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 269 269 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 270 270 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 271 271 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 272 272 self.perform_create(flashcard)
headers = self.get_success_headers(data) 273 273 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard) 274 274 response_data = FlashcardSerializer(flashcard)
return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) 275 275 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
276 276
277 277
@detail_route(methods=['POST']) 278 278 @detail_route(methods=['POST'])
def unhide(self, request, pk): 279 279 def unhide(self, request, pk):
""" 280 280 """
Unhide the given card 281 281 Unhide the given card
--- 282 282 ---
view_mocker: flashcards.api.mock_no_params 283 283 view_mocker: flashcards.api.mock_no_params
""" 284 284 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 285 285 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 286 286 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 287 287 return Response(status=HTTP_204_NO_CONTENT)
288 288
@detail_route(methods=['POST']) 289 289 @detail_route(methods=['POST'])
def report(self, request, pk): 290 290 def report(self, request, pk):
""" 291 291 """
Hide the given card 292 292 Hide the given card
--- 293 293 ---
view_mocker: flashcards.api.mock_no_params 294 294 view_mocker: flashcards.api.mock_no_params
""" 295 295 """
self.get_object().report(request.user) 296 296 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 297 297 return Response(status=HTTP_204_NO_CONTENT)
298 298
hide = report 299 299 hide = report
300 300
@detail_route(methods=['POST']) 301 301 @detail_route(methods=['POST'])
def pull(self, request, pk): 302 302 def pull(self, request, pk):
""" 303 303 """
Pull a card from the live feed into the user's deck. 304 304 Pull a card from the live feed into the user's deck.
--- 305 305 ---
view_mocker: flashcards.api.mock_no_params 306 306 view_mocker: flashcards.api.mock_no_params
""" 307 307 """
user = request.user 308 308 user = request.user
flashcard = self.get_object() 309 309 flashcard = self.get_object()
user.pull(flashcard) 310 310 user.pull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 311 311 return Response(status=HTTP_204_NO_CONTENT)
312 312
@detail_route(methods=['POST']) 313 313 @detail_route(methods=['POST'])
def unpull(self, request, pk): 314 314 def unpull(self, request, pk):
""" 315 315 """
Unpull a card from the user's deck 316 316 Unpull a card from the user's deck
--- 317 317 ---
view_mocker: flashcards.api.mock_no_params 318 318 view_mocker: flashcards.api.mock_no_params