# 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.exceptions import MethodNotAllowed
+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
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
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({})
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)})
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
+
+ def get_serializer_class(self):
+ if self.request.version == 'v2':
+ return serializers.UserListSerializerV2
+ return serializers.UserListSerializerV3
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)
+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(catalogue.models.Tag, 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']:
- catalogue.models.Tag.objects.add_tag(book, instance)
- 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.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):
- 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)
- return Response(UserListSerializer(instance).data)
+ instance.remove(book=book)
+ 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()
+
+
+@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
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)
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')
@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'
@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'
-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:
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
+
+ def get_serializer_class(self):
+ if self.request.version == 'v2':
+ return serializers.UserListSerializerV2
+ return serializers.UserListSerializerV3
+
+
+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