fix progress
[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.reported_timestamp = now()
269         serializer.instance.last_mode = 'text'
270         serializer.save()
271
272
273 @never_cache
274 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
275     permission_classes = [IsAuthenticated]
276     serializer_class = serializers.AudioProgressSerializer
277
278     def perform_update(self, serializer):
279         serializer.instance.reported_timestamp = now()
280         serializer.instance.last_mode = 'audio'
281         serializer.save()
282
283
284
285 @never_cache
286 class SyncView(ListAPIView):
287     permission_classes = [IsAuthenticated]
288     sync_id_field = 'slug'
289     sync_id_serializer_field = 'slug'
290     sync_user_field = 'user'
291
292     def get_queryset(self):
293         try:
294             timestamp = int(self.request.GET.get('ts'))
295         except:
296             timestamp = 0
297
298         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
299         
300         data = []
301         return self.get_queryset_for_ts(timestamp)
302
303     def get_queryset_for_ts(self, timestamp):
304         return self.model.objects.filter(
305             updated_at__gt=timestamp,
306             **{
307                 self.sync_user_field: self.request.user
308             }
309         ).order_by('updated_at')
310
311     def get_instance(self, user, data):
312         sync_id = data.get(self.sync_id_serializer_field)
313         if not sync_id:
314             return None
315         return self.model.objects.filter(**{
316             self.sync_user_field: user,
317             self.sync_id_field: sync_id
318         }).first()
319
320     def post(self, request):
321         new_ids = []
322         data = request.data
323         if not isinstance(data, list):
324             raise serializers.ValidationError('Payload should be a list')
325         for item in data:
326             instance = self.get_instance(request.user, item)
327             ser = self.get_serializer(
328                 instance=instance,
329                 data=item
330             )
331             ser.is_valid(raise_exception=True)
332             synced_instance = self.model.sync(
333                 request.user,
334                 instance,
335                 ser.validated_data
336             )
337             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
338                 new_ids.append({
339                     'client_id': ser.validated_data['client_id'],
340                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
341                 })
342         return Response(new_ids)
343
344
345 class ProgressSyncView(SyncView):
346     model = models.Progress
347     serializer_class = serializers.ProgressSerializer
348     
349     sync_id_field = 'book__slug'
350     sync_id_serializer_field = 'book_slug'
351
352
353 class UserListSyncView(SyncView):
354     model = models.UserList
355
356     def get_serializer_class(self):
357         if self.request.version == 'v2':
358             return serializers.UserListSerializerV2
359         return serializers.UserListSerializerV3
360
361
362 class UserListItemSyncView(SyncView):
363     model = models.UserListItem
364     serializer_class = serializers.UserListItemSerializer
365
366     sync_id_field = 'uuid'
367     sync_id_serializer_field = 'uuid'
368     sync_user_field = 'list__user'
369
370     def get_queryset_for_ts(self, timestamp):
371         qs = self.model.all_objects.filter(
372             updated_at__gt=timestamp,
373             **{
374                 self.sync_user_field: self.request.user
375             }
376         )
377         if self.request.query_params.get('favorites'):
378             qs = qs.filter(list__favorites=True)
379         return qs.order_by('updated_at')
380
381
382 class BookmarkSyncView(SyncView):
383     model = bookmarks.models.Bookmark
384     serializer_class = BookmarkSerializer
385
386     sync_id_field = 'uuid'
387     sync_id_serializer_field = 'uuid'
388
389     def get_instance(self, user, data):
390         ret = super().get_instance(user, data)
391         if ret is None:
392             if data.get('location'):
393                 ret = self.model.get_by_location(user, data['location'])
394         return ret