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