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