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.db.models import Q
6 from django.http import Http404
7 from django.utils.timezone import now, utc
8 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
9 from rest_framework.permissions import SAFE_METHODS, 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
139 def create(self, validated_data):
140 instance = models.UserList.get_by_name(
141 validated_data['user'],
142 validated_data['name'],
145 if 'books' in validated_data:
146 instance.userlistitem_set.all().delete()
147 for book in validated_data['books']:
148 instance.append(book)
151 def update(self, instance, validated_data):
152 super().update(instance, validated_data)
153 if 'books' in validated_data:
154 instance.userlistitem_set.all().delete()
155 for book in validated_data['books']:
156 instance.append(instance)
160 class UserListBooksSerializer(UserListSerializer):
162 model = models.UserList
166 class UserListItemSerializer(serializers.ModelSerializer):
167 client_id = serializers.CharField(write_only=True, required=False)
168 favorites = serializers.BooleanField(required=False)
169 list_slug = serializers.SlugRelatedField(
170 queryset=models.UserList.objects.all(),
175 timestamp = serializers.IntegerField(required=False)
176 book_slug = serializers.SlugRelatedField(
177 queryset=Book.objects.all(),
184 model = models.UserListItem
208 class ListsView(ListCreateAPIView):
209 permission_classes = [IsAuthenticated]
210 #pagination_class = None
211 serializer_class = UserListSerializer
213 def get_queryset(self):
214 return models.UserList.objects.filter(
215 user=self.request.user,
220 def perform_create(self, serializer):
221 serializer.save(user=self.request.user)
225 class ListView(RetrieveUpdateDestroyAPIView):
226 # TODO: check if can modify
227 permission_classes = [IsAuthenticatedOrReadOnly]
228 serializer_class = UserListSerializer
230 def get_object(self):
231 if self.request.method in SAFE_METHODS:
233 if self.request.user.is_authenticated:
234 q |= Q(user=self.request.user)
235 return get_object_or_404(
238 slug=self.kwargs['slug'],
241 return get_object_or_404(
242 models.UserList.all_objects.all(),
243 slug=self.kwargs['slug'],
244 user=self.request.user)
246 def perform_update(self, serializer):
247 serializer.save(user=self.request.user)
249 def post(self, request, slug):
250 serializer = UserListBooksSerializer(data=request.data)
251 serializer.is_valid(raise_exception=True)
252 instance = self.get_object()
253 for book in serializer.validated_data['books']:
254 instance.append(book)
255 return Response(self.get_serializer(instance).data)
257 def perform_destroy(self, instance):
258 instance.deleted = True
259 instance.updated_at = now()
264 class ListItemView(APIView):
265 permission_classes = [IsAuthenticated]
267 def delete(self, request, slug, book):
268 instance = get_object_or_404(
269 models.UserList, slug=slug, user=self.request.user)
270 book = get_object_or_404(catalogue.models.Book, slug=book)
271 instance.remove(book=book)
272 return Response(UserListSerializer(instance).data)
276 class ShelfView(ListAPIView):
277 permission_classes = [IsAuthenticated]
278 serializer_class = BookSerializer
279 pagination_class = None
281 def get_queryset(self):
282 state = self.kwargs['state']
283 if state not in ('reading', 'complete', 'likes'):
285 new_api = self.request.query_params.get('new_api')
286 after = self.request.query_params.get('after')
287 count = int(self.request.query_params.get('count', 50))
289 books = Book.objects.filter(userlistitem__list__user=self.request.user)
291 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
292 .values_list('book_id', flat=True)
293 books = Book.objects.filter(id__in=list(ids)).distinct()
294 books = order_books(books, new_api)
296 books = books_after(books, after, new_api)
298 books = books[:count]
304 class ProgressSerializer(serializers.ModelSerializer):
305 book = serializers.HyperlinkedRelatedField(
307 view_name='catalogue_api_book',
310 book_slug = serializers.SlugRelatedField(
311 queryset=Book.objects.all(),
314 timestamp = serializers.IntegerField(required=False)
317 model = models.Progress
320 'book', 'book_slug', 'last_mode', 'text_percent',
324 'implicit_text_percent',
325 'implicit_text_anchor',
326 'implicit_audio_percent',
327 'implicit_audio_timestamp',
337 class TextProgressSerializer(serializers.ModelSerializer):
339 model = models.Progress
344 read_only_fields = ['text_percent']
346 class AudioProgressSerializer(serializers.ModelSerializer):
348 model = models.Progress
349 fields = ['audio_percent', 'audio_timestamp']
350 read_only_fields = ['audio_percent']
354 class ProgressListView(ListAPIView):
355 permission_classes = [IsAuthenticated]
356 serializer_class = ProgressSerializer
358 def get_queryset(self):
359 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
363 def get_object(self):
365 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
366 except models.Progress.DoesNotExist:
367 book = get_object_or_404(Book, slug=self.kwargs['slug'])
368 return models.Progress(user=self.request.user, book=book)
373 class ProgressView(ProgressMixin, RetrieveAPIView):
374 permission_classes = [IsAuthenticated]
375 serializer_class = ProgressSerializer
379 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
380 permission_classes = [IsAuthenticated]
381 serializer_class = TextProgressSerializer
383 def perform_update(self, serializer):
384 serializer.instance.last_mode = 'text'
389 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
390 permission_classes = [IsAuthenticated]
391 serializer_class = AudioProgressSerializer
393 def perform_update(self, serializer):
394 serializer.instance.last_mode = 'audio'
400 class SyncView(ListAPIView):
401 permission_classes = [IsAuthenticated]
402 sync_id_field = 'slug'
403 sync_id_serializer_field = 'slug'
404 sync_user_field = 'user'
406 def get_queryset(self):
408 timestamp = int(self.request.GET.get('ts'))
412 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
415 return self.get_queryset_for_ts(timestamp)
417 def get_queryset_for_ts(self, timestamp):
418 return self.model.objects.filter(
419 updated_at__gt=timestamp,
421 self.sync_user_field: self.request.user
423 ).order_by('updated_at')
425 def get_instance(self, user, data):
426 sync_id = data.get(self.sync_id_serializer_field)
429 return self.model.objects.filter(**{
430 self.sync_user_field: user,
431 self.sync_id_field: sync_id
434 def post(self, request):
437 if not isinstance(data, list):
438 raise serializers.ValidationError('Payload should be a list')
440 instance = self.get_instance(request.user, item)
441 ser = self.get_serializer(
445 ser.is_valid(raise_exception=True)
446 synced_instance = self.model.sync(
451 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
453 'client_id': ser.validated_data['client_id'],
454 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
456 return Response(new_ids)
459 class ProgressSyncView(SyncView):
460 model = models.Progress
461 serializer_class = ProgressSerializer
463 sync_id_field = 'book__slug'
464 sync_id_serializer_field = 'book_slug'
467 class UserListSyncView(SyncView):
468 model = models.UserList
469 serializer_class = UserListSerializer
472 class UserListItemSyncView(SyncView):
473 model = models.UserListItem
474 serializer_class = UserListItemSerializer
476 sync_id_field = 'uuid'
477 sync_id_serializer_field = 'uuid'
478 sync_user_field = 'list__user'
480 def get_queryset_for_ts(self, timestamp):
481 qs = self.model.all_objects.filter(
482 updated_at__gt=timestamp,
484 self.sync_user_field: self.request.user
487 if self.request.query_params.get('favorites'):
488 qs = qs.filter(list__favorites=True)
489 return qs.order_by('updated_at')
492 class BookmarkSyncView(SyncView):
493 model = bookmarks.models.Bookmark
494 serializer_class = BookmarkSerializer
496 sync_id_field = 'uuid'
497 sync_id_serializer_field = 'uuid'
499 def get_instance(self, user, data):
500 ret = super().get_instance(user, data)
502 if data.get('location'):
503 ret = self.model.get_by_location(user, data['location'])