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
26 class SettingsSerializer(serializers.ModelSerializer):
28 model = models.UserProfile
29 fields = ['notifications']
32 class SettingsView(RetrieveUpdateAPIView):
33 permission_classes = [IsAuthenticated]
34 serializer_class = SettingsSerializer
37 return models.UserProfile.get_for(self.request.user)
41 class LikeView(APIView):
42 permission_classes = [IsAuthenticated]
44 def get(self, request, slug):
45 book = get_object_or_404(Book, slug=slug)
46 return Response({"likes": likes(request.user, book)})
48 def post(self, request, slug):
49 book = get_object_or_404(Book, slug=slug)
50 action = request.query_params.get('action', 'like')
52 models.UserList.like(request.user, book)
53 elif action == 'unlike':
54 models.UserList.unlike(request.user, book)
59 class LikeView2(APIView):
60 permission_classes = [IsAuthenticated]
62 def get(self, request, slug):
63 book = get_object_or_404(Book, slug=slug)
64 return Response({"likes": likes(request.user, book)})
66 def put(self, request, slug):
67 book = get_object_or_404(Book, slug=slug)
68 models.UserList.like(request.user, book)
69 return Response({"likes": likes(request.user, book)})
71 def delete(self, request, slug):
72 book = get_object_or_404(Book, slug=slug)
73 models.UserList.unlike(request.user, book)
74 return Response({"likes": likes(request.user, book)})
78 class LikesView(APIView):
79 permission_classes = [IsAuthenticated]
81 def get(self, request):
82 slugs = request.GET.getlist('slug')
83 books = Book.objects.filter(slug__in=slugs)
84 books = {b.id: b.slug for b in books}
86 res = get_sets_for_book_ids(ids, request.user)
87 res = {books[bid]: v for bid, v in res.items()}
93 class MyLikesView(APIView):
94 permission_classes = [IsAuthenticated]
96 def get(self, request):
97 ul = models.UserList.get_favorites_list(request.user)
101 ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
105 class UserListItemsField(serializers.Field):
106 def to_representation(self, value):
107 return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
109 def to_internal_value(self, value):
110 return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
113 class UserListSerializer(serializers.ModelSerializer):
114 client_id = serializers.CharField(write_only=True, required=False)
115 books = UserListItemsField(source='*', required=False)
116 timestamp = serializers.IntegerField(required=False)
119 model = models.UserList
129 read_only_fields = ['favorites']
136 def create(self, validated_data):
137 instance = models.UserList.get_by_name(
138 validated_data['user'],
139 validated_data['name'],
142 instance.userlistitem_set.all().delete()
143 for book in validated_data['books']:
144 instance.append(book)
147 def update(self, instance, validated_data):
148 instance.userlistitem_set.all().delete()
149 for book in validated_data['books']:
150 instance.append(instance)
153 class UserListBooksSerializer(UserListSerializer):
155 model = models.UserList
159 class UserListItemSerializer(serializers.ModelSerializer):
160 client_id = serializers.CharField(write_only=True, required=False)
161 favorites = serializers.BooleanField(required=False)
162 list_slug = serializers.SlugRelatedField(
163 queryset=models.UserList.objects.all(),
168 timestamp = serializers.IntegerField(required=False)
169 book_slug = serializers.SlugRelatedField(
170 queryset=Book.objects.all(),
177 model = models.UserListItem
201 class ListsView(ListCreateAPIView):
202 permission_classes = [IsAuthenticated]
203 #pagination_class = None
204 serializer_class = UserListSerializer
206 def get_queryset(self):
207 return models.UserList.objects.filter(
208 user=self.request.user,
213 def perform_create(self, serializer):
214 serializer.save(user=self.request.user)
218 class ListView(RetrieveUpdateDestroyAPIView):
219 # TODO: check if can modify
220 permission_classes = [IsAuthenticated]
221 serializer_class = UserListSerializer
223 def get_object(self):
224 return get_object_or_404(
226 slug=self.kwargs['slug'],
227 user=self.request.user)
229 def perform_update(self, serializer):
230 serializer.save(user=self.request.user)
232 def post(self, request, slug):
233 serializer = UserListBooksSerializer(data=request.data)
234 serializer.is_valid(raise_exception=True)
235 instance = self.get_object()
236 for book in serializer.validated_data['books']:
237 instance.append(book)
238 return Response(self.get_serializer(instance).data)
240 def perform_destroy(self, instance):
248 class ListItemView(APIView):
249 permission_classes = [IsAuthenticated]
251 def delete(self, request, slug, book):
252 instance = get_object_or_404(
253 models.UserList, slug=slug, user=self.request.user)
254 book = get_object_or_404(catalogue.models.Book, slug=book)
255 instance.remove(book=book)
256 return Response(UserListSerializer(instance).data)
260 class ShelfView(ListAPIView):
261 permission_classes = [IsAuthenticated]
262 serializer_class = BookSerializer
263 pagination_class = None
265 def get_queryset(self):
266 state = self.kwargs['state']
267 if state not in ('reading', 'complete', 'likes'):
269 new_api = self.request.query_params.get('new_api')
270 after = self.request.query_params.get('after')
271 count = int(self.request.query_params.get('count', 50))
273 books = Book.objects.filter(userlistitem__list__user=self.request.user)
275 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
276 .values_list('book_id', flat=True)
277 books = Book.objects.filter(id__in=list(ids)).distinct()
278 books = order_books(books, new_api)
280 books = books_after(books, after, new_api)
282 books = books[:count]
288 class ProgressSerializer(serializers.ModelSerializer):
289 book = serializers.HyperlinkedRelatedField(
291 view_name='catalogue_api_book',
294 book_slug = serializers.SlugRelatedField(
295 queryset=Book.objects.all(),
298 timestamp = serializers.IntegerField(required=False)
301 model = models.Progress
304 'book', 'book_slug', 'last_mode', 'text_percent',
308 'implicit_text_percent',
309 'implicit_text_anchor',
310 'implicit_audio_percent',
311 'implicit_audio_timestamp',
321 class TextProgressSerializer(serializers.ModelSerializer):
323 model = models.Progress
328 read_only_fields = ['text_percent']
330 class AudioProgressSerializer(serializers.ModelSerializer):
332 model = models.Progress
333 fields = ['audio_percent', 'audio_timestamp']
334 read_only_fields = ['audio_percent']
338 class ProgressListView(ListAPIView):
339 permission_classes = [IsAuthenticated]
340 serializer_class = ProgressSerializer
342 def get_queryset(self):
343 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
347 def get_object(self):
349 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
350 except models.Progress.DoesNotExist:
351 book = get_object_or_404(Book, slug=self.kwargs['slug'])
352 return models.Progress(user=self.request.user, book=book)
357 class ProgressView(ProgressMixin, RetrieveAPIView):
358 permission_classes = [IsAuthenticated]
359 serializer_class = ProgressSerializer
363 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
364 permission_classes = [IsAuthenticated]
365 serializer_class = TextProgressSerializer
367 def perform_update(self, serializer):
368 serializer.instance.last_mode = 'text'
373 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
374 permission_classes = [IsAuthenticated]
375 serializer_class = AudioProgressSerializer
377 def perform_update(self, serializer):
378 serializer.instance.last_mode = 'audio'
384 class SyncView(ListAPIView):
385 permission_classes = [IsAuthenticated]
386 sync_id_field = 'slug'
387 sync_id_serializer_field = 'slug'
388 sync_user_field = 'user'
390 def get_queryset(self):
392 timestamp = int(self.request.GET.get('ts'))
396 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
399 return self.get_queryset_for_ts(timestamp)
401 def get_queryset_for_ts(self, timestamp):
402 return self.model.objects.filter(
403 updated_at__gt=timestamp,
405 self.sync_user_field: self.request.user
407 ).order_by('updated_at')
409 def get_instance(self, user, data):
410 sync_id = data.get(self.sync_id_serializer_field)
413 return self.model.objects.filter(**{
414 self.sync_user_field: user,
415 self.sync_id_field: sync_id
418 def post(self, request):
421 if not isinstance(data, list):
422 raise serializers.ValidationError('Payload should be a list')
424 instance = self.get_instance(request.user, item)
425 ser = self.get_serializer(
429 ser.is_valid(raise_exception=True)
430 synced_instance = self.model.sync(
435 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
437 'client_id': ser.validated_data['client_id'],
438 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
440 return Response(new_ids)
443 class ProgressSyncView(SyncView):
444 model = models.Progress
445 serializer_class = ProgressSerializer
447 sync_id_field = 'book__slug'
448 sync_id_serializer_field = 'book_slug'
451 class UserListSyncView(SyncView):
452 model = models.UserList
453 serializer_class = UserListSerializer
456 class UserListItemSyncView(SyncView):
457 model = models.UserListItem
458 serializer_class = UserListItemSerializer
460 sync_id_field = 'uuid'
461 sync_id_serializer_field = 'uuid'
462 sync_user_field = 'list__user'
464 def get_queryset_for_ts(self, timestamp):
465 qs = self.model.objects.filter(
466 updated_at__gt=timestamp,
468 self.sync_user_field: self.request.user
471 if self.request.query_params.get('favorites'):
472 qs = qs.filter(list__favorites=True)
473 return qs.order_by('updated_at')
476 class BookmarkSyncView(SyncView):
477 model = bookmarks.models.Bookmark
478 serializer_class = BookmarkSerializer
480 sync_id_field = 'uuid'
481 sync_id_serializer_field = 'uuid'
483 def get_instance(self, user, data):
484 ret = super().get_instance(user, data)
486 if data.get('location'):
487 ret = self.model.get_by_location(user, data['location'])