fix imports
[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.update(
241             deleted=True,
242             updated_at=now()
243         )
244
245
246 @never_cache
247 class ListItemView(APIView):
248     permission_classes = [IsAuthenticated]
249
250     def delete(self, request, slug, book):
251         instance = get_object_or_404(
252             models.UserList, slug=slug, user=self.request.user)
253         book = get_object_or_404(catalogue.models.Book, slug=book)
254         instance.remove(book=book)
255         return Response(UserListSerializer(instance).data)
256
257
258 @vary_on_auth
259 class ShelfView(ListAPIView):
260     permission_classes = [IsAuthenticated]
261     serializer_class = BookSerializer
262     pagination_class = None
263
264     def get_queryset(self):
265         state = self.kwargs['state']
266         if state not in ('reading', 'complete', 'likes'):
267             raise Http404
268         new_api = self.request.query_params.get('new_api')
269         after = self.request.query_params.get('after')
270         count = int(self.request.query_params.get('count', 50))
271         if state == 'likes':
272             books = Book.objects.filter(userlistitem__list__user=self.request.user)
273         else:
274             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
275                 .values_list('book_id', flat=True)
276             books = Book.objects.filter(id__in=list(ids)).distinct()
277             books = order_books(books, new_api)
278         if after:
279             books = books_after(books, after, new_api)
280         if count:
281             books = books[:count]
282
283         return books
284
285
286
287 class ProgressSerializer(serializers.ModelSerializer):
288     book = serializers.HyperlinkedRelatedField(
289         read_only=True,
290         view_name='catalogue_api_book',
291         lookup_field='slug'
292     )
293     book_slug = serializers.SlugRelatedField(
294         queryset=Book.objects.all(),
295         source='book',
296         slug_field='slug')
297     timestamp = serializers.IntegerField(required=False)
298
299     class Meta:
300         model = models.Progress
301         fields = [
302             'timestamp',
303             'book', 'book_slug', 'last_mode', 'text_percent',
304             'text_anchor',
305             'audio_percent',
306             'audio_timestamp',
307             'implicit_text_percent',
308             'implicit_text_anchor',
309             'implicit_audio_percent',
310             'implicit_audio_timestamp',
311         ]
312         extra_kwargs = {
313             'last_mode': {
314                 'required': False,
315                 'default': 'text',
316             }
317         }
318
319
320 class TextProgressSerializer(serializers.ModelSerializer):
321     class Meta:
322         model = models.Progress
323         fields = [
324                 'text_percent',
325                 'text_anchor',
326                 ]
327         read_only_fields = ['text_percent']
328
329 class AudioProgressSerializer(serializers.ModelSerializer):
330     class Meta:
331         model = models.Progress
332         fields = ['audio_percent', 'audio_timestamp']
333         read_only_fields = ['audio_percent']
334
335
336 @never_cache
337 class ProgressListView(ListAPIView):
338     permission_classes = [IsAuthenticated]
339     serializer_class = ProgressSerializer
340
341     def get_queryset(self):
342         return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
343
344
345 class ProgressMixin:
346     def get_object(self):
347         try:
348             return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
349         except models.Progress.DoesNotExist:
350             book = get_object_or_404(Book, slug=self.kwargs['slug'])
351             return models.Progress(user=self.request.user, book=book)
352
353
354
355 @never_cache
356 class ProgressView(ProgressMixin, RetrieveAPIView):
357     permission_classes = [IsAuthenticated]
358     serializer_class = ProgressSerializer
359
360
361 @never_cache
362 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
363     permission_classes = [IsAuthenticated]
364     serializer_class = TextProgressSerializer
365
366     def perform_update(self, serializer):
367         serializer.instance.last_mode = 'text'
368         serializer.save()
369
370
371 @never_cache
372 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
373     permission_classes = [IsAuthenticated]
374     serializer_class = AudioProgressSerializer
375
376     def perform_update(self, serializer):
377         serializer.instance.last_mode = 'audio'
378         serializer.save()
379
380
381
382 @never_cache
383 class SyncView(ListAPIView):
384     permission_classes = [IsAuthenticated]
385     sync_id_field = 'slug'
386     sync_id_serializer_field = 'slug'
387     sync_user_field = 'user'
388
389     def get_queryset(self):
390         try:
391             timestamp = int(self.request.GET.get('ts'))
392         except:
393             timestamp = 0
394
395         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
396         
397         data = []
398         return self.get_queryset_for_ts(timestamp)
399
400     def get_queryset_for_ts(self, timestamp):
401         return self.model.objects.filter(
402             updated_at__gt=timestamp,
403             **{
404                 self.sync_user_field: self.request.user
405             }
406         ).order_by('updated_at')
407
408     def get_instance(self, user, data):
409         sync_id = data.get(self.sync_id_serializer_field)
410         if not sync_id:
411             return None
412         return self.model.objects.filter(**{
413             self.sync_user_field: user,
414             self.sync_id_field: sync_id
415         }).first()
416
417     def post(self, request):
418         new_ids = []
419         data = request.data
420         if not isinstance(data, list):
421             raise serializers.ValidationError('Payload should be a list')
422         for item in data:
423             instance = self.get_instance(request.user, item)
424             ser = self.get_serializer(
425                 instance=instance,
426                 data=item
427             )
428             ser.is_valid(raise_exception=True)
429             synced_instance = self.model.sync(
430                 request.user,
431                 instance,
432                 ser.validated_data
433             )
434             if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
435                 new_ids.append({
436                     'client_id': ser.validated_data['client_id'],
437                     self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
438                 })
439         return Response(new_ids)
440
441
442 class ProgressSyncView(SyncView):
443     model = models.Progress
444     serializer_class = ProgressSerializer
445     
446     sync_id_field = 'book__slug'
447     sync_id_serializer_field = 'book_slug'
448
449
450 class UserListSyncView(SyncView):
451     model = models.UserList
452     serializer_class = UserListSerializer
453
454
455 class UserListItemSyncView(SyncView):
456     model = models.UserListItem
457     serializer_class = UserListItemSerializer
458
459     sync_id_field = 'uuid'
460     sync_id_serializer_field = 'uuid'
461     sync_user_field = 'list__user'
462
463     def get_queryset_for_ts(self, timestamp):
464         qs = self.model.objects.filter(
465             updated_at__gt=timestamp,
466             **{
467                 self.sync_user_field: self.request.user
468             }
469         )
470         if self.request.query_params.get('favorites'):
471             qs = qs.filter(list__favorites=True)
472         return qs.order_by('updated_at')
473
474
475 class BookmarkSyncView(SyncView):
476     model = bookmarks.models.Bookmark
477     serializer_class = BookmarkSerializer
478
479     sync_id_field = 'uuid'
480     sync_id_serializer_field = 'uuid'
481
482     def get_instance(self, user, data):
483         ret = super().get_instance(user, data)
484         if ret is None:
485             if data.get('location'):
486                 ret = self.model.get_by_location(user, data['location'])
487         return ret