Using Piston auth in DRF views. Replacing API views.
authorRadek Czajka <rczajka@rczajka.pl>
Sat, 2 Feb 2019 00:46:31 +0000 (01:46 +0100)
committerRadek Czajka <rczajka@rczajka.pl>
Sat, 2 Feb 2019 00:46:31 +0000 (01:46 +0100)
13 files changed:
src/api/drf_auth.py [new file with mode: 0644]
src/api/emitters.py [deleted file]
src/api/fields.py
src/api/handlers.py
src/api/models.py
src/api/serializers.py [new file with mode: 0644]
src/api/tests/tests.py
src/api/urls.py
src/api/views.py [new file with mode: 0644]
src/catalogue/api/urls.py
src/catalogue/api/views.py
src/paypal/permissions.py [new file with mode: 0644]
src/wolnelektury/settings/contrib.py

diff --git a/src/api/drf_auth.py b/src/api/drf_auth.py
new file mode 100644 (file)
index 0000000..26018c6
--- /dev/null
@@ -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 (file)
index 50a2d41..0000000
+++ /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')
index 4bb44dc..1ce83cc 100644 (file)
@@ -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)
index f3cc4a7..7872a10 100644 (file)
@@ -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']
 
index 2481010..d2716b9 100644 (file)
@@ -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 (file)
index 0000000..a876387
--- /dev/null
@@ -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']
index 91e5bbf..6be34ed 100644 (file)
@@ -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("<epub>")):
                 self.assertEqual(
index 3c82e1e..7e393a2 100644 (file)
@@ -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<book_id>\d*?)/info\.html$', catalogue.views.book_info),
     url(r'tag/(?P<tag_id>\d*?)/info\.html$', catalogue.views.tag_info),
 
-    # epub preview
-    url(r'^epub/(?P<slug>[a-z0-9-]+)/$', epub_resource, name='api_epub'),
-
     # reading data
-    url(r'^reading/(?P<slug>[a-z0-9-]+)/$', reading_resource, name='api_reading'),
-    url(r'^reading/(?P<slug>[a-z0-9-]+)/(?P<state>[a-z]+)/$', reading_resource, name='api_reading'),
+    url(r'^reading/(?P<slug>[a-z0-9-]+)/$', views.BookUserDataView.as_view(), name='api_reading'),
+    url(r'^reading/(?P<slug>[a-z0-9-]+)/(?P<state>[a-z]+)/$', views.BookUserDataView.as_view(), name='api_reading'),
     url(r'^shelf/(?P<state>[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<slug>[a-z0-9-]+)/$', like_resource, name='api_like'),
 
diff --git a/src/api/views.py b/src/api/views.py
new file mode 100644 (file)
index 0000000..812be83
--- /dev/null
@@ -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)
index e476c8f..30f329a 100644 (file)
@@ -13,4 +13,6 @@ urlpatterns = [
         views.CollectionDetail.as_view(), name="collection-detail"),
 
     url(r'^books/(?P<slug>[^/]+)/$', views.BookDetail.as_view(), name='catalogue_api_book'),
+
+    url(r'^epub/(?P<slug>[a-z0-9-]+)/$', views.EpubView.as_view(), name='catalogue_api_epub'),
 ]
index 1439907..c20cccb 100644 (file)
@@ -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 (file)
index 0000000..9d0865b
--- /dev/null
@@ -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)
index daa119d..a25ef72 100644 (file)
@@ -52,5 +52,6 @@ REST_FRAMEWORK = {
         'api.renderers.LegacyXMLRenderer',
     ),
     'DEFAULT_AUTHENTICATION_CLASSES': (
+        'api.drf_auth.PistonOAuthAuthentication',
     )
 }