Commit c2b6dc85289a4cd1259acf873f432fd9be974322

Authored by Rachel Lee
1 parent e8cd6d2e30
Exists in master

Wrote test for section/{pk}/flashcards

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

flashcards/fields.py View file @ c2b6dc8
from django.db import models 1 1 from django.db import models
from validators import FlashcardMask, OverlapIntervalException 2 2 from validators import FlashcardMask, OverlapIntervalException
3 3
4 4
class MaskField(models.Field): 5 5 class MaskField(models.Field):
def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): 6 6 def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs):
self.blank_sep = blank_sep 7 7 self.blank_sep = blank_sep
self.range_sep = range_sep 8 8 self.range_sep = range_sep
super(MaskField, self).__init__(*args, **kwargs) 9 9 super(MaskField, self).__init__(*args, **kwargs)
10 10
@staticmethod 11 11 @staticmethod
def _using_array(connection): 12 12 def _using_array(connection):
return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2' 13 13 return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
14 14
def deconstruct(self): 15 15 def deconstruct(self):
name, path, args, kwargs = super(MaskField, self).deconstruct() 16 16 name, path, args, kwargs = super(MaskField, self).deconstruct()
kwargs['blank_sep'] = self.blank_sep 17 17 kwargs['blank_sep'] = self.blank_sep
kwargs['range_sep'] = self.range_sep 18 18 kwargs['range_sep'] = self.range_sep
return name, path, args, kwargs 19 19 return name, path, args, kwargs
20 20
def db_type(self, connection): 21 21 def db_type(self, connection):
return 'integer[2][]' if self._using_array(connection) else 'varchar' 22 22 return 'integer[2][]' if self._using_array(connection) else 'varchar'
23 23
def from_db_value(self, value, expression, connection, context): 24 24 def from_db_value(self, value, expression, connection, context):
if value is None: 25 25 if value is None:
return value 26 26 return value
if self._using_array(connection): 27 27 if self._using_array(connection):
return MaskField._psql_parse_mask(value) 28 28 return MaskField._psql_parse_mask(value)
return MaskField._varchar_parse_mask(value) 29 29 return MaskField._varchar_parse_mask(value)
30 30
def get_db_prep_value(self, value, connection, prepared=False): 31 31 def get_db_prep_value(self, value, connection, prepared=False):
if not prepared: 32 32 if not prepared:
value = self.get_prep_value(value) 33 33 value = self.get_prep_value(value)
if value is None: 34 34 if value is None:
return value 35 35 return value
if self._using_array(connection): 36 36 if self._using_array(connection):
return value 37 37 return value
return ','.join(['-'.join(map(str, i)) for i in value]) 38 38 return ','.join(['-'.join(map(str, i)) for i in value])
39 39
def to_python(self, value): 40 40 def to_python(self, value):
if value is None: 41 41 if value is None:
return value 42 42 return value
return sorted(list(FlashcardMask(value))) 43 43 return sorted(list(FlashcardMask(value)))
44 44
def get_prep_value(self, value): 45 45 def get_prep_value(self, value):
if value is None: 46 46 if value is None:
return value 47 47 return value
return sorted(map(list, FlashcardMask(value))) 48 48 return sorted(map(list, FlashcardMask(value)))
49 49
def get_prep_lookup(self, lookup_type, value): 50 50 def get_prep_lookup(self, lookup_type, value):
raise TypeError("Lookup not supported for MaskField") 51 51 raise TypeError("Lookup not supported for MaskField")
52 52
@staticmethod 53 53 @staticmethod
def _parse_mask(intervals): 54 54 def _parse_mask(intervals):
p_beg, p_end = -1, -1 55 55 p_beg, p_end = -1, -1
mask_list = [] 56 56 mask_list = []
for interval in intervals: 57 57 for interval in intervals:
beg, end = map(int, interval) 58 58 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 59 59 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise ValueError("Invalid range offsets in the mask") 60 60 raise ValueError("Invalid range offsets in the mask")
mask_list.append([beg, end]) 61 61 mask_list.append([beg, end])
p_beg, p_end = beg, end 62 62 p_beg, p_end = beg, end
flashcards/tests/test_api.py View file @ c2b6dc8
from django.core import mail 1 1 from django.core import mail
from flashcards.models import User, Section, Flashcard, WhitelistedAddress 2 2 from flashcards.models import User, Section, Flashcard, WhitelistedAddress
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
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/reset_password/(\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)
flashcards/views.py View file @ c2b6dc8
from django.contrib import auth 1 1 from django.contrib import auth
from django.db.models import Q 2 2 from django.db.models import Q
from flashcards.api import StandardResultsSetPagination 3 3 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard 4 4 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 5 5 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer 6 6 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 7 7 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 8 8 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 9 9 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
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'], permission_classes=[IsAuthenticated]) 27 27 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
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(), is_hidden=False).all() 34 34 section=self.get_object(), is_hidden=False).all()
35 35
return Response(FlashcardSerializer(flashcards, many=True)) 36 36 return Response(FlashcardSerializer(flashcards, many=True))
37 37
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 38 38 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 39 39 def enroll(self, request, pk):
""" 40 40 """
Add the current user to a specified section 41 41 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. 42 42 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 43 43 ---
omit_serializer: true 44 44 omit_serializer: true
parameters: 45 45 parameters:
- fake: None 46 46 - fake: None
parameters_strategy: 47 47 parameters_strategy:
form: replace 48 48 form: replace
""" 49 49 """
section = self.get_object() 50 50 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 51 51 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 52 52 raise ValidationError("You are already in this section.")
if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): 53 53 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.") 54 54 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 55 55 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 56 56 return Response(status=HTTP_204_NO_CONTENT)
57 57
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 58 58 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 59 59 def drop(self, request, pk):
""" 60 60 """
Remove the current user from a specified section 61 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 62 62 If the user is not in the class, the request will fail.
--- 63 63 ---
omit_serializer: true 64 64 omit_serializer: true
parameters: 65 65 parameters:
- fake: None 66 66 - fake: None
parameters_strategy: 67 67 parameters_strategy:
form: replace 68 68 form: replace
""" 69 69 """
section = self.get_object() 70 70 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 71 71 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 72 72 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 73 73 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 74 74 return Response(status=HTTP_204_NO_CONTENT)
75 75
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 76 76 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 77 77 def search(self, request):
query = request.GET.get('q', '').split(' ') 78 78 query = request.GET.get('q', '').split(' ')
q = Q() 79 79 q = Q()
for word in query: 80 80 for word in query:
q |= Q(course_title__icontains=word) 81 81 q |= Q(course_title__icontains=word)
qs = Section.objects.filter(q).distinct() 82 82 qs = Section.objects.filter(q).distinct()
serializer = SectionSerializer(qs, many=True) 83 83 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 84 84 return Response(serializer.data)
85 85
86 86
class UserSectionListView(ListAPIView): 87 87 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 88 88 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 89 89 permission_classes = [IsAuthenticated]
90 90
def get_queryset(self): 91 91 def get_queryset(self):
return self.request.user.sections.all() 92 92 return self.request.user.sections.all()
93 93
def paginate_queryset(self, queryset): return None 94 94 def paginate_queryset(self, queryset): return None
95 95
96 96
class UserDetail(GenericAPIView): 97 97 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 98 98 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 99 99 permission_classes = [IsAuthenticated]
100 100
def get_queryset(self): 101 101 def get_queryset(self):
return User.objects.all() 102 102 return User.objects.all()
103 103
def patch(self, request, format=None): 104 104 def patch(self, request, format=None):
""" 105 105 """
Updates the user's password, or verifies their email address 106 106 Updates the user's password, or verifies their email address
--- 107 107 ---
request_serializer: UserUpdateSerializer 108 108 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 109 109 response_serializer: UserSerializer
""" 110 110 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 111 111 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 112 112 data.is_valid(raise_exception=True)
data = data.validated_data 113 113 data = data.validated_data
114 114
if 'new_password' in data: 115 115 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 116 116 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 117 117 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 118 118 request.user.set_password(data['new_password'])
request.user.save() 119 119 request.user.save()
120 120
if 'confirmation_key' in data: 121 121 if 'confirmation_key' in data:
try: 122 122 try:
request.user.confirm_email(data['confirmation_key']) 123 123 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 124 124 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 125 125 raise ValidationError('confirmation_key is invalid')
126 126
return Response(UserSerializer(request.user).data) 127 127 return Response(UserSerializer(request.user).data)
128 128
def get(self, request, format=None): 129 129 def get(self, request, format=None):
""" 130 130 """
Return data about the user 131 131 Return data about the user
--- 132 132 ---
response_serializer: UserSerializer 133 133 response_serializer: UserSerializer
""" 134 134 """
serializer = UserSerializer(request.user, context={'request': request}) 135 135 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 136 136 return Response(serializer.data)
137 137
def delete(self, request): 138 138 def delete(self, request):
""" 139 139 """
Irrevocably delete the user and their data 140 140 Irrevocably delete the user and their data
141 141
Yes, really 142 142 Yes, really
""" 143 143 """
request.user.delete() 144 144 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 145 145 return Response(status=HTTP_204_NO_CONTENT)
146 146
147 147
@api_view(['POST']) 148 148 @api_view(['POST'])
def register(request, format=None): 149 149 def register(request, format=None):
""" 150 150 """
Register a new user 151 151 Register a new user
--- 152 152 ---
request_serializer: EmailPasswordSerializer 153 153 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 154 154 response_serializer: UserSerializer
""" 155 155 """
data = RegistrationSerializer(data=request.data) 156 156 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 157 157 data.is_valid(raise_exception=True)
158 158
User.objects.create_user(**data.validated_data) 159 159 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 160 160 user = authenticate(**data.validated_data)
auth.login(request, user) 161 161 auth.login(request, user)
162 162
body = ''' 163 163 body = '''
Visit the following link to confirm your email address: 164 164 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 165 165 https://flashy.cards/app/verify_email/%s
166 166
If you did not register for Flashy, no action is required. 167 167 If you did not register for Flashy, no action is required.
''' 168 168 '''
169 169
assert send_mail("Flashy email verification", 170 170 assert send_mail("Flashy email verification",
body % user.confirmation_key, 171 171 body % user.confirmation_key,
"noreply@flashy.cards", 172 172 "noreply@flashy.cards",
[user.email]) 173 173 [user.email])
174 174
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 175 175 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
176 176
177 177
@api_view(['POST']) 178 178 @api_view(['POST'])
def login(request): 179 179 def login(request):
""" 180 180 """
Authenticates user and returns user data if valid. 181 181 Authenticates user and returns user data if valid.
--- 182 182 ---
request_serializer: EmailPasswordSerializer 183 183 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 184 184 response_serializer: UserSerializer
""" 185 185 """
186 186
data = EmailPasswordSerializer(data=request.data) 187 187 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 188 188 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 189 189 user = authenticate(**data.validated_data)
190 190
if user is None: 191 191 if user is None:
raise AuthenticationFailed('Invalid email or password') 192 192 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 193 193 if not user.is_active:
raise NotAuthenticated('Account is disabled') 194 194 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 195 195 auth.login(request, user)
return Response(UserSerializer(request.user).data) 196 196 return Response(UserSerializer(request.user).data)
197 197
198 198
@api_view(['POST']) 199 199 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 200 200 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 201 201 def logout(request, format=None):
""" 202 202 """
Logs the authenticated user out. 203 203 Logs the authenticated user out.
""" 204 204 """
auth.logout(request) 205 205 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 206 206 return Response(status=HTTP_204_NO_CONTENT)
207 207
208 208
@api_view(['POST']) 209 209 @api_view(['POST'])
def request_password_reset(request, format=None): 210 210 def request_password_reset(request, format=None):
""" 211 211 """
Send a password reset token/link to the provided email. 212 212 Send a password reset token/link to the provided email.
--- 213 213 ---
request_serializer: PasswordResetRequestSerializer 214 214 request_serializer: PasswordResetRequestSerializer
""" 215 215 """
data = PasswordResetRequestSerializer(data=request.data) 216 216 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 217 217 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 218 218 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 219 219 token = default_token_generator.make_token(user)
220 220
body = ''' 221 221 body = '''
Visit the following link to reset your password: 222 222 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 223 223 https://flashy.cards/app/reset_password/%d/%s
224 224
If you did not request a password reset, no action is required. 225 225 If you did not request a password reset, no action is required.
''' 226 226 '''
227 227
send_mail("Flashy password reset", 228 228 send_mail("Flashy password reset",
body % (user.pk, token), 229 229 body % (user.pk, token),
"noreply@flashy.cards", 230 230 "noreply@flashy.cards",
[user.email]) 231 231 [user.email])
232 232
return Response(status=HTTP_204_NO_CONTENT) 233 233 return Response(status=HTTP_204_NO_CONTENT)
234 234
235 235
@api_view(['POST']) 236 236 @api_view(['POST'])
def reset_password(request, format=None): 237 237 def reset_password(request, format=None):
""" 238 238 """
Updates user's password to new password if token is valid. 239 239 Updates user's password to new password if token is valid.
--- 240 240 ---
request_serializer: PasswordResetSerializer 241 241 request_serializer: PasswordResetSerializer
""" 242 242 """
data = PasswordResetSerializer(data=request.data) 243 243 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 244 244 data.is_valid(raise_exception=True)
245 245
user = User.objects.get(id=data['uid'].value) 246 246 user = User.objects.get(id=data['uid'].value)
# Check token validity. 247 247 # Check token validity.
248 248
if default_token_generator.check_token(user, data['token'].value): 249 249 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 250 250 user.set_password(data['new_password'].value)
user.save() 251 251 user.save()
else: 252 252 else:
raise ValidationError('Could not verify reset token') 253 253 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 254 254 return Response(status=HTTP_204_NO_CONTENT)