fix list delete
[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.http import Http404
6 from django.utils.timezone import now, utc
7 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
8 from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly
9 from rest_framework.response import Response
10 from rest_framework import serializers
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 bookmarks.api.views import BookmarkSerializer
23
24
25 class SettingsSerializer(serializers.ModelSerializer):
26     class Meta:
27         model = models.UserProfile
28         fields = ['notifications']
29
30
31 class SettingsView(RetrieveUpdateAPIView):
32     permission_classes = [IsAuthenticated]
33     serializer_class = SettingsSerializer
34
35     def get_object(self):
36         return models.UserProfile.get_for(self.request.user)
37
38
39 @never_cache
40 class LikeView(APIView):
41     permission_classes = [IsAuthenticated]
42
43     def get(self, request, slug):
44         book = get_object_or_404(Book, slug=slug)
45         return Response({"likes": likes(request.user, book)})
46
47     def post(self, request, slug):
48         book = get_object_or_404(Book, slug=slug)
49         action = request.query_params.get('action', 'like')
50         if action == 'like':
51             models.UserList.like(request.user, book)
52         elif action == 'unlike':
53             models.UserList.unlike(request.user, book)
54         return Response({})
55
56
57 @never_cache
58 class LikeView2(APIView):
59     permission_classes = [IsAuthenticated]
60
61     def get(self, request, slug):
62         book = get_object_or_404(Book, slug=slug)
63         return Response({"likes": likes(request.user, book)})
64
65     def put(self, request, slug):
66         book = get_object_or_404(Book, slug=slug)
67         models.UserList.like(request.user, book)
68         return Response({"likes": likes(request.user, book)})
69
70     def delete(self, request, slug):
71         book = get_object_or_404(Book, slug=slug)
72         models.UserList.unlike(request.user, book)
73         return Response({"likes": likes(request.user, book)})
74
75
76 @never_cache
77 class LikesView(APIView):
78     permission_classes = [IsAuthenticated]
79
80     def get(self, request):
81         slugs = request.GET.getlist('slug')
82         books = Book.objects.filter(slug__in=slugs)
83         books = {b.id: b.slug for b in books}
84         ids = books.keys()
85         res = get_sets_for_book_ids(ids, request.user)
86         res = {books[bid]: v for bid, v in res.items()}
87
88         return Response(res)
89
90
91 @never_cache
92 class MyLikesView(APIView):
93     permission_classes = [IsAuthenticated]
94
95     def get(self, request):
96         ul = models.UserList.get_favorites_list(request.user)
97         if ul is None:
98             return Response([])
99         return Response(
100             ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
101         )
102
103
104 class UserListItemsField(serializers.Field):
105     def to_representation(self, value):
106         return value.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
107
108     def to_internal_value(self, value):
109         return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
110
111
112 class UserListSerializer(serializers.ModelSerializer):
113     client_id = serializers.CharField(write_only=True, required=False)
114     books = UserListItemsField(source='*', required=False)
115     timestamp = serializers.IntegerField(required=False)
116
117     class Meta:
118         model = models.UserList
119         fields = [
120             'timestamp',
121             'client_id',
122             'name',
123             'slug',
124             'favorites',
125             'deleted',
126             'books',
127         ]
128         read_only_fields = ['favorites']
129         extra_kwargs = {
130             'slug': {
131                 'required': False
132             }
133         }
134
135     def create(self, validated_data):
136         instance = models.UserList.get_by_name(
137             validated_data['user'],
138             validated_data['name'],
139             create=True
140         )
141         instance.userlistitem_set.all().delete()
142         for book in validated_data['books']:
143             instance.append(book)
144         return instance
145
146     def update(self, instance, validated_data):
147         instance.userlistitem_set.all().delete()
148         for book in validated_data['books']:
149             instance.append(instance)
150         return instance
151
152 class UserListBooksSerializer(UserListSerializer):
153     class Meta:
154         model = models.UserList
155         fields = ['books']
156
157
158 class UserListItemSerializer(serializers.ModelSerializer):
159     client_id = serializers.CharField(write_only=True, required=False)
160     favorites = serializers.BooleanField(required=False)
161     list_slug = serializers.SlugRelatedField(
162         queryset=models.UserList.objects.all(),
163         source='list',
164         slug_field='slug',
165         required=False,
166     )
167     timestamp = serializers.IntegerField(required=False)
168     book_slug = serializers.SlugRelatedField(
169         queryset=Book.objects.all(),
170         source='book',
171         slug_field='slug',
172         required=False
173     )
174
175     class Meta:
176         model = models.UserListItem
177         fields = [
178             'client_id',
179             'uuid',
180             'order',
181             'list_slug',
182             'timestamp',
183             'favorites',
184             'deleted',
185
186             'book_slug',
187             'fragment',
188             'quote',
189             'bookmark',
190             'note',
191         ]
192         extra_kwargs = {
193             'order': {
194                 'required': False
195             }
196         }
197
198
199 @never_cache
200 class ListsView(ListCreateAPIView):
201     permission_classes = [IsAuthenticated]
202     #pagination_class = None
203     serializer_class = UserListSerializer
204
205     def get_queryset(self):
206         return models.UserList.objects.filter(
207             user=self.request.user,
208             favorites=False,
209             deleted=False
210         )
211
212     def perform_create(self, serializer):
213         serializer.save(user=self.request.user)
214
215
216 @never_cache
217 class ListView(RetrieveUpdateDestroyAPIView):
218     # TODO: check if can modify
219     permission_classes = [IsAuthenticated]
220     serializer_class = UserListSerializer
221
222     def get_object(self):
223         return get_object_or_404(
224             models.UserList,
225             slug=self.kwargs['slug'],
226             user=self.request.user)
227
228     def perform_update(self, serializer):
229         serializer.save(user=self.request.user)
230
231     def post(self, request, slug):
232         serializer = UserListBooksSerializer(data=request.data)
233         serializer.is_valid(raise_exception=True)
234         instance = self.get_object()
235         for book in serializer.validated_data['books']:
236             instance.append(book)
237         return Response(self.get_serializer(instance).data)
238
239     def perform_destroy(self, instance):
240         instance.deleted = True
241         instance.updated_at = now()
242         instance.save()
243
244
245 @never_cache
246 class ListItemView(APIView):
247     permission_classes = [IsAuthenticated]
248
249     def delete(self, request, slug, book):
250         instance = get_object_or_404(
251             models.UserList, slug=slug, user=self.request.user)
252         book = get_object_or_404(catalogue.models.Book, slug=book)
253         instance.remove(book=book)
254         return Response(UserListSerializer(instance).data)
255
256
257 @vary_on_auth
258 class ShelfView(ListAPIView):
259     permission_classes = [IsAuthenticated]
260     serializer_class = BookSerializer
261     pagination_class = None
262
263     def get_queryset(self):
264         state = self.kwargs['state']
265         if state not in ('reading', 'complete', 'likes'):
266             raise Http404
267         new_api = self.request.query_params.get('new_api')
268         after = self.request.query_params.get('after')
269         count = int(self.request.query_params.get('count', 50))
270         if state == 'likes':
271             books = Book.objects.filter(userlistitem__list__user=self.request.user)
272         else:
273             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
274                 .values_list('book_id', flat=True)
275             books = Book.objects.filter(id__in=list(ids)).distinct()
276             books = order_books(books, new_api)
277         if after:
278             books = books_after(books, after, new_api)
279         if count:
280             books = books[:count]
281
282         return books
283
284
285
286 class ProgressSerializer(serializers.ModelSerializer):
287     book = serializers.HyperlinkedRelatedField(
288         read_only=True,
289         view_name='catalogue_api_book',
290         lookup_field='slug'
291     )
292     book_slug = serializers.SlugRelatedField(
293         queryset=Book.objects.all(),
294         source='book',
295         slug_field='slug')
296     timestamp = serializers.IntegerField(required=False)
297
298     class Meta:
299         model = models.Progress
300         fields = [
301             'timestamp',
302             'book', 'book_slug', 'last_mode', 'text_percent',
303             'text_anchor',
304             'audio_percent',
305             'audio_timestamp',
306             'implicit_text_percent',
307             'implicit_text_anchor',
308             'implicit_audio_percent',
309             'implicit_audio_timestamp',
310         ]
311         extra_kwargs = {
312             'last_mode': {
313                 'required': False,
314                 'default': 'text',
315             }
316         }
317
318
319 class TextProgressSerializer(serializers.ModelSerializer):
320     class Meta:
321         model = models.Progress
322         fields = [
323                 'text_percent',
324                 'text_anchor',
325                 ]
326         read_only_fields = ['text_percent']
327
328 class AudioProgressSerializer(serializers.ModelSerializer):
329     class Meta:
330         model = models.Progress
331         fields = ['audio_percent', 'audio_timestamp']
332         read_only_fields = ['audio_percent']
333
334
335 @never_cache
336 class ProgressListView(ListAPIView):
337     permission_classes = [IsAuthenticated]
338     serializer_class = ProgressSerializer
339
340     def get_queryset(self):
341         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
342
343
344 class ProgressMixin:
345     def get_object(self):
346         try:
347             return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
348         except models.Progress.DoesNotExist:
349             book = get_object_or_404(Book, slug=self.kwargs['slug'])
350             return models.Progress(user=self.request.user, book=book)
351
352
353
354 @never_cache
355 class ProgressView(ProgressMixin, RetrieveAPIView):
356     permission_classes = [IsAuthenticated]
357     serializer_class = ProgressSerializer
358
359
360 @never_cache
361 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
362     permission_classes = [IsAuthenticated]
363     serializer_class = TextProgressSerializer
364
365     def perform_update(self, serializer):
366         serializer.instance.last_mode = 'text'
367         serializer.save()
368
369
370 @never_cache
371 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
372     permission_classes = [IsAuthenticated]
373     serializer_class = AudioProgressSerializer
374
375     def perform_update(self, serializer):
376         serializer.instance.last_mode = 'audio'
377         serializer.save()
378
379
380
381 @never_cache
382 class SyncView(ListAPIView):
383     permission_classes = [IsAuthenticated]
384     sync_id_field = 'slug'
385     sync_id_serializer_field = 'slug'
386     sync_user_field = 'user'
387
388     def get_queryset(self):
389         try:
390             timestamp = int(self.request.GET.get('ts'))
391         except:
392             timestamp = 0
393
394         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
395         
396         data = []
397         return self.get_queryset_for_ts(timestamp)
398
399     def get_queryset_for_ts(self, timestamp):
400         return self.model.objects.filter(
401             updated_at__gt=timestamp,
402             **{
403                 self.sync_user_field: self.request.user
404             }
405         ).order_by('updated_at')
406
407     def get_instance(self, user, data):
408         sync_id = data.get(self.sync_id_serializer_field)
409         if not sync_id:
410             return None
411         return self.model.objects.filter(**{
412             self.sync_user_field: user,
413             self.sync_id_field: sync_id
414         }).first()
415
416     def post(self, request):
417         new_ids = []
418         data = request.data
419         if not isinstance(data, list):
420             raise serializers.ValidationError('Payload should be a list')
421         for item in data:
422             instance = self.get_instance(request.user, item)
423             ser = self.get_serializer(
424                 instance=instance,
425                 data=item
426             )
427             ser.is_valid(raise_exception=True)
428             synced_instance = self.model.sync(
429                 request.user,
430                 instance,
431                 ser.validated_data
432             )
433             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
434                 new_ids.append({
435                     'client_id': ser.validated_data['client_id'],
436                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
437                 })
438         return Response(new_ids)
439
440
441 class ProgressSyncView(SyncView):
442     model = models.Progress
443     serializer_class = ProgressSerializer
444     
445     sync_id_field = 'book__slug'
446     sync_id_serializer_field = 'book_slug'
447
448
449 class UserListSyncView(SyncView):
450     model = models.UserList
451     serializer_class = UserListSerializer
452
453
454 class UserListItemSyncView(SyncView):
455     model = models.UserListItem
456     serializer_class = UserListItemSerializer
457
458     sync_id_field = 'uuid'
459     sync_id_serializer_field = 'uuid'
460     sync_user_field = 'list__user'
461
462     def get_queryset_for_ts(self, timestamp):
463         qs = self.model.objects.filter(
464             updated_at__gt=timestamp,
465             **{
466                 self.sync_user_field: self.request.user
467             }
468         )
469         if self.request.query_params.get('favorites'):
470             qs = qs.filter(list__favorites=True)
471         return qs.order_by('updated_at')
472
473
474 class BookmarkSyncView(SyncView):
475     model = bookmarks.models.Bookmark
476     serializer_class = BookmarkSerializer
477
478     sync_id_field = 'uuid'
479     sync_id_serializer_field = 'uuid'
480
481     def get_instance(self, user, data):
482         ret = super().get_instance(user, data)
483         if ret is None:
484             if data.get('location'):
485                 ret = self.model.get_by_location(user, data['location'])
486         return ret