From 72bf5f00c5597d3b3c7a77e3d0d2331f5e354089 Mon Sep 17 00:00:00 2001 From: Andrew Buss Date: Sun, 10 May 2015 07:14:28 -0700 Subject: [PATCH] Falcon puuuuuuuush. Refactored api and url scheme. Flashcard push is incomplete --- README.md | 6 + flashcards/admin.py | 5 +- flashcards/fixtures/testusers.json | 21 +++ flashcards/models.py | 19 ++- flashcards/serializers.py | 26 +++- flashcards/tests/test_api.py | 82 ++++++----- flashcards/views.py | 271 ++++++++++++++++++++++--------------- flashy/settings.py | 15 +- flashy/urls.py | 29 ++-- requirements.txt | 2 - scripts/setup_production.sh | 2 + 11 files changed, 290 insertions(+), 188 deletions(-) create mode 100644 flashcards/fixtures/testusers.json diff --git a/README.md b/README.md index 4087d22..d831cb4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ +Flashy requires Python 2. + +Install virtualenv before continuing. This is most easily accomplished with: + + pip install virtualenv + Set up the environment by running: scripts/setup.sh diff --git a/flashcards/admin.py b/flashcards/admin.py index fca385b..a410b5b 100644 --- a/flashcards/admin.py +++ b/flashcards/admin.py @@ -1,13 +1,14 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin from flashcards.models import Flashcard, UserFlashcard, Section, FlashcardMask, \ UserFlashcardReview, LecturePeriod, User admin.site.register([ - User, Flashcard, FlashcardMask, UserFlashcard, UserFlashcardReview, Section, LecturePeriod -]) \ No newline at end of file +]) +admin.site.register(User, UserAdmin) \ No newline at end of file diff --git a/flashcards/fixtures/testusers.json b/flashcards/fixtures/testusers.json new file mode 100644 index 0000000..92563f2 --- /dev/null +++ b/flashcards/fixtures/testusers.json @@ -0,0 +1,21 @@ +[ + { + "fields": { + "username": "none@none.com", + "first_name": "", + "last_name": "", + "is_active": true, + "is_superuser": false, + "is_staff": false, + "last_login": null, + "groups": [], + "user_permissions": [], + "password": "pbkdf2_sha256$20000$9NRycTz13dZ8$jV/YWGHYcytwdWAvNfdHMHF9nezQnR7AlXoK1v6KxHo=", + "sections": [], + "email": "none@none.com", + "date_joined": "2015-05-10T13:37:31Z" + }, + "model": "flashcards.user", + "pk": 1 + } +] \ No newline at end of file diff --git a/flashcards/models.py b/flashcards/models.py index c448caa..93be3a7 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -151,19 +151,28 @@ class UserFlashcardReview(Model): class Section(Model): """ A UCSD course taught by an instructor during a quarter. - Different sections taught by the same instructor in the same quarter are considered identical. We use the term "section" to avoid collision with the builtin keyword "class" """ department = CharField(max_length=50) course_num = CharField(max_length=6) - # section_id = CharField(max_length=10) course_title = CharField(max_length=50) instructor = CharField(max_length=100) quarter = CharField(max_length=4) - whitelist = ManyToManyField(User, related_name="whitelisted_sections") + + def is_whitelisted(self): + """ + :return: whether a whitelist exists for this section + """ + return self.whitelist.exists() + + def is_user_on_whitelist(self, user): + """ + :return: whether the user is on the waitlist for this section + """ + assert user is User + return self.whitelist.filter(email=user.email).exists() class Meta: - unique_together = (('department', 'course_num', 'quarter', 'instructor'),) ordering = ['-course_title'] class LecturePeriod(Model): @@ -184,7 +193,7 @@ class WhitelistedAddress(Model): An email address that has been whitelisted for a section at an instructor's request """ email = EmailField() - section = ForeignKey(Section) + section = ForeignKey(Section, related_name='whitelist') class FlashcardReport(Model): diff --git a/flashcards/serializers.py b/flashcards/serializers.py index b843583..9c100b4 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -1,8 +1,9 @@ +from django.utils.datetime_safe import datetime from flashcards.models import Section, LecturePeriod, User, Flashcard from rest_framework import serializers from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField from rest_framework.relations import HyperlinkedRelatedField -from rest_framework.serializers import HyperlinkedModelSerializer, ModelSerializer, Serializer +from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.validators import UniqueValidator @@ -39,6 +40,7 @@ class PasswordResetSerializer(Serializer): except User.DoesNotExist: raise serializers.ValidationError('Could not verify reset token') + class UserUpdateSerializer(Serializer): old_password = CharField(required=False) new_password = CharField(required=False, allow_blank=False) @@ -62,15 +64,14 @@ class LecturePeriodSerializer(ModelSerializer): exclude = 'id', 'section' -class SectionSerializer(HyperlinkedModelSerializer): +class SectionSerializer(ModelSerializer): lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) class Meta: model = Section - exclude = 'whitelist', -class UserSerializer(HyperlinkedModelSerializer): +class UserSerializer(ModelSerializer): email = EmailField(required=False) sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') is_confirmed = BooleanField() @@ -79,7 +80,22 @@ class UserSerializer(HyperlinkedModelSerializer): model = User fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") -class FlashcardSerializer(HyperlinkedModelSerializer): + +class FlashcardSerializer(ModelSerializer): + is_hidden = BooleanField(read_only=True) + hide_reason = CharField(read_only=True) + + def validate_material_date(self, value): + # TODO: make this dynamic + quarter_start = datetime(2015, 3, 15) + quarter_end = datetime(2015, 6, 15) + if quarter_start <= value <= quarter_end: return value + else: + raise serializers.ValidationError("Material date is outside allowed range for this quarter") + + # + # def validate(self, data): + # if class Meta: model = Flashcard diff --git a/flashcards/tests/test_api.py b/flashcards/tests/test_api.py index 4e07a36..0bd9f21 100644 --- a/flashcards/tests/test_api.py +++ b/flashcards/tests/test_api.py @@ -1,67 +1,62 @@ from django.core import mail from flashcards.models import User, Section, Flashcard -from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED, \ + HTTP_403_FORBIDDEN from rest_framework.test import APITestCase from re import search from django.utils.timezone import now class LoginTests(APITestCase): - def setUp(self): - email = "test@flashy.cards" - User.objects.create_user(email=email, password="1234") + fixtures = ['testusers'] def test_login(self): url = '/api/login' - data = {'email': 'test@flashy.cards', 'password': '1234'} + data = {'email': 'none@none.com', 'password': '1234'} response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, HTTP_200_OK) - data = {'email': 'test@flashy.cards', 'password': '54321'} + data = {'email': 'none@none.com', 'password': '4321'} response = self.client.post(url, data, format='json') self.assertContains(response, 'Invalid email or password', status_code=403) - data = {'email': 'none@flashy.cards', 'password': '54321'} + data = {'email': 'bad@none.com', 'password': '1234'} response = self.client.post(url, data, format='json') self.assertContains(response, 'Invalid email or password', status_code=403) - data = {'password': '54321'} + data = {'password': '4321'} response = self.client.post(url, data, format='json') self.assertContains(response, 'email', status_code=400) - data = {'email': 'none@flashy.cards'} + data = {'email': 'none@none.com'} response = self.client.post(url, data, format='json') self.assertContains(response, 'password', status_code=400) - user = User.objects.get(email="test@flashy.cards") + user = User.objects.get(email="none@none.com") user.is_active = False user.save() - data = {'email': 'test@flashy.cards', 'password': '1234'} + data = {'email': 'none@none.com', 'password': '1234'} response = self.client.post(url, data, format='json') self.assertContains(response, 'Account is disabled', status_code=403) def test_logout(self): - url = '/api/logout' - self.client.login(email='test@flashy.cards', password='1234') - response = self.client.post(url) + self.client.login(email='none@none.com', password='1234') + response = self.client.post('/api/logout') self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) - # since we're not logged in, we should get a 401 response - response = self.client.get('/api/users/me', format='json') - self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) + # since we're not logged in, we should get a 403 response + response = self.client.get('/api/me', format='json') + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) class PasswordResetTest(APITestCase): - def setUp(self): - # create a user to test things with - email = "test@flashy.cards" - User.objects.create_user(email=email, password="12345") + fixtures = ['testusers'] def test_reset_password(self): # submit the request to reset the password - url = '/api/reset_password' - post_data = {'email': 'test@flashy.cards'} + url = '/api/request_password_reset' + post_data = {'email': 'none@none.com'} self.client.post(url, post_data, format='json') self.assertEqual(len(mail.outbox), 1) self.assertIn('reset your password', mail.outbox[0].body) @@ -69,18 +64,19 @@ class PasswordResetTest(APITestCase): # capture the reset token from the email capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', mail.outbox[0].body) - patch_data = {'new_password': '54321'} + patch_data = {'new_password': '4321'} patch_data['uid'] = capture.group(1) reset_token = capture.group(2) # try to reset the password with the wrong reset token patch_data['token'] = 'wrong_token' - response = self.client.patch(url, patch_data, format='json') + url = '/api/reset_password' + response = self.client.post(url, patch_data, format='json') self.assertContains(response, 'Could not verify reset token', status_code=400) # try to reset the password with the correct token patch_data['token'] = reset_token - response = self.client.patch(url, patch_data, format='json') + response = self.client.post(url, patch_data, format='json') self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) user = User.objects.get(id=patch_data['uid']) assert user.check_password(patch_data['new_password']) @@ -88,7 +84,7 @@ class PasswordResetTest(APITestCase): class RegistrationTest(APITestCase): def test_create_account(self): - url = '/api/users/me' + url = '/api/register' # missing password data = {'email': 'none@none.com'} @@ -120,6 +116,8 @@ class RegistrationTest(APITestCase): self.client.login(email='none@none.com', password='1234') # try activating with an invalid key + + url = '/api/me' response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) self.assertContains(response, 'confirmation_key is invalid', status_code=400) @@ -129,33 +127,29 @@ class RegistrationTest(APITestCase): class ProfileViewTest(APITestCase): - def setUp(self): - email = "profileviewtest@flashy.cards" - User.objects.create_user(email=email, password="1234") + fixtures = ['testusers'] def test_get_me(self): - url = '/api/users/me' + url = '/api/me' response = self.client.get(url, format='json') # since we're not logged in, we shouldn't be able to see this - self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) + self.assertEqual(response.status_code, 403) - self.client.login(email='profileviewtest@flashy.cards', password='1234') + self.client.login(email='none@none.com', password='1234') response = self.client.get(url, format='json') self.assertEqual(response.status_code, HTTP_200_OK) class PasswordChangeTest(APITestCase): - def setUp(self): - email = "none@none.com" - User.objects.create_user(email=email, password="1234") + fixtures = ['testusers'] def test_change_password(self): - url = '/api/users/me' + url = '/api/me' user = User.objects.get(email='none@none.com') self.assertTrue(user.check_password('1234')) response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') - self.assertContains(response, 'You must be logged in to change your password', status_code=403) + self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) self.client.login(email='none@none.com', password='1234') response = self.client.patch(url, {'new_password': '4321'}, format='json') @@ -173,12 +167,10 @@ class PasswordChangeTest(APITestCase): class DeleteUserTest(APITestCase): - def setUp(self): - email = "none@none.com" - User.objects.create_user(email=email, password="1234") + fixtures = ['testusers'] def test_delete_user(self): - url = '/api/users/me' + url = '/api/me' user = User.objects.get(email='none@none.com') self.client.login(email='none@none.com', password='1234') @@ -187,6 +179,8 @@ class DeleteUserTest(APITestCase): class FlashcardDetailTest(APITestCase): + fixtures = ['testusers'] + def setUp(self): section = Section(department="cse", course_num="5", course_title="cool course", instructor="gary", quarter="fa15") @@ -198,8 +192,10 @@ class FlashcardDetailTest(APITestCase): self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) self.flashcard.save() + def test_get_flashcard(self): - response = self.client.get("/api/flashcards/%d" % self.flashcard.id, format="json") + assert self.client.login(email='none@none.com', password='1234') + response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(response.data["text"], "jason") diff --git a/flashcards/views.py b/flashcards/views.py index 16b5129..d163689 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -1,17 +1,19 @@ +from django.contrib import auth from flashcards.api import StandardResultsSetPagination -from flashcards.models import Section, User, Flashcard +from flashcards.models import Section, User, Flashcard, FlashcardReport from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer +from rest_framework.decorators import detail_route, permission_classes, api_view +from rest_framework.generics import ListAPIView, DestroyAPIView, GenericAPIView +from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet +from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet from django.core.mail import send_mail -from django.contrib.auth import authenticate, login, logout +from django.contrib.auth import authenticate from django.contrib.auth.tokens import default_token_generator -from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED -from rest_framework.views import APIView +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED from rest_framework.response import Response -from rest_framework.generics import RetrieveAPIView -from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError +from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied from simple_email_confirmation import EmailAddress @@ -19,9 +21,48 @@ class SectionViewSet(ReadOnlyModelViewSet): queryset = Section.objects.all() serializer_class = SectionSerializer pagination_class = StandardResultsSetPagination + permission_classes = [IsAuthenticated] + + @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) + def enroll(self, request, pk): + """ + Add the current user to a specified section + If the class has a whitelist, but the user is not on the whitelist, the request will fail. + --- + omit_serializer: true + parameters: + - fake: None + parameters_strategy: + form: replace + """ + section = self.get_object() + if request.user.sections.filter(pk=section.pk).exists(): + return ValidationError("You are already in this section.") + if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): + return PermissionDenied("You must be on the whitelist to add this section.") + request.user.sections.add(section) + return Response(status=HTTP_204_NO_CONTENT) + + @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) + def drop(self, request, pk): + """ + Remove the current user from a specified section + If the user is not in the class, the request will fail. + --- + omit_serializer: true + parameters: + - fake: None + parameters_strategy: + form: replace + """ + section = self.get_object() + if not section.user_set.filter(pk=request.user.pk).exists(): + raise ValidationError("You are not in the section.") + section.user_set.remove(request.user) + return Response(status=HTTP_204_NO_CONTENT) -class UserSectionViewSet(ModelViewSet): +class UserSectionListView(ListAPIView): serializer_class = SectionSerializer permission_classes = [IsAuthenticated] @@ -31,7 +72,13 @@ class UserSectionViewSet(ModelViewSet): def paginate_queryset(self, queryset): return None -class UserDetail(APIView): +class UserDetail(GenericAPIView): + serializer_class = UserSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return User.objects.all() + def patch(self, request, format=None): """ Updates the user's password, or verifies their email address @@ -44,8 +91,6 @@ class UserDetail(APIView): data = data.validated_data if 'new_password' in data: - if not request.user.is_authenticated(): - raise NotAuthenticated('You must be logged in to change your password') if not request.user.check_password(data['old_password']): raise ValidationError('old_password is incorrect') request.user.set_password(data['new_password']) @@ -65,38 +110,9 @@ class UserDetail(APIView): --- response_serializer: UserSerializer """ - if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED) - serializer = UserSerializer(request.user) + serializer = UserSerializer(request.user, context={'request': request}) return Response(serializer.data) - def post(self, request, format=None): - """ - Register a new user - --- - request_serializer: EmailPasswordSerializer - response_serializer: UserSerializer - """ - data = RegistrationSerializer(data=request.data) - data.is_valid(raise_exception=True) - - User.objects.create_user(**data.validated_data) - user = authenticate(**data.validated_data) - login(request, user) - - body = ''' - Visit the following link to confirm your email address: - https://flashy.cards/app/verify_email/%s - - If you did not register for Flashy, no action is required. - ''' - - assert send_mail("Flashy email verification", - body % user.confirmation_key, - "noreply@flashy.cards", - [user.email]) - - return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) - def delete(self, request): """ Irrevocably delete the user and their data @@ -106,94 +122,131 @@ class UserDetail(APIView): request.user.delete() return Response(status=HTTP_204_NO_CONTENT) +@api_view(['POST']) +def register(request, format=None): + """ + Register a new user + --- + request_serializer: EmailPasswordSerializer + response_serializer: UserSerializer + """ + data = RegistrationSerializer(data=request.data) + data.is_valid(raise_exception=True) -class UserLogin(APIView): - def post(self, request): - """ - Authenticates user and returns user data if valid. - --- - request_serializer: EmailPasswordSerializer - response_serializer: UserSerializer - """ + User.objects.create_user(**data.validated_data) + user = authenticate(**data.validated_data) + auth.login(request, user) - data = EmailPasswordSerializer(data=request.data) - data.is_valid(raise_exception=True) - user = authenticate(**data.validated_data) + body = ''' + Visit the following link to confirm your email address: + https://flashy.cards/app/verify_email/%s - if user is None: - raise AuthenticationFailed('Invalid email or password') - if not user.is_active: - raise NotAuthenticated('Account is disabled') - login(request, user) - return Response(UserSerializer(request.user).data) + If you did not register for Flashy, no action is required. + ''' + assert send_mail("Flashy email verification", + body % user.confirmation_key, + "noreply@flashy.cards", + [user.email]) -class UserLogout(APIView): - permission_classes = (IsAuthenticated,) + return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) - def post(self, request, format=None): - """ - Logs the authenticated user out. - """ - logout(request) - return Response(status=HTTP_204_NO_CONTENT) +@api_view(['POST']) +def login(request): + """ + Authenticates user and returns user data if valid. + --- + request_serializer: EmailPasswordSerializer + response_serializer: UserSerializer + """ + + data = EmailPasswordSerializer(data=request.data) + data.is_valid(raise_exception=True) + user = authenticate(**data.validated_data) + + if user is None: + raise AuthenticationFailed('Invalid email or password') + if not user.is_active: + raise NotAuthenticated('Account is disabled') + auth.login(request, user) + return Response(UserSerializer(request.user).data) -class PasswordReset(APIView): +@api_view(['POST']) +@permission_classes((IsAuthenticated, )) +def logout(request, format=None): """ - Allows user to reset their password. + Logs the authenticated user out. """ + auth.logout(request) + return Response(status=HTTP_204_NO_CONTENT) - def post(self, request, format=None): - """ - Send a password reset token/link to the provided email. - --- - request_serializer: PasswordResetRequestSerializer - """ - data = PasswordResetRequestSerializer(data=request.data) - data.is_valid(raise_exception=True) - user = User.objects.get(email=data['email'].value) - token = default_token_generator.make_token(user) - body = ''' - Visit the following link to reset your password: - https://flashy.cards/app/reset_password/%d/%s +@api_view(['POST']) +def request_password_reset(request, format=None): + """ + Send a password reset token/link to the provided email. + --- + request_serializer: PasswordResetRequestSerializer + """ + data = PasswordResetRequestSerializer(data=request.data) + data.is_valid(raise_exception=True) + user = User.objects.get(email=data['email'].value) + token = default_token_generator.make_token(user) - If you did not request a password reset, no action is required. - ''' + body = ''' + Visit the following link to reset your password: + https://flashy.cards/app/reset_password/%d/%s - send_mail("Flashy password reset", - body % (user.pk, token), - "noreply@flashy.cards", - [user.email]) + If you did not request a password reset, no action is required. + ''' - return Response(status=HTTP_204_NO_CONTENT) + send_mail("Flashy password reset", + body % (user.pk, token), + "noreply@flashy.cards", + [user.email]) - def patch(self, request, format=None): - """ - Updates user's password to new password if token is valid. - --- - request_serializer: PasswordResetSerializer - """ - data = PasswordResetSerializer(data=request.data) - data.is_valid(raise_exception=True) + return Response(status=HTTP_204_NO_CONTENT) - user = User.objects.get(id=data['uid'].value) - # Check token validity. - if default_token_generator.check_token(user, data['token'].value): - user.set_password(data['new_password'].value) - user.save() - else: - raise ValidationError('Could not verify reset token') - return Response(status=HTTP_204_NO_CONTENT) +@api_view(['POST']) +def reset_password(request, format=None): + """ + Updates user's password to new password if token is valid. + --- + request_serializer: PasswordResetSerializer + """ + data = PasswordResetSerializer(data=request.data) + data.is_valid(raise_exception=True) + user = User.objects.get(id=data['uid'].value) + # Check token validity. -class FlashcardDetail(RetrieveAPIView): - queryset = Flashcard.objects.all() - serializer_class = FlashcardSerializer + if default_token_generator.check_token(user, data['token'].value): + user.set_password(data['new_password'].value) + user.save() + else: + raise ValidationError('Could not verify reset token') + return Response(status=HTTP_204_NO_CONTENT) -## def get(self, request): +class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): + queryset = Flashcard.objects.all() + serializer_class = FlashcardSerializer + permission_classes = [IsAuthenticated] - + @detail_route(methods=['post'], permission_classes=[IsAuthenticated]) + def report(self, request, pk): + """ + Report the given card + --- + omit_serializer: true + parameters: + - fake: None + parameters_strategy: + form: replace + """ + obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) + obj.reason = request.data['reason'] + obj.save() + return Response(status=HTTP_204_NO_CONTENT) diff --git a/flashy/settings.py b/flashy/settings.py index 4b3e681..ba446db 100644 --- a/flashy/settings.py +++ b/flashy/settings.py @@ -11,9 +11,10 @@ ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards'] AUTH_USER_MODEL = 'flashcards.User' REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 20 } -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'simple_email_confirmation', 'flashcards', 'django.contrib.admin', @@ -23,15 +24,11 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'django_ses', + 'rest_framework_swagger', 'rest_framework', -) - -REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', - 'PAGE_SIZE': 20 -} +] +if IN_PRODUCTION: INSTALLED_APPS += 'django_ses' MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', diff --git a/flashy/urls.py b/flashy/urls.py index 592f38d..e27fced 100644 --- a/flashy/urls.py +++ b/flashy/urls.py @@ -1,32 +1,35 @@ from django.conf.urls import include, url from django.contrib import admin -from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset, UserSectionViewSet, \ - FlashcardDetail +from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \ + reset_password, logout, login, register from flashy.frontend_serve import serve_with_default -from flashy.settings import DEBUG +from flashy.settings import DEBUG, IN_PRODUCTION from rest_framework.routers import DefaultRouter from flashcards.api import * router = DefaultRouter() router.register(r'sections', SectionViewSet) - -router.register(r'users/me/sections', UserSectionViewSet, base_name='usersection') +router.register(r'flashcards', FlashcardViewSet) urlpatterns = [ url(r'^api/docs/', include('rest_framework_swagger.urls')), - url(r'^api/users/me$', UserDetail.as_view()), - url(r'^api/login$', UserLogin.as_view()), - url(r'^api/logout$', UserLogout.as_view()), - url(r'^api/reset_password$', PasswordReset.as_view()), + url(r'^api/me$', UserDetail.as_view()), + url(r'^api/register', register), + url(r'^api/login$', login), + url(r'^api/logout$', logout), + url(r'^api/me/sections', UserSectionListView.as_view()), + url(r'^api/request_password_reset', request_password_reset), + url(r'^api/reset_password', reset_password), url(r'^api/', include(router.urls)), url(r'^admin/doc/', include('django.contrib.admindocs.urls')), url(r'^admin/', include(admin.site.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - url(r'^api/flashcards/(?P[0-9]+)$', FlashcardDetail.as_view(), name="flashcard-detail"), ] -urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) +if IN_PRODUCTION: + urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) -if DEBUG: urlpatterns += [url(r'^app/(?P.*)$', serve_with_default, - {'document_root': '../flashy-frontend', 'default_file': 'home.html'})] +if DEBUG: + urlpatterns += [url(r'^app/(?P.*)$', serve_with_default, + {'document_root': '../flashy-frontend', 'default_file': 'home.html'})] diff --git a/requirements.txt b/requirements.txt index ad35f7b..a8f70dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,5 @@ six==1.9.0 djangorestframework docutils django-simple-email-confirmation -django-ses coverage django-rest-swagger -newrelic diff --git a/scripts/setup_production.sh b/scripts/setup_production.sh index faefe7e..a3952f2 100755 --- a/scripts/setup_production.sh +++ b/scripts/setup_production.sh @@ -4,6 +4,8 @@ source venv/bin/activate source secrets pip install psycopg2 pip install gunicorn +pip install newrelic +pip install django-ses python manage.py migrate -- 1.9.1