Commit 72bf5f00c5597d3b3c7a77e3d0d2331f5e354089
1 parent
a55a90018e
Exists in
master
Falcon puuuuuuuush. Refactored api and url scheme. Flashcard push is incomplete
Showing 11 changed files with 290 additions and 188 deletions Side-by-side Diff
README.md
View file @
72bf5f0
flashcards/admin.py
View file @
72bf5f0
1 | 1 | from django.contrib import admin |
2 | +from django.contrib.auth.admin import UserAdmin | |
2 | 3 | from flashcards.models import Flashcard, UserFlashcard, Section, FlashcardMask, \ |
3 | 4 | UserFlashcardReview, LecturePeriod, User |
4 | 5 | |
5 | 6 | admin.site.register([ |
6 | - User, | |
7 | 7 | Flashcard, |
8 | 8 | FlashcardMask, |
9 | 9 | UserFlashcard, |
... | ... | @@ -11,4 +11,5 @@ |
11 | 11 | Section, |
12 | 12 | LecturePeriod |
13 | 13 | ]) |
14 | +admin.site.register(User, UserAdmin) |
flashcards/fixtures/testusers.json
View file @
72bf5f0
1 | +[ | |
2 | + { | |
3 | + "fields": { | |
4 | + "username": "none@none.com", | |
5 | + "first_name": "", | |
6 | + "last_name": "", | |
7 | + "is_active": true, | |
8 | + "is_superuser": false, | |
9 | + "is_staff": false, | |
10 | + "last_login": null, | |
11 | + "groups": [], | |
12 | + "user_permissions": [], | |
13 | + "password": "pbkdf2_sha256$20000$9NRycTz13dZ8$jV/YWGHYcytwdWAvNfdHMHF9nezQnR7AlXoK1v6KxHo=", | |
14 | + "sections": [], | |
15 | + "email": "none@none.com", | |
16 | + "date_joined": "2015-05-10T13:37:31Z" | |
17 | + }, | |
18 | + "model": "flashcards.user", | |
19 | + "pk": 1 | |
20 | + } | |
21 | +] |
flashcards/models.py
View file @
72bf5f0
... | ... | @@ -151,19 +151,28 @@ |
151 | 151 | class Section(Model): |
152 | 152 | """ |
153 | 153 | A UCSD course taught by an instructor during a quarter. |
154 | - Different sections taught by the same instructor in the same quarter are considered identical. | |
155 | 154 | We use the term "section" to avoid collision with the builtin keyword "class" |
156 | 155 | """ |
157 | 156 | department = CharField(max_length=50) |
158 | 157 | course_num = CharField(max_length=6) |
159 | - # section_id = CharField(max_length=10) | |
160 | 158 | course_title = CharField(max_length=50) |
161 | 159 | instructor = CharField(max_length=100) |
162 | 160 | quarter = CharField(max_length=4) |
163 | - whitelist = ManyToManyField(User, related_name="whitelisted_sections") | |
164 | 161 | |
162 | + def is_whitelisted(self): | |
163 | + """ | |
164 | + :return: whether a whitelist exists for this section | |
165 | + """ | |
166 | + return self.whitelist.exists() | |
167 | + | |
168 | + def is_user_on_whitelist(self, user): | |
169 | + """ | |
170 | + :return: whether the user is on the waitlist for this section | |
171 | + """ | |
172 | + assert user is User | |
173 | + return self.whitelist.filter(email=user.email).exists() | |
174 | + | |
165 | 175 | class Meta: |
166 | - unique_together = (('department', 'course_num', 'quarter', 'instructor'),) | |
167 | 176 | ordering = ['-course_title'] |
168 | 177 | |
169 | 178 | class LecturePeriod(Model): |
... | ... | @@ -184,7 +193,7 @@ |
184 | 193 | An email address that has been whitelisted for a section at an instructor's request |
185 | 194 | """ |
186 | 195 | email = EmailField() |
187 | - section = ForeignKey(Section) | |
196 | + section = ForeignKey(Section, related_name='whitelist') | |
188 | 197 | |
189 | 198 | |
190 | 199 | class FlashcardReport(Model): |
flashcards/serializers.py
View file @
72bf5f0
1 | +from django.utils.datetime_safe import datetime | |
1 | 2 | from flashcards.models import Section, LecturePeriod, User, Flashcard |
2 | 3 | from rest_framework import serializers |
3 | 4 | from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField |
4 | 5 | from rest_framework.relations import HyperlinkedRelatedField |
5 | -from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer, Serializer | |
6 | +from rest_framework.serializers import ModelSerializer, Serializer | |
6 | 7 | from rest_framework.validators import UniqueValidator |
7 | 8 | |
8 | 9 | |
... | ... | @@ -39,6 +40,7 @@ |
39 | 40 | except User.DoesNotExist: |
40 | 41 | raise serializers.ValidationError('Could not verify reset token') |
41 | 42 | |
43 | + | |
42 | 44 | class UserUpdateSerializer(Serializer): |
43 | 45 | old_password = CharField(required=False) |
44 | 46 | new_password = CharField(required=False, allow_blank=False) |
45 | 47 | |
46 | 48 | |
... | ... | @@ -62,15 +64,14 @@ |
62 | 64 | exclude = 'id', 'section' |
63 | 65 | |
64 | 66 | |
65 | -class SectionSerializer(HyperlinkedModelSerializer): | |
67 | +class SectionSerializer(ModelSerializer): | |
66 | 68 | lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) |
67 | 69 | |
68 | 70 | class Meta: |
69 | 71 | model = Section |
70 | - exclude = 'whitelist', | |
71 | 72 | |
72 | 73 | |
73 | -class UserSerializer(HyperlinkedModelSerializer): | |
74 | +class UserSerializer(ModelSerializer): | |
74 | 75 | email = EmailField(required=False) |
75 | 76 | sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') |
76 | 77 | is_confirmed = BooleanField() |
... | ... | @@ -79,7 +80,22 @@ |
79 | 80 | model = User |
80 | 81 | fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") |
81 | 82 | |
82 | -class FlashcardSerializer(HyperlinkedModelSerializer): | |
83 | + | |
84 | +class FlashcardSerializer(ModelSerializer): | |
85 | + is_hidden = BooleanField(read_only=True) | |
86 | + hide_reason = CharField(read_only=True) | |
87 | + | |
88 | + def validate_material_date(self, value): | |
89 | + # TODO: make this dynamic | |
90 | + quarter_start = datetime(2015, 3, 15) | |
91 | + quarter_end = datetime(2015, 6, 15) | |
92 | + if quarter_start <= value <= quarter_end: return value | |
93 | + else: | |
94 | + raise serializers.ValidationError("Material date is outside allowed range for this quarter") | |
95 | + | |
96 | + # | |
97 | + # def validate(self, data): | |
98 | + # if | |
83 | 99 | |
84 | 100 | class Meta: |
85 | 101 | model = Flashcard |
flashcards/tests/test_api.py
View file @
72bf5f0
1 | 1 | from django.core import mail |
2 | 2 | from flashcards.models import User, Section, Flashcard |
3 | -from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED | |
3 | +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED, \ | |
4 | + HTTP_403_FORBIDDEN | |
4 | 5 | from rest_framework.test import APITestCase |
5 | 6 | from re import search |
6 | 7 | from django.utils.timezone import now |
7 | 8 | |
8 | 9 | |
9 | 10 | class LoginTests(APITestCase): |
10 | - def setUp(self): | |
11 | - email = "test@flashy.cards" | |
12 | - User.objects.create_user(email=email, password="1234") | |
11 | + fixtures = ['testusers'] | |
13 | 12 | |
14 | 13 | def test_login(self): |
15 | 14 | url = '/api/login' |
16 | - data = {'email': 'test@flashy.cards', 'password': '1234'} | |
15 | + data = {'email': 'none@none.com', 'password': '1234'} | |
17 | 16 | response = self.client.post(url, data, format='json') |
18 | 17 | self.assertEqual(response.status_code, HTTP_200_OK) |
19 | 18 | |
20 | - data = {'email': 'test@flashy.cards', 'password': '54321'} | |
19 | + data = {'email': 'none@none.com', 'password': '4321'} | |
21 | 20 | response = self.client.post(url, data, format='json') |
22 | 21 | self.assertContains(response, 'Invalid email or password', status_code=403) |
23 | 22 | |
24 | - data = {'email': 'none@flashy.cards', 'password': '54321'} | |
23 | + data = {'email': 'bad@none.com', 'password': '1234'} | |
25 | 24 | response = self.client.post(url, data, format='json') |
26 | 25 | self.assertContains(response, 'Invalid email or password', status_code=403) |
27 | 26 | |
28 | - data = {'password': '54321'} | |
27 | + data = {'password': '4321'} | |
29 | 28 | response = self.client.post(url, data, format='json') |
30 | 29 | self.assertContains(response, 'email', status_code=400) |
31 | 30 | |
32 | - data = {'email': 'none@flashy.cards'} | |
31 | + data = {'email': 'none@none.com'} | |
33 | 32 | response = self.client.post(url, data, format='json') |
34 | 33 | self.assertContains(response, 'password', status_code=400) |
35 | 34 | |
36 | - user = User.objects.get(email="test@flashy.cards") | |
35 | + user = User.objects.get(email="none@none.com") | |
37 | 36 | user.is_active = False |
38 | 37 | user.save() |
39 | 38 | |
40 | - data = {'email': 'test@flashy.cards', 'password': '1234'} | |
39 | + data = {'email': 'none@none.com', 'password': '1234'} | |
41 | 40 | response = self.client.post(url, data, format='json') |
42 | 41 | self.assertContains(response, 'Account is disabled', status_code=403) |
43 | 42 | |
44 | 43 | def test_logout(self): |
45 | - url = '/api/logout' | |
46 | - self.client.login(email='test@flashy.cards', password='1234') | |
47 | - response = self.client.post(url) | |
44 | + self.client.login(email='none@none.com', password='1234') | |
45 | + response = self.client.post('/api/logout') | |
48 | 46 | self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) |
49 | 47 | |
50 | - # since we're not logged in, we should get a 401 response | |
51 | - response = self.client.get('/api/users/me', format='json') | |
52 | - self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) | |
48 | + # since we're not logged in, we should get a 403 response | |
49 | + response = self.client.get('/api/me', format='json') | |
50 | + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) | |
53 | 51 | |
54 | 52 | |
55 | 53 | class PasswordResetTest(APITestCase): |
56 | - def setUp(self): | |
57 | - # create a user to test things with | |
58 | - email = "test@flashy.cards" | |
59 | - User.objects.create_user(email=email, password="12345") | |
54 | + fixtures = ['testusers'] | |
60 | 55 | |
61 | 56 | def test_reset_password(self): |
62 | 57 | # submit the request to reset the password |
63 | - url = '/api/reset_password' | |
64 | - post_data = {'email': 'test@flashy.cards'} | |
58 | + url = '/api/request_password_reset' | |
59 | + post_data = {'email': 'none@none.com'} | |
65 | 60 | self.client.post(url, post_data, format='json') |
66 | 61 | self.assertEqual(len(mail.outbox), 1) |
67 | 62 | self.assertIn('reset your password', mail.outbox[0].body) |
68 | 63 | |
69 | 64 | |
... | ... | @@ -69,18 +64,19 @@ |
69 | 64 | # capture the reset token from the email |
70 | 65 | capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', |
71 | 66 | mail.outbox[0].body) |
72 | - patch_data = {'new_password': '54321'} | |
67 | + patch_data = {'new_password': '4321'} | |
73 | 68 | patch_data['uid'] = capture.group(1) |
74 | 69 | reset_token = capture.group(2) |
75 | 70 | |
76 | 71 | # try to reset the password with the wrong reset token |
77 | 72 | patch_data['token'] = 'wrong_token' |
78 | - response = self.client.patch(url, patch_data, format='json') | |
73 | + url = '/api/reset_password' | |
74 | + response = self.client.post(url, patch_data, format='json') | |
79 | 75 | self.assertContains(response, 'Could not verify reset token', status_code=400) |
80 | 76 | |
81 | 77 | # try to reset the password with the correct token |
82 | 78 | patch_data['token'] = reset_token |
83 | - response = self.client.patch(url, patch_data, format='json') | |
79 | + response = self.client.post(url, patch_data, format='json') | |
84 | 80 | self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) |
85 | 81 | user = User.objects.get(id=patch_data['uid']) |
86 | 82 | assert user.check_password(patch_data['new_password']) |
... | ... | @@ -88,7 +84,7 @@ |
88 | 84 | |
89 | 85 | class RegistrationTest(APITestCase): |
90 | 86 | def test_create_account(self): |
91 | - url = '/api/users/me' | |
87 | + url = '/api/register' | |
92 | 88 | |
93 | 89 | # missing password |
94 | 90 | data = {'email': 'none@none.com'} |
... | ... | @@ -120,6 +116,8 @@ |
120 | 116 | self.client.login(email='none@none.com', password='1234') |
121 | 117 | |
122 | 118 | # try activating with an invalid key |
119 | + | |
120 | + url = '/api/me' | |
123 | 121 | response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) |
124 | 122 | self.assertContains(response, 'confirmation_key is invalid', status_code=400) |
125 | 123 | |
126 | 124 | |
127 | 125 | |
128 | 126 | |
129 | 127 | |
130 | 128 | |
131 | 129 | |
... | ... | @@ -129,33 +127,29 @@ |
129 | 127 | |
130 | 128 | |
131 | 129 | class ProfileViewTest(APITestCase): |
132 | - def setUp(self): | |
133 | - email = "profileviewtest@flashy.cards" | |
134 | - User.objects.create_user(email=email, password="1234") | |
130 | + fixtures = ['testusers'] | |
135 | 131 | |
136 | 132 | def test_get_me(self): |
137 | - url = '/api/users/me' | |
133 | + url = '/api/me' | |
138 | 134 | response = self.client.get(url, format='json') |
139 | 135 | # since we're not logged in, we shouldn't be able to see this |
140 | - self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) | |
136 | + self.assertEqual(response.status_code, 403) | |
141 | 137 | |
142 | - self.client.login(email='profileviewtest@flashy.cards', password='1234') | |
138 | + self.client.login(email='none@none.com', password='1234') | |
143 | 139 | response = self.client.get(url, format='json') |
144 | 140 | self.assertEqual(response.status_code, HTTP_200_OK) |
145 | 141 | |
146 | 142 | |
147 | 143 | class PasswordChangeTest(APITestCase): |
148 | - def setUp(self): | |
149 | - email = "none@none.com" | |
150 | - User.objects.create_user(email=email, password="1234") | |
144 | + fixtures = ['testusers'] | |
151 | 145 | |
152 | 146 | def test_change_password(self): |
153 | - url = '/api/users/me' | |
147 | + url = '/api/me' | |
154 | 148 | user = User.objects.get(email='none@none.com') |
155 | 149 | self.assertTrue(user.check_password('1234')) |
156 | 150 | |
157 | 151 | response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') |
158 | - self.assertContains(response, 'You must be logged in to change your password', status_code=403) | |
152 | + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) | |
159 | 153 | |
160 | 154 | self.client.login(email='none@none.com', password='1234') |
161 | 155 | response = self.client.patch(url, {'new_password': '4321'}, format='json') |
162 | 156 | |
... | ... | @@ -173,12 +167,10 @@ |
173 | 167 | |
174 | 168 | |
175 | 169 | class DeleteUserTest(APITestCase): |
176 | - def setUp(self): | |
177 | - email = "none@none.com" | |
178 | - User.objects.create_user(email=email, password="1234") | |
170 | + fixtures = ['testusers'] | |
179 | 171 | |
180 | 172 | def test_delete_user(self): |
181 | - url = '/api/users/me' | |
173 | + url = '/api/me' | |
182 | 174 | user = User.objects.get(email='none@none.com') |
183 | 175 | |
184 | 176 | self.client.login(email='none@none.com', password='1234') |
... | ... | @@ -187,6 +179,8 @@ |
187 | 179 | |
188 | 180 | |
189 | 181 | class FlashcardDetailTest(APITestCase): |
182 | + fixtures = ['testusers'] | |
183 | + | |
190 | 184 | def setUp(self): |
191 | 185 | section = Section(department="cse", course_num="5", course_title="cool course", instructor="gary", |
192 | 186 | quarter="fa15") |
193 | 187 | |
... | ... | @@ -198,8 +192,10 @@ |
198 | 192 | self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) |
199 | 193 | self.flashcard.save() |
200 | 194 | |
195 | + | |
201 | 196 | def test_get_flashcard(self): |
202 | - response = self.client.get("/api/flashcards/%d" % self.flashcard.id, format="json") | |
197 | + assert self.client.login(email='none@none.com', password='1234') | |
198 | + response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") | |
203 | 199 | self.assertEqual(response.status_code, HTTP_200_OK) |
204 | 200 | self.assertEqual(response.data["text"], "jason") |
flashcards/views.py
View file @
72bf5f0
1 | +from django.contrib import auth | |
1 | 2 | from flashcards.api import StandardResultsSetPagination |
2 | -from flashcards.models import Section, User, Flashcard | |
3 | +from flashcards.models import Section, User, Flashcard, FlashcardReport | |
3 | 4 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ |
4 | 5 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer |
6 | +from rest_framework.decorators import detail_route, permission_classes, api_view | |
7 | +from rest_framework.generics import ListAPIView, DestroyAPIView, GenericAPIView | |
8 | +from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin | |
5 | 9 | from rest_framework.permissions import IsAuthenticated |
6 | -from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet | |
10 | +from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet | |
7 | 11 | from django.core.mail import send_mail |
8 | -from django.contrib.auth import authenticate, login, logout | |
12 | +from django.contrib.auth import authenticate | |
9 | 13 | from django.contrib.auth.tokens import default_token_generator |
10 | -from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED | |
11 | -from rest_framework.views import APIView | |
14 | +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED | |
12 | 15 | from rest_framework.response import Response |
13 | -from rest_framework.generics import RetrieveAPIView | |
14 | -from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError | |
16 | +from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied | |
15 | 17 | from simple_email_confirmation import EmailAddress |
16 | 18 | |
17 | 19 | |
18 | 20 | |
19 | 21 | |
... | ... | @@ -19,9 +21,48 @@ |
19 | 21 | queryset = Section.objects.all() |
20 | 22 | serializer_class = SectionSerializer |
21 | 23 | pagination_class = StandardResultsSetPagination |
24 | + permission_classes = [IsAuthenticated] | |
22 | 25 | |
26 | + @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) | |
27 | + def enroll(self, request, pk): | |
28 | + """ | |
29 | + Add the current user to a specified section | |
30 | + If the class has a whitelist, but the user is not on the whitelist, the request will fail. | |
31 | + --- | |
32 | + omit_serializer: true | |
33 | + parameters: | |
34 | + - fake: None | |
35 | + parameters_strategy: | |
36 | + form: replace | |
37 | + """ | |
38 | + section = self.get_object() | |
39 | + if request.user.sections.filter(pk=section.pk).exists(): | |
40 | + return ValidationError("You are already in this section.") | |
41 | + if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): | |
42 | + return PermissionDenied("You must be on the whitelist to add this section.") | |
43 | + request.user.sections.add(section) | |
44 | + return Response(status=HTTP_204_NO_CONTENT) | |
23 | 45 | |
24 | -class UserSectionViewSet(ModelViewSet): | |
46 | + @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) | |
47 | + def drop(self, request, pk): | |
48 | + """ | |
49 | + Remove the current user from a specified section | |
50 | + If the user is not in the class, the request will fail. | |
51 | + --- | |
52 | + omit_serializer: true | |
53 | + parameters: | |
54 | + - fake: None | |
55 | + parameters_strategy: | |
56 | + form: replace | |
57 | + """ | |
58 | + section = self.get_object() | |
59 | + if not section.user_set.filter(pk=request.user.pk).exists(): | |
60 | + raise ValidationError("You are not in the section.") | |
61 | + section.user_set.remove(request.user) | |
62 | + return Response(status=HTTP_204_NO_CONTENT) | |
63 | + | |
64 | + | |
65 | +class UserSectionListView(ListAPIView): | |
25 | 66 | serializer_class = SectionSerializer |
26 | 67 | permission_classes = [IsAuthenticated] |
27 | 68 | |
... | ... | @@ -31,7 +72,13 @@ |
31 | 72 | def paginate_queryset(self, queryset): return None |
32 | 73 | |
33 | 74 | |
34 | -class UserDetail(APIView): | |
75 | +class UserDetail(GenericAPIView): | |
76 | + serializer_class = UserSerializer | |
77 | + permission_classes = [IsAuthenticated] | |
78 | + | |
79 | + def get_queryset(self): | |
80 | + return User.objects.all() | |
81 | + | |
35 | 82 | def patch(self, request, format=None): |
36 | 83 | """ |
37 | 84 | Updates the user's password, or verifies their email address |
... | ... | @@ -44,8 +91,6 @@ |
44 | 91 | data = data.validated_data |
45 | 92 | |
46 | 93 | if 'new_password' in data: |
47 | - if not request.user.is_authenticated(): | |
48 | - raise NotAuthenticated('You must be logged in to change your password') | |
49 | 94 | if not request.user.check_password(data['old_password']): |
50 | 95 | raise ValidationError('old_password is incorrect') |
51 | 96 | request.user.set_password(data['new_password']) |
52 | 97 | |
... | ... | @@ -65,38 +110,9 @@ |
65 | 110 | --- |
66 | 111 | response_serializer: UserSerializer |
67 | 112 | """ |
68 | - if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED) | |
69 | - serializer = UserSerializer(request.user) | |
113 | + serializer = UserSerializer(request.user, context={'request': request}) | |
70 | 114 | return Response(serializer.data) |
71 | 115 | |
72 | - def post(self, request, format=None): | |
73 | - """ | |
74 | - Register a new user | |
75 | - --- | |
76 | - request_serializer: EmailPasswordSerializer | |
77 | - response_serializer: UserSerializer | |
78 | - """ | |
79 | - data = RegistrationSerializer(data=request.data) | |
80 | - data.is_valid(raise_exception=True) | |
81 | - | |
82 | - User.objects.create_user(**data.validated_data) | |
83 | - user = authenticate(**data.validated_data) | |
84 | - login(request, user) | |
85 | - | |
86 | - body = ''' | |
87 | - Visit the following link to confirm your email address: | |
88 | - https://flashy.cards/app/verify_email/%s | |
89 | - | |
90 | - If you did not register for Flashy, no action is required. | |
91 | - ''' | |
92 | - | |
93 | - assert send_mail("Flashy email verification", | |
94 | - body % user.confirmation_key, | |
95 | - "noreply@flashy.cards", | |
96 | - [user.email]) | |
97 | - | |
98 | - return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) | |
99 | - | |
100 | 116 | def delete(self, request): |
101 | 117 | """ |
102 | 118 | Irrevocably delete the user and their data |
103 | 119 | |
104 | 120 | |
105 | 121 | |
106 | 122 | |
107 | 123 | |
108 | 124 | |
109 | 125 | |
110 | 126 | |
111 | 127 | |
112 | 128 | |
113 | 129 | |
114 | 130 | |
115 | 131 | |
116 | 132 | |
117 | 133 | |
118 | 134 | |
119 | 135 | |
120 | 136 | |
121 | 137 | |
122 | 138 | |
123 | 139 | |
124 | 140 | |
... | ... | @@ -106,95 +122,132 @@ |
106 | 122 | request.user.delete() |
107 | 123 | return Response(status=HTTP_204_NO_CONTENT) |
108 | 124 | |
125 | +@api_view(['POST']) | |
126 | +def register(request, format=None): | |
127 | + """ | |
128 | + Register a new user | |
129 | + --- | |
130 | + request_serializer: EmailPasswordSerializer | |
131 | + response_serializer: UserSerializer | |
132 | + """ | |
133 | + data = RegistrationSerializer(data=request.data) | |
134 | + data.is_valid(raise_exception=True) | |
109 | 135 | |
110 | -class UserLogin(APIView): | |
111 | - def post(self, request): | |
112 | - """ | |
113 | - Authenticates user and returns user data if valid. | |
114 | - --- | |
115 | - request_serializer: EmailPasswordSerializer | |
116 | - response_serializer: UserSerializer | |
117 | - """ | |
136 | + User.objects.create_user(**data.validated_data) | |
137 | + user = authenticate(**data.validated_data) | |
138 | + auth.login(request, user) | |
118 | 139 | |
119 | - data = EmailPasswordSerializer(data=request.data) | |
120 | - data.is_valid(raise_exception=True) | |
121 | - user = authenticate(**data.validated_data) | |
140 | + body = ''' | |
141 | + Visit the following link to confirm your email address: | |
142 | + https://flashy.cards/app/verify_email/%s | |
122 | 143 | |
123 | - if user is None: | |
124 | - raise AuthenticationFailed('Invalid email or password') | |
125 | - if not user.is_active: | |
126 | - raise NotAuthenticated('Account is disabled') | |
127 | - login(request, user) | |
128 | - return Response(UserSerializer(request.user).data) | |
144 | + If you did not register for Flashy, no action is required. | |
145 | + ''' | |
129 | 146 | |
147 | + assert send_mail("Flashy email verification", | |
148 | + body % user.confirmation_key, | |
149 | + "noreply@flashy.cards", | |
150 | + [user.email]) | |
130 | 151 | |
131 | -class UserLogout(APIView): | |
132 | - permission_classes = (IsAuthenticated,) | |
152 | + return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) | |
133 | 153 | |
134 | - def post(self, request, format=None): | |
135 | - """ | |
136 | - Logs the authenticated user out. | |
137 | - """ | |
138 | - logout(request) | |
139 | - return Response(status=HTTP_204_NO_CONTENT) | |
154 | +@api_view(['POST']) | |
155 | +def login(request): | |
156 | + """ | |
157 | + Authenticates user and returns user data if valid. | |
158 | + --- | |
159 | + request_serializer: EmailPasswordSerializer | |
160 | + response_serializer: UserSerializer | |
161 | + """ | |
140 | 162 | |
163 | + data = EmailPasswordSerializer(data=request.data) | |
164 | + data.is_valid(raise_exception=True) | |
165 | + user = authenticate(**data.validated_data) | |
141 | 166 | |
142 | -class PasswordReset(APIView): | |
167 | + if user is None: | |
168 | + raise AuthenticationFailed('Invalid email or password') | |
169 | + if not user.is_active: | |
170 | + raise NotAuthenticated('Account is disabled') | |
171 | + auth.login(request, user) | |
172 | + return Response(UserSerializer(request.user).data) | |
173 | + | |
174 | + | |
175 | +@api_view(['POST']) | |
176 | +@permission_classes((IsAuthenticated, )) | |
177 | +def logout(request, format=None): | |
143 | 178 | """ |
144 | - Allows user to reset their password. | |
179 | + Logs the authenticated user out. | |
145 | 180 | """ |
181 | + auth.logout(request) | |
182 | + return Response(status=HTTP_204_NO_CONTENT) | |
146 | 183 | |
147 | - def post(self, request, format=None): | |
148 | - """ | |
149 | - Send a password reset token/link to the provided email. | |
150 | - --- | |
151 | - request_serializer: PasswordResetRequestSerializer | |
152 | - """ | |
153 | - data = PasswordResetRequestSerializer(data=request.data) | |
154 | - data.is_valid(raise_exception=True) | |
155 | - user = User.objects.get(email=data['email'].value) | |
156 | - token = default_token_generator.make_token(user) | |
157 | 184 | |
158 | - body = ''' | |
159 | - Visit the following link to reset your password: | |
160 | - https://flashy.cards/app/reset_password/%d/%s | |
185 | +@api_view(['POST']) | |
186 | +def request_password_reset(request, format=None): | |
187 | + """ | |
188 | + Send a password reset token/link to the provided email. | |
189 | + --- | |
190 | + request_serializer: PasswordResetRequestSerializer | |
191 | + """ | |
192 | + data = PasswordResetRequestSerializer(data=request.data) | |
193 | + data.is_valid(raise_exception=True) | |
194 | + user = User.objects.get(email=data['email'].value) | |
195 | + token = default_token_generator.make_token(user) | |
161 | 196 | |
162 | - If you did not request a password reset, no action is required. | |
163 | - ''' | |
197 | + body = ''' | |
198 | + Visit the following link to reset your password: | |
199 | + https://flashy.cards/app/reset_password/%d/%s | |
164 | 200 | |
165 | - send_mail("Flashy password reset", | |
166 | - body % (user.pk, token), | |
167 | - "noreply@flashy.cards", | |
168 | - [user.email]) | |
201 | + If you did not request a password reset, no action is required. | |
202 | + ''' | |
169 | 203 | |
170 | - return Response(status=HTTP_204_NO_CONTENT) | |
204 | + send_mail("Flashy password reset", | |
205 | + body % (user.pk, token), | |
206 | + "noreply@flashy.cards", | |
207 | + [user.email]) | |
171 | 208 | |
172 | - def patch(self, request, format=None): | |
173 | - """ | |
174 | - Updates user's password to new password if token is valid. | |
175 | - --- | |
176 | - request_serializer: PasswordResetSerializer | |
177 | - """ | |
178 | - data = PasswordResetSerializer(data=request.data) | |
179 | - data.is_valid(raise_exception=True) | |
209 | + return Response(status=HTTP_204_NO_CONTENT) | |
180 | 210 | |
181 | - user = User.objects.get(id=data['uid'].value) | |
182 | - # Check token validity. | |
183 | 211 | |
184 | - if default_token_generator.check_token(user, data['token'].value): | |
185 | - user.set_password(data['new_password'].value) | |
186 | - user.save() | |
187 | - else: | |
188 | - raise ValidationError('Could not verify reset token') | |
189 | - return Response(status=HTTP_204_NO_CONTENT) | |
212 | +@api_view(['POST']) | |
213 | +def reset_password(request, format=None): | |
214 | + """ | |
215 | + Updates user's password to new password if token is valid. | |
216 | + --- | |
217 | + request_serializer: PasswordResetSerializer | |
218 | + """ | |
219 | + data = PasswordResetSerializer(data=request.data) | |
220 | + data.is_valid(raise_exception=True) | |
190 | 221 | |
222 | + user = User.objects.get(id=data['uid'].value) | |
223 | + # Check token validity. | |
191 | 224 | |
192 | -class FlashcardDetail(RetrieveAPIView): | |
225 | + if default_token_generator.check_token(user, data['token'].value): | |
226 | + user.set_password(data['new_password'].value) | |
227 | + user.save() | |
228 | + else: | |
229 | + raise ValidationError('Could not verify reset token') | |
230 | + return Response(status=HTTP_204_NO_CONTENT) | |
231 | + | |
232 | + | |
233 | +class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): | |
193 | 234 | queryset = Flashcard.objects.all() |
194 | 235 | serializer_class = FlashcardSerializer |
236 | + permission_classes = [IsAuthenticated] | |
195 | 237 | |
196 | -## def get(self, request): | |
197 | - | |
198 | - | |
199 | - | |
238 | + @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) | |
239 | + def report(self, request, pk): | |
240 | + """ | |
241 | + Report the given card | |
242 | + --- | |
243 | + omit_serializer: true | |
244 | + parameters: | |
245 | + - fake: None | |
246 | + parameters_strategy: | |
247 | + form: replace | |
248 | + """ | |
249 | + obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) | |
250 | + obj.reason = request.data['reason'] | |
251 | + obj.save() | |
252 | + return Response(status=HTTP_204_NO_CONTENT) |
flashy/settings.py
View file @
72bf5f0
... | ... | @@ -11,9 +11,10 @@ |
11 | 11 | |
12 | 12 | AUTH_USER_MODEL = 'flashcards.User' |
13 | 13 | REST_FRAMEWORK = { |
14 | - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' | |
14 | + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', | |
15 | + 'PAGE_SIZE': 20 | |
15 | 16 | } |
16 | -INSTALLED_APPS = ( | |
17 | +INSTALLED_APPS = [ | |
17 | 18 | 'simple_email_confirmation', |
18 | 19 | 'flashcards', |
19 | 20 | 'django.contrib.admin', |
20 | 21 | |
... | ... | @@ -23,15 +24,11 @@ |
23 | 24 | 'django.contrib.sessions', |
24 | 25 | 'django.contrib.messages', |
25 | 26 | 'django.contrib.staticfiles', |
26 | - 'django_ses', | |
27 | + | |
27 | 28 | 'rest_framework_swagger', |
28 | 29 | 'rest_framework', |
29 | -) | |
30 | - | |
31 | -REST_FRAMEWORK = { | |
32 | - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', | |
33 | - 'PAGE_SIZE': 20 | |
34 | -} | |
30 | +] | |
31 | +if IN_PRODUCTION: INSTALLED_APPS += 'django_ses' | |
35 | 32 | |
36 | 33 | MIDDLEWARE_CLASSES = ( |
37 | 34 | 'django.contrib.sessions.middleware.SessionMiddleware', |
flashy/urls.py
View file @
72bf5f0
1 | 1 | from django.conf.urls import include, url |
2 | 2 | from django.contrib import admin |
3 | -from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset, UserSectionViewSet, \ | |
4 | - FlashcardDetail | |
3 | +from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \ | |
4 | + reset_password, logout, login, register | |
5 | 5 | from flashy.frontend_serve import serve_with_default |
6 | -from flashy.settings import DEBUG | |
6 | +from flashy.settings import DEBUG, IN_PRODUCTION | |
7 | 7 | from rest_framework.routers import DefaultRouter |
8 | 8 | from flashcards.api import * |
9 | 9 | |
10 | 10 | router = DefaultRouter() |
11 | 11 | router.register(r'sections', SectionViewSet) |
12 | +router.register(r'flashcards', FlashcardViewSet) | |
12 | 13 | |
13 | -router.register(r'users/me/sections', UserSectionViewSet, base_name='usersection') | |
14 | - | |
15 | 14 | urlpatterns = [ |
16 | 15 | url(r'^api/docs/', include('rest_framework_swagger.urls')), |
17 | - url(r'^api/users/me$', UserDetail.as_view()), | |
18 | - url(r'^api/login$', UserLogin.as_view()), | |
19 | - url(r'^api/logout$', UserLogout.as_view()), | |
20 | - url(r'^api/reset_password$', PasswordReset.as_view()), | |
16 | + url(r'^api/me$', UserDetail.as_view()), | |
17 | + url(r'^api/register', register), | |
18 | + url(r'^api/login$', login), | |
19 | + url(r'^api/logout$', logout), | |
20 | + url(r'^api/me/sections', UserSectionListView.as_view()), | |
21 | + url(r'^api/request_password_reset', request_password_reset), | |
22 | + url(r'^api/reset_password', reset_password), | |
21 | 23 | url(r'^api/', include(router.urls)), |
22 | 24 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), |
23 | 25 | url(r'^admin/', include(admin.site.urls)), |
24 | 26 | url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), |
25 | - url(r'^api/flashcards/(?P<pk>[0-9]+)$', FlashcardDetail.as_view(), name="flashcard-detail"), | |
26 | 27 | ] |
27 | 28 | |
28 | -urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) | |
29 | +if IN_PRODUCTION: | |
30 | + urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) | |
29 | 31 | |
30 | -if DEBUG: urlpatterns += [url(r'^app/(?P<path>.*)$', serve_with_default, | |
31 | - {'document_root': '../flashy-frontend', 'default_file': 'home.html'})] | |
32 | +if DEBUG: | |
33 | + urlpatterns += [url(r'^app/(?P<path>.*)$', serve_with_default, | |
34 | + {'document_root': '../flashy-frontend', 'default_file': 'home.html'})] |
requirements.txt
View file @
72bf5f0