Commit e4c3292e394167a97f267f18f2542fa7d29df0ea

Authored by Rohan Rangray
Exists in master

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

Mergin' all day.

Showing 6 changed files Inline Diff

Flashy requires Python 2. 1 1 Flashy requires Python 2. Srsly
2 2
3 All of these commands should be run from this directory (the one containing README.md)
4
Install virtualenv before continuing. This is most easily accomplished with: 3 5 Install virtualenv before continuing. This is most easily accomplished with:
4 6
pip install virtualenv 5 7 pip install virtualenv
6 8
Set up the environment by running: 7 9 Set up the environment by running:
8 10
scripts/setup.sh 9 11 scripts/setup.sh
10 12
If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv: 11 13 If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv:
12 14
. venv/bin/activate 13 15 . venv/bin/activate
14 16
If you still get errors about a module not being found, make sure your virtualenv is up to date. Re-run: 15 17 If you still get errors about a module not being found, make sure your virtualenv is up to date. Re-run:
16 18
flashcards/tests/test_api.py View file @ e4c3292
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)
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)
flashcards/views.py View file @ e4c3292
from django.contrib import auth 1 1 from django.contrib import auth
from django.db.models import Q 2
from flashcards.api import StandardResultsSetPagination 3 2 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard 4 3 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 5 4 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 6 5 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer 7 6 FlashcardUpdateSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 8 7 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 9 8 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 10 9 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 11 10 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 12 11 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 13 12 from django.core.mail import send_mail
from django.contrib.auth import authenticate 14 13 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 15 14 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 16 15 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 17 16 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 18 17 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 19 18 from simple_email_confirmation import EmailAddress
from datetime import datetime 20 19 from datetime import datetime
21 20
22 21
class SectionViewSet(ReadOnlyModelViewSet): 23 22 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 24 23 queryset = Section.objects.all()
serializer_class = SectionSerializer 25 24 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 26 25 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 27 26 permission_classes = [IsAuthenticated]
28 27
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 29 28 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def flashcards(self, request, pk): 30 29 def flashcards(self, request, pk):
""" 31 30 """
Gets flashcards for a section, excluding hidden cards. 32 31 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 33 32 Returned in strictly chronological order (material date).
""" 34 33 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 35 34 flashcards = Flashcard.cards_visible_to(request.user).filter( \
section=self.get_object(), is_hidden=False).all() 36 35 section=self.get_object(), is_hidden=False).all()
37 36
return Response(FlashcardSerializer(flashcards, many=True).data) 38 37 return Response(FlashcardSerializer(flashcards, many=True).data)
39 38
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 40 39 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 41 40 def enroll(self, request, pk):
""" 42 41 """
Add the current user to a specified section 43 42 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. 44 43 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 45 44 ---
omit_serializer: true 46 45 omit_serializer: true
parameters: 47 46 parameters:
- fake: None 48 47 - fake: None
parameters_strategy: 49 48 parameters_strategy:
form: replace 50 49 form: replace
""" 51 50 """
section = self.get_object() 52 51 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 53 52 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 54 53 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 55 54 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.") 56 55 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 57 56 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 58 57 return Response(status=HTTP_204_NO_CONTENT)
59 58
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 60 59 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 61 60 def drop(self, request, pk):
""" 62 61 """
Remove the current user from a specified section 63 62 Remove the current user from a specified section
If the user is not in the class, the request will fail. 64 63 If the user is not in the class, the request will fail.
--- 65 64 ---
omit_serializer: true 66 65 omit_serializer: true
parameters: 67 66 parameters:
- fake: None 68 67 - fake: None
parameters_strategy: 69 68 parameters_strategy:
form: replace 70 69 form: replace
""" 71 70 """
section = self.get_object() 72 71 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 73 72 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 74 73 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 75 74 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 76 75 return Response(status=HTTP_204_NO_CONTENT)
77 76
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 78 77 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 79 78 def search(self, request):
query = request.GET.get('q',None) 80 79 query = request.GET.get('q', None)
if not query: return Response('[]') 81 80 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:8] 82 81 qs = Section.search(query.split(' '))[:8]
serializer = SectionSerializer(qs, many=True) 83 82 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 84 83 return Response(serializer.data)
85 84
85 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
86 def deck(self, request, pk):
87 """
88 Gets the contents of a user's deck for a given section.
89 """
90 qs = Flashcard.objects.all()
91 qs = qs.filter(userflashcard__user=request.user)
92 serializer = FlashcardSerializer(qs, many=True)
93 return Response(serializer.data)
86 94
95
class UserSectionListView(ListAPIView): 87 96 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 88 97 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 89 98 permission_classes = [IsAuthenticated]
90 99
def get_queryset(self): 91 100 def get_queryset(self):
return self.request.user.sections.all() 92 101 return self.request.user.sections.all()
93 102
def paginate_queryset(self, queryset): return None 94 103 def paginate_queryset(self, queryset): return None
95 104
96 105
class UserDetail(GenericAPIView): 97 106 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 98 107 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 99 108 permission_classes = [IsAuthenticated]
100 109
def get_queryset(self): 101 110 def get_queryset(self):
return User.objects.all() 102 111 return User.objects.all()
103 112
def patch(self, request, format=None): 104 113 def patch(self, request, format=None):
""" 105 114 """
Updates the user's password, or verifies their email address 106 115 Updates the user's password, or verifies their email address
--- 107 116 ---
request_serializer: UserUpdateSerializer 108 117 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 109 118 response_serializer: UserSerializer
""" 110 119 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 111 120 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 112 121 data.is_valid(raise_exception=True)
data = data.validated_data 113 122 data = data.validated_data
114 123
if 'new_password' in data: 115 124 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 116 125 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 117 126 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 118 127 request.user.set_password(data['new_password'])
request.user.save() 119 128 request.user.save()
120 129
if 'confirmation_key' in data: 121 130 if 'confirmation_key' in data:
try: 122 131 try:
request.user.confirm_email(data['confirmation_key']) 123 132 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 124 133 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 125 134 raise ValidationError('confirmation_key is invalid')
126 135
return Response(UserSerializer(request.user).data) 127 136 return Response(UserSerializer(request.user).data)
128 137
def get(self, request, format=None): 129 138 def get(self, request, format=None):
""" 130 139 """
Return data about the user 131 140 Return data about the user
--- 132 141 ---
response_serializer: UserSerializer 133 142 response_serializer: UserSerializer
""" 134 143 """
serializer = UserSerializer(request.user, context={'request': request}) 135 144 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 136 145 return Response(serializer.data)
137 146
def delete(self, request): 138 147 def delete(self, request):
""" 139 148 """
Irrevocably delete the user and their data 140 149 Irrevocably delete the user and their data
141 150
Yes, really 142 151 Yes, really
""" 143 152 """
request.user.delete() 144 153 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 145 154 return Response(status=HTTP_204_NO_CONTENT)
146 155
147 156
@api_view(['POST']) 148 157 @api_view(['POST'])
def register(request, format=None): 149 158 def register(request, format=None):
""" 150 159 """
Register a new user 151 160 Register a new user
--- 152 161 ---
request_serializer: EmailPasswordSerializer 153 162 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 154 163 response_serializer: UserSerializer
""" 155 164 """
data = RegistrationSerializer(data=request.data) 156 165 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 157 166 data.is_valid(raise_exception=True)
158 167
User.objects.create_user(**data.validated_data) 159 168 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 160 169 user = authenticate(**data.validated_data)
auth.login(request, user) 161 170 auth.login(request, user)
162 171
body = ''' 163 172 body = '''
Visit the following link to confirm your email address: 164 173 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 165 174 https://flashy.cards/app/verify_email/%s
166 175
If you did not register for Flashy, no action is required. 167 176 If you did not register for Flashy, no action is required.
''' 168 177 '''
169 178
assert send_mail("Flashy email verification", 170 179 assert send_mail("Flashy email verification",
body % user.confirmation_key, 171 180 body % user.confirmation_key,
"noreply@flashy.cards", 172 181 "noreply@flashy.cards",
[user.email]) 173 182 [user.email])
174 183
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 175 184 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
176 185
177 186
@api_view(['POST']) 178 187 @api_view(['POST'])
def login(request): 179 188 def login(request):
""" 180 189 """
Authenticates user and returns user data if valid. 181 190 Authenticates user and returns user data if valid.
--- 182 191 ---
request_serializer: EmailPasswordSerializer 183 192 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 184 193 response_serializer: UserSerializer
""" 185 194 """
186 195
data = EmailPasswordSerializer(data=request.data) 187 196 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 188 197 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 189 198 user = authenticate(**data.validated_data)
190 199
if user is None: 191 200 if user is None:
raise AuthenticationFailed('Invalid email or password') 192 201 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 193 202 if not user.is_active:
raise NotAuthenticated('Account is disabled') 194 203 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 195 204 auth.login(request, user)
return Response(UserSerializer(request.user).data) 196 205 return Response(UserSerializer(request.user).data)
197 206
198 207
@api_view(['POST']) 199 208 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 200 209 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 201 210 def logout(request, format=None):
""" 202 211 """
Logs the authenticated user out. 203 212 Logs the authenticated user out.
""" 204 213 """
auth.logout(request) 205 214 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 206 215 return Response(status=HTTP_204_NO_CONTENT)
207 216
208 217
@api_view(['POST']) 209 218 @api_view(['POST'])
def request_password_reset(request, format=None): 210 219 def request_password_reset(request, format=None):
""" 211 220 """
Send a password reset token/link to the provided email. 212 221 Send a password reset token/link to the provided email.
--- 213 222 ---
request_serializer: PasswordResetRequestSerializer 214 223 request_serializer: PasswordResetRequestSerializer
""" 215 224 """
data = PasswordResetRequestSerializer(data=request.data) 216 225 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 217 226 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 218 227 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 219 228 token = default_token_generator.make_token(user)
220 229
body = ''' 221 230 body = '''
Visit the following link to reset your password: 222 231 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 223 232 https://flashy.cards/app/reset_password/%d/%s
224 233
If you did not request a password reset, no action is required. 225 234 If you did not request a password reset, no action is required.
''' 226 235 '''
227 236
send_mail("Flashy password reset", 228 237 send_mail("Flashy password reset",
body % (user.pk, token), 229 238 body % (user.pk, token),
"noreply@flashy.cards", 230 239 "noreply@flashy.cards",
[user.email]) 231 240 [user.email])
232 241
return Response(status=HTTP_204_NO_CONTENT) 233 242 return Response(status=HTTP_204_NO_CONTENT)
234 243
235 244
@api_view(['POST']) 236 245 @api_view(['POST'])
def reset_password(request, format=None): 237 246 def reset_password(request, format=None):
""" 238 247 """
Updates user's password to new password if token is valid. 239 248 Updates user's password to new password if token is valid.
--- 240 249 ---
request_serializer: PasswordResetSerializer 241 250 request_serializer: PasswordResetSerializer
""" 242 251 """
data = PasswordResetSerializer(data=request.data) 243 252 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 244 253 data.is_valid(raise_exception=True)
245 254
user = User.objects.get(id=data['uid'].value) 246 255 user = User.objects.get(id=data['uid'].value)
# Check token validity. 247 256 # Check token validity.
248 257
if default_token_generator.check_token(user, data['token'].value): 249 258 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 250 259 user.set_password(data['new_password'].value)
user.save() 251 260 user.save()
else: 252 261 else:
raise ValidationError('Could not verify reset token') 253 262 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 254 263 return Response(status=HTTP_204_NO_CONTENT)
255 264
256 265
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 257 266 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 258 267 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 259 268 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 260 269 permission_classes = [IsAuthenticated]
261 270
# Override create in CreateModelMixin 262 271 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 263 272 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 264 273 serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) 265 274 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 266 275 serializer.validated_data['author'] = request.user
self.perform_create(serializer) 267 276 self.perform_create(serializer)
headers = self.get_success_headers(serializer.data) 268 277 headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 269 278 return Response(serializer.data, status=HTTP_201_CREATED, headers=headers)
270 279
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 271 280 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def report(self, request, pk): 272 281 def report(self, request, pk):
""" 273 282 """
Report the given card 274 283 Report the given card
--- 275 284 ---
omit_serializer: true 276 285 omit_serializer: true
parameters: 277 286 parameters:
- fake: None 278 287 - fake: None
parameters_strategy: 279 288 parameters_strategy:
form: replace 280 289 form: replace
""" 281 290 """
obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) 282 291 obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object())
obj.reason = request.data['reason'] 283 292 obj.reason = request.data['reason']
obj.save() 284 293 obj.save()
return Response(status=HTTP_204_NO_CONTENT) 285 294 return Response(status=HTTP_204_NO_CONTENT)
scripts/check_python_version.py View file @ e4c3292
File was created 1
2
3 import sys
4
5 print(sys.version)
scripts/run_local.sh View file @ e4c3292
#!/bin/bash -xe 1 1 #!/bin/bash -xe
2
if [ ! -d "../flashy-frontend/" ]; then 2 3 if [ ! -d "../flashy-frontend/" ]; then
echo "In order to serve the frontend, flashy-frontend must be in the parent dir" 3 4 echo "In order to serve the frontend, flashy-frontend must be in the parent dir"
exit 1 4 5 exit 1
fi 5 6 fi
source venv/bin/activate 6 7
8 if [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
9 source venv/bin/activate
10 elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then
11 source venv/Scripts/activate
12 fi
13
python manage.py runserver 127.0.0.1:8080 7 14 python manage.py runserver 127.0.0.1:8080
8 15
scripts/setup.sh View file @ e4c3292
#!/bin/bash -xe 1 1 #!/bin/bash -xe
virtualenv venv 2 2 virtualenv venv
source venv/bin/activate 3 3
4 if [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
5 source venv/bin/activate
6 elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then
7 source venv/Scripts/activate
8 fi
9
pip install -r requirements.txt 4 10 pip install -r requirements.txt
virtualenv --relocatable venv 5 11 virtualenv --relocatable venv
#git submodule init 6
#git submodule update --depth 1 7
#pip install -e django 8