From 24f677a3979ba3f1bdfe420c03ad5e3f8b0398c2 Mon Sep 17 00:00:00 2001 From: Radek Czajka Date: Sat, 2 Feb 2019 01:46:31 +0100 Subject: [PATCH 1/1] Using Piston auth in DRF views. Replacing API views. --- src/api/drf_auth.py | 20 ++++++++++++ src/api/emitters.py | 16 ---------- src/api/fields.py | 9 ++++++ src/api/handlers.py | 48 ---------------------------- src/api/models.py | 13 +++++--- src/api/serializers.py | 18 +++++++++++ src/api/tests/tests.py | 5 +-- src/api/urls.py | 12 +++---- src/api/views.py | 41 ++++++++++++++++++++++++ src/catalogue/api/urls.py | 2 ++ src/catalogue/api/views.py | 11 +++++++ src/paypal/permissions.py | 7 ++++ src/wolnelektury/settings/contrib.py | 1 + 13 files changed, 124 insertions(+), 79 deletions(-) create mode 100644 src/api/drf_auth.py delete mode 100644 src/api/emitters.py create mode 100644 src/api/serializers.py create mode 100644 src/api/views.py create mode 100644 src/paypal/permissions.py diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py new file mode 100644 index 000000000..26018c61e --- /dev/null +++ b/src/api/drf_auth.py @@ -0,0 +1,20 @@ +""" +Transitional code: bridge between Piston's OAuth implementation +and DRF views. +""" +from piston.authentication import OAuthAuthentication +from rest_framework.authentication import BaseAuthentication + + +class PistonOAuthAuthentication(BaseAuthentication): + def __init__(self): + self.piston_auth = OAuthAuthentication() + + def authenticate_header(self, request): + return 'OAuth realm="API"' + + def authenticate(self, request): + if self.piston_auth.is_valid_request(request): + consumer, token, parameters = self.piston_auth.validate_token(request) + if consumer and token: + return token.user, token diff --git a/src/api/emitters.py b/src/api/emitters.py deleted file mode 100644 index 50a2d4101..000000000 --- a/src/api/emitters.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later. -# Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information. -# -""" -Wrappers for piston Emitter classes. -""" -from piston.emitters import Emitter - - -# hack -class EpubEmitter(Emitter): - def render(self, request): - return self.data - -Emitter.register('epub', EpubEmitter, 'application/epub+zip') diff --git a/src/api/fields.py b/src/api/fields.py index 4bb44dcb9..1ce83cc3e 100644 --- a/src/api/fields.py +++ b/src/api/fields.py @@ -1,5 +1,6 @@ from rest_framework import serializers from django.core.urlresolvers import reverse +from paypal.rest import user_is_subscribed class AbsoluteURLField(serializers.ReadOnlyField): @@ -32,3 +33,11 @@ class LegacyMixin(object): if field in value and value[field] is None: value[field] = '' return value + + +class UserPremiumField(serializers.ReadOnlyField): + def __init__(self, *args, **kwargs): + super(UserPremiumField, self).__init__(*args, source='*', **kwargs) + + def to_representation(self, value): + return user_is_subscribed(value) diff --git a/src/api/handlers.py b/src/api/handlers.py index f3cc4a7ce..7872a1034 100644 --- a/src/api/handlers.py +++ b/src/api/handlers.py @@ -27,7 +27,6 @@ from social.utils import likes from stats.utils import piwik_track from wolnelektury.utils import re_escape -from . import emitters # Register our emitters API_BASE = WL_BASE = MEDIA_BASE = lazy( lambda: u'https://' + Site.objects.get_current().domain, unicode)() @@ -320,18 +319,6 @@ class BooksHandler(BookDetailHandler): return rc.NOT_FOUND -class EpubHandler(BookDetailHandler): - def read(self, request, slug): - if not user_is_subscribed(request.user): - return rc.FORBIDDEN - try: - book = Book.objects.get(slug=slug) - except Book.DoesNotExist: - return rc.NOT_FOUND - response = HttpResponse(book.get_media('epub')) - return response - - class EBooksHandler(AnonymousBooksHandler): fields = ('author', 'href', 'title', 'cover') + tuple(Book.ebook_formats) + ('slug',) @@ -670,41 +657,6 @@ class PictureHandler(BaseHandler): return rc.NOT_FOUND -class UserDataHandler(BaseHandler): - model = BookUserData - fields = ('state', 'username', 'premium') - allowed_methods = ('GET', 'POST') - - def read(self, request, slug=None): - if not request.user.is_authenticated(): - return rc.FORBIDDEN - if slug is None: - return {'username': request.user.username, 'premium': user_is_subscribed(request.user)} - try: - book = Book.objects.get(slug=slug) - except Book.DoesNotExist: - return rc.NOT_FOUND - try: - data = BookUserData.objects.get(book=book, user=request.user) - except BookUserData.DoesNotExist: - return {'state': 'not_started'} - return data - - def create(self, request, slug, state): - try: - book = Book.objects.get(slug=slug) - except Book.DoesNotExist: - return rc.NOT_FOUND - if not request.user.is_authenticated(): - return rc.FORBIDDEN - if state not in ('reading', 'complete'): - return rc.NOT_FOUND - data, created = BookUserData.objects.get_or_create(book=book, user=request.user) - data.state = state - data.save() - return data - - class UserShelfHandler(BookDetailHandler): fields = book_list_fields + ['liked'] diff --git a/src/api/models.py b/src/api/models.py index 2481010be..d2716b922 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -47,10 +47,13 @@ class BookUserData(models.Model): complete = models.BooleanField(default=False) last_changed = models.DateTimeField(auto_now=True) - def get_state(self): + @property + def state(self): return 'complete' if self.complete else 'reading' - def set_state(self, state): - self.complete = state == 'complete' - - state = property(get_state, set_state) + @classmethod + def update(cls, book, user, state): + instance, created = cls.objects.get_or_create(book=book, user=user) + instance.complete = state == 'complete' + instance.save() + return instance diff --git a/src/api/serializers.py b/src/api/serializers.py new file mode 100644 index 000000000..a876387e9 --- /dev/null +++ b/src/api/serializers.py @@ -0,0 +1,18 @@ +from django.contrib.auth.models import User +from rest_framework import serializers +from .fields import UserPremiumField +from .models import BookUserData + + +class UserSerializer(serializers.ModelSerializer): + premium = UserPremiumField() + + class Meta: + model = User + fields = ['username', 'premium'] + + +class BookUserDataSerializer(serializers.ModelSerializer): + class Meta: + model = BookUserData + fields = ['state'] diff --git a/src/api/tests/tests.py b/src/api/tests/tests.py index 91e5bbf84..6be34ed6b 100644 --- a/src/api/tests/tests.py +++ b/src/api/tests/tests.py @@ -423,12 +423,13 @@ class AuthorizedTests(ApiTest): {"username": "test", "premium": False}) self.assertEqual( self.signed('/api/epub/grandchild/').status_code, - 401) # Not 403 because Piston. + 403) - with patch('api.handlers.user_is_subscribed', return_value=True): + with patch('api.fields.user_is_subscribed', return_value=True): self.assertEqual( self.signed_json('/api/username/'), {"username": "test", "premium": True}) + with patch('paypal.permissions.user_is_subscribed', return_value=True): with patch('django.core.files.storage.Storage.open', return_value=StringIO("")): self.assertEqual( diff --git a/src/api/urls.py b/src/api/urls.py index 3c82e1e5f..7e393a26f 100644 --- a/src/api/urls.py +++ b/src/api/urls.py @@ -11,6 +11,7 @@ import catalogue.views from api import handlers from api.helpers import CsrfExemptResource from api.piston_patch import oauth_user_auth +from . import views auth = OAuthAuthentication(realm="Wolne Lektury") @@ -43,11 +44,9 @@ book_list_resource = auth_resource(handler=handlers.BooksHandler) ebook_list_resource = Resource(handler=handlers.EBooksHandler) # book_list_resource = Resource(handler=handlers.BooksHandler) filter_book_resource = auth_resource(handler=handlers.FilterBooksHandler) -epub_resource = auth_resource(handler=handlers.EpubHandler) preview_resource = Resource(handler=handlers.BookPreviewHandler) -reading_resource = auth_resource(handler=handlers.UserDataHandler) shelf_resource = auth_resource(handler=handlers.UserShelfHandler) like_resource = auth_resource(handler=handlers.UserLikeHandler) @@ -81,14 +80,11 @@ urlpatterns = [ url(r'book/(?P\d*?)/info\.html$', catalogue.views.book_info), url(r'tag/(?P\d*?)/info\.html$', catalogue.views.tag_info), - # epub preview - url(r'^epub/(?P[a-z0-9-]+)/$', epub_resource, name='api_epub'), - # reading data - url(r'^reading/(?P[a-z0-9-]+)/$', reading_resource, name='api_reading'), - url(r'^reading/(?P[a-z0-9-]+)/(?P[a-z]+)/$', reading_resource, name='api_reading'), + url(r'^reading/(?P[a-z0-9-]+)/$', views.BookUserDataView.as_view(), name='api_reading'), + url(r'^reading/(?P[a-z0-9-]+)/(?P[a-z]+)/$', views.BookUserDataView.as_view(), name='api_reading'), url(r'^shelf/(?P[a-z]+)/$', shelf_resource, name='api_shelf'), - url(r'^username/$', reading_resource, name='api_username'), + url(r'^username/$', views.UserView.as_view(), name='api_username'), url(r'^like/(?P[a-z0-9-]+)/$', like_resource, name='api_like'), diff --git a/src/api/views.py b/src/api/views.py new file mode 100644 index 000000000..812be832c --- /dev/null +++ b/src/api/views.py @@ -0,0 +1,41 @@ +from django.http import Http404 +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.generics import RetrieveAPIView, get_object_or_404 +from catalogue.models import Book +from .models import BookUserData +from . import serializers + + +class UserView(RetrieveAPIView): + permission_classes = [IsAuthenticated] + serializer_class = serializers.UserSerializer + + def get_object(self): + return self.request.user + + +class BookUserDataView(RetrieveAPIView): + permission_classes = [IsAuthenticated] + serializer_class = serializers.BookUserDataSerializer + lookup_field = 'book__slug' + lookup_url_kwarg = 'slug' + + def get_queryset(self): + return BookUserData.objects.filter(user=self.request.user) + + def get(self, *args, **kwargs): + try: + return super(BookUserDataView, self).get(*args, **kwargs) + except Http404: + return Response({"state": "not_started"}) + + def post(self, request, slug, state): + if state not in ('reading', 'complete'): + raise Http404 + + book = get_object_or_404(Book, slug=slug) + instance = BookUserData.update(book, request.user, state) + serializer = self.get_serializer(instance) + return Response(serializer.data) diff --git a/src/catalogue/api/urls.py b/src/catalogue/api/urls.py index e476c8fbf..30f329a10 100644 --- a/src/catalogue/api/urls.py +++ b/src/catalogue/api/urls.py @@ -13,4 +13,6 @@ urlpatterns = [ views.CollectionDetail.as_view(), name="collection-detail"), url(r'^books/(?P[^/]+)/$', views.BookDetail.as_view(), name='catalogue_api_book'), + + url(r'^epub/(?P[a-z0-9-]+)/$', views.EpubView.as_view(), name='catalogue_api_epub'), ] diff --git a/src/catalogue/api/views.py b/src/catalogue/api/views.py index 143990723..c20cccb77 100644 --- a/src/catalogue/api/views.py +++ b/src/catalogue/api/views.py @@ -1,4 +1,6 @@ +from django.http import HttpResponse from rest_framework.generics import ListAPIView, RetrieveAPIView +from paypal.permissions import IsSubscribed from . import serializers from catalogue.models import Book, Collection @@ -18,3 +20,12 @@ class BookDetail(RetrieveAPIView): queryset = Book.objects.all() lookup_field = 'slug' serializer_class = serializers.BookDetailSerializer + + +class EpubView(RetrieveAPIView): + queryset = Book.objects.all() + lookup_field = 'slug' + permission_classes = [IsSubscribed] + + def get(self, *args, **kwargs): + return HttpResponse(self.get_object().get_media('epub')) diff --git a/src/paypal/permissions.py b/src/paypal/permissions.py new file mode 100644 index 000000000..9d0865b0e --- /dev/null +++ b/src/paypal/permissions.py @@ -0,0 +1,7 @@ +from rest_framework.permissions import BasePermission +from .rest import user_is_subscribed + + +class IsSubscribed(BasePermission): + def has_permission(self, request, view): + return request.user.is_authenticated and user_is_subscribed(request.user) diff --git a/src/wolnelektury/settings/contrib.py b/src/wolnelektury/settings/contrib.py index daa119d76..a25ef72d9 100644 --- a/src/wolnelektury/settings/contrib.py +++ b/src/wolnelektury/settings/contrib.py @@ -52,5 +52,6 @@ REST_FRAMEWORK = { 'api.renderers.LegacyXMLRenderer', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'api.drf_auth.PistonOAuthAuthentication', ) } -- 2.20.1