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
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 if 'books' in validated_data:
143 instance.userlistitem_set.all().delete()
144 for book in validated_data['books']:
145 instance.append(book)
148 def update(self, instance, validated_data):
149 if 'books' in validated_data:
150 instance.userlistitem_set.all().delete()
151 for book in validated_data['books']:
152 instance.append(instance)
155 class UserListBooksSerializer(UserListSerializer):
157 model = models.UserList
161 class UserListItemSerializer(serializers.ModelSerializer):
162 client_id = serializers.CharField(write_only=True, required=False)
163 favorites = serializers.BooleanField(required=False)
164 list_slug = serializers.SlugRelatedField(
165 queryset=models.UserList.objects.all(),
170 timestamp = serializers.IntegerField(required=False)
171 book_slug = serializers.SlugRelatedField(
172 queryset=Book.objects.all(),
179 model = models.UserListItem
203 class ListsView(ListCreateAPIView):
204 permission_classes = [IsAuthenticated]
205 #pagination_class = None
206 serializer_class = UserListSerializer
208 def get_queryset(self):
209 return models.UserList.objects.filter(
210 user=self.request.user,
215 def perform_create(self, serializer):
216 serializer.save(user=self.request.user)
220 class ListView(RetrieveUpdateDestroyAPIView):
221 # TODO: check if can modify
222 permission_classes = [IsAuthenticatedOrReadOnly]
223 serializer_class = UserListSerializer
225 def get_object(self):
226 if self.request.method in SAFE_METHODS:
228 if self.request.user.is_authenticated:
229 q |= Q(user=self.request.user)
230 return get_object_or_404(
233 slug=self.kwargs['slug'],
236 return get_object_or_404(
237 models.UserList.all_objects.all(),
238 slug=self.kwargs['slug'],
239 user=self.request.user)
241 def perform_update(self, serializer):
242 serializer.save(user=self.request.user)
244 def post(self, request, slug):
245 serializer = UserListBooksSerializer(data=request.data)
246 serializer.is_valid(raise_exception=True)
247 instance = self.get_object()
248 for book in serializer.validated_data['books']:
249 instance.append(book)
250 return Response(self.get_serializer(instance).data)
252 def perform_destroy(self, instance):
253 instance.deleted = True
254 instance.updated_at = now()
259 class ListItemView(APIView):
260 permission_classes = [IsAuthenticated]
262 def delete(self, request, slug, book):
263 instance = get_object_or_404(
264 models.UserList, slug=slug, user=self.request.user)
265 book = get_object_or_404(catalogue.models.Book, slug=book)
266 instance.remove(book=book)
267 return Response(UserListSerializer(instance).data)
271 class ShelfView(ListAPIView):
272 permission_classes = [IsAuthenticated]
273 serializer_class = BookSerializer
274 pagination_class = None
276 def get_queryset(self):
277 state = self.kwargs['state']
278 if state not in ('reading', 'complete', 'likes'):
280 new_api = self.request.query_params.get('new_api')
281 after = self.request.query_params.get('after')
282 count = int(self.request.query_params.get('count', 50))
284 books = Book.objects.filter(userlistitem__list__user=self.request.user)
286 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
287 .values_list('book_id', flat=True)
288 books = Book.objects.filter(id__in=list(ids)).distinct()
289 books = order_books(books, new_api)
291 books = books_after(books, after, new_api)
293 books = books[:count]
299 class ProgressSerializer(serializers.ModelSerializer):
300 book = serializers.HyperlinkedRelatedField(
302 view_name='catalogue_api_book',
305 book_slug = serializers.SlugRelatedField(
306 queryset=Book.objects.all(),
309 timestamp = serializers.IntegerField(required=False)
312 model = models.Progress
315 'book', 'book_slug', 'last_mode', 'text_percent',
319 'implicit_text_percent',
320 'implicit_text_anchor',
321 'implicit_audio_percent',
322 'implicit_audio_timestamp',
332 class TextProgressSerializer(serializers.ModelSerializer):
334 model = models.Progress
339 read_only_fields = ['text_percent']
341 class AudioProgressSerializer(serializers.ModelSerializer):
343 model = models.Progress
344 fields = ['audio_percent', 'audio_timestamp']
345 read_only_fields = ['audio_percent']
349 class ProgressListView(ListAPIView):
350 permission_classes = [IsAuthenticated]
351 serializer_class = ProgressSerializer
353 def get_queryset(self):
354 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
358 def get_object(self):
360 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
361 except models.Progress.DoesNotExist:
362 book = get_object_or_404(Book, slug=self.kwargs['slug'])
363 return models.Progress(user=self.request.user, book=book)
368 class ProgressView(ProgressMixin, RetrieveAPIView):
369 permission_classes = [IsAuthenticated]
370 serializer_class = ProgressSerializer
374 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
375 permission_classes = [IsAuthenticated]
376 serializer_class = TextProgressSerializer
378 def perform_update(self, serializer):
379 serializer.instance.last_mode = 'text'
384 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
385 permission_classes = [IsAuthenticated]
386 serializer_class = AudioProgressSerializer
388 def perform_update(self, serializer):
389 serializer.instance.last_mode = 'audio'
395 class SyncView(ListAPIView):
396 permission_classes = [IsAuthenticated]
397 sync_id_field = 'slug'
398 sync_id_serializer_field = 'slug'
399 sync_user_field = 'user'
401 def get_queryset(self):
403 timestamp = int(self.request.GET.get('ts'))
407 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
410 return self.get_queryset_for_ts(timestamp)
412 def get_queryset_for_ts(self, timestamp):
413 return self.model.objects.filter(
414 updated_at__gt=timestamp,
416 self.sync_user_field: self.request.user
418 ).order_by('updated_at')
420 def get_instance(self, user, data):
421 sync_id = data.get(self.sync_id_serializer_field)
424 return self.model.objects.filter(**{
425 self.sync_user_field: user,
426 self.sync_id_field: sync_id
429 def post(self, request):
432 if not isinstance(data, list):
433 raise serializers.ValidationError('Payload should be a list')
435 instance = self.get_instance(request.user, item)
436 ser = self.get_serializer(
440 ser.is_valid(raise_exception=True)
441 synced_instance = self.model.sync(
446 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
448 'client_id': ser.validated_data['client_id'],
449 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
451 return Response(new_ids)
454 class ProgressSyncView(SyncView):
455 model = models.Progress
456 serializer_class = ProgressSerializer
458 sync_id_field = 'book__slug'
459 sync_id_serializer_field = 'book_slug'
462 class UserListSyncView(SyncView):
463 model = models.UserList
464 serializer_class = UserListSerializer
467 class UserListItemSyncView(SyncView):
468 model = models.UserListItem
469 serializer_class = UserListItemSerializer
471 sync_id_field = 'uuid'
472 sync_id_serializer_field = 'uuid'
473 sync_user_field = 'list__user'
475 def get_queryset_for_ts(self, timestamp):
476 qs = self.model.objects.filter(
477 updated_at__gt=timestamp,
479 self.sync_user_field: self.request.user
482 if self.request.query_params.get('favorites'):
483 qs = qs.filter(list__favorites=True)
484 return qs.order_by('updated_at')
487 class BookmarkSyncView(SyncView):
488 model = bookmarks.models.Bookmark
489 serializer_class = BookmarkSerializer
491 sync_id_field = 'uuid'
492 sync_id_serializer_field = 'uuid'
494 def get_instance(self, user, data):
495 ret = super().get_instance(user, data)
497 if data.get('location'):
498 ret = self.model.get_by_location(user, data['location'])