Commit 8964ffa2688539aac9869da6528b5a346326249b

Authored by Rohan Rangray
1 parent a06c7fa689
Exists in master

Corrected FlashcardViewSet.edit and fixed FlashcardViewSet.create

Showing 5 changed files with 60 additions and 14 deletions Inline Diff

flashcards/serializers.py View file @ 8964ffa
from django.utils.datetime_safe import datetime 1 1 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 2 2 from django.utils.timezone import now
import pytz 3 3 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard 4 4 from flashcards.models import Section, LecturePeriod, User, Flashcard
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 6 6 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 7 7 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField
from rest_framework.relations import HyperlinkedRelatedField 8 8 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 9 9 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 10 10 from rest_framework.validators import UniqueValidator
from json import dumps, loads 11 11 from json import dumps, loads
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):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 72 72 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
lecture_times = CharField() 73 73 lecture_times = CharField()
short_name = CharField() 74 74 short_name = CharField()
long_name = CharField() 75 75 long_name = CharField()
76 76
class Meta: 77 77 class Meta:
model = Section 78 78 model = Section
79 79
80 80
class UserSerializer(ModelSerializer): 81 81 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 82 82 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 83 83 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 84 84 is_confirmed = BooleanField()
85 85
class Meta: 86 86 class Meta:
model = User 87 87 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 88 88 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
89 89
90 90
class MaskFieldSerializer(serializers.Field): 91 91 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 92 92 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 93 93 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 94 94 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 95 95 'overlap': 'Ensure this field does not have overlapping intervals.'
} 96 96 }
97 97
def to_representation(self, value): 98 98 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 99 99 return dumps(list(self._make_mask(value)))
100 100
def to_internal_value(self, value): 101 101 def to_internal_value(self, value):
return self._make_mask(loads(value)) 102 102 return self._make_mask(loads(value))
103 103
def _make_mask(self, data): 104 104 def _make_mask(self, data):
try: 105 105 try:
mask = FlashcardMask(data) 106 106 mask = FlashcardMask(data)
except ValueError: 107 107 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 108 108 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 109 109 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 110 110 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 111 111 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 112 112 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 113 113 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 114 114 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 115 115 return mask
116 116
117 117
class FlashcardSerializer(ModelSerializer): 118 118 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 119 119 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 120 120 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 121 121 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer() 122 122 mask = MaskFieldSerializer()
123 123
def validate_material_date(self, value): 124 124 def validate_material_date(self, value):
utc = pytz.UTC 125 125 utc = pytz.UTC
# TODO: make this dynamic 126 126 # TODO: make this dynamic
quarter_start = utc.localize(datetime(2015, 3, 15)) 127 127 quarter_start = utc.localize(datetime(2015, 3, 15))
quarter_end = utc.localize(datetime(2015, 6, 15)) 128 128 quarter_end = utc.localize(datetime(2015, 6, 15))
129 129
if quarter_start <= value <= quarter_end: 130 130 if quarter_start <= value <= quarter_end:
return value 131 131 return value
else: 132 132 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 133 133 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
134 134
def validate_previous(self, value): 135 135 def validate_previous(self, value):
if value is None: 136 136 if value is None:
return value 137 137 return value
if Flashcard.objects.filter(pk=value).count() > 0: 138 138 if Flashcard.objects.filter(pk=value).count() > 0:
return value 139 139 return value
raise serializers.ValidationError("Invalid previous Flashcard object") 140 140 raise serializers.ValidationError("Invalid previous Flashcard object")
141 141
def validate_pushed(self, value): 142 142 def validate_pushed(self, value):
if value > datetime.now(): 143 143 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 144 144 raise serializers.ValidationError("Invalid creation date for the Flashcard")
flashcards/tests/test_api.py View file @ 8964ffa
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, HTTP_404_NOT_FOUND
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
from django.utils.timezone import now 6 6 from django.utils.timezone import now
7 7
8 8
class LoginTests(APITestCase): 9 9 class LoginTests(APITestCase):
fixtures = ['testusers'] 10 10 fixtures = ['testusers']
11 11
def test_login(self): 12 12 def test_login(self):
url = '/api/login' 13 13 url = '/api/login'
data = {'email': 'none@none.com', 'password': '1234'} 14 14 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 15 15 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 16 16 self.assertEqual(response.status_code, HTTP_200_OK)
17 17
data = {'email': 'none@none.com', 'password': '4321'} 18 18 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 19 19 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 20 20 self.assertContains(response, 'Invalid email or password', status_code=403)
21 21
data = {'email': 'bad@none.com', 'password': '1234'} 22 22 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 23 23 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 24 24 self.assertContains(response, 'Invalid email or password', status_code=403)
25 25
data = {'password': '4321'} 26 26 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 27 27 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 28 28 self.assertContains(response, 'email', status_code=400)
29 29
data = {'email': 'none@none.com'} 30 30 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 31 31 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 32 32 self.assertContains(response, 'password', status_code=400)
33 33
user = User.objects.get(email="none@none.com") 34 34 user = User.objects.get(email="none@none.com")
user.is_active = False 35 35 user.is_active = False
user.save() 36 36 user.save()
37 37
data = {'email': 'none@none.com', 'password': '1234'} 38 38 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 39 39 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 40 40 self.assertContains(response, 'Account is disabled', status_code=403)
41 41
def test_logout(self): 42 42 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 43 43 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout') 44 44 response = self.client.post('/api/logout')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 45 45 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
46 46
# since we're not logged in, we should get a 403 response 47 47 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me', format='json') 48 48 response = self.client.get('/api/me', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 49 49 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
50 50
51 51
class PasswordResetTest(APITestCase): 52 52 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 53 53 fixtures = ['testusers']
54 54
def test_reset_password(self): 55 55 def test_reset_password(self):
# submit the request to reset the password 56 56 # submit the request to reset the password
url = '/api/request_password_reset' 57 57 url = '/api/request_password_reset'
post_data = {'email': 'none@none.com'} 58 58 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 59 59 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 60 60 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 61 61 self.assertIn('reset your password', mail.outbox[0].body)
62 62
# capture the reset token from the email 63 63 # capture the reset token from the email
capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', 64 64 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 65 65 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 66 66 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 67 67 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 68 68 reset_token = capture.group(2)
69 69
# try to reset the password with the wrong reset token 70 70 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 71 71 patch_data['token'] = 'wrong_token'
url = '/api/reset_password' 72 72 url = '/api/reset_password'
response = self.client.post(url, patch_data, format='json') 73 73 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 74 74 self.assertContains(response, 'Could not verify reset token', status_code=400)
75 75
# try to reset the password with the correct token 76 76 # try to reset the password with the correct token
patch_data['token'] = reset_token 77 77 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 78 78 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 79 79 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 80 80 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 81 81 assert user.check_password(patch_data['new_password'])
82 82
83 83
class RegistrationTest(APITestCase): 84 84 class RegistrationTest(APITestCase):
def test_create_account(self): 85 85 def test_create_account(self):
url = '/api/register' 86 86 url = '/api/register'
87 87
# missing password 88 88 # missing password
data = {'email': 'none@none.com'} 89 89 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 90 90 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 91 91 self.assertContains(response, 'password', status_code=400)
92 92
# missing email 93 93 # missing email
data = {'password': '1234'} 94 94 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 95 95 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 96 96 self.assertContains(response, 'email', status_code=400)
97 97
# create a user 98 98 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 99 99 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 100 100 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 101 101 self.assertEqual(response.status_code, HTTP_201_CREATED)
102 102
# user should not be confirmed 103 103 # user should not be confirmed
user = User.objects.get(email="none@none.com") 104 104 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 105 105 self.assertFalse(user.is_confirmed)
106 106
# check that the confirmation key was sent 107 107 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 108 108 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 109 109 self.assertIn(user.confirmation_key, mail.outbox[0].body)
110 110
# log the user out 111 111 # log the user out
self.client.logout() 112 112 self.client.logout()
113 113
# log the user in with their registered credentials 114 114 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 115 115 self.client.login(email='none@none.com', password='1234')
116 116
# try activating with an invalid key 117 117 # try activating with an invalid key
118 118
url = '/api/me' 119 119 url = '/api/me'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 120 120 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 121 121 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
122 122
# try activating with the valid key 123 123 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 124 124 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 125 125 self.assertTrue(response.data['is_confirmed'])
126 126
127 127
class ProfileViewTest(APITestCase): 128 128 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 129 129 fixtures = ['testusers']
130 130
def test_get_me(self): 131 131 def test_get_me(self):
url = '/api/me' 132 132 url = '/api/me'
response = self.client.get(url, format='json') 133 133 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 134 134 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 135 135 self.assertEqual(response.status_code, 403)
136 136
self.client.login(email='none@none.com', password='1234') 137 137 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 138 138 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 139 139 self.assertEqual(response.status_code, HTTP_200_OK)
140 140
141 141
class PasswordChangeTest(APITestCase): 142 142 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 143 143 fixtures = ['testusers']
144 144
def test_change_password(self): 145 145 def test_change_password(self):
url = '/api/me' 146 146 url = '/api/me'
user = User.objects.get(email='none@none.com') 147 147 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 148 148 self.assertTrue(user.check_password('1234'))
149 149
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 150 150 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 151 151 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
152 152
self.client.login(email='none@none.com', password='1234') 153 153 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 154 154 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 155 155 self.assertContains(response, 'old_password is required', status_code=400)
156 156
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 157 157 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 158 158 self.assertContains(response, 'old_password is incorrect', status_code=400)
159 159
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 160 160 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 161 161 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 162 162 user = User.objects.get(email='none@none.com')
163 163
self.assertFalse(user.check_password('1234')) 164 164 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 165 165 self.assertTrue(user.check_password('4321'))
166 166
167 167
class DeleteUserTest(APITestCase): 168 168 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 169 169 fixtures = ['testusers']
170 170
def test_delete_user(self): 171 171 def test_delete_user(self):
url = '/api/me' 172 172 url = '/api/me'
user = User.objects.get(email='none@none.com') 173 173 user = User.objects.get(email='none@none.com')
174 174
self.client.login(email='none@none.com', password='1234') 175 175 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 176 176 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 177 177 self.assertFalse(User.objects.filter(email='none@none.com').exists())
178 178
179 179
class FlashcardDetailTest(APITestCase): 180 180 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 181 181 fixtures = ['testusers', 'testsections']
182 182
def setUp(self): 183 183 def setUp(self):
section = Section.objects.get(pk=1) 184 184 section = Section.objects.get(pk=1)
user = User.objects.get(email='none@none.com') 185 185 user = User.objects.get(email='none@none.com')
186 186
self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) 187 187 self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user)
self.flashcard.save() 188 188 self.flashcard.save()
189 189
190 def test_edit_flashcard(self):
191 self.client.login(email='none@none.com', password='1234')
192 user = User.objects.get(email='none@none.com')
193 user.sections.add(Section.objects.get(pk=1))
194 user.save()
195
196 def test_create_flashcard(self):
197 self.client.login(email='none@none.com', password='1234')
198 user = User.objects.get(email='none@none.com')
199 user.sections.add(Section.objects.get(pk=1))
200 user.save()
201 data = {'text': 'this is a flashcard',
202 'material_date': str(datetime.now()),
203 'mask': '[]',
204 'section': '1',
205 'previous': None}
206 response = self.client.post("/api/flashcards/", data, format="json")
207 self.assertEqual(response.status_code, HTTP_201_CREATED)
208 self.assertEqual(response.data['text'], data['text'])
209 self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists())
210
def test_get_flashcard(self): 190 211 def test_get_flashcard(self):
self.client.login(email='none@none.com', password='1234') 191 212 self.client.login(email='none@none.com', password='1234')
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 192 213 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 193 214 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 194 215 self.assertEqual(response.data["text"], "jason")
195 216
196 217
class SectionViewSetTest(APITestCase): 197 218 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 198 219 fixtures = ['testusers', 'testsections']
199 220
def setUp(self): 200 221 def setUp(self):
self.client.login(email='none@none.com', password='1234') 201 222 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 202 223 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user) 203 224 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user)
self.flashcard.save() 204 225 self.flashcard.save()
self.section = Section.objects.get(pk=1) 205 226 self.section = Section.objects.get(pk=1)
206 227
def test_list_sections(self): 207 228 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 208 229 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 209 230 self.assertEqual(response.status_code, HTTP_200_OK)
210 231
def test_section_enroll(self): 211 232 def test_section_enroll(self):
section = self.section 212 233 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 213 234 self.assertFalse(self.user.sections.filter(pk=section.pk))
214 235
# test enrolling in a section without a whitelist 215 236 # test enrolling in a section without a whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 216 237 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
flashcards/tests/test_models.py View file @ 8964ffa
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
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
6 6
7 7
class RegistrationTests(TestCase): 8 8 class RegistrationTests(TestCase):
def setUp(self): 9 9 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 10 10 User.objects.create_user(email="none@none.com", password="1234")
11 11
def test_email_confirmation(self): 12 12 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 13 13 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 14 14 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 15 15 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 16 16 self.assertTrue(user.is_confirmed)
17 17
18 18
class UserTests(TestCase): 19 19 class UserTests(TestCase):
def setUp(self): 20 20 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 21 21 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 22 22 Section.objects.create(department='dept',
course_num='101a', 23 23 course_num='101a',
course_title='how 2 test', 24 24 course_title='how 2 test',
instructor='George Lucas', 25 25 instructor='George Lucas',
quarter='SP15') 26 26 quarter='SP15')
27 27
def test_section_list(self): 28 28 def test_section_list(self):
section = Section.objects.get(course_num='101a') 29 29 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 30 30 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 31 31 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 32 32 user.sections.add(section)
self.assertIn(section, user.sections.all()) 33 33 self.assertIn(section, user.sections.all())
user.sections.add(section) 34 34 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 35 35 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 36 36 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 37 37 self.assertEqual(user.sections.count(), 0)
38 38
39 39
class FlashcardMaskTest(TestCase): 40 40 class FlashcardMaskTest(TestCase):
41 def test_empty(self):
42 try:
43 fm = FlashcardMask([])
44 self.assertEqual(fm.max_offset(), -1)
45 except TypeError:
46 self.fail()
47 try:
48 fm = FlashcardMask('')
49 self.assertEqual(fm.max_offset(), -1)
50 except TypeError:
51 self.fail()
52 try:
53 fm = FlashcardMask(None)
54 self.assertEqual(fm.max_offset(), -1)
55 except TypeError:
56 self.fail()
57
def test_iterable(self): 41 58 def test_iterable(self):
try: 42 59 try:
FlashcardMask(1) 43 60 FlashcardMask(1)
except TypeError as te: 44 61 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 45 62 self.assertEqual(te.message, "Interval not a valid iterable")
try: 46 63 try:
FlashcardMask([1, 2, 4]) 47 64 FlashcardMask([1, 2, 4])
except TypeError as te: 48 65 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 49 66 self.assertEqual(te.message, "Interval not a valid iterable")
50 67
def test_interval(self): 51 68 def test_interval(self):
try: 52 69 try:
FlashcardMask([[1, 2, 3], [1]]) 53 70 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 54 71 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 55 72 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 56 73 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 57 74 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 58 75 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 59 76 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 60 77 try:
FlashcardMask(([1, 2], [1])) 61 78 FlashcardMask(([1, 2], [1]))
except TypeError as te: 62 79 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 63 80 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 64 81 try:
FlashcardMask("[1,2,3]") 65 82 FlashcardMask("[1,2,3]")
except TypeError as te: 66 83 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 67 84 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
68 85
def test_overlap(self): 69 86 def test_overlap(self):
try: 70 87 try:
FlashcardMask({(1, 2), (2, 5)}) 71 88 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 72 89 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 73 90 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 74 91 try:
FlashcardMask({(1, 20), (12, 15)}) 75 92 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 76 93 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 77 94 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 78 95 try:
FlashcardMask({(2, 1), (5, 2)}) 79 96 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 80 97 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 81 98 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
82 99
83 100
84 101
class FlashcardTests(TestCase): 85 102 class FlashcardTests(TestCase):
def setUp(self): 86 103 def setUp(self):
section = Section.objects.create(department='dept', 87 104 section = Section.objects.create(department='dept',
course_num='101a', 88 105 course_num='101a',
course_title='how 2 test', 89 106 course_title='how 2 test',
instructor='George Lucas', 90 107 instructor='George Lucas',
quarter='SP15') 91 108 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 92 109 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 93 110 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 94 111 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 95 112 section=section,
author=user, 96 113 author=user,
material_date=datetime.now(), 97 114 material_date=datetime.now(),
previous=None, 98 115 previous=None,
mask={(24,34), (0, 4)}) 99 116 mask={(24,34), (0, 4)})
user.save() 100 117 user.save()
section.save() 101 118 section.save()
flashcard.save() 102 119 flashcard.save()
103 120
def test_flashcard_edit(self): 104 121 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 105 122 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 106 123 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 107 124 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 108 125 user2.sections.add(section)
user2.save() 109 126 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 110 127 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 111 128 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 112 129 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 113 130 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 114 131 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 115 132 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 116 133 flashcard.edit(user2, {'text': 'This is the new text'})
self.assertNotEqual(flashcard.pk, pk_backup) 117 134 self.assertNotEqual(flashcard.pk, pk_backup)
flashcards/validators.py View file @ 8964ffa
from collections import Iterable 1 1 from collections import Iterable
2 2
3 3
class FlashcardMask(set): 4 4 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 5 5 def __init__(self, iterable, *args, **kwargs):
6 if iterable is None or iterable == '':
7 iterable = []
self._iterable_check(iterable) 6 8 self._iterable_check(iterable)
iterable = map(tuple, iterable) 7 9 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 8 10 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._interval_check() 9 11 self._interval_check()
self._overlap_check() 10 12 self._overlap_check()
11 13
def max_offset(self): 12 14 def max_offset(self):
return self._end 13 15 return self._end
14 16
def _iterable_check(self, iterable): 15 17 def _iterable_check(self, iterable):
if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): 16 18 if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]):
raise TypeError("Interval not a valid iterable") 17 19 raise TypeError("Interval not a valid iterable")
18 20
def _interval_check(self): 19 21 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 20 22 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 21 23 raise TypeError("Intervals must have exactly 2 elements, begin and end")
22 24
def _overlap_check(self): 23 25 def _overlap_check(self):
p_beg, p_end = -1, -1 24 26 p_beg, p_end = -1, -1
for interval in sorted(self): 25 27 for interval in sorted(self):
beg, end = map(int, interval) 26 28 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 27 29 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") 28 30 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 29 31 p_beg, p_end = beg, end
self._end = p_end 30 32 self._end = p_end
flashcards/views.py View file @ 8964ffa
from django.contrib import auth 1 1 from django.contrib import auth
from flashcards.api import StandardResultsSetPagination 2 2 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 3 3 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 4 4 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 5 5 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer 6 6 FlashcardUpdateSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view 7 7 from rest_framework.decorators import detail_route, permission_classes, api_view
from rest_framework.generics import ListAPIView, GenericAPIView 8 8 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 9 9 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 10 10 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 11 11 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 12 12 from django.core.mail import send_mail
from django.contrib.auth import authenticate 13 13 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 14 14 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 15 15 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 16 16 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 17 17 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 18 18 from simple_email_confirmation import EmailAddress
19 19
20 20
class SectionViewSet(ReadOnlyModelViewSet): 21 21 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 22 22 queryset = Section.objects.all()
serializer_class = SectionSerializer 23 23 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 24 24 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 25 25 permission_classes = [IsAuthenticated]
26 26
@detail_route(methods=['GET']) 27 27 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 28 28 def flashcards(self, request, pk):
""" 29 29 """
Gets flashcards for a section, excluding hidden cards. 30 30 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 31 31 Returned in strictly chronological order (material date).
""" 32 32 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 33 33 flashcards = Flashcard.cards_visible_to(request.user).filter( \
section=self.get_object()).all() 34 34 section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 35 35 return Response(FlashcardSerializer(flashcards, many=True).data)
36 36
@detail_route(methods=['post']) 37 37 @detail_route(methods=['post'])
def enroll(self, request, pk): 38 38 def enroll(self, request, pk):
""" 39 39 """
Add the current user to a specified section 40 40 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. 41 41 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 42 42 ---
omit_serializer: true 43 43 omit_serializer: true
parameters: 44 44 parameters:
- fake: None 45 45 - fake: None
parameters_strategy: 46 46 parameters_strategy:
form: replace 47 47 form: replace
""" 48 48 """
section = self.get_object() 49 49 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 50 50 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 51 51 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 52 52 if section.is_whitelisted and not section.is_user_on_whitelist(request.user):
raise PermissionDenied("You must be on the whitelist to add this section.") 53 53 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 54 54 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 55 55 return Response(status=HTTP_204_NO_CONTENT)
56 56
@detail_route(methods=['post']) 57 57 @detail_route(methods=['post'])
def drop(self, request, pk): 58 58 def drop(self, request, pk):
""" 59 59 """
Remove the current user from a specified section 60 60 Remove the current user from a specified section
If the user is not in the class, the request will fail. 61 61 If the user is not in the class, the request will fail.
--- 62 62 ---
omit_serializer: true 63 63 omit_serializer: true
parameters: 64 64 parameters:
- fake: None 65 65 - fake: None
parameters_strategy: 66 66 parameters_strategy:
form: replace 67 67 form: replace
""" 68 68 """
section = self.get_object() 69 69 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 70 70 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 71 71 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 72 72 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 73 73 return Response(status=HTTP_204_NO_CONTENT)
74 74
@detail_route(methods=['GET']) 75 75 @detail_route(methods=['GET'])
def search(self, request): 76 76 def search(self, request):
""" 77 77 """
Returns a list of sections which match a user's query 78 78 Returns a list of sections which match a user's query
""" 79 79 """
query = request.GET.get('q', None) 80 80 query = request.GET.get('q', None)
if not query: return Response('[]') 81 81 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 82 82 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 83 83 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 84 84 return Response(serializer.data)
85 85
@detail_route(methods=['GET']) 86 86 @detail_route(methods=['GET'])
def deck(self, request, pk): 87 87 def deck(self, request, pk):
""" 88 88 """
Gets the contents of a user's deck for a given section. 89 89 Gets the contents of a user's deck for a given section.
""" 90 90 """
qs = request.user.get_deck(self.get_object()) 91 91 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 92 92 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 93 93 return Response(serializer.data)
94 94
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 95 95 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 96 96 def ordered_deck(self, request, pk):
""" 97 97 """
Get a chronological order by material_date of flashcards for a section. 98 98 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 99 99 This excludes hidden card.
""" 100 100 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 101 101 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 102 102 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 103 103 return Response(serializer.data)
104 104
@detail_route(methods=['GET']) 105 105 @detail_route(methods=['GET'])
def feed(self, request, pk): 106 106 def feed(self, request, pk):
""" 107 107 """
Gets the contents of a user's feed for a section. 108 108 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 109 109 Exclude cards that are already in the user's deck
""" 110 110 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 111 111 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 112 112 return Response(serializer.data)
113 113
114 114
class UserSectionListView(ListAPIView): 115 115 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 116 116 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 117 117 permission_classes = [IsAuthenticated]
118 118
def get_queryset(self): 119 119 def get_queryset(self):
return self.request.user.sections.all() 120 120 return self.request.user.sections.all()
121 121
def paginate_queryset(self, queryset): return None 122 122 def paginate_queryset(self, queryset): return None
123 123
124 124
class UserDetail(GenericAPIView): 125 125 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 126 126 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 127 127 permission_classes = [IsAuthenticated]
128 128
def get_queryset(self): 129 129 def get_queryset(self):
return User.objects.all() 130 130 return User.objects.all()
131 131
def patch(self, request, format=None): 132 132 def patch(self, request, format=None):
""" 133 133 """
Updates the user's password, or verifies their email address 134 134 Updates the user's password, or verifies their email address
--- 135 135 ---
request_serializer: UserUpdateSerializer 136 136 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 137 137 response_serializer: UserSerializer
""" 138 138 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 139 139 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 140 140 data.is_valid(raise_exception=True)
data = data.validated_data 141 141 data = data.validated_data
142 142
if 'new_password' in data: 143 143 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 144 144 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 145 145 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 146 146 request.user.set_password(data['new_password'])
request.user.save() 147 147 request.user.save()
148 148
if 'confirmation_key' in data: 149 149 if 'confirmation_key' in data:
try: 150 150 try:
request.user.confirm_email(data['confirmation_key']) 151 151 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 152 152 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 153 153 raise ValidationError('confirmation_key is invalid')
154 154
return Response(UserSerializer(request.user).data) 155 155 return Response(UserSerializer(request.user).data)
156 156
def get(self, request, format=None): 157 157 def get(self, request, format=None):
""" 158 158 """
Return data about the user 159 159 Return data about the user
--- 160 160 ---
response_serializer: UserSerializer 161 161 response_serializer: UserSerializer
""" 162 162 """
serializer = UserSerializer(request.user, context={'request': request}) 163 163 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 164 164 return Response(serializer.data)
165 165
def delete(self, request): 166 166 def delete(self, request):
""" 167 167 """
Irrevocably delete the user and their data 168 168 Irrevocably delete the user and their data
169 169
Yes, really 170 170 Yes, really
""" 171 171 """
request.user.delete() 172 172 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 173 173 return Response(status=HTTP_204_NO_CONTENT)
174 174
175 175
@api_view(['POST']) 176 176 @api_view(['POST'])
def register(request, format=None): 177 177 def register(request, format=None):
""" 178 178 """
Register a new user 179 179 Register a new user
--- 180 180 ---
request_serializer: EmailPasswordSerializer 181 181 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 182 182 response_serializer: UserSerializer
""" 183 183 """
data = RegistrationSerializer(data=request.data) 184 184 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 185 185 data.is_valid(raise_exception=True)
186 186
User.objects.create_user(**data.validated_data) 187 187 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 188 188 user = authenticate(**data.validated_data)
auth.login(request, user) 189 189 auth.login(request, user)
190 190
body = ''' 191 191 body = '''
Visit the following link to confirm your email address: 192 192 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 193 193 https://flashy.cards/app/verifyemail/%s
194 194
If you did not register for Flashy, no action is required. 195 195 If you did not register for Flashy, no action is required.
''' 196 196 '''
197 197
assert send_mail("Flashy email verification", 198 198 assert send_mail("Flashy email verification",
body % user.confirmation_key, 199 199 body % user.confirmation_key,
"noreply@flashy.cards", 200 200 "noreply@flashy.cards",
[user.email]) 201 201 [user.email])
202 202
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 203 203 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
204 204
205 205
@api_view(['POST']) 206 206 @api_view(['POST'])
def login(request): 207 207 def login(request):
""" 208 208 """
Authenticates user and returns user data if valid. 209 209 Authenticates user and returns user data if valid.
--- 210 210 ---
request_serializer: EmailPasswordSerializer 211 211 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 212 212 response_serializer: UserSerializer
""" 213 213 """
214 214
data = EmailPasswordSerializer(data=request.data) 215 215 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 216 216 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 217 217 user = authenticate(**data.validated_data)
218 218
if user is None: 219 219 if user is None:
raise AuthenticationFailed('Invalid email or password') 220 220 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 221 221 if not user.is_active:
raise NotAuthenticated('Account is disabled') 222 222 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 223 223 auth.login(request, user)
return Response(UserSerializer(request.user).data) 224 224 return Response(UserSerializer(request.user).data)
225 225
226 226
@api_view(['POST']) 227 227 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 228 228 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 229 229 def logout(request, format=None):
""" 230 230 """
Logs the authenticated user out. 231 231 Logs the authenticated user out.
""" 232 232 """
auth.logout(request) 233 233 auth.logout(request)
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 request_password_reset(request, format=None): 238 238 def request_password_reset(request, format=None):
""" 239 239 """
Send a password reset token/link to the provided email. 240 240 Send a password reset token/link to the provided email.
--- 241 241 ---
request_serializer: PasswordResetRequestSerializer 242 242 request_serializer: PasswordResetRequestSerializer
""" 243 243 """
data = PasswordResetRequestSerializer(data=request.data) 244 244 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 245 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 246 246 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 247 247 token = default_token_generator.make_token(user)
248 248
body = ''' 249 249 body = '''
Visit the following link to reset your password: 250 250 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 251 251 https://flashy.cards/app/resetpassword/%d/%s
252 252
If you did not request a password reset, no action is required. 253 253 If you did not request a password reset, no action is required.
''' 254 254 '''
255 255
send_mail("Flashy password reset", 256 256 send_mail("Flashy password reset",
body % (user.pk, token), 257 257 body % (user.pk, token),
"noreply@flashy.cards", 258 258 "noreply@flashy.cards",
[user.email]) 259 259 [user.email])
260 260
return Response(status=HTTP_204_NO_CONTENT) 261 261 return Response(status=HTTP_204_NO_CONTENT)
262 262
263 263
@api_view(['POST']) 264 264 @api_view(['POST'])
def reset_password(request, format=None): 265 265 def reset_password(request, format=None):
""" 266 266 """
Updates user's password to new password if token is valid. 267 267 Updates user's password to new password if token is valid.
--- 268 268 ---
request_serializer: PasswordResetSerializer 269 269 request_serializer: PasswordResetSerializer
""" 270 270 """
data = PasswordResetSerializer(data=request.data) 271 271 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 272 272 data.is_valid(raise_exception=True)
273 273
user = User.objects.get(id=data['uid'].value) 274 274 user = User.objects.get(id=data['uid'].value)
# Check token validity. 275 275 # Check token validity.
276 276
if default_token_generator.check_token(user, data['token'].value): 277 277 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 278 278 user.set_password(data['new_password'].value)
user.save() 279 279 user.save()
else: 280 280 else:
raise ValidationError('Could not verify reset token') 281 281 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 282 282 return Response(status=HTTP_204_NO_CONTENT)
283 283
284 284
class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): 285 285 class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 286 286 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 287 287 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 288 288 permission_classes = [IsAuthenticated]
289 289
# Override create in CreateModelMixin 290 290 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 291 291 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 292 292 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 293 293 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 294 294 data = serializer.validated_data
self.perform_create(serializer) 295 295 if not request.user.is_in_section(data['section']):
headers = self.get_success_headers(serializer.data) 296 296 raise PermissionDenied("You have to be enrolled in this section to add a flashcard")
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 297 297 data['author'] = request.user
298 flashcard = Flashcard.objects.create(**data)
299 self.perform_create(flashcard)
300 headers = self.get_success_headers(data)
301 response_data = FlashcardSerializer(flashcard)
302 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
298 303
@detail_route(methods=['post']) 299 304 @detail_route(methods=['post'])
def report(self, request, pk): 300 305 def report(self, request, pk):
""" 301 306 """