basic sync view
[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 rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, DestroyAPIView, 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.utils import likes, get_set
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             book.like(request.user)
36         elif action == 'unlike':
37             book.unlike(request.user)
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         book.like(request.user)
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         book.unlike(request.user)
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         ids = catalogue.models.tag.TagRelation.objects.filter(tag__user=request.user).values_list('object_id', flat=True).distinct()
81         books = Book.objects.filter(id__in=ids)
82         books = {b.id: b.slug for b in books}
83         res = get_sets_for_book_ids(ids, request.user)
84         res = {books[bid]: v for bid, v in res.items()}
85
86         res = list(books.values())
87         res.sort()
88         return Response(res)
89
90
91 class TaggedBooksField(serializers.Field):
92     def to_representation(self, value):
93         return catalogue.models.Book.tagged.with_all([value]).values_list('slug', flat=True)
94
95     def to_internal_value(self, value):
96         return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
97
98
99 class UserListSerializer(serializers.ModelSerializer):
100     books = TaggedBooksField(source='*')
101
102     class Meta:
103         model = catalogue.models.Tag
104         fields = ['name', 'slug', 'books']
105         read_only_fields = ['slug']
106
107     def create(self, validated_data):
108         instance = get_set(validated_data['user'], validated_data['name'])
109         catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete()
110         for book in validated_data['books']:
111             catalogue.models.Tag.objects.add_tag(book, instance)
112         return instance
113
114     def update(self, instance, validated_data):
115         catalogue.models.tag.TagRelation.objects.filter(tag=instance).delete()
116         for book in validated_data['books']:
117             catalogue.models.Tag.objects.add_tag(book, instance)
118         return instance
119
120 class UserListBooksSerializer(UserListSerializer):
121     class Meta:
122         model = catalogue.models.Tag
123         fields = ['books']
124
125
126 @never_cache
127 class ListsView(ListCreateAPIView):
128     permission_classes = [IsAuthenticated]
129     #pagination_class = None
130     serializer_class = UserListSerializer
131
132     def get_queryset(self):
133         return catalogue.models.Tag.objects.filter(user=self.request.user).exclude(name='')
134
135     def perform_create(self, serializer):
136         serializer.save(user=self.request.user)
137
138
139 @never_cache
140 class ListView(RetrieveUpdateDestroyAPIView):
141     # TODO: check if can modify
142     permission_classes = [IsAuthenticated]
143     serializer_class = UserListSerializer
144
145     def get_object(self):
146         return get_object_or_404(catalogue.models.Tag, slug=self.kwargs['slug'], user=self.request.user)
147
148     def perform_update(self, serializer):
149         serializer.save(user=self.request.user)
150
151     def post(self, request, slug):
152         serializer = UserListBooksSerializer(data=request.data)
153         serializer.is_valid(raise_exception=True)
154         instance = self.get_object()
155         for book in serializer.validated_data['books']:
156             catalogue.models.Tag.objects.add_tag(book, instance)
157         return Response(self.get_serializer(instance).data)
158
159
160 @never_cache
161 class ListItemView(APIView):
162     permission_classes = [IsAuthenticated]
163
164     def delete(self, request, slug, book):
165         instance = get_object_or_404(catalogue.models.Tag, slug=slug, user=self.request.user)
166         book = get_object_or_404(catalogue.models.Book, slug=book)
167         catalogue.models.Tag.objects.remove_tag(book, instance)
168         return Response(UserListSerializer(instance).data)
169
170
171 @vary_on_auth
172 class ShelfView(ListAPIView):
173     permission_classes = [IsAuthenticated]
174     serializer_class = BookSerializer
175     pagination_class = None
176
177     def get_queryset(self):
178         state = self.kwargs['state']
179         if state not in ('reading', 'complete', 'likes'):
180             raise Http404
181         new_api = self.request.query_params.get('new_api')
182         after = self.request.query_params.get('after')
183         count = int(self.request.query_params.get('count', 50))
184         if state == 'likes':
185             books = Book.tagged.with_any(self.request.user.tag_set.all())
186         else:
187             ids = BookUserData.objects.filter(user=self.request.user, complete=state == 'complete')\
188                 .values_list('book_id', flat=True)
189             books = Book.objects.filter(id__in=list(ids)).distinct()
190             books = order_books(books, new_api)
191         if after:
192             books = books_after(books, after, new_api)
193         if count:
194             books = books[:count]
195
196         return books
197
198
199
200 class ProgressSerializer(serializers.ModelSerializer):
201     book = serializers.HyperlinkedRelatedField(
202         read_only=True,
203         view_name='catalogue_api_book',
204         lookup_field='slug'
205     )
206     book_slug = serializers.SlugRelatedField(source='book', read_only=True, slug_field='slug')
207
208     class Meta:
209         model = models.Progress
210         fields = ['book', 'book_slug', 'last_mode', 'text_percent',
211     'text_anchor',
212     'audio_percent',
213     'audio_timestamp',
214     'implicit_text_percent',
215     'implicit_text_anchor',
216     'implicit_audio_percent',
217     'implicit_audio_timestamp',
218     ]
219
220
221 class TextProgressSerializer(serializers.ModelSerializer):
222     class Meta:
223         model = models.Progress
224         fields = [
225                 'text_percent',
226                 'text_anchor',
227                 ]
228         read_only_fields = ['text_percent']
229
230 class AudioProgressSerializer(serializers.ModelSerializer):
231     class Meta:
232         model = models.Progress
233         fields = ['audio_percent', 'audio_timestamp']
234         read_only_fields = ['audio_percent']
235
236
237 @never_cache
238 class ProgressListView(ListAPIView):
239     permission_classes = [IsAuthenticated]
240     serializer_class = 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 = ProgressSerializer
260
261
262 @never_cache
263 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
264     permission_classes = [IsAuthenticated]
265     serializer_class = TextProgressSerializer
266
267     def perform_update(self, serializer):
268         serializer.instance.last_mode = 'text'
269         serializer.save()
270
271
272 @never_cache
273 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
274     permission_classes = [IsAuthenticated]
275     serializer_class = AudioProgressSerializer
276
277     def perform_update(self, serializer):
278         serializer.instance.last_mode = 'audio'
279         serializer.save()
280
281
282
283 class SyncSerializer(serializers.Serializer):
284     timestamp = serializers.IntegerField()
285     type = serializers.CharField()
286     id = serializers.CharField()
287
288     def to_representation(self, instance):
289         rep = super().to_representation(instance)
290         rep['object'] = instance['object'].data
291         return rep
292
293     def to_internal_value(self, data):
294         ret = super().to_internal_value(data)
295         ret['object'] = data['object']
296         return ret
297
298
299 class SyncView(ListAPIView):
300     permission_classes = [IsAuthenticated]
301     serializer_class = SyncSerializer
302
303     def get_queryset(self):
304         try:
305             timestamp = int(self.request.GET.get('ts'))
306         except:
307             timestamp = 0
308
309         timestamp = datetime.fromtimestamp(timestamp, tz=utc)
310         
311         data = []
312         for p in models.Progress.objects.filter(
313                 user=self.request.user,
314                 updated_at__gt=timestamp).order_by('updated_at'):
315             data.append({
316                 'timestamp': p.updated_at.timestamp(),
317                 'type': 'progress',
318                 'id': p.book.slug,
319                 'object': ProgressSerializer(
320                     p, context={'request': self.request}
321                 ) if not p.deleted else None
322             })
323         return data
324
325     def post(self, request):
326         data = request.data
327         for item in data:
328             ser = SyncSerializer(data=item)
329             ser.is_valid(raise_exception=True)
330             d = ser.validated_data
331             if d['type'] == 'progress':
332                 models.Progress.sync(
333                     user=request.user,
334                     slug=d['id'],
335                     ts=datetime.fromtimestamp(d['timestamp'], tz=utc),
336                     data=d['object']
337                 )
338         return Response()