Audio bookmarks
[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 pytz import utc
6 from django.http import Http404
7 from django.utils.timezone import now
8 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
9 from rest_framework.permissions import 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 = ['favorites']
130         extra_kwargs = {
131             'slug': {
132                 'required': False
133             }
134         }
135
136     def create(self, validated_data):
137         instance = models.UserList.get_by_name(
138             validated_data['user'],
139             validated_data['name'],
140             create=True
141         )
142         instance.userlistitem_set.all().delete()
143         for book in validated_data['books']:
144             instance.append(book)
145         return instance
146
147     def update(self, instance, validated_data):
148         instance.userlistitem_set.all().delete()
149         for book in validated_data['books']:
150             instance.append(instance)
151         return instance
152
153 class UserListBooksSerializer(UserListSerializer):
154     class Meta:
155         model = models.UserList
156         fields = ['books']
157
158
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(),
164         source='list',
165         slug_field='slug',
166         required=False,
167     )
168     timestamp = serializers.IntegerField(required=False)
169     book_slug = serializers.SlugRelatedField(
170         queryset=Book.objects.all(),
171         source='book',
172         slug_field='slug',
173         required=False
174     )
175
176     class Meta:
177         model = models.UserListItem
178         fields = [
179             'client_id',
180             'uuid',
181             'order',
182             'list_slug',
183             'timestamp',
184             'favorites',
185             'deleted',
186
187             'book_slug',
188             'fragment',
189             'quote',
190             'bookmark',
191             'note',
192         ]
193         extra_kwargs = {
194             'order': {
195                 'required': False
196             }
197         }
198
199
200 @never_cache
201 class ListsView(ListCreateAPIView):
202     permission_classes = [IsAuthenticated]
203     #pagination_class = None
204     serializer_class = UserListSerializer
205
206     def get_queryset(self):
207         return models.UserList.objects.filter(
208             user=self.request.user,
209             favorites=False,
210             deleted=False
211         )
212
213     def perform_create(self, serializer):
214         serializer.save(user=self.request.user)
215
216
217 @never_cache
218 class ListView(RetrieveUpdateDestroyAPIView):
219     # TODO: check if can modify
220     permission_classes = [IsAuthenticated]
221     serializer_class = UserListSerializer
222
223     def get_object(self):
224         return get_object_or_404(
225             models.UserList,
226             slug=self.kwargs['slug'],
227             user=self.request.user)
228
229     def perform_update(self, serializer):
230         serializer.save(user=self.request.user)
231
232     def post(self, request, slug):
233         serializer = UserListBooksSerializer(data=request.data)
234         serializer.is_valid(raise_exception=True)
235         instance = self.get_object()
236         for book in serializer.validated_data['books']:
237             instance.append(book)
238         return Response(self.get_serializer(instance).data)
239
240     def perform_destroy(self, instance):
241         instance.update(
242             deleted=True,
243             updated_at=now()
244         )
245
246
247 @never_cache
248 class ListItemView(APIView):
249     permission_classes = [IsAuthenticated]
250
251     def delete(self, request, slug, book):
252         instance = get_object_or_404(
253             models.UserList, slug=slug, user=self.request.user)
254         book = get_object_or_404(catalogue.models.Book, slug=book)
255         instance.remove(book=book)
256         return Response(UserListSerializer(instance).data)
257
258
259 @vary_on_auth
260 class ShelfView(ListAPIView):
261     permission_classes = [IsAuthenticated]
262     serializer_class = BookSerializer
263     pagination_class = None
264
265     def get_queryset(self):
266         state = self.kwargs['state']
267         if state not in ('reading', 'complete', 'likes'):
268             raise Http404
269         new_api = self.request.query_params.get('new_api')
270         after = self.request.query_params.get('after')
271         count = int(self.request.query_params.get('count', 50))
272         if state == 'likes':
273             books = Book.objects.filter(userlistitem__list__user=self.request.user)
274         else:
275             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
276                 .values_list('book_id', flat=True)
277             books = Book.objects.filter(id__in=list(ids)).distinct()
278             books = order_books(books, new_api)
279         if after:
280             books = books_after(books, after, new_api)
281         if count:
282             books = books[:count]
283
284         return books
285
286
287
288 class ProgressSerializer(serializers.ModelSerializer):
289     book = serializers.HyperlinkedRelatedField(
290         read_only=True,
291         view_name='catalogue_api_book',
292         lookup_field='slug'
293     )
294     book_slug = serializers.SlugRelatedField(
295         queryset=Book.objects.all(),
296         source='book',
297         slug_field='slug')
298     timestamp = serializers.IntegerField(required=False)
299
300     class Meta:
301         model = models.Progress
302         fields = [
303             'timestamp',
304             'book', 'book_slug', 'last_mode', 'text_percent',
305             'text_anchor',
306             'audio_percent',
307             'audio_timestamp',
308             'implicit_text_percent',
309             'implicit_text_anchor',
310             'implicit_audio_percent',
311             'implicit_audio_timestamp',
312         ]
313         extra_kwargs = {
314             'last_mode': {
315                 'required': False,
316                 'default': 'text',
317             }
318         }
319
320
321 class TextProgressSerializer(serializers.ModelSerializer):
322     class Meta:
323         model = models.Progress
324         fields = [
325                 'text_percent',
326                 'text_anchor',
327                 ]
328         read_only_fields = ['text_percent']
329
330 class AudioProgressSerializer(serializers.ModelSerializer):
331     class Meta:
332         model = models.Progress
333         fields = ['audio_percent', 'audio_timestamp']
334         read_only_fields = ['audio_percent']
335
336
337 @never_cache
338 class ProgressListView(ListAPIView):
339     permission_classes = [IsAuthenticated]
340     serializer_class = ProgressSerializer
341
342     def get_queryset(self):
343         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
344
345
346 class ProgressMixin:
347     def get_object(self):
348         try:
349             return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
350         except models.Progress.DoesNotExist:
351             book = get_object_or_404(Book, slug=self.kwargs['slug'])
352             return models.Progress(user=self.request.user, book=book)
353
354
355
356 @never_cache
357 class ProgressView(ProgressMixin, RetrieveAPIView):
358     permission_classes = [IsAuthenticated]
359     serializer_class = ProgressSerializer
360
361
362 @never_cache
363 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
364     permission_classes = [IsAuthenticated]
365     serializer_class = TextProgressSerializer
366
367     def perform_update(self, serializer):
368         serializer.instance.last_mode = 'text'
369         serializer.save()
370
371
372 @never_cache
373 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
374     permission_classes = [IsAuthenticated]
375     serializer_class = AudioProgressSerializer
376
377     def perform_update(self, serializer):
378         serializer.instance.last_mode = 'audio'
379         serializer.save()
380
381
382
383 @never_cache
384 class SyncView(ListAPIView):
385     permission_classes = [IsAuthenticated]
386     sync_id_field = 'slug'
387     sync_id_serializer_field = 'slug'
388     sync_user_field = 'user'
389
390     def get_queryset(self):
391         try:
392             timestamp = int(self.request.GET.get('ts'))
393         except:
394             timestamp = 0
395
396         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
397         
398         data = []
399         return self.get_queryset_for_ts(timestamp)
400
401     def get_queryset_for_ts(self, timestamp):
402         return self.model.objects.filter(
403             updated_at__gt=timestamp,
404             **{
405                 self.sync_user_field: self.request.user
406             }
407         ).order_by('updated_at')
408
409     def get_instance(self, user, data):
410         sync_id = data.get(self.sync_id_serializer_field)
411         if not sync_id:
412             return None
413         return self.model.objects.filter(**{
414             self.sync_user_field: user,
415             self.sync_id_field: sync_id
416         }).first()
417
418     def post(self, request):
419         new_ids = []
420         data = request.data
421         if not isinstance(data, list):
422             raise serializers.ValidationError('Payload should be a list')
423         for item in data:
424             instance = self.get_instance(request.user, item)
425             ser = self.get_serializer(
426                 instance=instance,
427                 data=item
428             )
429             ser.is_valid(raise_exception=True)
430             synced_instance = self.model.sync(
431                 request.user,
432                 instance,
433                 ser.validated_data
434             )
435             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
436                 new_ids.append({
437                     'client_id': ser.validated_data['client_id'],
438                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
439                 })
440         return Response(new_ids)
441
442
443 class ProgressSyncView(SyncView):
444     model = models.Progress
445     serializer_class = ProgressSerializer
446     
447     sync_id_field = 'book__slug'
448     sync_id_serializer_field = 'book_slug'
449
450
451 class UserListSyncView(SyncView):
452     model = models.UserList
453     serializer_class = UserListSerializer
454
455
456 class UserListItemSyncView(SyncView):
457     model = models.UserListItem
458     serializer_class = UserListItemSerializer
459
460     sync_id_field = 'uuid'
461     sync_id_serializer_field = 'uuid'
462     sync_user_field = 'list__user'
463
464     def get_queryset_for_ts(self, timestamp):
465         qs = self.model.objects.filter(
466             updated_at__gt=timestamp,
467             **{
468                 self.sync_user_field: self.request.user
469             }
470         )
471         if self.request.query_params.get('favorites'):
472             qs = qs.filter(list__favorites=True)
473         return qs.order_by('updated_at')
474
475
476 class BookmarkSyncView(SyncView):
477     model = bookmarks.models.Bookmark
478     serializer_class = BookmarkSerializer
479
480     sync_id_field = 'uuid'
481     sync_id_serializer_field = 'uuid'
482
483     def get_instance(self, user, data):
484         ret = super().get_instance(user, data)
485         if ret is None:
486             if data.get('location'):
487                 ret = self.model.get_by_location(user, data['location'])
488         return ret