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.
4 from datetime import datetime
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
24 class LikeView(APIView):
25 permission_classes = [IsAuthenticated]
27 def get(self, request, slug):
28 book = get_object_or_404(Book, slug=slug)
29 return Response({"likes": likes(request.user, book)})
31 def post(self, request, slug):
32 book = get_object_or_404(Book, slug=slug)
33 action = request.query_params.get('action', 'like')
35 models.UserList.like(request.user, book)
36 elif action == 'unlike':
37 models.UserList.unlike(request.user, book)
42 class LikeView2(APIView):
43 permission_classes = [IsAuthenticated]
45 def get(self, request, slug):
46 book = get_object_or_404(Book, slug=slug)
47 return Response({"likes": likes(request.user, book)})
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)})
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)})
61 class LikesView(APIView):
62 permission_classes = [IsAuthenticated]
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}
69 res = get_sets_for_book_ids(ids, request.user)
70 res = {books[bid]: v for bid, v in res.items()}
76 class MyLikesView(APIView):
77 permission_classes = [IsAuthenticated]
79 def get(self, request):
80 ul = models.UserList.get_favorites_list(request.user)
84 ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
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)
92 def to_internal_value(self, value):
93 return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
96 class UserListSerializer(serializers.ModelSerializer):
97 books = UserListItemsField(source='*')
100 model = models.UserList
101 fields = ['name', 'slug', 'books']
102 read_only_fields = ['slug']
104 def create(self, validated_data):
105 instance = models.UserList.get_by_name(
106 validated_data['user'],
107 validated_data['name'],
110 instance.userlistitem_set.all().delete()
111 for book in validated_data['books']:
112 instance.append(book)
115 def update(self, instance, validated_data):
116 instance.userlistitem_set.all().delete()
117 for book in validated_data['books']:
118 instance.append(instance)
121 class UserListBooksSerializer(UserListSerializer):
123 model = models.UserList
128 class ListsView(ListCreateAPIView):
129 permission_classes = [IsAuthenticated]
130 #pagination_class = None
131 serializer_class = UserListSerializer
133 def get_queryset(self):
134 return models.UserList.objects.filter(
135 user=self.request.user,
140 def perform_create(self, serializer):
141 serializer.save(user=self.request.user)
145 class ListView(RetrieveUpdateDestroyAPIView):
146 # TODO: check if can modify
147 permission_classes = [IsAuthenticated]
148 serializer_class = UserListSerializer
150 def get_object(self):
151 return get_object_or_404(
153 slug=self.kwargs['slug'],
154 user=self.request.user)
156 def perform_update(self, serializer):
157 serializer.save(user=self.request.user)
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)
167 def perform_destroy(self, instance):
175 class ListItemView(APIView):
176 permission_classes = [IsAuthenticated]
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)
187 class ShelfView(ListAPIView):
188 permission_classes = [IsAuthenticated]
189 serializer_class = BookSerializer
190 pagination_class = None
192 def get_queryset(self):
193 state = self.kwargs['state']
194 if state not in ('reading', 'complete', 'likes'):
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))
200 books = Book.objects.filter(userlistitem__list__user=self.request.user)
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)
207 books = books_after(books, after, new_api)
209 books = books[:count]
215 class ProgressSerializer(serializers.ModelSerializer):
216 book = serializers.HyperlinkedRelatedField(
218 view_name='catalogue_api_book',
221 book_slug = serializers.SlugRelatedField(source='book', read_only=True, slug_field='slug')
224 model = models.Progress
225 fields = ['book', 'book_slug', 'last_mode', 'text_percent',
229 'implicit_text_percent',
230 'implicit_text_anchor',
231 'implicit_audio_percent',
232 'implicit_audio_timestamp',
236 class TextProgressSerializer(serializers.ModelSerializer):
238 model = models.Progress
243 read_only_fields = ['text_percent']
245 class AudioProgressSerializer(serializers.ModelSerializer):
247 model = models.Progress
248 fields = ['audio_percent', 'audio_timestamp']
249 read_only_fields = ['audio_percent']
253 class ProgressListView(ListAPIView):
254 permission_classes = [IsAuthenticated]
255 serializer_class = ProgressSerializer
257 def get_queryset(self):
258 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
262 def get_object(self):
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)
272 class ProgressView(ProgressMixin, RetrieveAPIView):
273 permission_classes = [IsAuthenticated]
274 serializer_class = ProgressSerializer
278 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
279 permission_classes = [IsAuthenticated]
280 serializer_class = TextProgressSerializer
282 def perform_update(self, serializer):
283 serializer.instance.last_mode = 'text'
288 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
289 permission_classes = [IsAuthenticated]
290 serializer_class = AudioProgressSerializer
292 def perform_update(self, serializer):
293 serializer.instance.last_mode = 'audio'
298 class SyncSerializer(serializers.Serializer):
299 timestamp = serializers.IntegerField()
300 type = serializers.CharField()
301 id = serializers.CharField()
303 def to_representation(self, instance):
304 rep = super().to_representation(instance)
305 rep['object'] = instance['object'].data
308 def to_internal_value(self, data):
309 ret = super().to_internal_value(data)
310 ret['object'] = data['object']
314 class SyncView(ListAPIView):
315 permission_classes = [IsAuthenticated]
316 serializer_class = SyncSerializer
318 def get_queryset(self):
320 timestamp = int(self.request.GET.get('ts'))
324 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
327 for p in models.Progress.objects.filter(
328 user=self.request.user,
329 updated_at__gt=timestamp).order_by('updated_at'):
331 'timestamp': p.updated_at.timestamp(),
334 'object': ProgressSerializer(
335 p, context={'request': self.request}
336 ) if not p.deleted else None
340 def post(self, request):
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(
350 ts=datetime.fromtimestamp(d['timestamp'], tz=utc),