Support audiobook > 24h
[wolnelektury.git] / src / social / api / views.py
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.
3 #
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
24
25
26 class SettingsSerializer(serializers.ModelSerializer):
27     class Meta:
28         model = models.UserProfile
29         fields = ['notifications']
30
31
32 class SettingsView(RetrieveUpdateAPIView):
33     permission_classes = [IsAuthenticated]
34     serializer_class = SettingsSerializer
35
36     def get_object(self):
37         return models.UserProfile.get_for(self.request.user)
38
39
40 @never_cache
41 class LikeView(APIView):
42     permission_classes = [IsAuthenticated]
43
44     def get(self, request, slug):
45         book = get_object_or_404(Book, slug=slug)
46         return Response({"likes": likes(request.user, book)})
47
48     def post(self, request, slug):
49         book = get_object_or_404(Book, slug=slug)
50         action = request.query_params.get('action', 'like')
51         if action == 'like':
52             models.UserList.like(request.user, book)
53         elif action == 'unlike':
54             models.UserList.unlike(request.user, book)
55         return Response({})
56
57
58 @never_cache
59 class LikeView2(APIView):
60     permission_classes = [IsAuthenticated]
61
62     def get(self, request, slug):
63         book = get_object_or_404(Book, slug=slug)
64         return Response({"likes": likes(request.user, book)})
65
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)})
70
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)})
75
76
77 @never_cache
78 class LikesView(APIView):
79     permission_classes = [IsAuthenticated]
80
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}
85         ids = books.keys()
86         res = get_sets_for_book_ids(ids, request.user)
87         res = {books[bid]: v for bid, v in res.items()}
88
89         return Response(res)
90
91
92 @never_cache
93 class MyLikesView(APIView):
94     permission_classes = [IsAuthenticated]
95
96     def get(self, request):
97         ul = models.UserList.get_favorites_list(request.user)
98         if ul is None:
99             return Response([])
100         return Response(
101             ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
102         )
103
104
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)
108
109     def to_internal_value(self, value):
110         return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
111
112
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)
117
118     class Meta:
119         model = models.UserList
120         fields = [
121             'timestamp',
122             'client_id',
123             'name',
124             'slug',
125             'favorites',
126             'deleted',
127             'books',
128         ]
129         read_only_fields = [
130             'favorites',
131             'slug',
132         ]
133         extra_kwargs = {
134             'slug': {
135                 'required': False
136             }
137         }
138
139     def create(self, validated_data):
140         instance = models.UserList.get_by_name(
141             validated_data['user'],
142             validated_data['name'],
143             create=True
144         )
145         if 'books' in validated_data:
146             instance.userlistitem_set.all().delete()
147             for book in validated_data['books']:
148                 instance.append(book)
149         return instance
150
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)
157         return instance
158
159
160 class UserListBooksSerializer(UserListSerializer):
161     class Meta:
162         model = models.UserList
163         fields = ['books']
164
165
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(),
171         source='list',
172         slug_field='slug',
173         required=False,
174     )
175     timestamp = serializers.IntegerField(required=False)
176     book_slug = serializers.SlugRelatedField(
177         queryset=Book.objects.all(),
178         source='book',
179         slug_field='slug',
180         required=False
181     )
182
183     class Meta:
184         model = models.UserListItem
185         fields = [
186             'client_id',
187             'uuid',
188             'order',
189             'list_slug',
190             'timestamp',
191             'favorites',
192             'deleted',
193
194             'book_slug',
195             'fragment',
196             'quote',
197             'bookmark',
198             'note',
199         ]
200         extra_kwargs = {
201             'order': {
202                 'required': False
203             }
204         }
205
206
207 @never_cache
208 class ListsView(ListCreateAPIView):
209     permission_classes = [IsAuthenticated]
210     #pagination_class = None
211     serializer_class = UserListSerializer
212
213     def get_queryset(self):
214         return models.UserList.objects.filter(
215             user=self.request.user,
216             favorites=False,
217             deleted=False
218         )
219
220     def perform_create(self, serializer):
221         serializer.save(user=self.request.user)
222
223
224 @never_cache
225 class ListView(RetrieveUpdateDestroyAPIView):
226     # TODO: check if can modify
227     permission_classes = [IsAuthenticatedOrReadOnly]
228     serializer_class = UserListSerializer
229
230     def get_object(self):
231         if self.request.method in SAFE_METHODS:
232             q = Q(deleted=False)
233             if self.request.user.is_authenticated:
234                 q |= Q(user=self.request.user)
235             return get_object_or_404(
236                 models.UserList,
237                 q,
238                 slug=self.kwargs['slug'],
239             )
240         else:
241             return get_object_or_404(
242                 models.UserList.all_objects.all(),
243                 slug=self.kwargs['slug'],
244                 user=self.request.user)
245
246     def perform_update(self, serializer):
247         serializer.save(user=self.request.user)
248
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)
256
257     def perform_destroy(self, instance):
258         instance.deleted = True
259         instance.updated_at = now()
260         instance.save()
261
262
263 @never_cache
264 class ListItemView(APIView):
265     permission_classes = [IsAuthenticated]
266
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)
273
274
275 @vary_on_auth
276 class ShelfView(ListAPIView):
277     permission_classes = [IsAuthenticated]
278     serializer_class = BookSerializer
279     pagination_class = None
280
281     def get_queryset(self):
282         state = self.kwargs['state']
283         if state not in ('reading', 'complete', 'likes'):
284             raise Http404
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))
288         if state == 'likes':
289             books = Book.objects.filter(userlistitem__list__user=self.request.user)
290         else:
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)
295         if after:
296             books = books_after(books, after, new_api)
297         if count:
298             books = books[:count]
299
300         return books
301
302
303
304 class ProgressSerializer(serializers.ModelSerializer):
305     book = serializers.HyperlinkedRelatedField(
306         read_only=True,
307         view_name='catalogue_api_book',
308         lookup_field='slug'
309     )
310     book_slug = serializers.SlugRelatedField(
311         queryset=Book.objects.all(),
312         source='book',
313         slug_field='slug')
314     timestamp = serializers.IntegerField(required=False)
315
316     class Meta:
317         model = models.Progress
318         fields = [
319             'timestamp',
320             'book', 'book_slug', 'last_mode', 'text_percent',
321             'text_anchor',
322             'audio_percent',
323             'audio_timestamp',
324             'implicit_text_percent',
325             'implicit_text_anchor',
326             'implicit_audio_percent',
327             'implicit_audio_timestamp',
328         ]
329         extra_kwargs = {
330             'last_mode': {
331                 'required': False,
332                 'default': 'text',
333             }
334         }
335
336
337 class TextProgressSerializer(serializers.ModelSerializer):
338     class Meta:
339         model = models.Progress
340         fields = [
341                 'text_percent',
342                 'text_anchor',
343                 ]
344         read_only_fields = ['text_percent']
345
346 class AudioProgressSerializer(serializers.ModelSerializer):
347     class Meta:
348         model = models.Progress
349         fields = ['audio_percent', 'audio_timestamp']
350         read_only_fields = ['audio_percent']
351
352
353 @never_cache
354 class ProgressListView(ListAPIView):
355     permission_classes = [IsAuthenticated]
356     serializer_class = ProgressSerializer
357
358     def get_queryset(self):
359         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
360
361
362 class ProgressMixin:
363     def get_object(self):
364         try:
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)
369
370
371
372 @never_cache
373 class ProgressView(ProgressMixin, RetrieveAPIView):
374     permission_classes = [IsAuthenticated]
375     serializer_class = ProgressSerializer
376
377
378 @never_cache
379 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
380     permission_classes = [IsAuthenticated]
381     serializer_class = TextProgressSerializer
382
383     def perform_update(self, serializer):
384         serializer.instance.last_mode = 'text'
385         serializer.save()
386
387
388 @never_cache
389 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
390     permission_classes = [IsAuthenticated]
391     serializer_class = AudioProgressSerializer
392
393     def perform_update(self, serializer):
394         serializer.instance.last_mode = 'audio'
395         serializer.save()
396
397
398
399 @never_cache
400 class SyncView(ListAPIView):
401     permission_classes = [IsAuthenticated]
402     sync_id_field = 'slug'
403     sync_id_serializer_field = 'slug'
404     sync_user_field = 'user'
405
406     def get_queryset(self):
407         try:
408             timestamp = int(self.request.GET.get('ts'))
409         except:
410             timestamp = 0
411
412         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
413         
414         data = []
415         return self.get_queryset_for_ts(timestamp)
416
417     def get_queryset_for_ts(self, timestamp):
418         return self.model.objects.filter(
419             updated_at__gt=timestamp,
420             **{
421                 self.sync_user_field: self.request.user
422             }
423         ).order_by('updated_at')
424
425     def get_instance(self, user, data):
426         sync_id = data.get(self.sync_id_serializer_field)
427         if not sync_id:
428             return None
429         return self.model.objects.filter(**{
430             self.sync_user_field: user,
431             self.sync_id_field: sync_id
432         }).first()
433
434     def post(self, request):
435         new_ids = []
436         data = request.data
437         if not isinstance(data, list):
438             raise serializers.ValidationError('Payload should be a list')
439         for item in data:
440             instance = self.get_instance(request.user, item)
441             ser = self.get_serializer(
442                 instance=instance,
443                 data=item
444             )
445             ser.is_valid(raise_exception=True)
446             synced_instance = self.model.sync(
447                 request.user,
448                 instance,
449                 ser.validated_data
450             )
451             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
452                 new_ids.append({
453                     'client_id': ser.validated_data['client_id'],
454                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
455                 })
456         return Response(new_ids)
457
458
459 class ProgressSyncView(SyncView):
460     model = models.Progress
461     serializer_class = ProgressSerializer
462     
463     sync_id_field = 'book__slug'
464     sync_id_serializer_field = 'book_slug'
465
466
467 class UserListSyncView(SyncView):
468     model = models.UserList
469     serializer_class = UserListSerializer
470
471
472 class UserListItemSyncView(SyncView):
473     model = models.UserListItem
474     serializer_class = UserListItemSerializer
475
476     sync_id_field = 'uuid'
477     sync_id_serializer_field = 'uuid'
478     sync_user_field = 'list__user'
479
480     def get_queryset_for_ts(self, timestamp):
481         qs = self.model.all_objects.filter(
482             updated_at__gt=timestamp,
483             **{
484                 self.sync_user_field: self.request.user
485             }
486         )
487         if self.request.query_params.get('favorites'):
488             qs = qs.filter(list__favorites=True)
489         return qs.order_by('updated_at')
490
491
492 class BookmarkSyncView(SyncView):
493     model = bookmarks.models.Bookmark
494     serializer_class = BookmarkSerializer
495
496     sync_id_field = 'uuid'
497     sync_id_serializer_field = 'uuid'
498
499     def get_instance(self, user, data):
500         ret = super().get_instance(user, data)
501         if ret is None:
502             if data.get('location'):
503                 ret = self.model.get_by_location(user, data['location'])
504         return ret