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