Commit 8fa22f31e3ed3a9f389dd5045ecf3f68999eb021

Authored by Laura Hawkins
Exists in master

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

Showing 15 changed files Side-by-side Diff

1   -Set up the environment by running setup.sh
  1 +Set up the environment by running:
2 2  
  3 + scripts/setup.sh
  4 +
3 5 If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv:
4 6  
5 7 . venv/bin/activate
6 8  
  9 +If you still get errors about a module not being found, make sure your virtualenv is up to date. Re-run:
  10 +
  11 + scripts/setup.sh
  12 +
7 13 Run tests:
8 14  
9   - ./run_tests.sh
  15 + scripts/run_tests.sh
10 16  
11 17 Run development server (local):
12 18  
flashcards/api.py View file @ 8fa22f3
... ... @@ -15,5 +15,5 @@
15 15 def has_object_permission(self, request, view, obj):
16 16 if request.method == 'POST':
17 17 return True
18   - return request.user.is_active
  18 + return request.user.is_authenticated()
flashcards/tests/test_api.py View file @ 8fa22f3
  1 +from django.core import mail
1 2 from flashcards.models import User
2   -from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN, HTTP_401_UNAUTHORIZED
  3 +from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED
3 4 from rest_framework.test import APITestCase
  5 +from re import search
4 6  
5 7  
6 8 class LoginTests(APITestCase):
7 9  
8 10  
9 11  
10 12  
11 13  
12 14  
... ... @@ -26,27 +28,91 @@
26 28 response = self.client.post(url, data, format='json')
27 29 self.assertContains(response, 'email', status_code=400)
28 30  
  31 + data = {'email': 'none@flashy.cards'}
  32 + response = self.client.post(url, data, format='json')
  33 + self.assertContains(response, 'password', status_code=400)
29 34  
  35 + user = User.objects.get(email="test@flashy.cards")
  36 + user.is_active = False
  37 + user.save()
  38 +
  39 + data = {'email': 'test@flashy.cards', 'password': '1234'}
  40 + response = self.client.post(url, data, format='json')
  41 + self.assertContains(response, 'Account is disabled', status_code=403)
  42 +
  43 + def test_logout(self):
  44 + self.client.login(email='none@none.com', password='1234')
  45 + self.client.post('/api/logout')
  46 +
  47 + response = self.client.get('/api/users/me', format='json')
  48 + # since we're not logged in, we shouldn't be able to see this
  49 + self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
  50 +
  51 +class PasswordResetTest(APITestCase):
  52 + def setUp(self):
  53 + email = "test@flashy.cards"
  54 + User.objects.create_user(email=email, password="12345")
  55 +
  56 + def test_reset_password(self):
  57 + url = '/api/reset_password'
  58 + post_data = {'email': 'test@flashy.cards'}
  59 + patch_data = {'new_password': '54321',
  60 + 'uid': '', 'token': ''}
  61 + self.client.post(url, post_data, format='json')
  62 + self.assertEqual(len(mail.outbox), 1)
  63 + self.assertIn('reset your password', mail.outbox[0].body)
  64 +
  65 + capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)',
  66 + mail.outbox[0].body)
  67 + patch_data['uid'] = capture.group(1)
  68 + patch_data['token'] = capture.group(2)
  69 + self.client.patch(url, patch_data, format='json')
  70 + user = User.objects.get(id=patch_data['uid'])
  71 + assert user.check_password(patch_data['new_password'])
  72 +
  73 +
30 74 class RegistrationTest(APITestCase):
31 75 def test_create_account(self):
32 76 url = '/api/users/me'
33   - data = {'email': 'none@none.com', 'password': '1234'}
34   - response = self.client.post(url, data, format='json')
35   - self.assertEqual(response.status_code, HTTP_201_CREATED)
36 77  
37   - data = {'email': 'none@none.com', 'password': '1234'}
38   - response = self.client.post('/api/login', data, format='json')
39   - self.assertEqual(response.status_code, HTTP_200_OK)
40   -
  78 + # missing password
41 79 data = {'email': 'none@none.com'}
42 80 response = self.client.post(url, data, format='json')
43 81 self.assertContains(response, 'password', status_code=400)
44 82  
  83 + # missing email
45 84 data = {'password': '1234'}
46 85 response = self.client.post(url, data, format='json')
47 86 self.assertContains(response, 'email', status_code=400)
48 87  
  88 + # create a user
  89 + data = {'email': 'none@none.com', 'password': '1234'}
  90 + response = self.client.post(url, data, format='json')
  91 + self.assertEqual(response.status_code, HTTP_201_CREATED)
49 92  
  93 + # user should not be confirmed
  94 + user = User.objects.get(email="none@none.com")
  95 + self.assertFalse(user.is_confirmed)
  96 +
  97 + # check that the confirmation key was sent
  98 + self.assertEqual(len(mail.outbox), 1)
  99 + self.assertIn(user.confirmation_key, mail.outbox[0].body)
  100 +
  101 + # log the user out
  102 + self.client.logout()
  103 +
  104 + # log the user in with their registered credentials
  105 + self.client.login(email='none@none.com', password='1234')
  106 +
  107 + # try activating with an invalid key
  108 + response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
  109 + self.assertContains(response, 'confirmation_key is invalid', status_code=400)
  110 +
  111 + # try activating with the valid key
  112 + response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
  113 + self.assertTrue(response.data['is_confirmed'])
  114 +
  115 +
50 116 class ProfileViewTest(APITestCase):
51 117 def setUp(self):
52 118 email = "profileviewtest@flashy.cards"
... ... @@ -61,4 +127,47 @@
61 127 self.client.login(email='profileviewtest@flashy.cards', password='1234')
62 128 response = self.client.get(url, format='json')
63 129 self.assertEqual(response.status_code, HTTP_200_OK)
  130 +
  131 +
  132 +class PasswordChangeTest(APITestCase):
  133 + def setUp(self):
  134 + email = "none@none.com"
  135 + User.objects.create_user(email=email, password="1234")
  136 +
  137 + def test_change_password(self):
  138 + url = '/api/users/me'
  139 + user = User.objects.get(email='none@none.com')
  140 + self.assertTrue(user.check_password('1234'))
  141 +
  142 + response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
  143 + self.assertContains(response, 'You must be logged in to change your password', status_code=403)
  144 +
  145 + self.client.login(email='none@none.com', password='1234')
  146 + response = self.client.patch(url, {'new_password': '4321'}, format='json')
  147 + self.assertContains(response, 'old_password is required', status_code=400)
  148 +
  149 + response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
  150 + self.assertContains(response, 'old_password is incorrect', status_code=400)
  151 +
  152 + response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
  153 + self.assertEqual(response.status_code, 200)
  154 + user = User.objects.get(email='none@none.com')
  155 +
  156 + self.assertFalse(user.check_password('1234'))
  157 + self.assertTrue(user.check_password('4321'))
  158 +
  159 +
  160 +class DeleteUserTest(APITestCase):
  161 + def setUp(self):
  162 + email = "none@none.com"
  163 + User.objects.create_user(email=email, password="1234")
  164 +
  165 + def test_delete_user(self):
  166 + url = '/api/users/me'
  167 + user = User.objects.get(email='none@none.com')
  168 +
  169 + self.client.login(email='none@none.com', password='1234')
  170 + self.client.delete(url)
  171 +
  172 + self.assertFalse(User.objects.filter(email='none@none.com').exists())
flashcards/tests/test_models.py View file @ 8fa22f3
1 1 from django.test import TestCase
2 2 from flashcards.models import User, Section
  3 +from simple_email_confirmation import EmailAddress
3 4  
4 5  
  6 +class RegistrationTests(TestCase):
  7 + def setUp(self):
  8 + User.objects.create_user(email="none@none.com", password="1234")
  9 +
  10 + def test_email_confirmation(self):
  11 + user = User.objects.get(email="none@none.com")
  12 + self.assertFalse(user.is_confirmed)
  13 + user.confirm_email(user.confirmation_key)
  14 + self.assertTrue(user.is_confirmed)
  15 +
  16 +
5 17 class UserTests(TestCase):
6 18 def setUp(self):
7   - User.objects.create(email="none@none.com", password="1234")
  19 + User.objects.create_user(email="none@none.com", password="1234")
8 20 Section.objects.create(department='dept',
9 21 course_num='101a',
10 22 course_title='how 2 test',
... ... @@ -19,4 +31,6 @@
19 31 self.assertIn(section, user.sections.all())
20 32 user.sections.add(section)
21 33 self.assertEqual(user.sections.count(), 1)
  34 + user.sections.remove(section)
  35 + self.assertEqual(user.sections.count(), 0)
flashcards/views.py View file @ 8fa22f3
... ... @@ -2,6 +2,7 @@
2 2 from flashcards.models import Section, User
3 3 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
4 4 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer
  5 +from rest_framework.permissions import IsAuthenticated
5 6 from rest_framework.viewsets import ReadOnlyModelViewSet
6 7 from django.core.mail import send_mail
7 8 from django.contrib.auth import authenticate, login, logout
... ... @@ -10,6 +11,7 @@
10 11 from rest_framework.views import APIView
11 12 from rest_framework.response import Response
12 13 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError
  14 +from simple_email_confirmation import EmailAddress
13 15  
14 16  
15 17 class SectionViewSet(ReadOnlyModelViewSet):
16 18  
17 19  
18 20  
... ... @@ -34,15 +36,21 @@
34 36 """
35 37 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
36 38 data.is_valid(raise_exception=True)
  39 + data = data.validated_data
37 40  
38 41 if 'new_password' in data:
  42 + if not request.user.is_authenticated():
  43 + raise NotAuthenticated('You must be logged in to change your password')
39 44 if not request.user.check_password(data['old_password']):
40 45 raise ValidationError('old_password is incorrect')
41   - request.user.set_password(request.data['new_password'])
  46 + request.user.set_password(data['new_password'])
42 47 request.user.save()
43 48  
44   - if 'confirmation_key' in data and not request.user.confirm_email(data['confirmation_key']):
45   - raise ValidationError('confirmation_key is invalid')
  49 + if 'confirmation_key' in data:
  50 + try:
  51 + request.user.confirm_email(data['confirmation_key'])
  52 + except EmailAddress.DoesNotExist:
  53 + raise ValidationError('confirmation_key is invalid')
46 54  
47 55 return Response(UserSerializer(request.user).data)
48 56  
... ... @@ -52,7 +60,7 @@
52 60 ---
53 61 response_serializer: UserSerializer
54 62 """
55   - if not request.user.is_active: return Response(status=HTTP_401_UNAUTHORIZED)
  63 + if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED)
56 64 serializer = UserSerializer(request.user)
57 65 return Response(serializer.data)
58 66  
... ... @@ -72,7 +80,7 @@
72 80  
73 81 body = '''
74 82 Visit the following link to confirm your email address:
75   - http://flashy.cards/app/verify_email/%s
  83 + https://flashy.cards/app/verify_email/%s
76 84  
77 85 If you did not register for Flashy, no action is required.
78 86 '''
... ... @@ -116,6 +124,7 @@
116 124  
117 125  
118 126 class UserLogout(APIView):
  127 + permission_classes = (IsAuthenticated,)
119 128 def post(self, request, format=None):
120 129 """
121 130 Logs the authenticated user out.
... ... @@ -142,7 +151,7 @@
142 151  
143 152 body = '''
144 153 Visit the following link to reset your password:
145   - http://flashy.cards/app/reset_password/%d/%s
  154 + https://flashy.cards/app/reset_password/%d/%s
146 155  
147 156 If you did not request a password reset, no action is required.
148 157 '''
1 1 # mysite_uwsgi.ini file
2 2 [uwsgi]
3 3  
  4 +# set the uid and gid for user flashy
4 5 uid = 1007
5 6 gid = 1007
6 7 # Django-related settings
7 8  
... ... @@ -12,12 +13,12 @@
12 13 home = /srv/flashy-backend/venv/
13 14 logger = file:/var/log/uwsgi
14 15 # process-related settings
15   -# master
16   -# master = true
17 16 # maximum number of worker processes
18 17 # processes = 1
19 18 http = :7001
20   -# ... with appropriate permissions - may be needed
21   -# chmod-socket = 664
22   -# touch-reload = '/tmp/reload_uwsgi'
  19 +
  20 +# load the secrets file
  21 +for-readline = file:/srv/flashy-backend/secrets
  22 + env = %(_)
  23 +endfor =
flashy/settings.py View file @ 8fa22f3
... ... @@ -7,7 +7,7 @@
7 7  
8 8 DEBUG = not IN_PRODUCTION
9 9  
10   -ALLOWED_HOSTS = []
  10 +ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards']
11 11  
12 12 AUTH_USER_MODEL = 'flashcards.User'
13 13  
nginxconf/flashy.cards View file @ 8fa22f3
1 1 upstream backend_production {
2 2 # server unix:/tmp/flashy.sock;
3   - server localhost:7001;
  3 + server localhost:7002;
4 4 }
5 5  
6 6 server {
7 7  
8 8  
... ... @@ -17,14 +17,16 @@
17 17 expires 30d;
18 18 }
19 19  
20   - location ^~ /app {
21   - alias /srv/flashy-frontend;
  20 + location ^~ /app/ {
  21 + alias /srv/flashy-frontend/;
  22 + try_files $uri /app/home.html;
22 23 }
23 24  
24 25 location ~ /(api|admin|api-auth)/ {
25   - add_header 'Access-Control-Allow-Origin' 'http://localhost/';
  26 + add_header 'Access-Control-Allow-Origin' 'http://localhost';
26 27 add_header 'Access-Control-Allow-Credentials' 'true';
27 28 add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PATCH, PUT';
  29 + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,Origin,Authorization,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,X-CSRFToken';
28 30 proxy_pass http://backend_production;
29 31 proxy_redirect http://backend_production $scheme://flashy.cards;
30 32 proxy_set_header Host $host;
requirements.txt View file @ 8fa22f3
1   -beautifulsoup4
  1 +#beautifulsoup4
2 2 Django>=1.8
3 3 #django-websocket-redis==0.4.3
4 4 #gevent==1.0.1
5 5  
... ... @@ -7,10 +7,8 @@
7 7 six==1.9.0
8 8 djangorestframework
9 9 docutils
10   -gunicorn
11 10 django-simple-email-confirmation
12 11 django-ses
13 12 coverage
14 13 django-rest-swagger
15   -psycopg2
run_tests.sh View file @ 8fa22f3
1   -#!/bin/bash -xe
2   -venv/bin/python venv/bin/coverage run --source='flashcards' manage.py test flashcards/tests/
3   -venv/bin/python venv/bin/coverage html
scripts/run_production.sh View file @ 8fa22f3
  1 +#!/bin/bash -xe
  2 +source secrets.sh
  3 +source venv/bin/activate
  4 +/srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi
scripts/run_tests.sh View file @ 8fa22f3
  1 +#!/bin/bash -xe
  2 +venv/bin/python venv/bin/coverage run --source='flashcards' manage.py test flashcards/tests/
  3 +venv/bin/python venv/bin/coverage html
scripts/setup.sh View file @ 8fa22f3
  1 +#!/bin/bash -xe
  2 +virtualenv venv
  3 +source venv/bin/activate
  4 +pip install -r requirements.txt
  5 +virtualenv --relocatable venv
  6 +#git submodule init
  7 +#git submodule update --depth 1
  8 +#pip install -e django
scripts/setup_production.sh View file @ 8fa22f3
  1 +#!/bin/bash -xe
  2 +whoami
  3 +source venv/bin/activate
  4 +source secrets
  5 +pip install psycopg2
  6 +pip install gunicorn
  7 +python manage.py migrate
1   -#!/bin/bash -xe
2   -virtualenv venv
3   -source venv/bin/activate
4   -pip install -r requirements.txt
5   -virtualenv --relocatable venv
6   -#git submodule init
7   -#git submodule update --depth 1
8   -#pip install -e django