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