0ea769c4e468d99b99ef882d325948a0f3a209ff
[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.exceptions import MethodNotAllowed
9 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
10 from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly
11 from rest_framework.response import Response
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 . import serializers
24 from bookmarks.api.views import BookmarkSerializer
25
26
27
28 class SettingsView(RetrieveUpdateAPIView):
29     permission_classes = [IsAuthenticated]
30     serializer_class = serializers.SettingsSerializer
31
32     def get_object(self):
33         return models.UserProfile.get_for(self.request.user)
34
35
36 @never_cache
37 class LikeView(APIView):
38     permission_classes = [IsAuthenticated]
39
40     def get(self, request, slug):
41         book = get_object_or_404(Book, slug=slug)
42         return Response({"likes": likes(request.user, book)})
43
44     def post(self, request, slug):
45         book = get_object_or_404(Book, slug=slug)
46         action = request.query_params.get('action', 'like')
47         if action == 'like':
48             models.UserList.like(request.user, book)
49         elif action == 'unlike':
50             models.UserList.unlike(request.user, book)
51         return Response({})
52
53
54 @never_cache
55 class LikeView2(APIView):
56     permission_classes = [IsAuthenticated]
57
58     def get(self, request, slug):
59         book = get_object_or_404(Book, slug=slug)
60         return Response({"likes": likes(request.user, book)})
61
62     def put(self, request, slug):
63         book = get_object_or_404(Book, slug=slug)
64         models.UserList.like(request.user, book)
65         return Response({"likes": likes(request.user, book)})
66
67     def delete(self, request, slug):
68         book = get_object_or_404(Book, slug=slug)
69         models.UserList.unlike(request.user, book)
70         return Response({"likes": likes(request.user, book)})
71
72
73 @never_cache
74 class LikesView(APIView):
75     permission_classes = [IsAuthenticated]
76
77     def get(self, request):
78         slugs = request.GET.getlist('slug')
79         books = Book.objects.filter(slug__in=slugs)
80         books = {b.id: b.slug for b in books}
81         ids = books.keys()
82         res = get_sets_for_book_ids(ids, request.user)
83         res = {books[bid]: v for bid, v in res.items()}
84
85         return Response(res)
86
87
88 @never_cache
89 class MyLikesView(APIView):
90     permission_classes = [IsAuthenticated]
91
92     def get(self, request):
93         ul = models.UserList.get_favorites_list(request.user)
94         if ul is None:
95             return Response([])
96         return Response(
97             ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
98         )
99
100
101 @never_cache
102 class ListsView(ListCreateAPIView):
103     permission_classes = [IsAuthenticated]
104     #pagination_class = None
105
106     def get_serializer_class(self):
107         if self.request.version == 'v2':
108             return serializers.UserListSerializerV2
109         return serializers.UserListSerializerV3
110
111     def get_queryset(self):
112         return models.UserList.objects.filter(
113             user=self.request.user,
114             favorites=False,
115             deleted=False
116         )
117
118     def perform_create(self, serializer):
119         serializer.save(user=self.request.user)
120
121
122 def get_userlist(slug, request):
123     if request.method in SAFE_METHODS:
124         q = Q(deleted=False)
125         if request.user.is_authenticated:
126             q |= Q(user=request.user)
127         return get_object_or_404(
128             models.UserList,
129             q,
130             slug=slug,
131         )
132     else:
133         return get_object_or_404(
134             models.UserList.all_objects.all(),
135             slug=slug,
136             user=request.user
137         )
138
139
140 @never_cache
141 class ListView(RetrieveUpdateDestroyAPIView):
142     # TODO: check if can modify
143     permission_classes = [IsAuthenticatedOrReadOnly]
144
145     def get_serializer_class(self):
146         if self.request.version == 'v2':
147             return serializers.UserListSerializerV2
148         return serializers.UserListSerializerV3
149
150     def get_object(self):
151         return get_userlist(self.kwargs['slug'], self.request)
152
153     def perform_update(self, serializer):
154         serializer.save(user=self.request.user)
155
156     def post(self, request, slug):
157         if request.version == 'v2':
158             # Accept posting a list of books here.
159             serializer = serializers.UserListBooksSerializer(data=request.data)
160             serializer.is_valid(raise_exception=True)
161             instance = self.get_object()
162             for book in serializer.validated_data['books']:
163                 instance.append(book)
164             return Response(self.get_serializer(instance).data)
165         else:
166             raise MethodNotAllowed(method=request.method)
167
168     def perform_destroy(self, instance):
169         instance.deleted = True
170         instance.updated_at = now()
171         instance.save()
172
173
174 @never_cache
175 class ListItemViewV2(APIView):
176     """v2 only"""
177     permission_classes = [IsAuthenticated]
178
179     def delete(self, request, slug, book):
180         instance = get_object_or_404(
181             models.UserList, slug=slug, user=self.request.user)
182         book = get_object_or_404(catalogue.models.Book, slug=book)
183         instance.remove(book=book)
184         return Response(serializers.UserListSerializerV2(instance).data)
185
186
187 @never_cache
188 class ListItemListViewV3(ListCreateAPIView):
189     permission_classes = [IsAuthenticatedOrReadOnly]
190
191     def get_queryset(self):
192         lst = get_userlist(self.kwargs['slug'], self.request)
193         return lst.userlistitem_set.all().order_by('order')
194
195     def get_serializer_class(self):
196         if self.request.method == 'GET':
197             return serializers.UserListItemReadSerializer
198         else:
199             return serializers.UserListItemSerializer
200     
201     def get_serializer(self, *args, **kwargs):
202         serializer_class = self.get_serializer_class()
203         kwargs.setdefault('context', self.get_serializer_context())
204
205         if isinstance(self.request.data, list):
206             kwargs['many'] = True
207
208         return serializer_class(*args, **kwargs)
209
210     def perform_create(self, serializer):
211         lst = get_userlist(self.kwargs['slug'], self.request)
212         serializer.save(list=lst)
213
214
215 class ListItemsView(APIView):
216     permission_classes = [IsAuthenticated]
217
218     def delete(self, request):
219         if not isinstance(self.request.data, list):
220             return Response({"error": "no data"}, status=400)
221         models.UserListItem.objects.filter(
222             list__user=self.request.user,
223             uuid__in=self.request.data
224         ).delete()
225         return Response({})
226
227
228 @never_cache
229 class ListItemViewV3(RetrieveUpdateDestroyAPIView):
230     permission_classes = [IsAuthenticated]
231     lookup_field = 'uuid'
232
233     def get_queryset(self):
234         return models.UserListItem.objects.filter(
235             list__user=self.request.user
236         )
237
238     def get_serializer_class(self):
239         if self.request.method == 'GET':
240             return serializers.UserListItemReadSerializer
241         else:
242             return serializers.UserListItemSerializer
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 @never_cache
274 class ProgressListView(ListAPIView):
275     permission_classes = [IsAuthenticated]
276     serializer_class = serializers.ProgressSerializer
277
278     def get_queryset(self):
279         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
280
281
282 class ProgressMixin:
283     def get_object(self):
284         try:
285             return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
286         except models.Progress.DoesNotExist:
287             book = get_object_or_404(Book, slug=self.kwargs['slug'])
288             return models.Progress(user=self.request.user, book=book)
289
290
291
292 @never_cache
293 class ProgressView(ProgressMixin, RetrieveAPIView):
294     permission_classes = [IsAuthenticated]
295     serializer_class = serializers.ProgressSerializer
296
297
298 @never_cache
299 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
300     permission_classes = [IsAuthenticated]
301     serializer_class = serializers.TextProgressSerializer
302
303     def perform_update(self, serializer):
304         serializer.instance.reported_timestamp = now()
305         serializer.instance.last_mode = 'text'
306         serializer.save()
307
308
309 @never_cache
310 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
311     permission_classes = [IsAuthenticated]
312     serializer_class = serializers.AudioProgressSerializer
313
314     def perform_update(self, serializer):
315         serializer.instance.reported_timestamp = now()
316         serializer.instance.last_mode = 'audio'
317         serializer.save()
318
319
320
321 @never_cache
322 class SyncView(ListAPIView):
323     permission_classes = [IsAuthenticated]
324     sync_id_field = 'slug'
325     sync_id_serializer_field = 'slug'
326     sync_user_field = 'user'
327
328     def get_queryset(self):
329         try:
330             timestamp = int(self.request.GET.get('ts'))
331         except:
332             timestamp = 0
333
334         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
335         
336         data = []
337         return self.get_queryset_for_ts(timestamp)
338
339     def get_queryset_for_ts(self, timestamp):
340         return self.model.objects.filter(
341             updated_at__gt=timestamp,
342             **{
343                 self.sync_user_field: self.request.user
344             }
345         ).order_by('updated_at')
346
347     def get_instance(self, user, data):
348         sync_id = data.get(self.sync_id_serializer_field)
349         if not sync_id:
350             return None
351         return self.model.objects.filter(**{
352             self.sync_user_field: user,
353             self.sync_id_field: sync_id
354         }).first()
355
356     def post(self, request):
357         new_ids = []
358         data = request.data
359         if not isinstance(data, list):
360             raise serializers.ValidationError('Payload should be a list')
361         for item in data:
362             instance = self.get_instance(request.user, item)
363             ser = self.get_serializer(
364                 instance=instance,
365                 data=item
366             )
367             ser.is_valid(raise_exception=True)
368             synced_instance = self.model.sync(
369                 request.user,
370                 instance,
371                 ser.validated_data
372             )
373             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
374                 new_ids.append({
375                     'client_id': ser.validated_data['client_id'],
376                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
377                 })
378         return Response(new_ids)
379
380
381 class ProgressSyncView(SyncView):
382     model = models.Progress
383     serializer_class = serializers.ProgressSerializer
384     
385     sync_id_field = 'book__slug'
386     sync_id_serializer_field = 'book_slug'
387
388
389 class UserListSyncView(SyncView):
390     model = models.UserList
391
392     def get_serializer_class(self):
393         if self.request.version == 'v2':
394             return serializers.UserListSerializerV2
395         return serializers.UserListSerializerV3
396
397
398 class UserListItemSyncView(SyncView):
399     model = models.UserListItem
400     serializer_class = serializers.UserListItemSerializer
401
402     sync_id_field = 'uuid'
403     sync_id_serializer_field = 'uuid'
404     sync_user_field = 'list__user'
405
406     def get_queryset_for_ts(self, timestamp):
407         qs = self.model.all_objects.filter(
408             updated_at__gt=timestamp,
409             **{
410                 self.sync_user_field: self.request.user
411             }
412         )
413         if self.request.query_params.get('favorites'):
414             qs = qs.filter(list__favorites=True)
415         return qs.order_by('updated_at')
416
417
418 class BookmarkSyncView(SyncView):
419     model = bookmarks.models.Bookmark
420     serializer_class = BookmarkSerializer
421
422     sync_id_field = 'uuid'
423     sync_id_serializer_field = 'uuid'
424
425     def get_instance(self, user, data):
426         ret = super().get_instance(user, data)
427         if ret is None:
428             if data.get('location'):
429                 ret = self.model.get_by_location(user, data['location'])
430         return ret