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 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
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 book.like(request.user)
36 elif action == 'unlike':
37 book.unlike(request.user)
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 book.like(request.user)
52 return Response({"likes": likes(request.user, book)})
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)})
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 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()}
86 res = list(books.values())
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)
95 def to_internal_value(self, value):
96 return {'books': catalogue.models.Book.objects.filter(slug__in=value)}
99 class UserListSerializer(serializers.ModelSerializer):
100 books = TaggedBooksField(source='*')
103 model = catalogue.models.Tag
104 fields = ['name', 'slug', 'books']
105 read_only_fields = ['slug']
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)
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)
120 class UserListBooksSerializer(UserListSerializer):
122 model = catalogue.models.Tag
127 class ListsView(ListCreateAPIView):
128 permission_classes = [IsAuthenticated]
129 #pagination_class = None
130 serializer_class = UserListSerializer
132 def get_queryset(self):
133 return catalogue.models.Tag.objects.filter(user=self.request.user).exclude(name='')
135 def perform_create(self, serializer):
136 serializer.save(user=self.request.user)
140 class ListView(RetrieveUpdateDestroyAPIView):
141 # TODO: check if can modify
142 permission_classes = [IsAuthenticated]
143 serializer_class = UserListSerializer
145 def get_object(self):
146 return get_object_or_404(catalogue.models.Tag, slug=self.kwargs['slug'], user=self.request.user)
148 def perform_update(self, serializer):
149 serializer.save(user=self.request.user)
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)
161 class ListItemView(APIView):
162 permission_classes = [IsAuthenticated]
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)
172 class ShelfView(ListAPIView):
173 permission_classes = [IsAuthenticated]
174 serializer_class = BookSerializer
175 pagination_class = None
177 def get_queryset(self):
178 state = self.kwargs['state']
179 if state not in ('reading', 'complete', 'likes'):
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))
185 books = Book.tagged.with_any(self.request.user.tag_set.all())
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)
192 books = books_after(books, after, new_api)
194 books = books[:count]
200 class ProgressSerializer(serializers.ModelSerializer):
201 book = serializers.HyperlinkedRelatedField(
203 view_name='catalogue_api_book',
206 book_slug = serializers.SlugRelatedField(source='book', read_only=True, slug_field='slug')
209 model = models.Progress
210 fields = ['book', 'book_slug', 'last_mode', 'text_percent',
214 'implicit_text_percent',
215 'implicit_text_anchor',
216 'implicit_audio_percent',
217 'implicit_audio_timestamp',
221 class TextProgressSerializer(serializers.ModelSerializer):
223 model = models.Progress
228 read_only_fields = ['text_percent']
230 class AudioProgressSerializer(serializers.ModelSerializer):
232 model = models.Progress
233 fields = ['audio_percent', 'audio_timestamp']
234 read_only_fields = ['audio_percent']
238 class ProgressListView(ListAPIView):
239 permission_classes = [IsAuthenticated]
240 serializer_class = ProgressSerializer
242 def get_queryset(self):
243 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
247 def get_object(self):
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)
257 class ProgressView(ProgressMixin, RetrieveAPIView):
258 permission_classes = [IsAuthenticated]
259 serializer_class = ProgressSerializer
263 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
264 permission_classes = [IsAuthenticated]
265 serializer_class = TextProgressSerializer
267 def perform_update(self, serializer):
268 serializer.instance.last_mode = 'text'
273 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
274 permission_classes = [IsAuthenticated]
275 serializer_class = AudioProgressSerializer
277 def perform_update(self, serializer):
278 serializer.instance.last_mode = 'audio'
283 class SyncSerializer(serializers.Serializer):
284 timestamp = serializers.IntegerField()
285 type = serializers.CharField()
286 id = serializers.CharField()
288 def to_representation(self, instance):
289 rep = super().to_representation(instance)
290 rep['object'] = instance['object'].data
293 def to_internal_value(self, data):
294 ret = super().to_internal_value(data)
295 ret['object'] = data['object']
299 class SyncView(ListAPIView):
300 permission_classes = [IsAuthenticated]
301 serializer_class = SyncSerializer
303 def get_queryset(self):
305 timestamp = int(self.request.GET.get('ts'))
309 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
312 for p in models.Progress.objects.filter(
313 user=self.request.user,
314 updated_at__gt=timestamp).order_by('updated_at'):
316 'timestamp': p.updated_at.timestamp(),
319 'object': ProgressSerializer(
320 p, context={'request': self.request}
321 ) if not p.deleted else None
325 def post(self, request):
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(
335 ts=datetime.fromtimestamp(d['timestamp'], tz=utc),