X-Git-Url: https://git.mdrn.pl/wolnelektury.git/blobdiff_plain/a84167ce03ffdca5a9ff74f5e3c2f40dc05fbbf4..37c0f94336f330a8660a46b8b3052a24f4c9d5b5:/src/social/api/views.py?ds=sidebyside diff --git a/src/social/api/views.py b/src/social/api/views.py index c58efd1c9..a43bec277 100644 --- a/src/social/api/views.py +++ b/src/social/api/views.py @@ -2,12 +2,12 @@ # 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 rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, DestroyAPIView, get_object_or_404 -from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from django.utils.timezone import now, utc +from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404 +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 @@ -15,9 +15,21 @@ from catalogue.api.helpers import order_books, books_after from catalogue.api.serializers import BookSerializer from catalogue.models import Book import catalogue.models -from social.utils import likes, get_set 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 @@ -32,9 +44,9 @@ class LikeView(APIView): book = get_object_or_404(Book, slug=slug) action = request.query_params.get('action', 'like') if action == 'like': - book.like(request.user) + models.UserList.like(request.user, book) elif action == 'unlike': - book.unlike(request.user) + models.UserList.unlike(request.user, book) return Response({}) @@ -48,12 +60,12 @@ class LikeView2(APIView): def put(self, request, slug): book = get_object_or_404(Book, slug=slug) - book.like(request.user) + models.UserList.like(request.user, book) return Response({"likes": likes(request.user, book)}) def delete(self, request, slug): book = get_object_or_404(Book, slug=slug) - book.unlike(request.user) + models.UserList.unlike(request.user, book) return Response({"likes": likes(request.user, book)}) @@ -77,60 +89,26 @@ class MyLikesView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - ids = catalogue.models.tag.TagRelation.objects.filter(tag__user=request.user).values_list('object_id', flat=True).distinct() - books = Book.objects.filter(id__in=ids) - books = {b.id: b.slug for b in books} - res = get_sets_for_book_ids(ids, request.user) - res = {books[bid]: v for bid, v in res.items()} - - res = list(books.values()) - res.sort() - return Response(res) - - -class TaggedBooksField(serializers.Field): - def to_representation(self, value): - return catalogue.models.Book.tagged.with_all([value]).values_list('slug', flat=True) - - def to_internal_value(self, value): - return {'books': catalogue.models.Book.objects.filter(slug__in=value)} - - -class UserListSerializer(serializers.ModelSerializer): - books = TaggedBooksField(source='*') - - class Meta: - model = catalogue.models.Tag - fields = ['name', 'slug', 'books'] - read_only_fields = ['slug'] - - def create(self, validated_data): - instance = get_set(validated_data['user'], validated_data['name']) - catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete() - for book in validated_data['books']: - catalogue.models.Tag.objects.add_tag(book, instance) - return instance - - def update(self, instance, validated_data): - catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete() - for book in validated_data['books']: - catalogue.models.Tag.objects.add_tag(book, instance) - return instance - -class UserListBooksSerializer(UserListSerializer): - class Meta: - model = catalogue.models.Tag - fields = ['books'] + ul = models.UserList.get_favorites_list(request.user) + if ul is None: + return Response([]) + return Response( + ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True) + ) @never_cache class ListsView(ListCreateAPIView): permission_classes = [IsAuthenticated] #pagination_class = None - serializer_class = UserListSerializer + serializer_class = serializers.UserListSerializer def get_queryset(self): - return catalogue.models.Tag.objects.filter(user=self.request.user).exclude(name='') + return models.UserList.objects.filter( + user=self.request.user, + favorites=False, + deleted=False + ) def perform_create(self, serializer): serializer.save(user=self.request.user) @@ -139,32 +117,51 @@ class ListsView(ListCreateAPIView): @never_cache class ListView(RetrieveUpdateDestroyAPIView): # TODO: check if can modify - permission_classes = [IsAuthenticated] - serializer_class = UserListSerializer + permission_classes = [IsAuthenticatedOrReadOnly] + serializer_class = serializers.UserListSerializer def get_object(self): - return get_object_or_404(catalogue.models.Tag, slug=self.kwargs['slug'], user=self.request.user) + if self.request.method in SAFE_METHODS: + q = Q(deleted=False) + if self.request.user.is_authenticated: + q |= Q(user=self.request.user) + return get_object_or_404( + models.UserList, + q, + slug=self.kwargs['slug'], + ) + else: + return get_object_or_404( + models.UserList.all_objects.all(), + slug=self.kwargs['slug'], + user=self.request.user) def perform_update(self, serializer): serializer.save(user=self.request.user) def post(self, request, slug): - serializer = UserListBooksSerializer(data=request.data) + serializer = serializers.UserListBooksSerializer(data=request.data) serializer.is_valid(raise_exception=True) instance = self.get_object() for book in serializer.validated_data['books']: - catalogue.models.Tag.objects.add_tag(book, instance) + instance.append(book) return Response(self.get_serializer(instance).data) + def perform_destroy(self, instance): + instance.deleted = True + instance.updated_at = now() + instance.save() + @never_cache class ListItemView(APIView): permission_classes = [IsAuthenticated] def delete(self, request, slug, book): - instance = get_object_or_404(catalogue.models.Tag, slug=slug, user=self.request.user) + instance = get_object_or_404( + models.UserList, slug=slug, user=self.request.user) book = get_object_or_404(catalogue.models.Book, slug=book) - catalogue.models.Tag.objects.remove_tag(book, instance) + instance.remove(book=book) return Response(UserListSerializer(instance).data) @@ -182,7 +179,7 @@ class ShelfView(ListAPIView): after = self.request.query_params.get('after') count = int(self.request.query_params.get('count', 50)) if state == 'likes': - books = Book.tagged.with_any(self.request.user.tag_set.all()) + books = Book.objects.filter(userlistitem__list__user=self.request.user) else: ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\ .values_list('book_id', flat=True) @@ -196,48 +193,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(source='book', read_only=True, slug_field='slug') - - class Meta: - model = models.Progress - fields = ['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', - ] - - -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') @@ -256,13 +215,13 @@ 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.last_mode = 'text' @@ -272,7 +231,7 @@ 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.last_mode = 'audio' @@ -280,25 +239,12 @@ class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView): -class SyncSerializer(serializers.Serializer): - timestamp = serializers.IntegerField() - type = serializers.CharField() - id = serializers.CharField() - - def to_representation(self, instance): - rep = super().to_representation(instance) - rep['object'] = instance['object'].data - return rep - - def to_internal_value(self, data): - ret = super().to_internal_value(data) - ret['object'] = data['object'] - return ret - - +@never_cache class SyncView(ListAPIView): permission_classes = [IsAuthenticated] - serializer_class = SyncSerializer + sync_id_field = 'slug' + sync_id_serializer_field = 'slug' + sync_user_field = 'user' def get_queryset(self): try: @@ -309,30 +255,93 @@ class SyncView(ListAPIView): timestamp = datetime.fromtimestamp(timestamp, tz=utc) data = [] - for p in models.Progress.objects.filter( - user=self.request.user, - updated_at__gt=timestamp).order_by('updated_at'): - data.append({ - 'timestamp': p.updated_at.timestamp(), - 'type': 'progress', - 'id': p.book.slug, - 'object': ProgressSerializer( - p, context={'request': self.request} - ) if not p.deleted else None - }) - return data + return self.get_queryset_for_ts(timestamp) + + def get_queryset_for_ts(self, timestamp): + return self.model.objects.filter( + updated_at__gt=timestamp, + **{ + self.sync_user_field: self.request.user + } + ).order_by('updated_at') + + def get_instance(self, user, data): + sync_id = data.get(self.sync_id_serializer_field) + if not sync_id: + return None + return self.model.objects.filter(**{ + self.sync_user_field: user, + self.sync_id_field: sync_id + }).first() 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: - ser = SyncSerializer(data=item) + instance = self.get_instance(request.user, item) + ser = self.get_serializer( + instance=instance, + data=item + ) ser.is_valid(raise_exception=True) - d = ser.validated_data - if d['type'] == 'progress': - models.Progress.sync( - user=request.user, - slug=d['id'], - ts=datetime.fromtimestamp(d['timestamp'], tz=utc), - data=d['object'] - ) - return Response() + synced_instance = self.model.sync( + request.user, + instance, + ser.validated_data + ) + if instance is None and 'client_id' in ser.validated_data and synced_instance is not None: + new_ids.append({ + 'client_id': ser.validated_data['client_id'], + self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field), + }) + return Response(new_ids) + + +class ProgressSyncView(SyncView): + model = models.Progress + serializer_class = serializers.ProgressSerializer + + sync_id_field = 'book__slug' + sync_id_serializer_field = 'book_slug' + + +class UserListSyncView(SyncView): + model = models.UserList + serializer_class = serializers.UserListSerializer + + +class UserListItemSyncView(SyncView): + model = models.UserListItem + 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.all_objects.filter( + updated_at__gt=timestamp, + **{ + self.sync_user_field: self.request.user + } + ) + if self.request.query_params.get('favorites'): + qs = qs.filter(list__favorites=True) + return qs.order_by('updated_at') + + +class BookmarkSyncView(SyncView): + model = bookmarks.models.Bookmark + serializer_class = BookmarkSerializer + + sync_id_field = 'uuid' + sync_id_serializer_field = 'uuid' + + def get_instance(self, user, data): + ret = super().get_instance(user, data) + if ret is None: + if data.get('location'): + ret = self.model.get_by_location(user, data['location']) + return ret