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