fix
[wolnelektury.git] / src / social / api / views.py
index cad07df..2c23835 100644 (file)
@@ -2,13 +2,13 @@
 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
 #
 from datetime import datetime
-from pytz import utc
+from django.db.models import Q
 from django.http import Http404
-from django.utils.timezone import now
+from django.utils.timezone import now, utc
+from rest_framework.exceptions import MethodNotAllowed
 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
-from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
+from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly
 from rest_framework.response import Response
-from rest_framework import serializers
 from rest_framework.views import APIView
 from api.models import BookUserData
 from api.utils import vary_on_auth, never_cache
@@ -20,9 +20,19 @@ from social.views import get_sets_for_book_ids
 from social.utils import likes
 from social import models
 import bookmarks.models
+from . import serializers
 from bookmarks.api.views import BookmarkSerializer
 
 
+
+class SettingsView(RetrieveUpdateAPIView):
+    permission_classes = [IsAuthenticated]
+    serializer_class = serializers.SettingsSerializer
+
+    def get_object(self):
+        return models.UserProfile.get_for(self.request.user)
+
+
 @never_cache
 class LikeView(APIView):
     permission_classes = [IsAuthenticated]
@@ -88,106 +98,15 @@ class MyLikesView(APIView):
         )
 
 
-class UserListItemsField(serializers.Field):
-    def to_representation(self, value):
-        return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
-
-    def to_internal_value(self, value):
-        return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
-
-
-class UserListSerializer(serializers.ModelSerializer):
-    client_id = serializers.CharField(write_only=True, required=False)
-    books = UserListItemsField(source='*', required=False)
-    timestamp = serializers.IntegerField(required=False)
-
-    class Meta:
-        model = models.UserList
-        fields = [
-            'timestamp',
-            'client_id',
-            'name',
-            'slug',
-            'favorites',
-            'deleted',
-            'books',
-        ]
-        read_only_fields = ['favorites']
-        extra_kwargs = {
-            'slug': {
-                'required': False
-            }
-        }
-
-    def create(self, validated_data):
-        instance = models.UserList.get_by_name(
-            validated_data['user'],
-            validated_data['name'],
-            create=True
-        )
-        instance.userlistitem_set.all().delete()
-        for book in validated_data['books']:
-            instance.append(book)
-        return instance
-
-    def update(self, instance, validated_data):
-        instance.userlistitem_set.all().delete()
-        for book in validated_data['books']:
-            instance.append(instance)
-        return instance
-
-class UserListBooksSerializer(UserListSerializer):
-    class Meta:
-        model = models.UserList
-        fields = ['books']
-
-
-class UserListItemSerializer(serializers.ModelSerializer):
-    client_id = serializers.CharField(write_only=True, required=False)
-    favorites = serializers.BooleanField(required=False)
-    list_slug = serializers.SlugRelatedField(
-        queryset=models.UserList.objects.all(),
-        source='list',
-        slug_field='slug',
-        required=False,
-    )
-    timestamp = serializers.IntegerField(required=False)
-    book_slug = serializers.SlugRelatedField(
-        queryset=Book.objects.all(),
-        source='book',
-        slug_field='slug',
-        required=False
-    )
-
-    class Meta:
-        model = models.UserListItem
-        fields = [
-            'client_id',
-            'uuid',
-            'order',
-            'list_slug',
-            'timestamp',
-            'favorites',
-            'deleted',
-
-            'book_slug',
-            'fragment',
-            'quote',
-            'bookmark',
-            'note',
-        ]
-        extra_kwargs = {
-            'order': {
-                'required': False
-            }
-        }
-
-
 @never_cache
 class ListsView(ListCreateAPIView):
     permission_classes = [IsAuthenticated]
     #pagination_class = None
-    serializer_class = UserListSerializer
+
+    def get_serializer_class(self):
+        if self.request.version == 'v2':
+            return serializers.UserListSerializerV2
+        return serializers.UserListSerializerV3
 
     def get_queryset(self):
         return models.UserList.objects.filter(
@@ -200,38 +119,61 @@ class ListsView(ListCreateAPIView):
         serializer.save(user=self.request.user)
 
 
+def get_userlist(slug, request):
+    if request.method in SAFE_METHODS:
+        q = Q(deleted=False)
+        if request.user.is_authenticated:
+            q |= Q(user=request.user)
+        return get_object_or_404(
+            models.UserList,
+            q,
+            slug=slug,
+        )
+    else:
+        return get_object_or_404(
+            models.UserList.all_objects.all(),
+            slug=slug,
+            user=request.user
+        )
+
+
 @never_cache
 class ListView(RetrieveUpdateDestroyAPIView):
     # TODO: check if can modify
-    permission_classes = [IsAuthenticated]
-    serializer_class = UserListSerializer
+    permission_classes = [IsAuthenticatedOrReadOnly]
+
+    def get_serializer_class(self):
+        if self.request.version == 'v2':
+            return serializers.UserListSerializerV2
+        return serializers.UserListSerializerV3
 
     def get_object(self):
-        return get_object_or_404(
-            models.UserList,
-            slug=self.kwargs['slug'],
-            user=self.request.user)
+        return get_userlist(self.kwargs['slug'], self.request)
 
     def perform_update(self, serializer):
         serializer.save(user=self.request.user)
 
     def post(self, request, slug):
-        serializer = UserListBooksSerializer(data=request.data)
-        serializer.is_valid(raise_exception=True)
-        instance = self.get_object()
-        for book in serializer.validated_data['books']:
-            instance.append(book)
-        return Response(self.get_serializer(instance).data)
+        if request.version == 'v2':
+            # Accept posting a list of books here.
+            serializer = serializers.UserListBooksSerializer(data=request.data)
+            serializer.is_valid(raise_exception=True)
+            instance = self.get_object()
+            for book in serializer.validated_data['books']:
+                instance.append(book)
+            return Response(self.get_serializer(instance).data)
+        else:
+            raise MethodNotAllowed(method=request.method)
 
     def perform_destroy(self, instance):
-        instance.update(
-            deleted=True,
-            updated_at=now()
-        )
+        instance.deleted = True
+        instance.updated_at = now()
+        instance.save()
 
 
 @never_cache
-class ListItemView(APIView):
+class ListItemViewV2(APIView):
+    """v2 only"""
     permission_classes = [IsAuthenticated]
 
     def delete(self, request, slug, book):
@@ -239,7 +181,42 @@ class ListItemView(APIView):
             models.UserList, slug=slug, user=self.request.user)
         book = get_object_or_404(catalogue.models.Book, slug=book)
         instance.remove(book=book)
-        return Response(UserListSerializer(instance).data)
+        return Response(serializers.UserListSerializerV2(instance).data)
+
+
+@never_cache
+class ListItemListViewV3(ListCreateAPIView):
+    permission_classes = [IsAuthenticatedOrReadOnly]
+    serializer_class = serializers.UserListItemSerializer
+
+    def get_queryset(self):
+        lst = get_userlist(self.kwargs['slug'], self.request)
+        return lst.userlistitem_set.all().order_by('order')
+
+    def get_serializer(self, *args, **kwargs):
+        serializer_class = self.get_serializer_class()
+        kwargs.setdefault('context', self.get_serializer_context())
+
+        if isinstance(self.request.data, list):
+            kwargs['many'] = True
+
+        return serializer_class(*args, **kwargs)
+
+    def perform_create(self, serializer):
+        lst = get_userlist(self.kwargs['slug'], self.request)
+        serializer.save(list=lst)
+
+
+@never_cache
+class ListItemViewV3(RetrieveUpdateDestroyAPIView):
+    permission_classes = [IsAuthenticated]
+    serializer_class = serializers.UserListItemSerializer
+    lookup_field = 'uuid'
+
+    def get_queryset(self):
+        return models.UserListItem.objects.filter(
+            list__user=self.request.user
+        )
 
 
 @vary_on_auth
@@ -270,60 +247,10 @@ class ShelfView(ListAPIView):
         return books
 
 
-
-class ProgressSerializer(serializers.ModelSerializer):
-    book = serializers.HyperlinkedRelatedField(
-        read_only=True,
-        view_name='catalogue_api_book',
-        lookup_field='slug'
-    )
-    book_slug = serializers.SlugRelatedField(
-        queryset=Book.objects.all(),
-        source='book',
-        slug_field='slug')
-    timestamp = serializers.IntegerField(required=False)
-
-    class Meta:
-        model = models.Progress
-        fields = [
-            'timestamp',
-            'book', 'book_slug', 'last_mode', 'text_percent',
-            'text_anchor',
-            'audio_percent',
-            'audio_timestamp',
-            'implicit_text_percent',
-            'implicit_text_anchor',
-            'implicit_audio_percent',
-            'implicit_audio_timestamp',
-        ]
-        extra_kwargs = {
-            'last_mode': {
-                'required': False,
-                'default': 'text',
-            }
-        }
-
-
-class TextProgressSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = models.Progress
-        fields = [
-                'text_percent',
-                'text_anchor',
-                ]
-        read_only_fields = ['text_percent']
-
-class AudioProgressSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = models.Progress
-        fields = ['audio_percent', 'audio_timestamp']
-        read_only_fields = ['audio_percent']
-
-
 @never_cache
 class ProgressListView(ListAPIView):
     permission_classes = [IsAuthenticated]
-    serializer_class = ProgressSerializer
+    serializer_class = serializers.ProgressSerializer
 
     def get_queryset(self):
         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
@@ -342,15 +269,16 @@ class ProgressMixin:
 @never_cache
 class ProgressView(ProgressMixin, RetrieveAPIView):
     permission_classes = [IsAuthenticated]
-    serializer_class = ProgressSerializer
+    serializer_class = serializers.ProgressSerializer
 
 
 @never_cache
 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
     permission_classes = [IsAuthenticated]
-    serializer_class = TextProgressSerializer
+    serializer_class = serializers.TextProgressSerializer
 
     def perform_update(self, serializer):
+        serializer.instance.reported_timestamp = now()
         serializer.instance.last_mode = 'text'
         serializer.save()
 
@@ -358,9 +286,10 @@ class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
 @never_cache
 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
     permission_classes = [IsAuthenticated]
-    serializer_class = AudioProgressSerializer
+    serializer_class = serializers.AudioProgressSerializer
 
     def perform_update(self, serializer):
+        serializer.instance.reported_timestamp = now()
         serializer.instance.last_mode = 'audio'
         serializer.save()
 
@@ -404,6 +333,8 @@ class SyncView(ListAPIView):
     def post(self, request):
         new_ids = []
         data = request.data
+        if not isinstance(data, list):
+            raise serializers.ValidationError('Payload should be a list')
         for item in data:
             instance = self.get_instance(request.user, item)
             ser = self.get_serializer(
@@ -426,7 +357,7 @@ class SyncView(ListAPIView):
 
 class ProgressSyncView(SyncView):
     model = models.Progress
-    serializer_class = ProgressSerializer
+    serializer_class = serializers.ProgressSerializer
     
     sync_id_field = 'book__slug'
     sync_id_serializer_field = 'book_slug'
@@ -434,19 +365,23 @@ class ProgressSyncView(SyncView):
 
 class UserListSyncView(SyncView):
     model = models.UserList
-    serializer_class = UserListSerializer
+
+    def get_serializer_class(self):
+        if self.request.version == 'v2':
+            return serializers.UserListSerializerV2
+        return serializers.UserListSerializerV3
 
 
 class UserListItemSyncView(SyncView):
     model = models.UserListItem
-    serializer_class = UserListItemSerializer
+    serializer_class = serializers.UserListItemSerializer
 
     sync_id_field = 'uuid'
     sync_id_serializer_field = 'uuid'
     sync_user_field = 'list__user'
 
     def get_queryset_for_ts(self, timestamp):
-        qs = self.model.objects.filter(
+        qs = self.model.all_objects.filter(
             updated_at__gt=timestamp,
             **{
                 self.sync_user_field: self.request.user