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 import models
21 import bookmarks.models
22 from bookmarks.api.views import BookmarkSerializer
26 class LikeView(APIView):
27 permission_classes = [IsAuthenticated]
29 def get(self, request, slug):
30 book = get_object_or_404(Book, slug=slug)
31 return Response({"likes": likes(request.user, book)})
33 def post(self, request, slug):
34 book = get_object_or_404(Book, slug=slug)
35 action = request.query_params.get('action', 'like')
37 models.UserList.like(request.user, book)
38 elif action == 'unlike':
39 models.UserList.unlike(request.user, book)
44 class LikeView2(APIView):
45 permission_classes = [IsAuthenticated]
47 def get(self, request, slug):
48 book = get_object_or_404(Book, slug=slug)
49 return Response({"likes": likes(request.user, book)})
51 def put(self, request, slug):
52 book = get_object_or_404(Book, slug=slug)
53 models.UserList.like(request.user, book)
54 return Response({"likes": likes(request.user, book)})
56 def delete(self, request, slug):
57 book = get_object_or_404(Book, slug=slug)
58 models.UserList.unlike(request.user, book)
59 return Response({"likes": likes(request.user, book)})
63 class LikesView(APIView):
64 permission_classes = [IsAuthenticated]
66 def get(self, request):
67 slugs = request.GET.getlist('slug')
68 books = Book.objects.filter(slug__in=slugs)
69 books = {b.id: b.slug for b in books}
71 res = get_sets_for_book_ids(ids, request.user)
72 res = {books[bid]: v for bid, v in res.items()}
78 class MyLikesView(APIView):
79 permission_classes = [IsAuthenticated]
81 def get(self, request):
82 ul = models.UserList.get_favorites_list(request.user)
86 ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
90 class UserListItemsField(serializers.Field):
91 def to_representation(self, value):
92 return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
94 def to_internal_value(self, value):
95 return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
98 class UserListSerializer(serializers.ModelSerializer):
99 client_id = serializers.CharField(write_only=True, required=False)
100 books = UserListItemsField(source='*', required=False)
101 timestamp = serializers.IntegerField(required=False)
104 model = models.UserList
114 read_only_fields = ['favorites']
121 def create(self, validated_data):
122 instance = models.UserList.get_by_name(
123 validated_data['user'],
124 validated_data['name'],
127 instance.userlistitem_set.all().delete()
128 for book in validated_data['books']:
129 instance.append(book)
132 def update(self, instance, validated_data):
133 instance.userlistitem_set.all().delete()
134 for book in validated_data['books']:
135 instance.append(instance)
138 class UserListBooksSerializer(UserListSerializer):
140 model = models.UserList
144 class UserListItemSerializer(serializers.ModelSerializer):
145 client_id = serializers.CharField(write_only=True, required=False)
146 favorites = serializers.BooleanField(required=False)
147 list_slug = serializers.SlugRelatedField(
148 queryset=models.UserList.objects.all(),
153 timestamp = serializers.IntegerField(required=False)
154 book_slug = serializers.SlugRelatedField(
155 queryset=Book.objects.all(),
162 model = models.UserListItem
186 class ListsView(ListCreateAPIView):
187 permission_classes = [IsAuthenticated]
188 #pagination_class = None
189 serializer_class = UserListSerializer
191 def get_queryset(self):
192 return models.UserList.objects.filter(
193 user=self.request.user,
198 def perform_create(self, serializer):
199 serializer.save(user=self.request.user)
203 class ListView(RetrieveUpdateDestroyAPIView):
204 # TODO: check if can modify
205 permission_classes = [IsAuthenticated]
206 serializer_class = UserListSerializer
208 def get_object(self):
209 return get_object_or_404(
211 slug=self.kwargs['slug'],
212 user=self.request.user)
214 def perform_update(self, serializer):
215 serializer.save(user=self.request.user)
217 def post(self, request, slug):
218 serializer = UserListBooksSerializer(data=request.data)
219 serializer.is_valid(raise_exception=True)
220 instance = self.get_object()
221 for book in serializer.validated_data['books']:
222 instance.append(book)
223 return Response(self.get_serializer(instance).data)
225 def perform_destroy(self, instance):
233 class ListItemView(APIView):
234 permission_classes = [IsAuthenticated]
236 def delete(self, request, slug, book):
237 instance = get_object_or_404(
238 models.UserList, slug=slug, user=self.request.user)
239 book = get_object_or_404(catalogue.models.Book, slug=book)
240 instance.remove(book=book)
241 return Response(UserListSerializer(instance).data)
245 class ShelfView(ListAPIView):
246 permission_classes = [IsAuthenticated]
247 serializer_class = BookSerializer
248 pagination_class = None
250 def get_queryset(self):
251 state = self.kwargs['state']
252 if state not in ('reading', 'complete', 'likes'):
254 new_api = self.request.query_params.get('new_api')
255 after = self.request.query_params.get('after')
256 count = int(self.request.query_params.get('count', 50))
258 books = Book.objects.filter(userlistitem__list__user=self.request.user)
260 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
261 .values_list('book_id', flat=True)
262 books = Book.objects.filter(id__in=list(ids)).distinct()
263 books = order_books(books, new_api)
265 books = books_after(books, after, new_api)
267 books = books[:count]
273 class ProgressSerializer(serializers.ModelSerializer):
274 book = serializers.HyperlinkedRelatedField(
276 view_name='catalogue_api_book',
279 book_slug = serializers.SlugRelatedField(
280 queryset=Book.objects.all(),
283 timestamp = serializers.IntegerField(required=False)
286 model = models.Progress
289 'book', 'book_slug', 'last_mode', 'text_percent',
293 'implicit_text_percent',
294 'implicit_text_anchor',
295 'implicit_audio_percent',
296 'implicit_audio_timestamp',
306 class TextProgressSerializer(serializers.ModelSerializer):
308 model = models.Progress
313 read_only_fields = ['text_percent']
315 class AudioProgressSerializer(serializers.ModelSerializer):
317 model = models.Progress
318 fields = ['audio_percent', 'audio_timestamp']
319 read_only_fields = ['audio_percent']
323 class ProgressListView(ListAPIView):
324 permission_classes = [IsAuthenticated]
325 serializer_class = ProgressSerializer
327 def get_queryset(self):
328 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
332 def get_object(self):
334 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
335 except models.Progress.DoesNotExist:
336 book = get_object_or_404(Book, slug=self.kwargs['slug'])
337 return models.Progress(user=self.request.user, book=book)
342 class ProgressView(ProgressMixin, RetrieveAPIView):
343 permission_classes = [IsAuthenticated]
344 serializer_class = ProgressSerializer
348 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
349 permission_classes = [IsAuthenticated]
350 serializer_class = TextProgressSerializer
352 def perform_update(self, serializer):
353 serializer.instance.last_mode = 'text'
358 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
359 permission_classes = [IsAuthenticated]
360 serializer_class = AudioProgressSerializer
362 def perform_update(self, serializer):
363 serializer.instance.last_mode = 'audio'
369 class SyncView(ListAPIView):
370 permission_classes = [IsAuthenticated]
371 sync_id_field = 'slug'
372 sync_id_serializer_field = 'slug'
373 sync_user_field = 'user'
375 def get_queryset(self):
377 timestamp = int(self.request.GET.get('ts'))
381 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
384 return self.get_queryset_for_ts(timestamp)
386 def get_queryset_for_ts(self, timestamp):
387 return self.model.objects.filter(
388 updated_at__gt=timestamp,
390 self.sync_user_field: self.request.user
392 ).order_by('updated_at')
394 def get_instance(self, user, data):
395 sync_id = data.get(self.sync_id_serializer_field)
398 return self.model.objects.filter(**{
399 self.sync_user_field: user,
400 self.sync_id_field: sync_id
403 def post(self, request):
407 instance = self.get_instance(request.user, item)
408 ser = self.get_serializer(
412 ser.is_valid(raise_exception=True)
413 synced_instance = self.model.sync(
418 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
420 'client_id': ser.validated_data['client_id'],
421 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
423 return Response(new_ids)
426 class ProgressSyncView(SyncView):
427 model = models.Progress
428 serializer_class = ProgressSerializer
430 sync_id_field = 'book__slug'
431 sync_id_serializer_field = 'book_slug'
434 class UserListSyncView(SyncView):
435 model = models.UserList
436 serializer_class = UserListSerializer
439 class UserListItemSyncView(SyncView):
440 model = models.UserListItem
441 serializer_class = UserListItemSerializer
443 sync_id_field = 'uuid'
444 sync_id_serializer_field = 'uuid'
445 sync_user_field = 'list__user'
447 def get_queryset_for_ts(self, timestamp):
448 qs = self.model.objects.filter(
449 updated_at__gt=timestamp,
451 self.sync_user_field: self.request.user
454 if self.request.query_params.get('favorites'):
455 qs = qs.filter(list__favorites=True)
456 return qs.order_by('updated_at')
459 class BookmarkSyncView(SyncView):
460 model = bookmarks.models.Bookmark
461 serializer_class = BookmarkSerializer
463 sync_id_field = 'uuid'
464 sync_id_serializer_field = 'uuid'
466 def get_queryset_for_ts(self, timestamp):
467 return self.model.objects.filter(
468 user=self.request.user,
469 created_at__gt=timestamp
470 ).order_by('created_at')