Commit a06c7fa68916860d93a21f7d2addf6325e54153a

Authored by Andrew Buss
Exists in master

Merge branch 'master' of git.ucsd.edu:110swag/flashy-backend

Showing 2 changed files Inline Diff

flashcards/tests/test_api.py View file @ a06c7fa
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 *
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)
self.flashcard.save() 188 188 self.flashcard.save()
189 189
def test_get_flashcard(self): 190 190 def test_get_flashcard(self):
self.client.login(email='none@none.com', password='1234') 191 191 self.client.login(email='none@none.com', password='1234')
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 192 192 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 193 193 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 194 194 self.assertEqual(response.data["text"], "jason")
195 195
196 196
class SectionViewSetTest(APITestCase): 197 197 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 198 198 fixtures = ['testusers', 'testsections']
199 199
def setUp(self): 200 200 def setUp(self):
self.client.login(email='none@none.com', password='1234') 201 201 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 202 202 self.user = User.objects.get(email='none@none.com')
203 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user)
204 self.flashcard.save()
self.section = Section.objects.get(pk=1) 203 205 self.section = Section.objects.get(pk=1)
flashcard = Flashcard(text="jason", section=self.section, material_date=now(), author=self.user) 204
flashcard.save() 205
206 206
def test_list_sections(self): 207 207 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 208 208 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 209 209 self.assertEqual(response.status_code, HTTP_200_OK)
210 210
def test_section_enroll(self): 211 211 def test_section_enroll(self):
section = self.section 212 212 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 213 213 self.assertFalse(self.user.sections.filter(pk=section.pk))
214 214
flashcards/views.py View file @ a06c7fa
from django.contrib import auth 1 1 from django.contrib import auth
from flashcards.api import StandardResultsSetPagination 2 2 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 3 3 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 4 4 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 5 5 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer 6 6 FlashcardUpdateSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view 7 7 from rest_framework.decorators import detail_route, permission_classes, api_view
from rest_framework.generics import ListAPIView, GenericAPIView 8 8 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 9 9 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 10 10 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 11 11 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 12 12 from django.core.mail import send_mail
from django.contrib.auth import authenticate 13 13 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 14 14 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 15 15 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 16 16 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 17 17 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 18 18 from simple_email_confirmation import EmailAddress
19 19
20 20
class SectionViewSet(ReadOnlyModelViewSet): 21 21 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 22 22 queryset = Section.objects.all()
serializer_class = SectionSerializer 23 23 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 24 24 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 25 25 permission_classes = [IsAuthenticated]
26 26
@detail_route(methods=['GET']) 27 27 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 28 28 def flashcards(self, request, pk):
""" 29 29 """
Gets flashcards for a section, excluding hidden cards. 30 30 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 31 31 Returned in strictly chronological order (material date).
""" 32 32 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()) 33 33 flashcards = Flashcard.cards_visible_to(request.user).filter( \
34 section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 34 35 return Response(FlashcardSerializer(flashcards, many=True).data)
35 36
@detail_route(methods=['post']) 36 37 @detail_route(methods=['post'])
def enroll(self, request, pk): 37 38 def enroll(self, request, pk):
""" 38 39 """
Add the current user to a specified section 39 40 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 40 41 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 41 42 ---
omit_serializer: true 42 43 omit_serializer: true
parameters: 43 44 parameters:
- fake: None 44 45 - fake: None
parameters_strategy: 45 46 parameters_strategy:
form: replace 46 47 form: replace
""" 47 48 """
section = self.get_object() 48 49 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 49 50 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 50 51 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 51 52 if section.is_whitelisted and not section.is_user_on_whitelist(request.user):
raise PermissionDenied("You must be on the whitelist to add this section.") 52 53 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 53 54 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 54 55 return Response(status=HTTP_204_NO_CONTENT)
55 56
@detail_route(methods=['post']) 56 57 @detail_route(methods=['post'])
def drop(self, request, pk): 57 58 def drop(self, request, pk):
""" 58 59 """
Remove the current user from a specified section 59 60 Remove the current user from a specified section
If the user is not in the class, the request will fail. 60 61 If the user is not in the class, the request will fail.
--- 61 62 ---
omit_serializer: true 62 63 omit_serializer: true
parameters: 63 64 parameters:
- fake: None 64 65 - fake: None
parameters_strategy: 65 66 parameters_strategy:
form: replace 66 67 form: replace
""" 67 68 """
section = self.get_object() 68 69 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 69 70 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 70 71 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 71 72 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 72 73 return Response(status=HTTP_204_NO_CONTENT)
73 74
@detail_route(methods=['GET']) 74 75 @detail_route(methods=['GET'])
def search(self, request): 75 76 def search(self, request):
""" 76 77 """
Returns a list of sections which match a user's query 77 78 Returns a list of sections which match a user's query
""" 78 79 """
query = request.GET.get('q', None) 79 80 query = request.GET.get('q', None)
if not query: return Response('[]') 80 81 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 81 82 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 82 83 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 83 84 return Response(serializer.data)
84 85
@detail_route(methods=['GET']) 85 86 @detail_route(methods=['GET'])
def deck(self, request, pk): 86 87 def deck(self, request, pk):
""" 87 88 """
Gets the contents of a user's deck for a given section. 88 89 Gets the contents of a user's deck for a given section.
""" 89 90 """
qs = request.user.get_deck(self.get_object()) 90 91 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 91 92 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 92 93 return Response(serializer.data)
93 94
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 94 95 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 95 96 def ordered_deck(self, request, pk):
""" 96 97 """
Get a chronological order by material_date of flashcards for a section. 97 98 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 98 99 This excludes hidden card.
""" 99 100 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 100 101 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 101 102 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 102 103 return Response(serializer.data)
103 104
@detail_route(methods=['GET']) 104 105 @detail_route(methods=['GET'])
def feed(self, request, pk): 105 106 def feed(self, request, pk):
""" 106 107 """
Gets the contents of a user's feed for a section. 107 108 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 108 109 Exclude cards that are already in the user's deck
""" 109 110 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 110 111 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 111 112 return Response(serializer.data)
112 113
113 114
class UserSectionListView(ListAPIView): 114 115 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 115 116 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 116 117 permission_classes = [IsAuthenticated]
117 118
def get_queryset(self): 118 119 def get_queryset(self):
return self.request.user.sections.all() 119 120 return self.request.user.sections.all()
120 121
def paginate_queryset(self, queryset): return None 121 122 def paginate_queryset(self, queryset): return None
122 123
123 124
class UserDetail(GenericAPIView): 124 125 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 125 126 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 126 127 permission_classes = [IsAuthenticated]
127 128
def get_queryset(self): 128 129 def get_queryset(self):
return User.objects.all() 129 130 return User.objects.all()
130 131
def patch(self, request, format=None): 131 132 def patch(self, request, format=None):
""" 132 133 """
Updates the user's password, or verifies their email address 133 134 Updates the user's password, or verifies their email address
--- 134 135 ---
request_serializer: UserUpdateSerializer 135 136 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 136 137 response_serializer: UserSerializer
""" 137 138 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 138 139 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 139 140 data.is_valid(raise_exception=True)
data = data.validated_data 140 141 data = data.validated_data
141 142
if 'new_password' in data: 142 143 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 143 144 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 144 145 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 145 146 request.user.set_password(data['new_password'])
request.user.save() 146 147 request.user.save()
147 148
if 'confirmation_key' in data: 148 149 if 'confirmation_key' in data:
try: 149 150 try:
request.user.confirm_email(data['confirmation_key']) 150 151 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 151 152 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 152 153 raise ValidationError('confirmation_key is invalid')
153 154
return Response(UserSerializer(request.user).data) 154 155 return Response(UserSerializer(request.user).data)
155 156
def get(self, request, format=None): 156 157 def get(self, request, format=None):
""" 157 158 """
Return data about the user 158 159 Return data about the user
--- 159 160 ---
response_serializer: UserSerializer 160 161 response_serializer: UserSerializer
""" 161 162 """
serializer = UserSerializer(request.user, context={'request': request}) 162 163 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 163 164 return Response(serializer.data)
164 165
def delete(self, request): 165 166 def delete(self, request):
""" 166 167 """
Irrevocably delete the user and their data 167 168 Irrevocably delete the user and their data
168 169
Yes, really 169 170 Yes, really
""" 170 171 """
request.user.delete() 171 172 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 172 173 return Response(status=HTTP_204_NO_CONTENT)
173 174
174 175
@api_view(['POST']) 175 176 @api_view(['POST'])
def register(request, format=None): 176 177 def register(request, format=None):
""" 177 178 """
Register a new user 178 179 Register a new user
--- 179 180 ---
request_serializer: EmailPasswordSerializer 180 181 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 181 182 response_serializer: UserSerializer
""" 182 183 """
data = RegistrationSerializer(data=request.data) 183 184 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 184 185 data.is_valid(raise_exception=True)
185 186
User.objects.create_user(**data.validated_data) 186 187 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 187 188 user = authenticate(**data.validated_data)
auth.login(request, user) 188 189 auth.login(request, user)
189 190
body = ''' 190 191 body = '''
Visit the following link to confirm your email address: 191 192 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 192 193 https://flashy.cards/app/verifyemail/%s
193 194
If you did not register for Flashy, no action is required. 194 195 If you did not register for Flashy, no action is required.
''' 195 196 '''
196 197
assert send_mail("Flashy email verification", 197 198 assert send_mail("Flashy email verification",
body % user.confirmation_key, 198 199 body % user.confirmation_key,
"noreply@flashy.cards", 199 200 "noreply@flashy.cards",
[user.email]) 200 201 [user.email])
201 202
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 202 203 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
203 204
204 205
@api_view(['POST']) 205 206 @api_view(['POST'])
def login(request): 206 207 def login(request):
""" 207 208 """
Authenticates user and returns user data if valid. 208 209 Authenticates user and returns user data if valid.
--- 209 210 ---
request_serializer: EmailPasswordSerializer 210 211 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 211 212 response_serializer: UserSerializer
""" 212 213 """
213 214
data = EmailPasswordSerializer(data=request.data) 214 215 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 215 216 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 216 217 user = authenticate(**data.validated_data)
217 218
if user is None: 218 219 if user is None:
raise AuthenticationFailed('Invalid email or password') 219 220 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 220 221 if not user.is_active:
raise NotAuthenticated('Account is disabled') 221 222 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 222 223 auth.login(request, user)
return Response(UserSerializer(request.user).data) 223 224 return Response(UserSerializer(request.user).data)
224 225
225 226
@api_view(['POST']) 226 227 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 227 228 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 228 229 def logout(request, format=None):
""" 229 230 """
Logs the authenticated user out. 230 231 Logs the authenticated user out.
""" 231 232 """
auth.logout(request) 232 233 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 233 234 return Response(status=HTTP_204_NO_CONTENT)
234 235
235 236
@api_view(['POST']) 236 237 @api_view(['POST'])
def request_password_reset(request, format=None): 237 238 def request_password_reset(request, format=None):
""" 238 239 """
Send a password reset token/link to the provided email. 239 240 Send a password reset token/link to the provided email.
--- 240 241 ---
request_serializer: PasswordResetRequestSerializer 241 242 request_serializer: PasswordResetRequestSerializer
""" 242 243 """
data = PasswordResetRequestSerializer(data=request.data) 243 244 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 244 245 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 245 246 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 246 247 token = default_token_generator.make_token(user)
247 248
body = ''' 248 249 body = '''
Visit the following link to reset your password: 249 250 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 250 251 https://flashy.cards/app/resetpassword/%d/%s
251 252
If you did not request a password reset, no action is required. 252 253 If you did not request a password reset, no action is required.
''' 253 254 '''
254 255
send_mail("Flashy password reset", 255 256 send_mail("Flashy password reset",
body % (user.pk, token), 256 257 body % (user.pk, token),
"noreply@flashy.cards", 257 258 "noreply@flashy.cards",
[user.email]) 258 259 [user.email])
259 260
return Response(status=HTTP_204_NO_CONTENT) 260 261 return Response(status=HTTP_204_NO_CONTENT)
261 262
262 263
@api_view(['POST']) 263 264 @api_view(['POST'])
def reset_password(request, format=None): 264 265 def reset_password(request, format=None):
""" 265 266 """
Updates user's password to new password if token is valid. 266 267 Updates user's password to new password if token is valid.
--- 267 268 ---
request_serializer: PasswordResetSerializer 268 269 request_serializer: PasswordResetSerializer
""" 269 270 """
data = PasswordResetSerializer(data=request.data) 270 271 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 271 272 data.is_valid(raise_exception=True)
272 273
user = User.objects.get(id=data['uid'].value) 273 274 user = User.objects.get(id=data['uid'].value)
# Check token validity. 274 275 # Check token validity.
275 276
if default_token_generator.check_token(user, data['token'].value): 276 277 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 277 278 user.set_password(data['new_password'].value)
user.save() 278 279 user.save()
else: 279 280 else:
raise ValidationError('Could not verify reset token') 280 281 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 281 282 return Response(status=HTTP_204_NO_CONTENT)
282 283
283 284
class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): 284 285 class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 285 286 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 286 287 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 287 288 permission_classes = [IsAuthenticated]
288 289
# Override create in CreateModelMixin 289 290 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 290 291 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 291 292 serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) 292 293 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 293 294 serializer.validated_data['author'] = request.user
self.perform_create(serializer) 294 295 self.perform_create(serializer)
headers = self.get_success_headers(serializer.data) 295 296 headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 296 297 return Response(serializer.data, status=HTTP_201_CREATED, headers=headers)