Commit fe44f16085e529d9a8a31758f110c519bab14500

Authored by Andrew Buss
1 parent 8fc000ea2a
Exists in master

remove ordered_deck, allow filtering by hidden to flashcard list

Showing 4 changed files with 7 additions and 20 deletions Inline Diff

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