Separate list item read-only serializer.
[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 @never_cache
216 class ListItemViewV3(RetrieveUpdateDestroyAPIView):
217     permission_classes = [IsAuthenticated]
218     lookup_field = 'uuid'
219
220     def get_queryset(self):
221         return models.UserListItem.objects.filter(
222             list__user=self.request.user
223         )
224
225     def get_serializer_class(self):
226         if self.request.method == 'GET':
227             return serializers.UserListItemReadSerializer
228         else:
229             return serializers.UserListItemSerializer
230
231
232 @vary_on_auth
233 class ShelfView(ListAPIView):
234     permission_classes = [IsAuthenticated]
235     serializer_class = BookSerializer
236     pagination_class = None
237
238     def get_queryset(self):
239         state = self.kwargs['state']
240         if state not in ('reading', 'complete', 'likes'):
241             raise Http404
242         new_api = self.request.query_params.get('new_api')
243         after = self.request.query_params.get('after')
244         count = int(self.request.query_params.get('count', 50))
245         if state == 'likes':
246             books = Book.objects.filter(userlistitem__list__user=self.request.user)
247         else:
248             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
249                 .values_list('book_id', flat=True)
250             books = Book.objects.filter(id__in=list(ids)).distinct()
251             books = order_books(books, new_api)
252         if after:
253             books = books_after(books, after, new_api)
254         if count:
255             books = books[:count]
256
257         return books
258
259
260 @never_cache
261 class ProgressListView(ListAPIView):
262     permission_classes = [IsAuthenticated]
263     serializer_class = serializers.ProgressSerializer
264
265     def get_queryset(self):
266         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
267
268
269 class ProgressMixin:
270     def get_object(self):
271         try:
272             return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
273         except models.Progress.DoesNotExist:
274             book = get_object_or_404(Book, slug=self.kwargs['slug'])
275             return models.Progress(user=self.request.user, book=book)
276
277
278
279 @never_cache
280 class ProgressView(ProgressMixin, RetrieveAPIView):
281     permission_classes = [IsAuthenticated]
282     serializer_class = serializers.ProgressSerializer
283
284
285 @never_cache
286 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
287     permission_classes = [IsAuthenticated]
288     serializer_class = serializers.TextProgressSerializer
289
290     def perform_update(self, serializer):
291         serializer.instance.reported_timestamp = now()
292         serializer.instance.last_mode = 'text'
293         serializer.save()
294
295
296 @never_cache
297 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
298     permission_classes = [IsAuthenticated]
299     serializer_class = serializers.AudioProgressSerializer
300
301     def perform_update(self, serializer):
302         serializer.instance.reported_timestamp = now()
303         serializer.instance.last_mode = 'audio'
304         serializer.save()
305
306
307
308 @never_cache
309 class SyncView(ListAPIView):
310     permission_classes = [IsAuthenticated]
311     sync_id_field = 'slug'
312     sync_id_serializer_field = 'slug'
313     sync_user_field = 'user'
314
315     def get_queryset(self):
316         try:
317             timestamp = int(self.request.GET.get('ts'))
318         except:
319             timestamp = 0
320
321         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
322         
323         data = []
324         return self.get_queryset_for_ts(timestamp)
325
326     def get_queryset_for_ts(self, timestamp):
327         return self.model.objects.filter(
328             updated_at__gt=timestamp,
329             **{
330                 self.sync_user_field: self.request.user
331             }
332         ).order_by('updated_at')
333
334     def get_instance(self, user, data):
335         sync_id = data.get(self.sync_id_serializer_field)
336         if not sync_id:
337             return None
338         return self.model.objects.filter(**{
339             self.sync_user_field: user,
340             self.sync_id_field: sync_id
341         }).first()
342
343     def post(self, request):
344         new_ids = []
345         data = request.data
346         if not isinstance(data, list):
347             raise serializers.ValidationError('Payload should be a list')
348         for item in data:
349             instance = self.get_instance(request.user, item)
350             ser = self.get_serializer(
351                 instance=instance,
352                 data=item
353             )
354             ser.is_valid(raise_exception=True)
355             synced_instance = self.model.sync(
356                 request.user,
357                 instance,
358                 ser.validated_data
359             )
360             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
361                 new_ids.append({
362                     'client_id': ser.validated_data['client_id'],
363                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
364                 })
365         return Response(new_ids)
366
367
368 class ProgressSyncView(SyncView):
369     model = models.Progress
370     serializer_class = serializers.ProgressSerializer
371     
372     sync_id_field = 'book__slug'
373     sync_id_serializer_field = 'book_slug'
374
375
376 class UserListSyncView(SyncView):
377     model = models.UserList
378
379     def get_serializer_class(self):
380         if self.request.version == 'v2':
381             return serializers.UserListSerializerV2
382         return serializers.UserListSerializerV3
383
384
385 class UserListItemSyncView(SyncView):
386     model = models.UserListItem
387     serializer_class = serializers.UserListItemSerializer
388
389     sync_id_field = 'uuid'
390     sync_id_serializer_field = 'uuid'
391     sync_user_field = 'list__user'
392
393     def get_queryset_for_ts(self, timestamp):
394         qs = self.model.all_objects.filter(
395             updated_at__gt=timestamp,
396             **{
397                 self.sync_user_field: self.request.user
398             }
399         )
400         if self.request.query_params.get('favorites'):
401             qs = qs.filter(list__favorites=True)
402         return qs.order_by('updated_at')
403
404
405 class BookmarkSyncView(SyncView):
406     model = bookmarks.models.Bookmark
407     serializer_class = BookmarkSerializer
408
409     sync_id_field = 'uuid'
410     sync_id_serializer_field = 'uuid'
411
412     def get_instance(self, user, data):
413         ret = super().get_instance(user, data)
414         if ret is None:
415             if data.get('location'):
416                 ret = self.model.get_by_location(user, data['location'])
417         return ret