Commit 72bf5f00c5597d3b3c7a77e3d0d2331f5e354089

Authored by Andrew Buss
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

  1 +Flashy requires Python 2.
  2 +
  3 +Install virtualenv before continuing. This is most easily accomplished with:
  4 +
  5 + pip install virtualenv
  6 +
1 7 Set up the environment by running:
2 8  
3 9 scripts/setup.sh
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
... ... @@ -8,8 +8,6 @@
8 8 djangorestframework
9 9 docutils
10 10 django-simple-email-confirmation
11   -django-ses
12 11 coverage
13 12 django-rest-swagger
14   -newrelic
scripts/setup_production.sh View file @ 72bf5f0
... ... @@ -4,5 +4,7 @@
4 4 source secrets
5 5 pip install psycopg2
6 6 pip install gunicorn
  7 +pip install newrelic
  8 +pip install django-ses
7 9 python manage.py migrate