Commit f66f9ca7d57d78ac11fbdd127116c521b15c8e68

Authored by Rohan Rangray
1 parent 512a58de33
Exists in master

Fixed SwaggerUI problem with /study/

Showing 7 changed files with 71 additions and 47 deletions Inline Diff

flashcards/serializers.py View file @ f66f9ca
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
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz 5 5 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 6 6 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 7 7 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty 8 8 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 9 9 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 10 10 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 11 11 from flashy.settings import QUARTER_END, QUARTER_START
12 12
13 13
class EmailSerializer(Serializer): 14 14 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 15 email = EmailField(required=True)
16 16
17 17
class EmailPasswordSerializer(EmailSerializer): 18 18 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 19 password = CharField(required=True)
20 20
21 21
class RegistrationSerializer(EmailPasswordSerializer): 22 22 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 23 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 24
25 25
class PasswordResetRequestSerializer(EmailSerializer): 26 26 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 27 def validate_email(self, value):
try: 28 28 try:
User.objects.get(email=value) 29 29 User.objects.get(email=value)
return value 30 30 return value
except User.DoesNotExist: 31 31 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 32 raise serializers.ValidationError('No user exists with that email')
33 33
34 34
class PasswordResetSerializer(Serializer): 35 35 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 36 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 37 uid = IntegerField(required=True)
token = CharField(required=True) 38 38 token = CharField(required=True)
39 39
def validate_uid(self, value): 40 40 def validate_uid(self, value):
try: 41 41 try:
User.objects.get(id=value) 42 42 User.objects.get(id=value)
return value 43 43 return value
except User.DoesNotExist: 44 44 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 45 raise serializers.ValidationError('Could not verify reset token')
46 46
47 47
class UserUpdateSerializer(Serializer): 48 48 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 49 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 50 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 51 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 52 # reset_token = CharField(required=False)
53 53
def validate(self, data): 54 54 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 55 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 56 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 57 return data
58 58
59 59
class Password(Serializer): 60 60 class Password(Serializer):
email = EmailField(required=True) 61 61 email = EmailField(required=True)
password = CharField(required=True) 62 62 password = CharField(required=True)
63 63
64 64
class LecturePeriodSerializer(ModelSerializer): 65 65 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 66 class Meta:
model = LecturePeriod 67 67 model = LecturePeriod
exclude = 'id', 'section' 68 68 exclude = 'id', 'section'
69 69
70 70
class SectionSerializer(ModelSerializer): 71 71 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 72 72 lecture_times = CharField()
short_name = CharField() 73 73 short_name = CharField()
long_name = CharField() 74 74 long_name = CharField()
75 75
class Meta: 76 76 class Meta:
model = Section 77 77 model = Section
78 78
79 79
class DeepSectionSerializer(SectionSerializer): 80 80 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 81 81 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
82 82
83 83
class UserSerializer(ModelSerializer): 84 84 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 85 85 email = EmailField(required=False)
sections = SectionSerializer(many=True) 86 86 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 87 87 is_confirmed = BooleanField()
88 88
class Meta: 89 89 class Meta:
model = User 90 90 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 91 91 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
92 92
93 93
class MaskFieldSerializer(serializers.Field): 94 94 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 95 95 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 96 96 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 97 97 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 98 98 'overlap': 'Ensure this field does not have overlapping intervals.'
} 99 99 }
100 100
def to_representation(self, value): 101 101 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 102 102 return dumps(list(self._make_mask(value)))
103 103
def to_internal_value(self, value): 104 104 def to_internal_value(self, value):
return self._make_mask(loads(value)) 105 105 return self._make_mask(loads(value))
106 106
def _make_mask(self, data): 107 107 def _make_mask(self, data):
try: 108 108 try:
mask = FlashcardMask(data) 109 109 mask = FlashcardMask(data)
except ValueError: 110 110 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 111 111 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 112 112 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 113 113 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 114 114 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 115 115 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 116 116 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 117 117 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 118 118 return mask
119 119
120 120
class FlashcardSerializer(ModelSerializer): 121 121 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 122 122 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 123 123 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 124 124 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 125 125 mask = MaskFieldSerializer(allow_null=True)
126 126
def validate_material_date(self, value): 127 127 def validate_material_date(self, value):
# TODO: make this dynamic 128 128 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 129 129 if QUARTER_START <= value <= QUARTER_END:
return value 130 130 return value
else: 131 131 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 132 132 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
133 133
def validate_pushed(self, value): 134 134 def validate_pushed(self, value):
if value > datetime.now(): 135 135 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 136 136 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 137 137 return value
138 138
def validate_mask(self, value): 139 139 def validate_mask(self, value):
if value is None: 140 140 if value is None:
return None 141 141 return None
if len(self.initial_data['text']) < value.max_offset(): 142 142 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 143 143 raise serializers.ValidationError("Mask out of bounds")
return value 144 144 return value
145 145
class Meta: 146 146 class Meta:
model = Flashcard 147 147 model = Flashcard
exclude = 'author', 'previous' 148 148 exclude = 'author', 'previous'
149 149
150 150
class FlashcardUpdateSerializer(serializers.Serializer): 151 151 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 152 152 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 153 153 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 154 154 mask = MaskFieldSerializer(required=False)
155 155
def validate_material_date(self, date): 156 156 def validate_material_date(self, date):
if date > QUARTER_END: 157 157 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 158 158 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 159 159 return date
160 160
def validate(self, attrs): 161 161 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 162 162 # 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']): 163 163 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 164 164 raise serializers.ValidationError("No new value passed in")
return attrs 165 165 return attrs
166 166
167 167
class QuizRequestSerializer(serializers.Serializer): 168 168 class QuizRequestSerializer(serializers.Serializer):
# sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True) 169
sections = ListField(child=IntegerField(min_value=1), required=False) 170 169 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 171 170 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 172 171 material_date_end = DateTimeField(default=QUARTER_END)
173 172
def __init__(self, user, instance=None, data=empty, **kwargs): 174 173 def update(self, instance, validated_data):
assert instance is not None or data is not empty 175 174 pass
super(QuizRequestSerializer, self).__init__(instance=instance, data=data, **kwargs) 176
self.user = user 177
self.user_flashcard = None 178
179 175
def create(self, validated_data): 180 176 def create(self, validated_data):
return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard) 181 177 return validated_data
182 178
def update(self, instance, validated_data): 183
for attr in validated_data: 184
setattr(instance, attr, validated_data[attr]) 185
instance.save() 186
return instance 187
188
def _get_user_flashcard(self, attrs): 189
user_flashcard_filter = UserFlashcard.objects.filter( 190
user=self.user, flashcard__section__in=attrs['sections'], 191
flashcard__material_date__gte=attrs['material_date_begin'], 192
flashcard__material_date__lte=attrs['material_date_end'] 193
) 194
if not user_flashcard_filter.exists(): 195
raise serializers.ValidationError("Your deck for that section is empty") 196
self.user_flashcard = user_flashcard_filter.order_by('?').first() 197
198
def validate_material_date_begin(self, value): 199 179 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 200 180 if QUARTER_START <= value <= QUARTER_END:
return value 201 181 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 202 182 raise serializers.ValidationError("Invalid begin date for the flashcard range")
203 183
def validate_material_date_end(self, value): 204 184 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 205 185 if QUARTER_START <= value <= QUARTER_END:
return value 206 186 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 207 187 raise serializers.ValidationError("Invalid end date for the flashcard range")
208 188
def validate_sections(self, value): 209 189 def validate_sections(self, value):
if value is None: 210 190 if value is None:
return self.user.sections 211 191 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 212 192 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 213 193 if not section_filter.exists():
raise serializers.ValidationError("You aren't enrolled in those section(s)") 214 194 raise serializers.ValidationError("Those aren't valid sections")
return section_filter 215 195 return value
216 196
def validate(self, attrs): 217 197 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 218 198 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 219 199 raise serializers.ValidationError("Invalid range")
if 'sections' not in attrs: 220 200 if 'sections' not in attrs:
attrs['sections'] = self.validate_sections(None) 221 201 attrs['sections'] = self.validate_sections(None)
self._get_user_flashcard(attrs) 222
return attrs 223 202 return attrs
224 203
225 204
class QuizResponseSerializer(ModelSerializer): 226 205 class QuizResponseSerializer(ModelSerializer):
pk = PrimaryKeyRelatedField(queryset=UserFlashcardQuiz.objects.all(), many=True) 227 206 pk = PrimaryKeyRelatedField(queryset=UserFlashcardQuiz.objects.all(), many=True)
section = PrimaryKeyRelatedField(queryset=Section.objects.all()) 228 207 section = PrimaryKeyRelatedField(queryset=Section.objects.all())
flashcards/tests/test_api.py View file @ f66f9ca
from django.core import mail 1 1 from django.core import mail
from flashcards.models import * 2 2 from flashcards.models import *
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
import datetime 6 6 from datetime import datetime
from django.utils.timezone import now 7 7 from django.utils.timezone import now
from flashcards.validators import FlashcardMask 8 8 from flashcards.validators import FlashcardMask
from flashcards.serializers import FlashcardSerializer 9 9 from flashcards.serializers import FlashcardSerializer
10 10
11 11
class LoginTests(APITestCase): 12 12 class LoginTests(APITestCase):
fixtures = ['testusers'] 13 13 fixtures = ['testusers']
14 14
def test_login(self): 15 15 def test_login(self):
url = '/api/login/' 16 16 url = '/api/login/'
data = {'email': 'none@none.com', 'password': '1234'} 17 17 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 18 18 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 19 19 self.assertEqual(response.status_code, HTTP_200_OK)
20 20
data = {'email': 'none@none.com', 'password': '4321'} 21 21 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 22 22 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 23 23 self.assertContains(response, 'Invalid email or password', status_code=403)
24 24
data = {'email': 'bad@none.com', 'password': '1234'} 25 25 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 26 26 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 27 27 self.assertContains(response, 'Invalid email or password', status_code=403)
28 28
data = {'password': '4321'} 29 29 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 30 30 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 31 31 self.assertContains(response, 'email', status_code=400)
32 32
data = {'email': 'none@none.com'} 33 33 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 34 34 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 35 35 self.assertContains(response, 'password', status_code=400)
36 36
user = User.objects.get(email="none@none.com") 37 37 user = User.objects.get(email="none@none.com")
user.is_active = False 38 38 user.is_active = False
user.save() 39 39 user.save()
40 40
data = {'email': 'none@none.com', 'password': '1234'} 41 41 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 42 42 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 43 43 self.assertContains(response, 'Account is disabled', status_code=403)
44 44
def test_logout(self): 45 45 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 46 46 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout/') 47 47 response = self.client.post('/api/logout/')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 48 48 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
49 49
# since we're not logged in, we should get a 403 response 50 50 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me/', format='json') 51 51 response = self.client.get('/api/me/', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 52 52 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
53 53
54 54
class PasswordResetTest(APITestCase): 55 55 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 56 56 fixtures = ['testusers']
57 57
def test_reset_password(self): 58 58 def test_reset_password(self):
# submit the request to reset the password 59 59 # submit the request to reset the password
url = '/api/request_password_reset/' 60 60 url = '/api/request_password_reset/'
post_data = {'email': 'none@none.com'} 61 61 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 62 62 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 63 63 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 64 64 self.assertIn('reset your password', mail.outbox[0].body)
65 65
# capture the reset token from the email 66 66 # capture the reset token from the email
capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)', 67 67 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 68 68 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 69 69 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 70 70 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 71 71 reset_token = capture.group(2)
72 72
# try to reset the password with the wrong reset token 73 73 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 74 74 patch_data['token'] = 'wrong_token'
url = '/api/reset_password/' 75 75 url = '/api/reset_password/'
response = self.client.post(url, patch_data, format='json') 76 76 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 77 77 self.assertContains(response, 'Could not verify reset token', status_code=400)
78 78
# try to reset the password with the correct token 79 79 # try to reset the password with the correct token
patch_data['token'] = reset_token 80 80 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 81 81 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 82 82 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 83 83 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 84 84 assert user.check_password(patch_data['new_password'])
85 85
86 86
class RegistrationTest(APITestCase): 87 87 class RegistrationTest(APITestCase):
def test_create_account(self): 88 88 def test_create_account(self):
url = '/api/register/' 89 89 url = '/api/register/'
90 90
# missing password 91 91 # missing password
data = {'email': 'none@none.com'} 92 92 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 93 93 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 94 94 self.assertContains(response, 'password', status_code=400)
95 95
# missing email 96 96 # missing email
data = {'password': '1234'} 97 97 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 98 98 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 99 99 self.assertContains(response, 'email', status_code=400)
100 100
# create a user 101 101 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 102 102 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 103 103 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 104 104 self.assertEqual(response.status_code, HTTP_201_CREATED)
105 105
# user should not be confirmed 106 106 # user should not be confirmed
user = User.objects.get(email="none@none.com") 107 107 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 108 108 self.assertFalse(user.is_confirmed)
109 109
# check that the confirmation key was sent 110 110 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 111 111 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 112 112 self.assertIn(user.confirmation_key, mail.outbox[0].body)
113 113
# log the user out 114 114 # log the user out
self.client.logout() 115 115 self.client.logout()
116 116
# log the user in with their registered credentials 117 117 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 118 118 self.client.login(email='none@none.com', password='1234')
119 119
# try activating with an invalid key 120 120 # try activating with an invalid key
121 121
url = '/api/me/' 122 122 url = '/api/me/'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 123 123 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 124 124 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
125 125
# try activating with the valid key 126 126 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 127 127 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 128 128 self.assertTrue(response.data['is_confirmed'])
129 129
130 130
class ProfileViewTest(APITestCase): 131 131 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 132 132 fixtures = ['testusers']
133 133
def test_get_me(self): 134 134 def test_get_me(self):
url = '/api/me/' 135 135 url = '/api/me/'
response = self.client.get(url, format='json') 136 136 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 137 137 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 138 138 self.assertEqual(response.status_code, 403)
139 139
self.client.login(email='none@none.com', password='1234') 140 140 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 141 141 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 142 142 self.assertEqual(response.status_code, HTTP_200_OK)
143 143
144 144
class UserSectionsTest(APITestCase): 145 145 class UserSectionsTest(APITestCase):
fixtures = ['testusers', 'testsections'] 146 146 fixtures = ['testusers', 'testsections']
147 147
def setUp(self): 148 148 def setUp(self):
self.user = User.objects.get(pk=1) 149 149 self.user = User.objects.get(pk=1)
self.client.login(email='none@none.com', password='1234') 150 150 self.client.login(email='none@none.com', password='1234')
self.section = Section.objects.get(pk=1) 151 151 self.section = Section.objects.get(pk=1)
self.section.enroll(self.user) 152 152 self.section.enroll(self.user)
153 153
def test_get_user_sections(self): 154 154 def test_get_user_sections(self):
response = self.client.get('/api/me/sections/', format='json') 155 155 response = self.client.get('/api/me/sections/', format='json')
self.assertEqual(response.status_code, 200) 156 156 self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Goldstein') 157 157 self.assertContains(response, 'Goldstein')
158 158
159 159
class PasswordChangeTest(APITestCase): 160 160 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 161 161 fixtures = ['testusers']
162 162
def test_change_password(self): 163 163 def test_change_password(self):
url = '/api/me/' 164 164 url = '/api/me/'
user = User.objects.get(email='none@none.com') 165 165 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 166 166 self.assertTrue(user.check_password('1234'))
167 167
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 168 168 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 169 169 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
170 170
self.client.login(email='none@none.com', password='1234') 171 171 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 172 172 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 173 173 self.assertContains(response, 'old_password is required', status_code=400)
174 174
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 175 175 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 176 176 self.assertContains(response, 'old_password is incorrect', status_code=400)
177 177
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 178 178 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 179 179 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 180 180 user = User.objects.get(email='none@none.com')
181 181
self.assertFalse(user.check_password('1234')) 182 182 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 183 183 self.assertTrue(user.check_password('4321'))
184 184
185 185
class DeleteUserTest(APITestCase): 186 186 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 187 187 fixtures = ['testusers']
188 188
def test_delete_user(self): 189 189 def test_delete_user(self):
url = '/api/me/' 190 190 url = '/api/me/'
user = User.objects.get(email='none@none.com') 191 191 user = User.objects.get(email='none@none.com')
192 192
self.client.login(email='none@none.com', password='1234') 193 193 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 194 194 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 195 195 self.assertFalse(User.objects.filter(email='none@none.com').exists())
196 196
197 197
class FlashcardDetailTest(APITestCase): 198 198 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 199 199 fixtures = ['testusers', 'testsections']
200 200
def setUp(self): 201 201 def setUp(self):
self.section = Section.objects.get(pk=1) 202 202 self.section = Section.objects.get(pk=1)
self.user = User.objects.get(email='none@none.com') 203 203 self.user = User.objects.get(email='none@none.com')
self.section.enroll(self.user) 204 204 self.section.enroll(self.user)
self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2), 205 205 self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2),
author=self.user) 206 206 author=self.user)
self.inaccessible_flashcard.save() 207 207 self.inaccessible_flashcard.save()
self.flashcard = Flashcard(text="jason", section=self.section, author=self.user) 208 208 self.flashcard = Flashcard(text="jason", section=self.section, author=self.user)
self.flashcard.save() 209 209 self.flashcard.save()
#self.flashcard.add_to_deck(self.user) 210 210 #self.flashcard.add_to_deck(self.user)
self.client.login(email='none@none.com', password='1234') 211 211 self.client.login(email='none@none.com', password='1234')
212 212
def test_edit_flashcard(self): 213 213 def test_edit_flashcard(self):
user = self.user 214 214 user = self.user
flashcard = self.flashcard 215 215 flashcard = self.flashcard
url = "/api/flashcards/{}/".format(flashcard.pk) 216 216 url = "/api/flashcards/{}/".format(flashcard.pk)
data = {'text': 'new wow for the flashcard', 217 217 data = {'text': 'new wow for the flashcard',
'mask': '[[0,4]]'} 218 218 'mask': '[[0,4]]'}
self.assertNotEqual(flashcard.text, data['text']) 219 219 self.assertNotEqual(flashcard.text, data['text'])
response = self.client.patch(url, data, format='json') 220 220 response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 221 221 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data['text'], data['text']) 222 222 self.assertEqual(response.data['text'], data['text'])
data = {'material_date': datetime.datetime(2015, 4, 12, 2, 2, 2), 223 223 data = {'material_date': datetime(2015, 4, 12, 2, 2, 2),
'mask': '[[1, 3]]'} 224 224 'mask': '[[1, 3]]'}
user2 = User.objects.create(email='wow@wow.wow', password='wow') 225 225 user2 = User.objects.create(email='wow@wow.wow', password='wow')
user2.sections.add(self.section) 226 226 user2.sections.add(self.section)
user2.save() 227 227 user2.save()
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 228 228 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
response = self.client.patch(url, data, format='json') 229 229 response = self.client.patch(url, data, format='json')
serializer = FlashcardSerializer(data=response.data) 230 230 serializer = FlashcardSerializer(data=response.data)
serializer.is_valid(raise_exception=True) 231 231 serializer.is_valid(raise_exception=True)
self.assertEqual(response.status_code, HTTP_200_OK) 232 232 self.assertEqual(response.status_code, HTTP_200_OK)
# self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date'])) 233 233 # self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date']))
self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]])) 234 234 self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]]))
data = {'mask': '[[3,6]]'} 235 235 data = {'mask': '[[3,6]]'}
response = self.client.patch(url, data, format='json') 236 236 response = self.client.patch(url, data, format='json')
user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard) 237 237 user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard)
self.assertEqual(response.status_code, HTTP_200_OK) 238 238 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]])) 239 239 self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]]))
240 240
def test_create_flashcard(self): 241 241 def test_create_flashcard(self):
data = {'text': 'this is a flashcard', 242 242 data = {'text': 'this is a flashcard',
'material_date': now(), 243 243 'material_date': now(),
'mask': '[]', 244 244 'mask': '[]',
'section': '1', 245 245 'section': '1',
'previous': None} 246 246 'previous': None}
response = self.client.post("/api/flashcards/", data, format="json") 247 247 response = self.client.post("/api/flashcards/", data, format="json")
self.assertEqual(response.status_code, HTTP_201_CREATED) 248 248 self.assertEqual(response.status_code, HTTP_201_CREATED)
self.assertEqual(response.data['text'], data['text']) 249 249 self.assertEqual(response.data['text'], data['text'])
self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists()) 250 250 self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists())
251 251
def test_get_flashcard(self): 252 252 def test_get_flashcard(self):
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 253 253 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 254 254 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 255 255 self.assertEqual(response.data["text"], "jason")
256 256
def test_hide_flashcard(self): 257 257 def test_hide_flashcard(self):
response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json') 258 258 response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 259 259 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.flashcard.is_hidden_from(self.user)) 260 260 self.assertTrue(self.flashcard.is_hidden_from(self.user))
261 261
response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json') 262 262 response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json')
# This should fail because the user is not enrolled in section id 2 263 263 # This should fail because the user is not enrolled in section id 2
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 264 264 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
265 265
def test_unhide_flashcard(self): 266 266 def test_unhide_flashcard(self):
self.flashcard.hide_from(self.user) 267 267 self.flashcard.hide_from(self.user)
268 268
response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json') 269 269 response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 270 270 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
271 271
response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json') 272 272 response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json')
273 273
# This should fail because the user is not enrolled in section id 2 274 274 # This should fail because the user is not enrolled in section id 2
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 275 275 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
276 276
277 277
class SectionViewSetTest(APITestCase): 278 278 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 279 279 fixtures = ['testusers', 'testsections']
280 280
def setUp(self): 281 281 def setUp(self):
self.client.login(email='none@none.com', password='1234') 282 282 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 283 283 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), 284 284 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(),
author=self.user) 285 285 author=self.user)
self.flashcard.save() 286 286 self.flashcard.save()
self.section = Section.objects.get(pk=1) 287 287 self.section = Section.objects.get(pk=1)
288 288
def test_list_sections(self): 289 289 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 290 290 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 291 291 self.assertEqual(response.status_code, HTTP_200_OK)
292 292
def test_section_enroll(self): 293 293 def test_section_enroll(self):
section = self.section 294 294 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 295 295 self.assertFalse(self.user.sections.filter(pk=section.pk))
296 296
# test enrolling in a section without a whitelist 297 297 # test enrolling in a section without a whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 298 298 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 299 299 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 300 300 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
301 301
section = Section.objects.get(pk=2) 302 302 section = Section.objects.get(pk=2)
WhitelistedAddress.objects.create(email='bad@none.com', section=section) 303 303 WhitelistedAddress.objects.create(email='bad@none.com', section=section)
304 304
# test enrolling in a section when not on the whitelist 305 305 # test enrolling in a section when not on the whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 306 306 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 307 307 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertFalse(self.user.sections.filter(pk=section.pk).exists()) 308 308 self.assertFalse(self.user.sections.filter(pk=section.pk).exists())
309 309
WhitelistedAddress.objects.create(email=self.user.email, section=section) 310 310 WhitelistedAddress.objects.create(email=self.user.email, section=section)
311 311
# test enrolling in a section when on the whitelist 312 312 # test enrolling in a section when on the whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 313 313 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 314 314 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 315 315 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
316 316
def test_section_drop(self): 317 317 def test_section_drop(self):
section = self.section 318 318 section = self.section
flashcards/tests/test_models.py View file @ f66f9ca
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, UserFlashcardQuiz 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
from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer 6 6 from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer
from flashy.settings import QUARTER_START, QUARTER_END 7 7 from flashy.settings import QUARTER_START, QUARTER_END
8 8
9 9
class RegistrationTests(TestCase): 10 10 class RegistrationTests(TestCase):
def setUp(self): 11 11 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 12 12 User.objects.create_user(email="none@none.com", password="1234")
13 13
def test_email_confirmation(self): 14 14 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 15 15 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 16 16 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 17 17 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 18 18 self.assertTrue(user.is_confirmed)
19 19
20 20
class UserTests(TestCase): 21 21 class UserTests(TestCase):
def setUp(self): 22 22 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 23 23 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 24 24 Section.objects.create(department='dept',
course_num='101a', 25 25 course_num='101a',
course_title='how 2 test', 26 26 course_title='how 2 test',
instructor='George Lucas', 27 27 instructor='George Lucas',
quarter='SP15') 28 28 quarter='SP15')
29 29
def test_section_list(self): 30 30 def test_section_list(self):
section = Section.objects.get(course_num='101a') 31 31 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 32 32 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 33 33 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 34 34 user.sections.add(section)
self.assertIn(section, user.sections.all()) 35 35 self.assertIn(section, user.sections.all())
user.sections.add(section) 36 36 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 37 37 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 38 38 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 39 39 self.assertEqual(user.sections.count(), 0)
40 40
41 41
class FlashcardMaskTest(TestCase): 42 42 class FlashcardMaskTest(TestCase):
def test_empty(self): 43 43 def test_empty(self):
try: 44 44 try:
fm = FlashcardMask([]) 45 45 fm = FlashcardMask([])
self.assertEqual(fm.max_offset(), -1) 46 46 self.assertEqual(fm.max_offset(), -1)
except TypeError: 47 47 except TypeError:
self.fail() 48 48 self.fail()
try: 49 49 try:
fm = FlashcardMask('') 50 50 fm = FlashcardMask('')
self.assertEqual(fm.max_offset(), -1) 51 51 self.assertEqual(fm.max_offset(), -1)
except TypeError: 52 52 except TypeError:
self.fail() 53 53 self.fail()
try: 54 54 try:
fm = FlashcardMask(None) 55 55 fm = FlashcardMask(None)
self.assertEqual(fm.max_offset(), -1) 56 56 self.assertEqual(fm.max_offset(), -1)
except TypeError: 57 57 except TypeError:
self.fail() 58 58 self.fail()
59 59
def test_iterable(self): 60 60 def test_iterable(self):
try: 61 61 try:
FlashcardMask(1) 62 62 FlashcardMask(1)
except TypeError as te: 63 63 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 64 64 self.assertEqual(te.message, "Interval not a valid iterable")
try: 65 65 try:
FlashcardMask([1, 2, 4]) 66 66 FlashcardMask([1, 2, 4])
except TypeError as te: 67 67 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 68 68 self.assertEqual(te.message, "Interval not a valid iterable")
69 69
def test_interval(self): 70 70 def test_interval(self):
try: 71 71 try:
FlashcardMask([[1, 2, 3], [1]]) 72 72 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 73 73 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 74 74 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 75 75 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 76 76 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 77 77 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 78 78 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 79 79 try:
FlashcardMask(([1, 2], [1])) 80 80 FlashcardMask(([1, 2], [1]))
except TypeError as te: 81 81 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 82 82 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 83 83 try:
FlashcardMask("[1,2,3]") 84 84 FlashcardMask("[1,2,3]")
except TypeError as te: 85 85 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 86 86 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
87 87
def test_overlap(self): 88 88 def test_overlap(self):
try: 89 89 try:
FlashcardMask({(1, 2), (2, 5)}) 90 90 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 91 91 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 92 92 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 93 93 try:
FlashcardMask({(1, 20), (12, 15)}) 94 94 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 95 95 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 96 96 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 97 97 try:
FlashcardMask({(2, 1), (5, 2)}) 98 98 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 99 99 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 100 100 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
101 101
102 102
class FlashcardTests(TestCase): 103 103 class FlashcardTests(TestCase):
def setUp(self): 104 104 def setUp(self):
section = Section.objects.create(department='dept', 105 105 section = Section.objects.create(department='dept',
course_num='101a', 106 106 course_num='101a',
course_title='how 2 test', 107 107 course_title='how 2 test',
instructor='George Lucas', 108 108 instructor='George Lucas',
quarter='SP15') 109 109 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 110 110 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 111 111 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 112 112 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 113 113 section=section,
author=user, 114 114 author=user,
material_date=datetime.now(), 115 115 material_date=datetime.now(),
previous=None, 116 116 previous=None,
mask={(24, 34), (0, 4)}) 117 117 mask={(24, 34), (0, 4)})
user.save() 118 118 user.save()
section.save() 119 119 section.save()
flashcard.save() 120 120 flashcard.save()
121 121
def test_flashcard_edit(self): 122 122 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 123 123 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 124 124 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 125 125 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 126 126 user2.sections.add(section)
user2.save() 127 127 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 128 128 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 129 129 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 130 130 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 131 131 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 132 132 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 133 133 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 134 134 flashcard.edit(user2, {'text': 'This is the new text'})
self.assertNotEqual(flashcard.pk, pk_backup) 135 135 self.assertNotEqual(flashcard.pk, pk_backup)
self.assertEqual(flashcard.text, 'This is the new text') 136 136 self.assertEqual(flashcard.text, 'This is the new text')
137 137
def test_mask_field(self): 138 138 def test_mask_field(self):
user = User.objects.get(email="none@none.com") 139 139 user = User.objects.get(email="none@none.com")
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 140 140 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
self.assertTrue(isinstance(flashcard.mask, set)) 141 141 self.assertTrue(isinstance(flashcard.mask, set))
self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) 142 142 self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask]))
blank1, blank2 = sorted(list(flashcard.mask)) 143 143 blank1, blank2 = sorted(list(flashcard.mask))
self.assertEqual(flashcard.text[slice(*blank1)], 'This') 144 144 self.assertEqual(flashcard.text[slice(*blank1)], 'This')
self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') 145 145 self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard')
try: 146 146 try:
flashcard.mask = {(10, 34), (0, 14)} 147 147 flashcard.mask = {(10, 34), (0, 14)}
flashcard.save() 148 148 flashcard.save()
self.fail() 149 149 self.fail()
except OverlapIntervalException: 150 150 except OverlapIntervalException:
self.assertTrue(True) 151 151 self.assertTrue(True)
152 152
153 153
class UserFlashcardQuizTests(TestCase): 154 154 class UserFlashcardQuizTests(TestCase):
def setUp(self): 155 155 def setUp(self):
self.section = Section.objects.create(department='dept', 156 156 self.section = Section.objects.create(department='dept',
course_num='101a', 157 157 course_num='101a',
course_title='how 2 test', 158 158 course_title='how 2 test',
instructor='George Lucas', 159 159 instructor='George Lucas',
quarter='SP15') 160 160 quarter='SP15')
self.user = User.objects.create_user(email="none@none.com", password="1234") 161 161 self.user = User.objects.create_user(email="none@none.com", password="1234")
self.user.sections.add(self.section) 162 162 self.user.sections.add(self.section)
self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 163 163 self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=self.section, 164 164 section=self.section,
author=self.user, 165 165 author=self.user,
material_date=datetime.now(), 166 166 material_date=datetime.now(),
previous=None, 167 167 previous=None,
mask=[(24, 33), (0, 4)]) 168 168 mask=[(24, 33), (0, 4)])
self.user.save() 169 169 self.user.save()
self.section.save() 170 170 self.section.save()
self.flashcard.save() 171 171 self.flashcard.save()
self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard, 172 172 self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard,
user=self.user, 173 173 user=self.user,
mask=self.flashcard.mask, 174 174 mask=self.flashcard.mask,
pulled=datetime.now()) 175 175 pulled=datetime.now())
self.user_flashcard.save() 176 176 self.user_flashcard.save()
self.user_flashcard.refresh_from_db() 177 177 self.user_flashcard.refresh_from_db()
self.flashcard.refresh_from_db() 178 178 self.flashcard.refresh_from_db()
179 179
def test_quiz_request(self): 180 180 def test_quiz_request(self):
data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END} 181 181 data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END}
serializer = QuizRequestSerializer(user=self.user, data=data) 182 182 serializer = QuizRequestSerializer(data=data)
serializer.is_valid(raise_exception=True) 183 183 serializer.is_valid(raise_exception=True)
user_flashcard_quiz = serializer.create(serializer.validated_data) 184 184 validated_data = serializer.create(serializer.validated_data)
185 user_flashcard = UserFlashcard.objects.filter(user=self.user,
186 flashcard__section__pk__in=validated_data['sections'],
187 flashcard__material_date__gte=validated_data['material_date_begin'],
188 flashcard__material_date__lte=validated_data['material_date_end'])
189 self.assertTrue(user_flashcard.exists())
190 user_flashcard = user_flashcard.first()
191 self.assertEqual(user_flashcard, self.user_flashcard)
192 mask = user_flashcard.mask.get_random_blank()
193 word = user_flashcard.flashcard.text[slice(*mask)]
194 user_flashcard_quiz = UserFlashcardQuiz(user_flashcard=user_flashcard,
195 blanked_word=word)
196 user_flashcard_quiz.save()
self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz)) 185 197 self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz))
mask = user_flashcard_quiz.user_flashcard.mask.get_random_blank() 186 198 self.assertIn(mask, [[24, 33], [0, 4]])
self.assertIn(mask, [(24, 33), (0, 4)]) 187
user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)] 188 199 user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)]
self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"]) 189 200 self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"])
user_flashcard_quiz.save() 190 201 user_flashcard_quiz.save()
response = QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data 191 202 response = QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data
self.assertEqual(response['pk'], 1) 192 203 self.assertEqual(response['pk'], 1)
self.assertEqual(response['section'], 1) 193 204 self.assertEqual(response['section'], 1)
self.assertEqual(response['text'], user_flashcard_quiz.user_flashcard.flashcard.text) 194 205 self.assertEqual(response['text'], user_flashcard_quiz.user_flashcard.flashcard.text)
flashcards/validators.py View file @ f66f9ca
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)[0] 20 20 return list(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
39 39
flashcards/views.py View file @ f66f9ca
from random import sample 1 1 from random import sample
2 2
import django 3 3 import django
from django.contrib import auth 4 4 from django.contrib import auth
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, UserFlashcardQuiz 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.notifications import notify_new_card 8 8 from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 9 9 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 10 10 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 11 11 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer 12 12 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 13 13 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 14 14 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 15 15 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 16 16 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 17 17 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 18 18 from django.core.mail import send_mail
from django.contrib.auth import authenticate 19 19 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 20 20 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 21 21 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 22 22 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 23 23 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 24 24 from simple_email_confirmation import EmailAddress
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 """
try: 50 50 try:
self.get_object().enroll(request.user) 51 51 self.get_object().enroll(request.user)
except django.core.exceptions.PermissionDenied as e: 52 52 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 53 53 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 54 54 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 55 55 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 56 56 return Response(status=HTTP_204_NO_CONTENT)
57 57
@detail_route(methods=['POST']) 58 58 @detail_route(methods=['POST'])
def drop(self, request, pk): 59 59 def drop(self, request, pk):
""" 60 60 """
Remove the current user from a specified section 61 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 62 62 If the user is not in the class, the request will fail.
--- 63 63 ---
view_mocker: flashcards.api.mock_no_params 64 64 view_mocker: flashcards.api.mock_no_params
""" 65 65 """
try: 66 66 try:
self.get_object().drop(request.user) 67 67 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 68 68 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 69 69 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 70 70 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 71 71 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 72 72 return Response(status=HTTP_204_NO_CONTENT)
73 73
@list_route(methods=['GET']) 74 74 @list_route(methods=['GET'])
def search(self, request): 75 75 def search(self, request):
""" 76 76 """
Returns a list of sections which match a user's query 77 77 Returns a list of sections which match a user's query
--- 78 78 ---
parameters: 79 79 parameters:
- name: q 80 80 - name: q
description: space-separated list of terms 81 81 description: space-separated list of terms
required: true 82 82 required: true
type: form 83 83 type: form
response_serializer: SectionSerializer 84 84 response_serializer: SectionSerializer
""" 85 85 """
query = request.GET.get('q', None) 86 86 query = request.GET.get('q', None)
if not query: return Response('[]') 87 87 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 88 88 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 89 89 data = SectionSerializer(qs, many=True).data
return Response(data) 90 90 return Response(data)
91 91
@detail_route(methods=['GET']) 92 92 @detail_route(methods=['GET'])
def deck(self, request, pk): 93 93 def deck(self, request, pk):
""" 94 94 """
Gets the contents of a user's deck for a given section. 95 95 Gets the contents of a user's deck for a given section.
""" 96 96 """
qs = request.user.get_deck(self.get_object()) 97 97 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 98 98 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 99 99 return Response(serializer.data)
100 100
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) 101 101 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 102 102 def ordered_deck(self, request, pk):
""" 103 103 """
Get a chronological order by material_date of flashcards for a section. 104 104 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 105 105 This excludes hidden card.
""" 106 106 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 107 107 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 108 108 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 109 109 return Response(serializer.data)
110 110
@detail_route(methods=['GET']) 111 111 @detail_route(methods=['GET'])
def feed(self, request, pk): 112 112 def feed(self, request, pk):
""" 113 113 """
Gets the contents of a user's feed for a section. 114 114 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 115 115 Exclude cards that are already in the user's deck
""" 116 116 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 117 117 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 118 118 return Response(serializer.data)
119 119
120 120
class UserSectionListView(ListAPIView): 121 121 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 122 122 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 123 123 permission_classes = [IsAuthenticated]
124 124
def get_queryset(self): 125 125 def get_queryset(self):
return self.request.user.sections.all() 126 126 return self.request.user.sections.all()
127 127
def paginate_queryset(self, queryset): return None 128 128 def paginate_queryset(self, queryset): return None
129 129
130 130
class UserDetail(GenericAPIView): 131 131 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 132 132 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 133 133 permission_classes = [IsAuthenticated]
134 134
def patch(self, request, format=None): 135 135 def patch(self, request, format=None):
""" 136 136 """
Updates the user's password, or verifies their email address 137 137 Updates the user's password, or verifies their email address
--- 138 138 ---
request_serializer: UserUpdateSerializer 139 139 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 140 140 response_serializer: UserSerializer
""" 141 141 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 142 142 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 143 143 data.is_valid(raise_exception=True)
data = data.validated_data 144 144 data = data.validated_data
145 145
if 'new_password' in data: 146 146 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 147 147 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 148 148 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 149 149 request.user.set_password(data['new_password'])
request.user.save() 150 150 request.user.save()
151 151
if 'confirmation_key' in data: 152 152 if 'confirmation_key' in data:
try: 153 153 try:
request.user.confirm_email(data['confirmation_key']) 154 154 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 155 155 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 156 156 raise ValidationError('confirmation_key is invalid')
157 157
return Response(UserSerializer(request.user).data) 158 158 return Response(UserSerializer(request.user).data)
159 159
def get(self, request, format=None): 160 160 def get(self, request, format=None):
""" 161 161 """
Return data about the user 162 162 Return data about the user
--- 163 163 ---
response_serializer: UserSerializer 164 164 response_serializer: UserSerializer
""" 165 165 """
serializer = UserSerializer(request.user, context={'request': request}) 166 166 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 167 167 return Response(serializer.data)
168 168
def delete(self, request): 169 169 def delete(self, request):
""" 170 170 """
Irrevocably delete the user and their data 171 171 Irrevocably delete the user and their data
172 172
Yes, really 173 173 Yes, really
""" 174 174 """
request.user.delete() 175 175 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 176 176 return Response(status=HTTP_204_NO_CONTENT)
177 177
178 178
@api_view(['POST']) 179 179 @api_view(['POST'])
def register(request, format=None): 180 180 def register(request, format=None):
""" 181 181 """
Register a new user 182 182 Register a new user
--- 183 183 ---
request_serializer: EmailPasswordSerializer 184 184 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 185 185 response_serializer: UserSerializer
""" 186 186 """
data = RegistrationSerializer(data=request.data) 187 187 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 188 188 data.is_valid(raise_exception=True)
189 189
User.objects.create_user(**data.validated_data) 190 190 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 191 191 user = authenticate(**data.validated_data)
auth.login(request, user) 192 192 auth.login(request, user)
193 193
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 194 194 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
195 195
196 196
@api_view(['POST']) 197 197 @api_view(['POST'])
def login(request): 198 198 def login(request):
""" 199 199 """
Authenticates user and returns user data if valid. 200 200 Authenticates user and returns user data if valid.
--- 201 201 ---
request_serializer: EmailPasswordSerializer 202 202 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 203 203 response_serializer: UserSerializer
""" 204 204 """
205 205
data = EmailPasswordSerializer(data=request.data) 206 206 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 207 207 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 208 208 user = authenticate(**data.validated_data)
209 209
if user is None: 210 210 if user is None:
raise AuthenticationFailed('Invalid email or password') 211 211 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 212 212 if not user.is_active:
raise NotAuthenticated('Account is disabled') 213 213 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 214 214 auth.login(request, user)
return Response(UserSerializer(request.user).data) 215 215 return Response(UserSerializer(request.user).data)
216 216
217 217
@api_view(['POST']) 218 218 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 219 219 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 220 220 def logout(request, format=None):
""" 221 221 """
Logs the authenticated user out. 222 222 Logs the authenticated user out.
""" 223 223 """
auth.logout(request) 224 224 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 225 225 return Response(status=HTTP_204_NO_CONTENT)
226 226
227 227
@api_view(['POST']) 228 228 @api_view(['POST'])
def request_password_reset(request, format=None): 229 229 def request_password_reset(request, format=None):
""" 230 230 """
Send a password reset token/link to the provided email. 231 231 Send a password reset token/link to the provided email.
--- 232 232 ---
request_serializer: PasswordResetRequestSerializer 233 233 request_serializer: PasswordResetRequestSerializer
""" 234 234 """
data = PasswordResetRequestSerializer(data=request.data) 235 235 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 236 236 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 237 237 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 238 238 return Response(status=HTTP_204_NO_CONTENT)
239 239
240 240
@api_view(['POST']) 241 241 @api_view(['POST'])
def reset_password(request, format=None): 242 242 def reset_password(request, format=None):
""" 243 243 """
Updates user's password to new password if token is valid. 244 244 Updates user's password to new password if token is valid.
--- 245 245 ---
request_serializer: PasswordResetSerializer 246 246 request_serializer: PasswordResetSerializer
""" 247 247 """
data = PasswordResetSerializer(data=request.data) 248 248 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 249 249 data.is_valid(raise_exception=True)
250 250
user = User.objects.get(id=data['uid'].value) 251 251 user = User.objects.get(id=data['uid'].value)
# Check token validity. 252 252 # Check token validity.
253 253
if default_token_generator.check_token(user, data['token'].value): 254 254 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 255 255 user.set_password(data['new_password'].value)
user.save() 256 256 user.save()
else: 257 257 else:
raise ValidationError('Could not verify reset token') 258 258 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 259 259 return Response(status=HTTP_204_NO_CONTENT)
260 260
261 261
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 262 262 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 263 263 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 264 264 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 265 265 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
266 266
# Override create in CreateModelMixin 267 267 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 268 268 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 269 269 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 270 270 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 271 271 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 272 272 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 273 273 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 274 274 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 275 275 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 276 276 self.perform_create(flashcard)
notify_new_card(flashcard) 277 277 notify_new_card(flashcard)
headers = self.get_success_headers(data) 278 278 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard).data 279 279 response_data = FlashcardSerializer(flashcard).data
280 280
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 281 281 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
282 282
@detail_route(methods=['POST']) 283 283 @detail_route(methods=['POST'])
def unhide(self, request, pk): 284 284 def unhide(self, request, pk):
""" 285 285 """
Unhide the given card 286 286 Unhide the given card
--- 287 287 ---
view_mocker: flashcards.api.mock_no_params 288 288 view_mocker: flashcards.api.mock_no_params
""" 289 289 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 290 290 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 291 291 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 292 292 return Response(status=HTTP_204_NO_CONTENT)
293 293
@detail_route(methods=['POST']) 294 294 @detail_route(methods=['POST'])
def report(self, request, pk): 295 295 def report(self, request, pk):
""" 296 296 """
Hide the given card 297 297 Hide the given card
--- 298 298 ---
view_mocker: flashcards.api.mock_no_params 299 299 view_mocker: flashcards.api.mock_no_params
""" 300 300 """
self.get_object().report(request.user) 301 301 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 302 302 return Response(status=HTTP_204_NO_CONTENT)
303 303
hide = report 304 304 hide = report
305 305
@detail_route(methods=['POST']) 306 306 @detail_route(methods=['POST'])
def pull(self, request, pk): 307 307 def pull(self, request, pk):
""" 308 308 """
Pull a card from the live feed into the user's deck. 309 309 Pull a card from the live feed into the user's deck.
--- 310 310 ---
view_mocker: flashcards.api.mock_no_params 311 311 view_mocker: flashcards.api.mock_no_params
""" 312 312 """
user = request.user 313 313 user = request.user
flashcard = self.get_object() 314 314 flashcard = self.get_object()
user.pull(flashcard) 315 315 user.pull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 316 316 return Response(status=HTTP_204_NO_CONTENT)
317 317
@detail_route(methods=['POST']) 318 318 @detail_route(methods=['POST'])
def unpull(self, request, pk): 319 319 def unpull(self, request, pk):
""" 320 320 """
Unpull a card from the user's deck 321 321 Unpull a card from the user's deck
--- 322 322 ---
view_mocker: flashcards.api.mock_no_params 323 323 view_mocker: flashcards.api.mock_no_params
""" 324 324 """
user = request.user 325 325 user = request.user
flashcard = self.get_object() 326 326 flashcard = self.get_object()
user.unpull(flashcard) 327 327 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 328 328 return Response(status=HTTP_204_NO_CONTENT)
329 329
def partial_update(self, request, *args, **kwargs): 330 330 def partial_update(self, request, *args, **kwargs):
""" 331 331 """
Edit settings related to a card for the user. 332 332 Edit settings related to a card for the user.
--- 333 333 ---
request_serializer: FlashcardUpdateSerializer 334 334 request_serializer: FlashcardUpdateSerializer
""" 335 335 """
user = request.user 336 336 user = request.user
flashcard = self.get_object() 337 337 flashcard = self.get_object()
data = FlashcardUpdateSerializer(data=request.data) 338 338 data = FlashcardUpdateSerializer(data=request.data)
data.is_valid(raise_exception=True) 339 339 data.is_valid(raise_exception=True)
new_flashcard = data.validated_data 340 340 new_flashcard = data.validated_data
new_flashcard = flashcard.edit(user, new_flashcard) 341 341 new_flashcard = flashcard.edit(user, new_flashcard)
return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK) 342 342 return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK)
343 343
344 344
class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin): 345 345 class UserFlashcardQuizViewSet(GenericViewSet, CreateModelMixin, UpdateModelMixin):
queryset = UserFlashcardQuiz.objects.all() 346
serializer_class = QuizAnswerRequestSerializer 347
permission_classes = [IsAuthenticated, IsFlashcardReviewer] 348 346 permission_classes = [IsAuthenticated, IsFlashcardReviewer]
347 queryset = UserFlashcardQuiz.objects.all()
349 348
349 def get_serializer_class(self):
requirements.txt View file @ f66f9ca
#beautifulsoup4 1 1 #beautifulsoup4
Django>=1.8 2 2 Django>=1.8
django-websocket-redis 3 3 django-websocket-redis
#gevent==1.0.1 4 4 #gevent==1.0.1
#greenlet==0.4.5 5 5 #greenlet==0.4.5
#redis==2.10.3 6 6 redis==2.10.3
six==1.9.0 7 7 six==1.9.0
djangorestframework 8 8 djangorestframework
docutils 9 9 docutils
django-simple-email-confirmation 10 10 django-simple-email-confirmation
coverage 11 11 coverage
django-rest-swagger 12 12 django-rest-swagger
pytz 13 13 pytz
14 14
scripts/run_production.sh View file @ f66f9ca
#!/bin/bash -xe 1 1 #!/bin/bash -xe
source secrets.sh 2 2 source secrets.sh
source venv/bin/activate 3 3 source venv/bin/activate
# newrelic-admin run-program /srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi 4 4 # newrelic-admin run-program /srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi
uwsgi /etc/uwsgi/websocket.ini --touch-reload=/etc/uwsgi/websocket.ini & 5 5 uwsgi /etc/uwsgi/websocket.ini --touch-reload=/etc/uwsgi/websocket.ini &
newrelic-admin run-program uwsgi /etc/uwsgi/flashy.ini --touch-reload=/etc/uwsgi/flashy.ini 6 6 newrelic-admin run-program uwsgi /etc/uwsgi/flashy.ini --touch-reload=/etc/uwsgi/flashy.ini
7 trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT