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