a43bec2770bc7e85d66aed34b1f90f290145b1f2
[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.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
9 from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly
10 from rest_framework.response import Response
11 from rest_framework.views import APIView
12 from api.models import BookUserData
13 from api.utils import vary_on_auth, never_cache
14 from catalogue.api.helpers import order_books, books_after
15 from catalogue.api.serializers import BookSerializer
16 from catalogue.models import Book
17 import catalogue.models
18 from social.views import get_sets_for_book_ids
19 from social.utils import likes
20 from social import models
21 import bookmarks.models
22 from . import serializers
23 from bookmarks.api.views import BookmarkSerializer
24
25
26
27 class SettingsView(RetrieveUpdateAPIView):
28     permission_classes = [IsAuthenticated]
29     serializer_class = serializers.SettingsSerializer
30
31     def get_object(self):
32         return models.UserProfile.get_for(self.request.user)
33
34
35 @never_cache
36 class LikeView(APIView):
37     permission_classes = [IsAuthenticated]
38
39     def get(self, request, slug):
40         book = get_object_or_404(Book, slug=slug)
41         return Response({"likes": likes(request.user, book)})
42
43     def post(self, request, slug):
44         book = get_object_or_404(Book, slug=slug)
45         action = request.query_params.get('action', 'like')
46         if action == 'like':
47             models.UserList.like(request.user, book)
48         elif action == 'unlike':
49             models.UserList.unlike(request.user, book)
50         return Response({})
51
52
53 @never_cache
54 class LikeView2(APIView):
55     permission_classes = [IsAuthenticated]
56
57     def get(self, request, slug):
58         book = get_object_or_404(Book, slug=slug)
59         return Response({"likes": likes(request.user, book)})
60
61     def put(self, request, slug):
62         book = get_object_or_404(Book, slug=slug)
63         models.UserList.like(request.user, book)
64         return Response({"likes": likes(request.user, book)})
65
66     def delete(self, request, slug):
67         book = get_object_or_404(Book, slug=slug)
68         models.UserList.unlike(request.user, book)
69         return Response({"likes": likes(request.user, book)})
70
71
72 @never_cache
73 class LikesView(APIView):
74     permission_classes = [IsAuthenticated]
75
76     def get(self, request):
77         slugs = request.GET.getlist('slug')
78         books = Book.objects.filter(slug__in=slugs)
79         books = {b.id: b.slug for b in books}
80         ids = books.keys()
81         res = get_sets_for_book_ids(ids, request.user)
82         res = {books[bid]: v for bid, v in res.items()}
83
84         return Response(res)
85
86
87 @never_cache
88 class MyLikesView(APIView):
89     permission_classes = [IsAuthenticated]
90
91     def get(self, request):
92         ul = models.UserList.get_favorites_list(request.user)
93         if ul is None:
94             return Response([])
95         return Response(
96             ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
97         )
98
99
100 @never_cache
101 class ListsView(ListCreateAPIView):
102     permission_classes = [IsAuthenticated]
103     #pagination_class = None
104     serializer_class = serializers.UserListSerializer
105
106     def get_queryset(self):
107         return models.UserList.objects.filter(
108             user=self.request.user,
109             favorites=False,
110             deleted=False
111         )
112
113     def perform_create(self, serializer):
114         serializer.save(user=self.request.user)
115
116
117 @never_cache
118 class ListView(RetrieveUpdateDestroyAPIView):
119     # TODO: check if can modify
120     permission_classes = [IsAuthenticatedOrReadOnly]
121     serializer_class = serializers.UserListSerializer
122
123     def get_object(self):
124         if self.request.method in SAFE_METHODS:
125             q = Q(deleted=False)
126             if self.request.user.is_authenticated:
127                 q |= Q(user=self.request.user)
128             return get_object_or_404(
129                 models.UserList,
130                 q,
131                 slug=self.kwargs['slug'],
132             )
133         else:
134             return get_object_or_404(
135                 models.UserList.all_objects.all(),
136                 slug=self.kwargs['slug'],
137                 user=self.request.user)
138
139     def perform_update(self, serializer):
140         serializer.save(user=self.request.user)
141
142     def post(self, request, slug):
143         serializer = serializers.UserListBooksSerializer(data=request.data)
144         serializer.is_valid(raise_exception=True)
145         instance = self.get_object()
146         for book in serializer.validated_data['books']:
147             instance.append(book)
148         return Response(self.get_serializer(instance).data)
149
150     def perform_destroy(self, instance):
151         instance.deleted = True
152         instance.updated_at = now()
153         instance.save()
154
155
156 @never_cache
157 class ListItemView(APIView):
158     permission_classes = [IsAuthenticated]
159
160     def delete(self, request, slug, book):
161         instance = get_object_or_404(
162             models.UserList, slug=slug, user=self.request.user)
163         book = get_object_or_404(catalogue.models.Book, slug=book)
164         instance.remove(book=book)
165         return Response(UserListSerializer(instance).data)
166
167
168 @vary_on_auth
169 class ShelfView(ListAPIView):
170     permission_classes = [IsAuthenticated]
171     serializer_class = BookSerializer
172     pagination_class = None
173
174     def get_queryset(self):
175         state = self.kwargs['state']
176         if state not in ('reading', 'complete', 'likes'):
177             raise Http404
178         new_api = self.request.query_params.get('new_api')
179         after = self.request.query_params.get('after')
180         count = int(self.request.query_params.get('count', 50))
181         if state == 'likes':
182             books = Book.objects.filter(userlistitem__list__user=self.request.user)
183         else:
184             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
185                 .values_list('book_id', flat=True)
186             books = Book.objects.filter(id__in=list(ids)).distinct()
187             books = order_books(books, new_api)
188         if after:
189             books = books_after(books, after, new_api)
190         if count:
191             books = books[:count]
192
193         return books
194
195
196 @never_cache
197 class ProgressListView(ListAPIView):
198     permission_classes = [IsAuthenticated]
199     serializer_class = serializers.ProgressSerializer
200
201     def get_queryset(self):
202         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
203
204
205 class ProgressMixin:
206     def get_object(self):
207         try:
208             return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
209         except models.Progress.DoesNotExist:
210             book = get_object_or_404(Book, slug=self.kwargs['slug'])
211             return models.Progress(user=self.request.user, book=book)
212
213
214
215 @never_cache
216 class ProgressView(ProgressMixin, RetrieveAPIView):
217     permission_classes = [IsAuthenticated]
218     serializer_class = serializers.ProgressSerializer
219
220
221 @never_cache
222 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
223     permission_classes = [IsAuthenticated]
224     serializer_class = serializers.TextProgressSerializer
225
226     def perform_update(self, serializer):
227         serializer.instance.last_mode = 'text'
228         serializer.save()
229
230
231 @never_cache
232 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
233     permission_classes = [IsAuthenticated]
234     serializer_class = serializers.AudioProgressSerializer
235
236     def perform_update(self, serializer):
237         serializer.instance.last_mode = 'audio'
238         serializer.save()
239
240
241
242 @never_cache
243 class SyncView(ListAPIView):
244     permission_classes = [IsAuthenticated]
245     sync_id_field = 'slug'
246     sync_id_serializer_field = 'slug'
247     sync_user_field = 'user'
248
249     def get_queryset(self):
250         try:
251             timestamp = int(self.request.GET.get('ts'))
252         except:
253             timestamp = 0
254
255         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
256         
257         data = []
258         return self.get_queryset_for_ts(timestamp)
259
260     def get_queryset_for_ts(self, timestamp):
261         return self.model.objects.filter(
262             updated_at__gt=timestamp,
263             **{
264                 self.sync_user_field: self.request.user
265             }
266         ).order_by('updated_at')
267
268     def get_instance(self, user, data):
269         sync_id = data.get(self.sync_id_serializer_field)
270         if not sync_id:
271             return None
272         return self.model.objects.filter(**{
273             self.sync_user_field: user,
274             self.sync_id_field: sync_id
275         }).first()
276
277     def post(self, request):
278         new_ids = []
279         data = request.data
280         if not isinstance(data, list):
281             raise serializers.ValidationError('Payload should be a list')
282         for item in data:
283             instance = self.get_instance(request.user, item)
284             ser = self.get_serializer(
285                 instance=instance,
286                 data=item
287             )
288             ser.is_valid(raise_exception=True)
289             synced_instance = self.model.sync(
290                 request.user,
291                 instance,
292                 ser.validated_data
293             )
294             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
295                 new_ids.append({
296                     'client_id': ser.validated_data['client_id'],
297                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
298                 })
299         return Response(new_ids)
300
301
302 class ProgressSyncView(SyncView):
303     model = models.Progress
304     serializer_class = serializers.ProgressSerializer
305     
306     sync_id_field = 'book__slug'
307     sync_id_serializer_field = 'book_slug'
308
309
310 class UserListSyncView(SyncView):
311     model = models.UserList
312     serializer_class = serializers.UserListSerializer
313
314
315 class UserListItemSyncView(SyncView):
316     model = models.UserListItem
317     serializer_class = serializers.UserListItemSerializer
318
319     sync_id_field = 'uuid'
320     sync_id_serializer_field = 'uuid'
321     sync_user_field = 'list__user'
322
323     def get_queryset_for_ts(self, timestamp):
324         qs = self.model.all_objects.filter(
325             updated_at__gt=timestamp,
326             **{
327                 self.sync_user_field: self.request.user
328             }
329         )
330         if self.request.query_params.get('favorites'):
331             qs = qs.filter(list__favorites=True)
332         return qs.order_by('updated_at')
333
334
335 class BookmarkSyncView(SyncView):
336     model = bookmarks.models.Bookmark
337     serializer_class = BookmarkSerializer
338
339     sync_id_field = 'uuid'
340     sync_id_serializer_field = 'uuid'
341
342     def get_instance(self, user, data):
343         ret = super().get_instance(user, data)
344         if ret is None:
345             if data.get('location'):
346                 ret = self.model.get_by_location(user, data['location'])
347         return ret