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
5 from django.http import Http404
6 from django.utils.timezone import now, utc
7 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
8 from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
9 from rest_framework.response import Response
10 from rest_framework import serializers
11 from rest_framework.views import APIView
12 from api.models import BookUserData
13 from api.utils import vary_on_auth, never_cache
14 from catalogue.api.helpers import order_books, books_after
15 from catalogue.api.serializers import BookSerializer
16 from catalogue.models import Book
17 import catalogue.models
18 from social.views import get_sets_for_book_ids
19 from social.utils import likes
20 from social import models
21 import bookmarks.models
22 from bookmarks.api.views import BookmarkSerializer
25 class SettingsSerializer(serializers.ModelSerializer):
27 model = models.UserProfile
28 fields = ['notifications']
31 class SettingsView(RetrieveUpdateAPIView):
32 permission_classes = [IsAuthenticated]
33 serializer_class = SettingsSerializer
36 return models.UserProfile.get_for(self.request.user)
40 class LikeView(APIView):
41 permission_classes = [IsAuthenticated]
43 def get(self, request, slug):
44 book = get_object_or_404(Book, slug=slug)
45 return Response({"likes": likes(request.user, book)})
47 def post(self, request, slug):
48 book = get_object_or_404(Book, slug=slug)
49 action = request.query_params.get('action', 'like')
51 models.UserList.like(request.user, book)
52 elif action == 'unlike':
53 models.UserList.unlike(request.user, book)
58 class LikeView2(APIView):
59 permission_classes = [IsAuthenticated]
61 def get(self, request, slug):
62 book = get_object_or_404(Book, slug=slug)
63 return Response({"likes": likes(request.user, book)})
65 def put(self, request, slug):
66 book = get_object_or_404(Book, slug=slug)
67 models.UserList.like(request.user, book)
68 return Response({"likes": likes(request.user, book)})
70 def delete(self, request, slug):
71 book = get_object_or_404(Book, slug=slug)
72 models.UserList.unlike(request.user, book)
73 return Response({"likes": likes(request.user, book)})
77 class LikesView(APIView):
78 permission_classes = [IsAuthenticated]
80 def get(self, request):
81 slugs = request.GET.getlist('slug')
82 books = Book.objects.filter(slug__in=slugs)
83 books = {b.id: b.slug for b in books}
85 res = get_sets_for_book_ids(ids, request.user)
86 res = {books[bid]: v for bid, v in res.items()}
92 class MyLikesView(APIView):
93 permission_classes = [IsAuthenticated]
95 def get(self, request):
96 ul = models.UserList.get_favorites_list(request.user)
100 ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
104 class UserListItemsField(serializers.Field):
105 def to_representation(self, value):
106 return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
108 def to_internal_value(self, value):
109 return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
112 class UserListSerializer(serializers.ModelSerializer):
113 client_id = serializers.CharField(write_only=True, required=False)
114 books = UserListItemsField(source='*', required=False)
115 timestamp = serializers.IntegerField(required=False)
118 model = models.UserList
128 read_only_fields = ['favorites']
135 def create(self, validated_data):
136 instance = models.UserList.get_by_name(
137 validated_data['user'],
138 validated_data['name'],
141 instance.userlistitem_set.all().delete()
142 for book in validated_data['books']:
143 instance.append(book)
146 def update(self, instance, validated_data):
147 instance.userlistitem_set.all().delete()
148 for book in validated_data['books']:
149 instance.append(instance)
152 class UserListBooksSerializer(UserListSerializer):
154 model = models.UserList
158 class UserListItemSerializer(serializers.ModelSerializer):
159 client_id = serializers.CharField(write_only=True, required=False)
160 favorites = serializers.BooleanField(required=False)
161 list_slug = serializers.SlugRelatedField(
162 queryset=models.UserList.objects.all(),
167 timestamp = serializers.IntegerField(required=False)
168 book_slug = serializers.SlugRelatedField(
169 queryset=Book.objects.all(),
176 model = models.UserListItem
200 class ListsView(ListCreateAPIView):
201 permission_classes = [IsAuthenticated]
202 #pagination_class = None
203 serializer_class = UserListSerializer
205 def get_queryset(self):
206 return models.UserList.objects.filter(
207 user=self.request.user,
212 def perform_create(self, serializer):
213 serializer.save(user=self.request.user)
217 class ListView(RetrieveUpdateDestroyAPIView):
218 # TODO: check if can modify
219 permission_classes = [IsAuthenticated]
220 serializer_class = UserListSerializer
222 def get_object(self):
223 return get_object_or_404(
225 slug=self.kwargs['slug'],
226 user=self.request.user)
228 def perform_update(self, serializer):
229 serializer.save(user=self.request.user)
231 def post(self, request, slug):
232 serializer = UserListBooksSerializer(data=request.data)
233 serializer.is_valid(raise_exception=True)
234 instance = self.get_object()
235 for book in serializer.validated_data['books']:
236 instance.append(book)
237 return Response(self.get_serializer(instance).data)
239 def perform_destroy(self, instance):
240 instance.deleted = True
241 instance.updated_at = now()
246 class ListItemView(APIView):
247 permission_classes = [IsAuthenticated]
249 def delete(self, request, slug, book):
250 instance = get_object_or_404(
251 models.UserList, slug=slug, user=self.request.user)
252 book = get_object_or_404(catalogue.models.Book, slug=book)
253 instance.remove(book=book)
254 return Response(UserListSerializer(instance).data)
258 class ShelfView(ListAPIView):
259 permission_classes = [IsAuthenticated]
260 serializer_class = BookSerializer
261 pagination_class = None
263 def get_queryset(self):
264 state = self.kwargs['state']
265 if state not in ('reading', 'complete', 'likes'):
267 new_api = self.request.query_params.get('new_api')
268 after = self.request.query_params.get('after')
269 count = int(self.request.query_params.get('count', 50))
271 books = Book.objects.filter(userlistitem__list__user=self.request.user)
273 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
274 .values_list('book_id', flat=True)
275 books = Book.objects.filter(id__in=list(ids)).distinct()
276 books = order_books(books, new_api)
278 books = books_after(books, after, new_api)
280 books = books[:count]
286 class ProgressSerializer(serializers.ModelSerializer):
287 book = serializers.HyperlinkedRelatedField(
289 view_name='catalogue_api_book',
292 book_slug = serializers.SlugRelatedField(
293 queryset=Book.objects.all(),
296 timestamp = serializers.IntegerField(required=False)
299 model = models.Progress
302 'book', 'book_slug', 'last_mode', 'text_percent',
306 'implicit_text_percent',
307 'implicit_text_anchor',
308 'implicit_audio_percent',
309 'implicit_audio_timestamp',
319 class TextProgressSerializer(serializers.ModelSerializer):
321 model = models.Progress
326 read_only_fields = ['text_percent']
328 class AudioProgressSerializer(serializers.ModelSerializer):
330 model = models.Progress
331 fields = ['audio_percent', 'audio_timestamp']
332 read_only_fields = ['audio_percent']
336 class ProgressListView(ListAPIView):
337 permission_classes = [IsAuthenticated]
338 serializer_class = ProgressSerializer
340 def get_queryset(self):
341 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
345 def get_object(self):
347 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
348 except models.Progress.DoesNotExist:
349 book = get_object_or_404(Book, slug=self.kwargs['slug'])
350 return models.Progress(user=self.request.user, book=book)
355 class ProgressView(ProgressMixin, RetrieveAPIView):
356 permission_classes = [IsAuthenticated]
357 serializer_class = ProgressSerializer
361 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
362 permission_classes = [IsAuthenticated]
363 serializer_class = TextProgressSerializer
365 def perform_update(self, serializer):
366 serializer.instance.last_mode = 'text'
371 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
372 permission_classes = [IsAuthenticated]
373 serializer_class = AudioProgressSerializer
375 def perform_update(self, serializer):
376 serializer.instance.last_mode = 'audio'
382 class SyncView(ListAPIView):
383 permission_classes = [IsAuthenticated]
384 sync_id_field = 'slug'
385 sync_id_serializer_field = 'slug'
386 sync_user_field = 'user'
388 def get_queryset(self):
390 timestamp = int(self.request.GET.get('ts'))
394 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
397 return self.get_queryset_for_ts(timestamp)
399 def get_queryset_for_ts(self, timestamp):
400 return self.model.objects.filter(
401 updated_at__gt=timestamp,
403 self.sync_user_field: self.request.user
405 ).order_by('updated_at')
407 def get_instance(self, user, data):
408 sync_id = data.get(self.sync_id_serializer_field)
411 return self.model.objects.filter(**{
412 self.sync_user_field: user,
413 self.sync_id_field: sync_id
416 def post(self, request):
419 if not isinstance(data, list):
420 raise serializers.ValidationError('Payload should be a list')
422 instance = self.get_instance(request.user, item)
423 ser = self.get_serializer(
427 ser.is_valid(raise_exception=True)
428 synced_instance = self.model.sync(
433 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
435 'client_id': ser.validated_data['client_id'],
436 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
438 return Response(new_ids)
441 class ProgressSyncView(SyncView):
442 model = models.Progress
443 serializer_class = ProgressSerializer
445 sync_id_field = 'book__slug'
446 sync_id_serializer_field = 'book_slug'
449 class UserListSyncView(SyncView):
450 model = models.UserList
451 serializer_class = UserListSerializer
454 class UserListItemSyncView(SyncView):
455 model = models.UserListItem
456 serializer_class = UserListItemSerializer
458 sync_id_field = 'uuid'
459 sync_id_serializer_field = 'uuid'
460 sync_user_field = 'list__user'
462 def get_queryset_for_ts(self, timestamp):
463 qs = self.model.objects.filter(
464 updated_at__gt=timestamp,
466 self.sync_user_field: self.request.user
469 if self.request.query_params.get('favorites'):
470 qs = qs.filter(list__favorites=True)
471 return qs.order_by('updated_at')
474 class BookmarkSyncView(SyncView):
475 model = bookmarks.models.Bookmark
476 serializer_class = BookmarkSerializer
478 sync_id_field = 'uuid'
479 sync_id_serializer_field = 'uuid'
481 def get_instance(self, user, data):
482 ret = super().get_instance(user, data)
484 if data.get('location'):
485 ret = self.model.get_by_location(user, data['location'])