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 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 = [IsAuthenticatedOrReadOnly]
221 serializer_class = UserListSerializer
223 def get_object(self):
224 if self.request.method in SAFE_METHODS:
226 if self.request.user.is_authenticated:
227 q |= Q(user=self.request.user)
228 return get_object_or_404(
231 slug=self.kwargs['slug'],
234 return get_object_or_404(
236 slug=self.kwargs['slug'],
237 user=self.request.user)
239 def perform_update(self, serializer):
240 serializer.save(user=self.request.user)
242 def post(self, request, slug):
243 serializer = UserListBooksSerializer(data=request.data)
244 serializer.is_valid(raise_exception=True)
245 instance = self.get_object()
246 for book in serializer.validated_data['books']:
247 instance.append(book)
248 return Response(self.get_serializer(instance).data)
250 def perform_destroy(self, instance):
251 instance.deleted = True
252 instance.updated_at = now()
257 class ListItemView(APIView):
258 permission_classes = [IsAuthenticated]
260 def delete(self, request, slug, book):
261 instance = get_object_or_404(
262 models.UserList, slug=slug, user=self.request.user)
263 book = get_object_or_404(catalogue.models.Book, slug=book)
264 instance.remove(book=book)
265 return Response(UserListSerializer(instance).data)
269 class ShelfView(ListAPIView):
270 permission_classes = [IsAuthenticated]
271 serializer_class = BookSerializer
272 pagination_class = None
274 def get_queryset(self):
275 state = self.kwargs['state']
276 if state not in ('reading', 'complete', 'likes'):
278 new_api = self.request.query_params.get('new_api')
279 after = self.request.query_params.get('after')
280 count = int(self.request.query_params.get('count', 50))
282 books = Book.objects.filter(userlistitem__list__user=self.request.user)
284 ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
285 .values_list('book_id', flat=True)
286 books = Book.objects.filter(id__in=list(ids)).distinct()
287 books = order_books(books, new_api)
289 books = books_after(books, after, new_api)
291 books = books[:count]
297 class ProgressSerializer(serializers.ModelSerializer):
298 book = serializers.HyperlinkedRelatedField(
300 view_name='catalogue_api_book',
303 book_slug = serializers.SlugRelatedField(
304 queryset=Book.objects.all(),
307 timestamp = serializers.IntegerField(required=False)
310 model = models.Progress
313 'book', 'book_slug', 'last_mode', 'text_percent',
317 'implicit_text_percent',
318 'implicit_text_anchor',
319 'implicit_audio_percent',
320 'implicit_audio_timestamp',
330 class TextProgressSerializer(serializers.ModelSerializer):
332 model = models.Progress
337 read_only_fields = ['text_percent']
339 class AudioProgressSerializer(serializers.ModelSerializer):
341 model = models.Progress
342 fields = ['audio_percent', 'audio_timestamp']
343 read_only_fields = ['audio_percent']
347 class ProgressListView(ListAPIView):
348 permission_classes = [IsAuthenticated]
349 serializer_class = ProgressSerializer
351 def get_queryset(self):
352 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
356 def get_object(self):
358 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
359 except models.Progress.DoesNotExist:
360 book = get_object_or_404(Book, slug=self.kwargs['slug'])
361 return models.Progress(user=self.request.user, book=book)
366 class ProgressView(ProgressMixin, RetrieveAPIView):
367 permission_classes = [IsAuthenticated]
368 serializer_class = ProgressSerializer
372 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
373 permission_classes = [IsAuthenticated]
374 serializer_class = TextProgressSerializer
376 def perform_update(self, serializer):
377 serializer.instance.last_mode = 'text'
382 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
383 permission_classes = [IsAuthenticated]
384 serializer_class = AudioProgressSerializer
386 def perform_update(self, serializer):
387 serializer.instance.last_mode = 'audio'
393 class SyncView(ListAPIView):
394 permission_classes = [IsAuthenticated]
395 sync_id_field = 'slug'
396 sync_id_serializer_field = 'slug'
397 sync_user_field = 'user'
399 def get_queryset(self):
401 timestamp = int(self.request.GET.get('ts'))
405 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
408 return self.get_queryset_for_ts(timestamp)
410 def get_queryset_for_ts(self, timestamp):
411 return self.model.objects.filter(
412 updated_at__gt=timestamp,
414 self.sync_user_field: self.request.user
416 ).order_by('updated_at')
418 def get_instance(self, user, data):
419 sync_id = data.get(self.sync_id_serializer_field)
422 return self.model.objects.filter(**{
423 self.sync_user_field: user,
424 self.sync_id_field: sync_id
427 def post(self, request):
430 if not isinstance(data, list):
431 raise serializers.ValidationError('Payload should be a list')
433 instance = self.get_instance(request.user, item)
434 ser = self.get_serializer(
438 ser.is_valid(raise_exception=True)
439 synced_instance = self.model.sync(
444 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
446 'client_id': ser.validated_data['client_id'],
447 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
449 return Response(new_ids)
452 class ProgressSyncView(SyncView):
453 model = models.Progress
454 serializer_class = ProgressSerializer
456 sync_id_field = 'book__slug'
457 sync_id_serializer_field = 'book_slug'
460 class UserListSyncView(SyncView):
461 model = models.UserList
462 serializer_class = UserListSerializer
465 class UserListItemSyncView(SyncView):
466 model = models.UserListItem
467 serializer_class = UserListItemSerializer
469 sync_id_field = 'uuid'
470 sync_id_serializer_field = 'uuid'
471 sync_user_field = 'list__user'
473 def get_queryset_for_ts(self, timestamp):
474 qs = self.model.objects.filter(
475 updated_at__gt=timestamp,
477 self.sync_user_field: self.request.user
480 if self.request.query_params.get('favorites'):
481 qs = qs.filter(list__favorites=True)
482 return qs.order_by('updated_at')
485 class BookmarkSyncView(SyncView):
486 model = bookmarks.models.Bookmark
487 serializer_class = BookmarkSerializer
489 sync_id_field = 'uuid'
490 sync_id_serializer_field = 'uuid'
492 def get_instance(self, user, data):
493 ret = super().get_instance(user, data)
495 if data.get('location'):
496 ret = self.model.get_by_location(user, data['location'])