Commit ce17f969fddc838e8589befcdeca0f79d71b869c
1 parent
ddd9b21455
Exists in
master
Restructured api, moved more validation to serializers, added snazzy apidocs
Showing 9 changed files with 290 additions and 197 deletions Side-by-side Diff
flashcards/api.py
View file @
ce17f96
1 | -from django.core.mail import send_mail | |
2 | -from django.contrib.auth import authenticate, login, logout | |
3 | -from django.contrib.auth.tokens import default_token_generator | |
1 | +from rest_framework.pagination import PageNumberPagination | |
4 | 2 | from rest_framework.permissions import BasePermission |
5 | -from rest_framework.status import HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED | |
6 | -from rest_framework.views import APIView | |
7 | -from rest_framework.response import Response | |
8 | -from rest_framework.exceptions import ValidationError, NotFound | |
9 | -from flashcards.serializers import * | |
10 | 3 | |
11 | 4 | |
5 | +class StandardResultsSetPagination(PageNumberPagination): | |
6 | + page_size = 40 | |
7 | + page_size_query_param = 'page_size' | |
8 | + max_page_size = 1000 | |
9 | + | |
10 | + | |
12 | 11 | class UserDetailPermissions(BasePermission): |
13 | 12 | def has_object_permission(self, request, view, obj): |
14 | 13 | if request.method == 'POST': |
15 | 14 | return True |
16 | 15 | return request.user.is_active |
17 | - | |
18 | - | |
19 | -class UserDetail(APIView): | |
20 | - def patch(self, request, format=None): | |
21 | - """ | |
22 | - This method checks either the email or the password passed in | |
23 | - is valid. If confirmation key is correct, it validates the | |
24 | - user. It updates the password if the new password | |
25 | - is valid. | |
26 | - | |
27 | - """ | |
28 | - currentuser = request.user | |
29 | - | |
30 | - if 'confirmation_key' in request.data: | |
31 | - if not currentuser.confirm_email(request.data['confirmation_key']): | |
32 | - raise ValidationError('confirmation_key is invalid') | |
33 | - | |
34 | - if 'new_password' in request.data: | |
35 | - if not currentuser.check_password(request.data['old_password']): | |
36 | - raise ValidationError('Invalid old password') | |
37 | - if not request.data['new_password']: | |
38 | - raise ValidationError('Password cannot be blank') | |
39 | - currentuser.set_password(request.data['new_password']) | |
40 | - currentuser.save() | |
41 | - | |
42 | - return Response(UserSerializer(request.user).data) | |
43 | - | |
44 | - def get(self, request, format=None): | |
45 | - if not request.user.is_active: return Response(status=HTTP_401_UNAUTHORIZED) | |
46 | - serializer = UserSerializer(request.user) | |
47 | - return Response(serializer.data) | |
48 | - | |
49 | - def post(self, request, format=None): | |
50 | - if 'email' not in request.data: | |
51 | - raise ValidationError('Email is required') | |
52 | - if 'password' not in request.data: | |
53 | - raise ValidationError('Password is required') | |
54 | - email = request.data['email'] | |
55 | - existing_users = User.objects.filter(email=email) | |
56 | - if existing_users.exists(): | |
57 | - raise ValidationError("An account with this email already exists") | |
58 | - user = User.objects.create_user(email=email, password=request.data['password']) | |
59 | - | |
60 | - body = ''' | |
61 | - Visit the following link to confirm your email address: | |
62 | - http://flashy.cards/app/verify_email/%s | |
63 | - | |
64 | - If you did not register for Flashy, no action is required. | |
65 | - ''' | |
66 | - send_mail("Flashy email verification", | |
67 | - body % user.confirmation_key, | |
68 | - "noreply@flashy.cards", | |
69 | - [user.email]) | |
70 | - | |
71 | - user = authenticate(email=email, password=request.data['password']) | |
72 | - login(request, user) | |
73 | - return Response(UserSerializer(user).data, status=HTTP_201_CREATED) | |
74 | - | |
75 | - def delete(self, request): | |
76 | - request.user.delete() | |
77 | - return Response(status=HTTP_204_NO_CONTENT) | |
78 | - | |
79 | - | |
80 | -class UserLogin(APIView): | |
81 | - """ | |
82 | - Authenticates user and returns user data if valid. Handles invalid | |
83 | - users. | |
84 | - """ | |
85 | - | |
86 | - def post(self, request, format=None): | |
87 | - """ | |
88 | - Authenticates and logs in the user and returns their data if valid. | |
89 | - """ | |
90 | - if 'email' not in request.data: | |
91 | - raise ValidationError('Email is required') | |
92 | - if 'password' not in request.data: | |
93 | - raise ValidationError('Password is required') | |
94 | - | |
95 | - email = request.data['email'] | |
96 | - password = request.data['password'] | |
97 | - user = authenticate(email=email, password=password) | |
98 | - | |
99 | - if user is None: | |
100 | - raise ValidationError('Invalid email or password') | |
101 | - if not user.is_active: | |
102 | - raise ValidationError('Account is disabled') | |
103 | - login(request, user) | |
104 | - return Response(UserSerializer(user).data) | |
105 | - | |
106 | - | |
107 | -class UserLogout(APIView): | |
108 | - """ | |
109 | - Authenticated user log out. | |
110 | - """ | |
111 | - | |
112 | - def post(self, request, format=None): | |
113 | - """ | |
114 | - Logs the authenticated user out. | |
115 | - """ | |
116 | - logout(request) | |
117 | - return Response(status=HTTP_204_NO_CONTENT) | |
118 | - | |
119 | - | |
120 | -class PasswordReset(APIView): | |
121 | - """ | |
122 | - Allows user to reset their password. | |
123 | - System sends an email to the user's email with a token that may be verified | |
124 | - to reset their password. | |
125 | - """ | |
126 | - | |
127 | - def post(self, request, format=None): | |
128 | - """ | |
129 | - Send a password reset token/link to the provided email. | |
130 | - """ | |
131 | - if 'email' not in request.data: | |
132 | - raise ValidationError('Email is required') | |
133 | - | |
134 | - email = request.data['email'] | |
135 | - | |
136 | - # Find the user since they are not logged in. | |
137 | - try: | |
138 | - user = User.objects.get(email=email) | |
139 | - except User.DoesNotExist: | |
140 | - # Don't leak that email does not exist. | |
141 | - raise NotFound('Email does not exist') | |
142 | - | |
143 | - token = default_token_generator.make_token(user) | |
144 | - | |
145 | - body = ''' | |
146 | - Visit the following link to reset your password: | |
147 | - http://flashy.cards/app/reset_password/%d/%s | |
148 | - | |
149 | - If you did not request a password reset, no action is required. | |
150 | - ''' | |
151 | - | |
152 | - send_mail("Flashy password reset", | |
153 | - body % (user.pk, token), | |
154 | - "noreply@flashy.cards", | |
155 | - [user.email]) | |
156 | - | |
157 | - return Response(status=HTTP_204_NO_CONTENT) | |
158 | - | |
159 | - def patch(self, request, format=None): | |
160 | - """ | |
161 | - Updates user's password to new password if token is valid. | |
162 | - """ | |
163 | - if 'new_password' not in request.data: | |
164 | - raise ValidationError('New password is required') | |
165 | - if not request.data['new_password']: | |
166 | - raise ValidationError('Password cannot be blank') | |
167 | - | |
168 | - user = request.user | |
169 | - | |
170 | - # Check token validity. | |
171 | - if default_token_generator.check_token(user, request.data['token']): | |
172 | - user.set_password(request.data['new_password']) | |
173 | - user.save() | |
174 | - | |
175 | - return Response(status=HTTP_204_NO_CONTENT) |
flashcards/migrations/0002_auto_20150504_1327.py
View file @
ce17f96
1 | +# -*- coding: utf-8 -*- | |
2 | +from __future__ import unicode_literals | |
3 | + | |
4 | +from django.db import models, migrations | |
5 | +import django.core.validators | |
6 | +import flashcards.models | |
7 | + | |
8 | + | |
9 | +class Migration(migrations.Migration): | |
10 | + | |
11 | + dependencies = [ | |
12 | + ('flashcards', '0001_initial'), | |
13 | + ] | |
14 | + | |
15 | + operations = [ | |
16 | + migrations.AlterModelOptions( | |
17 | + name='section', | |
18 | + options={}, | |
19 | + ), | |
20 | + migrations.AlterModelManagers( | |
21 | + name='user', | |
22 | + managers=[ | |
23 | + ('objects', flashcards.models.EmailOnlyUserManager()), | |
24 | + ], | |
25 | + ), | |
26 | + migrations.AlterField( | |
27 | + model_name='flashcardreport', | |
28 | + name='reason', | |
29 | + field=models.CharField(max_length=255, blank=True), | |
30 | + ), | |
31 | + migrations.AlterField( | |
32 | + model_name='lectureperiod', | |
33 | + name='week_day', | |
34 | + field=models.IntegerField(help_text=b'1-indexed day of week, starting at Sunday'), | |
35 | + ), | |
36 | + migrations.AlterField( | |
37 | + model_name='user', | |
38 | + name='sections', | |
39 | + field=models.ManyToManyField(help_text=b'The sections which the user is enrolled in', to='flashcards.Section'), | |
40 | + ), | |
41 | + migrations.AlterField( | |
42 | + model_name='user', | |
43 | + name='username', | |
44 | + field=models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, error_messages={'unique': 'A user with that username already exists.'}, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')]), | |
45 | + ), | |
46 | + ] |
flashcards/models.py
View file @
ce17f96
flashcards/serializers.py
View file @
ce17f96
1 | 1 | from flashcards.models import Section, LecturePeriod, User |
2 | -from rest_framework.fields import EmailField, BooleanField | |
2 | +from rest_framework import serializers | |
3 | +from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField | |
3 | 4 | from rest_framework.relations import HyperlinkedRelatedField |
4 | -from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer | |
5 | +from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer, Serializer | |
6 | +from rest_framework.validators import UniqueValidator | |
7 | + | |
8 | + | |
9 | +class EmailSerializer(Serializer): | |
10 | + email = EmailField(required=True) | |
11 | + | |
12 | + | |
13 | +class EmailPasswordSerializer(EmailSerializer): | |
14 | + password = CharField(required=True) | |
15 | + | |
16 | + | |
17 | +class RegistrationSerializer(EmailPasswordSerializer): | |
18 | + email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) | |
19 | + | |
20 | + | |
21 | +class PasswordResetRequestSerializer(EmailSerializer): | |
22 | + def validate_email(self, value): | |
23 | + try: | |
24 | + User.objects.get(email=value) | |
25 | + return value | |
26 | + except User.DoesNotExist: | |
27 | + raise serializers.ValidationError('No user exists with that email') | |
28 | + | |
29 | + | |
30 | +class PasswordResetSerializer(Serializer): | |
31 | + new_password = CharField(required=True, allow_blank=False) | |
32 | + uid = IntegerField(required=True) | |
33 | + token = CharField(required=True) | |
34 | + | |
35 | + def validate_uid(self, value): | |
36 | + try: | |
37 | + User.objects.get(id=value) | |
38 | + return value | |
39 | + except User.DoesNotExist: | |
40 | + raise serializers.ValidationError('Could not verify reset token') | |
41 | + | |
42 | +class UserUpdateSerializer(Serializer): | |
43 | + old_password = CharField(required=False) | |
44 | + new_password = CharField(required=False, allow_blank=False) | |
45 | + confirmation_key = CharField(required=False) | |
46 | + # reset_token = CharField(required=False) | |
47 | + | |
48 | + def validate(self, data): | |
49 | + if 'new_password' in data and 'old_password' not in data: | |
50 | + raise serializers.ValidationError('old_password is required to set a new_password') | |
51 | + return data | |
52 | + | |
53 | + | |
54 | +class Password(Serializer): | |
55 | + email = EmailField(required=True) | |
56 | + password = CharField(required=True) | |
5 | 57 | |
6 | 58 | |
7 | 59 | class LecturePeriodSerializer(ModelSerializer): |
flashcards/tests/test_api.py
View file @
ce17f96
1 | -from django.test import Client | |
2 | 1 | from flashcards.models import User |
3 | -from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED | |
4 | -from rest_framework.test import APITestCase, APIClient | |
2 | +from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN, HTTP_401_UNAUTHORIZED | |
3 | +from rest_framework.test import APITestCase | |
5 | 4 | |
6 | 5 | |
7 | 6 | class LoginTests(APITestCase): |
8 | 7 | |
9 | 8 | |
... | ... | @@ -17,15 +16,15 @@ |
17 | 16 | |
18 | 17 | data = {'email': 'test@flashy.cards', 'password': '54321'} |
19 | 18 | response = self.client.post(url, data, format='json') |
20 | - self.assertContains(response, 'Invalid email or password', status_code=400) | |
19 | + self.assertContains(response, 'Invalid email or password', status_code=403) | |
21 | 20 | |
22 | 21 | data = {'email': 'none@flashy.cards', 'password': '54321'} |
23 | 22 | response = self.client.post(url, data, format='json') |
24 | - self.assertContains(response, 'Invalid email or password', status_code=400) | |
23 | + self.assertContains(response, 'Invalid email or password', status_code=403) | |
25 | 24 | |
26 | 25 | data = {'password': '54321'} |
27 | 26 | response = self.client.post(url, data, format='json') |
28 | - self.assertContains(response, 'Email is required', status_code=400) | |
27 | + self.assertContains(response, 'email', status_code=400) | |
29 | 28 | |
30 | 29 | |
31 | 30 | class RegistrationTest(APITestCase): |
32 | 31 | |
33 | 32 | |
34 | 33 | |
... | ... | @@ -39,15 +38,15 @@ |
39 | 38 | response = self.client.post('/api/login', data, format='json') |
40 | 39 | self.assertEqual(response.status_code, HTTP_200_OK) |
41 | 40 | |
42 | - | |
43 | 41 | data = {'email': 'none@none.com'} |
44 | 42 | response = self.client.post(url, data, format='json') |
45 | - self.assertContains(response, 'Password is required', status_code=400) | |
43 | + self.assertContains(response, 'password', status_code=400) | |
46 | 44 | |
47 | 45 | data = {'password': '1234'} |
48 | 46 | response = self.client.post(url, data, format='json') |
49 | - self.assertContains(response, 'Email is required', status_code=400) | |
47 | + self.assertContains(response, 'email', status_code=400) | |
50 | 48 | |
49 | + | |
51 | 50 | class ProfileViewTest(APITestCase): |
52 | 51 | def setUp(self): |
53 | 52 | email = "profileviewtest@flashy.cards" |
... | ... | @@ -56,7 +55,7 @@ |
56 | 55 | def test_get_me(self): |
57 | 56 | url = '/api/users/me' |
58 | 57 | response = self.client.get(url, format='json') |
59 | - # since we're not logged in, we shouldn't see anything here | |
58 | + # since we're not logged in, we shouldn't be able to see this | |
60 | 59 | self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) |
61 | 60 | |
62 | 61 | self.client.login(email='profileviewtest@flashy.cards', password='1234') |
flashcards/views.py
View file @
ce17f96
1 | -from flashcards.models import Section, LecturePeriod | |
2 | -from flashcards.serializers import SectionSerializer, LecturePeriodSerializer | |
3 | -from rest_framework.permissions import IsAuthenticatedOrReadOnly | |
4 | -from rest_framework.viewsets import ModelViewSet | |
5 | -from rest_framework.pagination import PageNumberPagination | |
1 | +from flashcards.api import StandardResultsSetPagination | |
2 | +from flashcards.models import Section, User | |
3 | +from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ | |
4 | + PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer | |
5 | +from rest_framework.viewsets import ReadOnlyModelViewSet | |
6 | +from django.core.mail import send_mail | |
7 | +from django.contrib.auth import authenticate, login, logout | |
8 | +from django.contrib.auth.tokens import default_token_generator | |
9 | +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED | |
10 | +from rest_framework.views import APIView | |
11 | +from rest_framework.response import Response | |
12 | +from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError | |
6 | 13 | |
7 | 14 | |
8 | -class StandardResultsSetPagination(PageNumberPagination): | |
9 | - page_size = 40 | |
10 | - page_size_query_param = 'page_size' | |
11 | - max_page_size = 1000 | |
12 | - | |
13 | - | |
14 | -class SectionViewSet(ModelViewSet): | |
15 | +class SectionViewSet(ReadOnlyModelViewSet): | |
15 | 16 | queryset = Section.objects.all() |
16 | 17 | serializer_class = SectionSerializer |
17 | - permission_classes = (IsAuthenticatedOrReadOnly,) | |
18 | 18 | pagination_class = StandardResultsSetPagination |
19 | 19 | |
20 | 20 | |
21 | -class LecturePeriodViewSet(ModelViewSet): | |
22 | - queryset = LecturePeriod.objects.all() | |
23 | - serializer_class = LecturePeriodSerializer | |
24 | - permission_classes = (IsAuthenticatedOrReadOnly,) | |
25 | - pagination_class = StandardResultsSetPagination | |
21 | +class UserDetail(APIView): | |
22 | + def patch(self, request, format=None): | |
23 | + """ | |
24 | + Updates the user's password, or verifies their email address | |
25 | + --- | |
26 | + request_serializer: UserUpdateSerializer | |
27 | + response_serializer: UserSerializer | |
28 | + """ | |
29 | + data = UserUpdateSerializer(data=request.data, context={'user': request.user}) | |
30 | + data.is_valid(raise_exception=True) | |
31 | + | |
32 | + if 'new_password' in data: | |
33 | + if not request.user.check_password(data['old_password']): | |
34 | + raise ValidationError('old_password is incorrect') | |
35 | + request.user.set_password(request.data['new_password']) | |
36 | + request.user.save() | |
37 | + | |
38 | + if 'confirmation_key' in data and not request.user.confirm_email(data['confirmation_key']): | |
39 | + raise ValidationError('confirmation_key is invalid') | |
40 | + | |
41 | + return Response(UserSerializer(request.user).data) | |
42 | + | |
43 | + def get(self, request, format=None): | |
44 | + """ | |
45 | + Return data about the user | |
46 | + --- | |
47 | + response_serializer: UserSerializer | |
48 | + """ | |
49 | + if not request.user.is_active: return Response(status=HTTP_401_UNAUTHORIZED) | |
50 | + serializer = UserSerializer(request.user) | |
51 | + return Response(serializer.data) | |
52 | + | |
53 | + def post(self, request, format=None): | |
54 | + """ | |
55 | + Register a new user | |
56 | + --- | |
57 | + request_serializer: EmailPasswordSerializer | |
58 | + response_serializer: UserSerializer | |
59 | + """ | |
60 | + data = RegistrationSerializer(data=request.data) | |
61 | + data.is_valid(raise_exception=True) | |
62 | + | |
63 | + User.objects.create_user(**data.validated_data) | |
64 | + user = authenticate(**data.validated_data) | |
65 | + login(request, user) | |
66 | + | |
67 | + body = ''' | |
68 | + Visit the following link to confirm your email address: | |
69 | + http://flashy.cards/app/verify_email/%s | |
70 | + | |
71 | + If you did not register for Flashy, no action is required. | |
72 | + ''' | |
73 | + | |
74 | + assert send_mail("Flashy email verification", | |
75 | + body % user.confirmation_key, | |
76 | + "noreply@flashy.cards", | |
77 | + [user.email]) | |
78 | + | |
79 | + return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) | |
80 | + | |
81 | + def delete(self, request): | |
82 | + """ | |
83 | + Irrevocably delete the user and their data | |
84 | + | |
85 | + Yes, really | |
86 | + """ | |
87 | + request.user.delete() | |
88 | + return Response(status=HTTP_204_NO_CONTENT) | |
89 | + | |
90 | + | |
91 | +class UserLogin(APIView): | |
92 | + def post(self, request): | |
93 | + """ | |
94 | + Authenticates user and returns user data if valid. | |
95 | + --- | |
96 | + request_serializer: EmailPasswordSerializer | |
97 | + response_serializer: UserSerializer | |
98 | + """ | |
99 | + | |
100 | + data = EmailPasswordSerializer(data=request.data) | |
101 | + data.is_valid(raise_exception=True) | |
102 | + user = authenticate(**data.validated_data) | |
103 | + | |
104 | + if user is None: | |
105 | + raise AuthenticationFailed('Invalid email or password') | |
106 | + if not user.is_active: | |
107 | + raise NotAuthenticated('Account is disabled') | |
108 | + login(request, user) | |
109 | + return Response(UserSerializer(request.user).data) | |
110 | + | |
111 | + | |
112 | +class UserLogout(APIView): | |
113 | + """ | |
114 | + Authenticated user log out. | |
115 | + """ | |
116 | + | |
117 | + def post(self, request, format=None): | |
118 | + """ | |
119 | + Logs the authenticated user out. | |
120 | + """ | |
121 | + logout(request) | |
122 | + return Response(status=HTTP_204_NO_CONTENT) | |
123 | + | |
124 | + | |
125 | +class PasswordReset(APIView): | |
126 | + """ | |
127 | + Allows user to reset their password. | |
128 | + """ | |
129 | + | |
130 | + def post(self, request, format=None): | |
131 | + """ | |
132 | + Send a password reset token/link to the provided email. | |
133 | + --- | |
134 | + request_serializer: PasswordResetRequestSerializer | |
135 | + """ | |
136 | + data = PasswordResetRequestSerializer(data=request.data) | |
137 | + data.is_valid(raise_exception=True) | |
138 | + user = User.objects.get(email=data['email'].value) | |
139 | + token = default_token_generator.make_token(user) | |
140 | + | |
141 | + body = ''' | |
142 | + Visit the following link to reset your password: | |
143 | + http://flashy.cards/app/reset_password/%d/%s | |
144 | + | |
145 | + If you did not request a password reset, no action is required. | |
146 | + ''' | |
147 | + | |
148 | + send_mail("Flashy password reset", | |
149 | + body % (user.pk, token), | |
150 | + "noreply@flashy.cards", | |
151 | + [user.email]) | |
152 | + | |
153 | + return Response(status=HTTP_204_NO_CONTENT) | |
154 | + | |
155 | + def patch(self, request, format=None): | |
156 | + """ | |
157 | + Updates user's password to new password if token is valid. | |
158 | + --- | |
159 | + request_serializer: PasswordResetSerializer | |
160 | + """ | |
161 | + data = PasswordResetSerializer(data=request.data) | |
162 | + data.is_valid(raise_exception=True) | |
163 | + | |
164 | + user = User.objects.get(id=data['uid'].value) | |
165 | + # Check token validity. | |
166 | + | |
167 | + if default_token_generator.check_token(user, data['token'].value): | |
168 | + user.set_password(data['new_password'].value) | |
169 | + user.save() | |
170 | + else: | |
171 | + raise ValidationError('Could not verify reset token') | |
172 | + return Response(status=HTTP_204_NO_CONTENT) |
flashy/settings.py
View file @
ce17f96
... | ... | @@ -21,6 +21,7 @@ |
21 | 21 | 'django.contrib.messages', |
22 | 22 | 'django.contrib.staticfiles', |
23 | 23 | 'django_ses', |
24 | + 'rest_framework_swagger', | |
24 | 25 | 'rest_framework', |
25 | 26 | ) |
26 | 27 | |
... | ... | @@ -93,4 +94,8 @@ |
93 | 94 | EMAIL_BACKEND = 'django_ses.SESBackend' |
94 | 95 | |
95 | 96 | SECRET_KEY = os.environ.get('SECRET_KEY', 'LOL DEFAULT SECRET KEY') |
97 | + | |
98 | +SWAGGER_SETTINGS = { | |
99 | + 'doc_expansion': 'list' | |
100 | +} |
flashy/urls.py
View file @
ce17f96
1 | 1 | from django.conf.urls import include, url |
2 | 2 | from django.contrib import admin |
3 | -from flashcards.views import SectionViewSet, LecturePeriodViewSet | |
3 | +from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset | |
4 | 4 | from rest_framework.routers import DefaultRouter |
5 | 5 | from flashcards.api import * |
6 | 6 | |
... | ... | @@ -8,6 +8,7 @@ |
8 | 8 | router.register(r'sections', SectionViewSet) |
9 | 9 | |
10 | 10 | urlpatterns = [ |
11 | + url(r'^api/docs/', include('rest_framework_swagger.urls')), | |
11 | 12 | url(r'^api/users/me$', UserDetail.as_view()), |
12 | 13 | url(r'^api/login$', UserLogin.as_view()), |
13 | 14 | url(r'^api/logout$', UserLogout.as_view()), |