1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
4 from datetime import datetime
6 from django.http import Http404
7 from django.utils.timezone import now
8 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
9 from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
10 from rest_framework.response import Response
11 from rest_framework import serializers
12 from rest_framework.views import APIView
13 from api.models import BookUserData
14 from api.utils import vary_on_auth, never_cache
15 from catalogue.api.helpers import order_books, books_after
16 from catalogue.api.serializers import BookSerializer
17 from catalogue.models import Book
18 import catalogue.models
19 from social.views import get_sets_for_book_ids
20 from social.utils import likes
21 from social import models
22 import bookmarks.models
23 from bookmarks.api.views import BookmarkSerializer
27 class LikeView(APIView):
28 permission_classes = [IsAuthenticated]
30 def get(self, request, slug):
31 book = get_object_or_404(Book, slug=slug)
32 return Response({"likes": likes(request.user, book)})
34 def post(self, request, slug):
35 book = get_object_or_404(Book, slug=slug)
36 action = request.query_params.get('action', 'like')
38 models.UserList.like(request.user, book)
39 elif action == 'unlike':
40 models.UserList.unlike(request.user, book)
45 class LikeView2(APIView):
46 permission_classes = [IsAuthenticated]
48 def get(self, request, slug):
49 book = get_object_or_404(Book, slug=slug)
50 return Response({"likes": likes(request.user, book)})
52 def put(self, request, slug):
53 book = get_object_or_404(Book, slug=slug)
54 models.UserList.like(request.user, book)
55 return Response({"likes": likes(request.user, book)})
57 def delete(self, request, slug):
58 book = get_object_or_404(Book, slug=slug)
59 models.UserList.unlike(request.user, book)
60 return Response({"likes": likes(request.user, book)})
64 class LikesView(APIView):
65 permission_classes = [IsAuthenticated]
67 def get(self, request):
68 slugs = request.GET.getlist('slug')
69 books = Book.objects.filter(slug__in=slugs)
70 books = {b.id: b.slug for b in books}
72 res = get_sets_for_book_ids(ids, request.user)
73 res = {books[bid]: v for bid, v in res.items()}
79 class MyLikesView(APIView):
80 permission_classes = [IsAuthenticated]
82 def get(self, request):
83 ul = models.UserList.get_favorites_list(request.user)
87 ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
91 class UserListItemsField(serializers.Field):
92 def to_representation(self, value):
93 return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
95 def to_internal_value(self, value):
96 return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
99 class UserListSerializer(serializers.ModelSerializer):
100 client_id = serializers.CharField(write_only=True, required=False)
101 books = UserListItemsField(source='*', required=False)
102 timestamp = serializers.IntegerField(required=False)
105 model = models.UserList
115 read_only_fields = ['favorites']
122 def create(self, validated_data):
123 instance = models.UserList.get_by_name(
124 validated_data['user'],
125 validated_data['name'],
128 instance.userlistitem_set.all().delete()
129 for book in validated_data['books']:
130 instance.append(book)
133 def update(self, instance, validated_data):
134 instance.userlistitem_set.all().delete()
135 for book in validated_data['books']:
136 instance.append(instance)
139 class UserListBooksSerializer(UserListSerializer):
141 model = models.UserList
145 class UserListItemSerializer(serializers.ModelSerializer):
146 client_id = serializers.CharField(write_only=True, required=False)
147 favorites = serializers.BooleanField(required=False)
148 list_slug = serializers.SlugRelatedField(
149 queryset=models.UserList.objects.all(),
154 timestamp = serializers.IntegerField(required=False)
155 book_slug = serializers.SlugRelatedField(
156 queryset=Book.objects.all(),
163 model = models.UserListItem
187 class ListsView(ListCreateAPIView):
188 permission_classes = [IsAuthenticated]
189 #pagination_class = None
190 serializer_class = UserListSerializer
192 def get_queryset(self):
193 return models.UserList.objects.filter(
194 user=self.request.user,
199 def perform_create(self, serializer):
200 serializer.save(user=self.request.user)
204 class ListView(RetrieveUpdateDestroyAPIView):
205 # TODO: check if can modify
206 permission_classes = [IsAuthenticated]
207 serializer_class = UserListSerializer
209 def get_object(self):
210 return get_object_or_404(
212 slug=self.kwargs['slug'],
213 user=self.request.user)
215 def perform_update(self, serializer):
216 serializer.save(user=self.request.user)
218 def post(self, request, slug):
219 serializer = UserListBooksSerializer(data=request.data)
220 serializer.is_valid(raise_exception=True)
221 instance = self.get_object()
222 for book in serializer.validated_data['books']:
223 instance.append(book)
224 return Response(self.get_serializer(instance).data)
226 def perform_destroy(self, instance):
234 class ListItemView(APIView):
235 permission_classes = [IsAuthenticated]
237 def delete(self, request, slug, book):
238 instance = get_object_or_404(
239 models.UserList, slug=slug, user=self.request.user)
240 book = get_object_or_404(catalogue.models.Book, slug=book)
241 instance.remove(book=book)
242 return Response(UserListSerializer(instance).data)
246 class ShelfView(ListAPIView):
247 permission_classes = [IsAuthenticated]
248 serializer_class = BookSerializer
249 pagination_class = None
251 def get_queryset(self):
252 state = self.kwargs['state']
253 if state not in ('reading', 'complete', 'likes'):
255 new_api = self.request.query_params.get('new_api')
256 after = self.request.query_params.get('after')
257 count = int(self.request.query_params.get('count', 50))
259 books = Book.objects.filter(userlistitem__list__user=self.request.user)
261 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
262 .values_list('book_id', flat=True)
263 books = Book.objects.filter(id__in=list(ids)).distinct()
264 books = order_books(books, new_api)
266 books = books_after(books, after, new_api)
268 books = books[:count]
274 class ProgressSerializer(serializers.ModelSerializer):
275 book = serializers.HyperlinkedRelatedField(
277 view_name='catalogue_api_book',
280 book_slug = serializers.SlugRelatedField(
281 queryset=Book.objects.all(),
284 timestamp = serializers.IntegerField(required=False)
287 model = models.Progress
290 'book', 'book_slug', 'last_mode', 'text_percent',
294 'implicit_text_percent',
295 'implicit_text_anchor',
296 'implicit_audio_percent',
297 'implicit_audio_timestamp',
307 class TextProgressSerializer(serializers.ModelSerializer):
309 model = models.Progress
314 read_only_fields = ['text_percent']
316 class AudioProgressSerializer(serializers.ModelSerializer):
318 model = models.Progress
319 fields = ['audio_percent', 'audio_timestamp']
320 read_only_fields = ['audio_percent']
324 class ProgressListView(ListAPIView):
325 permission_classes = [IsAuthenticated]
326 serializer_class = ProgressSerializer
328 def get_queryset(self):
329 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
333 def get_object(self):
335 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
336 except models.Progress.DoesNotExist:
337 book = get_object_or_404(Book, slug=self.kwargs['slug'])
338 return models.Progress(user=self.request.user, book=book)
343 class ProgressView(ProgressMixin, RetrieveAPIView):
344 permission_classes = [IsAuthenticated]
345 serializer_class = ProgressSerializer
349 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
350 permission_classes = [IsAuthenticated]
351 serializer_class = TextProgressSerializer
353 def perform_update(self, serializer):
354 serializer.instance.last_mode = 'text'
359 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
360 permission_classes = [IsAuthenticated]
361 serializer_class = AudioProgressSerializer
363 def perform_update(self, serializer):
364 serializer.instance.last_mode = 'audio'
370 class SyncView(ListAPIView):
371 permission_classes = [IsAuthenticated]
372 sync_id_field = 'slug'
373 sync_id_serializer_field = 'slug'
374 sync_user_field = 'user'
376 def get_queryset(self):
378 timestamp = int(self.request.GET.get('ts'))
382 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
385 return self.get_queryset_for_ts(timestamp)
387 def get_queryset_for_ts(self, timestamp):
388 return self.model.objects.filter(
389 updated_at__gt=timestamp,
391 self.sync_user_field: self.request.user
393 ).order_by('updated_at')
395 def get_instance(self, user, data):
396 sync_id = data.get(self.sync_id_serializer_field)
399 return self.model.objects.filter(**{
400 self.sync_user_field: user,
401 self.sync_id_field: sync_id
404 def post(self, request):
408 instance = self.get_instance(request.user, item)
409 ser = self.get_serializer(
413 ser.is_valid(raise_exception=True)
414 synced_instance = self.model.sync(
419 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
421 'client_id': ser.validated_data['client_id'],
422 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
424 return Response(new_ids)
427 class ProgressSyncView(SyncView):
428 model = models.Progress
429 serializer_class = ProgressSerializer
431 sync_id_field = 'book__slug'
432 sync_id_serializer_field = 'book_slug'
435 class UserListSyncView(SyncView):
436 model = models.UserList
437 serializer_class = UserListSerializer
440 class UserListItemSyncView(SyncView):
441 model = models.UserListItem
442 serializer_class = UserListItemSerializer
444 sync_id_field = 'uuid'
445 sync_id_serializer_field = 'uuid'
446 sync_user_field = 'list__user'
448 def get_queryset_for_ts(self, timestamp):
449 qs = self.model.objects.filter(
450 updated_at__gt=timestamp,
452 self.sync_user_field: self.request.user
455 if self.request.query_params.get('favorites'):
456 qs = qs.filter(list__favorites=True)
457 return qs.order_by('updated_at')
460 class BookmarkSyncView(SyncView):
461 model = bookmarks.models.Bookmark
462 serializer_class = BookmarkSerializer
464 sync_id_field = 'uuid'
465 sync_id_serializer_field = 'uuid'
467 def get_instance(self, user, data):
468 ret = super().get_instance(user, data)
470 if data.get('location'):
471 ret = self.model.get_by_location(user, data['location'])