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