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
5 from django.db.models import Q
6 from django.http import Http404
7 from django.utils.timezone import now, utc
8 from rest_framework.exceptions import MethodNotAllowed
9 from rest_framework.generics import ListAPIView, ListCreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, get_object_or_404
10 from rest_framework.permissions import SAFE_METHODS, IsAuthenticated, IsAuthenticatedOrReadOnly
11 from rest_framework.response import Response
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.utils import likes
21 from social import models
22 import bookmarks.models
23 from . import serializers
24 from bookmarks.api.views import BookmarkSerializer
28 class SettingsView(RetrieveUpdateAPIView):
29 permission_classes = [IsAuthenticated]
30 serializer_class = serializers.SettingsSerializer
33 return models.UserProfile.get_for(self.request.user)
37 class LikeView(APIView):
38 permission_classes = [IsAuthenticated]
40 def get(self, request, slug):
41 book = get_object_or_404(Book, slug=slug)
42 return Response({"likes": likes(request.user, book)})
44 def post(self, request, slug):
45 book = get_object_or_404(Book, slug=slug)
46 action = request.query_params.get('action', 'like')
48 models.UserList.like(request.user, book)
49 elif action == 'unlike':
50 models.UserList.unlike(request.user, book)
55 class LikeView2(APIView):
56 permission_classes = [IsAuthenticated]
58 def get(self, request, slug):
59 book = get_object_or_404(Book, slug=slug)
60 return Response({"likes": likes(request.user, book)})
62 def put(self, request, slug):
63 book = get_object_or_404(Book, slug=slug)
64 models.UserList.like(request.user, book)
65 return Response({"likes": likes(request.user, book)})
67 def delete(self, request, slug):
68 book = get_object_or_404(Book, slug=slug)
69 models.UserList.unlike(request.user, book)
70 return Response({"likes": likes(request.user, book)})
74 class LikesView(APIView):
75 permission_classes = [IsAuthenticated]
77 def get(self, request):
78 slugs = request.GET.getlist('slug')
79 books = Book.objects.filter(slug__in=slugs)
80 books = {b.id: b.slug for b in books}
82 res = get_sets_for_book_ids(ids, request.user)
83 res = {books[bid]: v for bid, v in res.items()}
89 class MyLikesView(APIView):
90 permission_classes = [IsAuthenticated]
92 def get(self, request):
93 ul = models.UserList.get_favorites_list(request.user)
97 ul.userlistitem_set.exclude(deleted=True).exclude(book=None).values_list('book__slug', flat=True)
102 class ListsView(ListCreateAPIView):
103 permission_classes = [IsAuthenticated]
104 #pagination_class = None
106 def get_serializer_class(self):
107 if self.request.version == 'v2':
108 return serializers.UserListSerializerV2
109 return serializers.UserListSerializerV3
111 def get_queryset(self):
112 return models.UserList.objects.filter(
113 user=self.request.user,
118 def perform_create(self, serializer):
119 serializer.save(user=self.request.user)
122 def get_userlist(slug, request):
123 if request.method in SAFE_METHODS:
125 if request.user.is_authenticated:
126 q |= Q(user=request.user)
127 return get_object_or_404(
133 return get_object_or_404(
134 models.UserList.all_objects.all(),
141 class ListView(RetrieveUpdateDestroyAPIView):
142 # TODO: check if can modify
143 permission_classes = [IsAuthenticatedOrReadOnly]
145 def get_serializer_class(self):
146 if self.request.version == 'v2':
147 return serializers.UserListSerializerV2
148 return serializers.UserListSerializerV3
150 def get_object(self):
151 return get_userlist(self.kwargs['slug'], self.request)
153 def perform_update(self, serializer):
154 serializer.save(user=self.request.user)
156 def post(self, request, slug):
157 if request.version == 'v2':
158 # Accept posting a list of books here.
159 serializer = serializers.UserListBooksSerializer(data=request.data)
160 serializer.is_valid(raise_exception=True)
161 instance = self.get_object()
162 for book in serializer.validated_data['books']:
163 instance.append(book)
164 return Response(self.get_serializer(instance).data)
166 raise MethodNotAllowed(method=request.method)
168 def perform_destroy(self, instance):
169 instance.deleted = True
170 instance.updated_at = now()
175 class ListItemViewV2(APIView):
177 permission_classes = [IsAuthenticated]
179 def delete(self, request, slug, book):
180 instance = get_object_or_404(
181 models.UserList, slug=slug, user=self.request.user)
182 book = get_object_or_404(catalogue.models.Book, slug=book)
183 instance.remove(book=book)
184 return Response(serializers.UserListSerializerV2(instance).data)
188 class ListItemListViewV3(ListCreateAPIView):
189 permission_classes = [IsAuthenticatedOrReadOnly]
191 def get_queryset(self):
192 lst = get_userlist(self.kwargs['slug'], self.request)
193 return lst.userlistitem_set.all().order_by('order')
195 def get_serializer_class(self):
196 if self.request.method == 'GET':
197 return serializers.UserListItemReadSerializer
199 return serializers.UserListItemSerializer
201 def get_serializer(self, *args, **kwargs):
202 serializer_class = self.get_serializer_class()
203 kwargs.setdefault('context', self.get_serializer_context())
205 if isinstance(self.request.data, list):
206 kwargs['many'] = True
208 return serializer_class(*args, **kwargs)
210 def perform_create(self, serializer):
211 lst = get_userlist(self.kwargs['slug'], self.request)
212 serializer.save(list=lst)
216 class ListItemsForBook(ListAPIView):
217 permission_classes = [IsAuthenticated]
218 serializer_class = serializers.UserListItemReadSerializer
220 def get_queryset(self):
221 book = get_object_or_404(catalogue.models.Book, slug=self.kwargs['book'])
222 return models.UserListItem.objects.filter(
223 list__user=self.request.user,
228 class ListItemsView(APIView):
229 permission_classes = [IsAuthenticated]
231 def delete(self, request):
232 if not isinstance(self.request.data, list):
233 return Response({"error": "no data"}, status=400)
234 models.UserListItem.objects.filter(
235 list__user=self.request.user,
236 uuid__in=self.request.data
242 class ListItemViewV3(RetrieveUpdateDestroyAPIView):
243 permission_classes = [IsAuthenticated]
244 lookup_field = 'uuid'
246 def get_queryset(self):
247 return models.UserListItem.objects.filter(
248 list__user=self.request.user
251 def get_serializer_class(self):
252 if self.request.method == 'GET':
253 return serializers.UserListItemReadSerializer
255 return serializers.UserListItemSerializer
259 class ShelfView(ListAPIView):
260 permission_classes = [IsAuthenticated]
261 serializer_class = BookSerializer
262 pagination_class = None
264 def get_queryset(self):
265 state = self.kwargs['state']
266 if state not in ('reading', 'complete', 'likes'):
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))
272 books = Book.objects.filter(userlistitem__list__user=self.request.user)
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)
279 books = books_after(books, after, new_api)
281 books = books[:count]
287 class ProgressListView(ListAPIView):
288 permission_classes = [IsAuthenticated]
289 serializer_class = serializers.ProgressSerializer
291 def get_queryset(self):
292 return models.Progress.objects.filter(user=self.request.user).order_by('-updated_at')
296 def get_object(self):
298 return models.Progress.objects.get(user=self.request.user, book__slug=self.kwargs['slug'])
299 except models.Progress.DoesNotExist:
300 book = get_object_or_404(Book, slug=self.kwargs['slug'])
301 return models.Progress(user=self.request.user, book=book)
306 class ProgressView(ProgressMixin, RetrieveAPIView):
307 permission_classes = [IsAuthenticated]
308 serializer_class = serializers.ProgressSerializer
312 class TextProgressView(ProgressMixin, RetrieveUpdateAPIView):
313 permission_classes = [IsAuthenticated]
314 serializer_class = serializers.TextProgressSerializer
316 def perform_update(self, serializer):
317 serializer.instance.reported_timestamp = now()
318 serializer.instance.last_mode = 'text'
323 class AudioProgressView(ProgressMixin, RetrieveUpdateAPIView):
324 permission_classes = [IsAuthenticated]
325 serializer_class = serializers.AudioProgressSerializer
327 def perform_update(self, serializer):
328 serializer.instance.reported_timestamp = now()
329 serializer.instance.last_mode = 'audio'
335 class SyncView(ListAPIView):
336 permission_classes = [IsAuthenticated]
337 sync_id_field = 'slug'
338 sync_id_serializer_field = 'slug'
339 sync_user_field = 'user'
341 def get_queryset(self):
343 timestamp = int(self.request.GET.get('ts'))
347 timestamp = datetime.fromtimestamp(timestamp, tz=utc)
350 return self.get_queryset_for_ts(timestamp)
352 def get_queryset_for_ts(self, timestamp):
353 return self.model.objects.filter(
354 updated_at__gt=timestamp,
356 self.sync_user_field: self.request.user
358 ).order_by('updated_at')
360 def get_instance(self, user, data):
361 sync_id = data.get(self.sync_id_serializer_field)
364 return self.model.objects.filter(**{
365 self.sync_user_field: user,
366 self.sync_id_field: sync_id
369 def post(self, request):
372 if not isinstance(data, list):
373 raise serializers.ValidationError('Payload should be a list')
375 instance = self.get_instance(request.user, item)
376 ser = self.get_serializer(
380 ser.is_valid(raise_exception=True)
381 synced_instance = self.model.sync(
386 if instance is None and 'client_id' in ser.validated_data and synced_instance is not None:
388 'client_id': ser.validated_data['client_id'],
389 self.sync_id_serializer_field: getattr(synced_instance, self.sync_id_field),
391 return Response(new_ids)
394 class ProgressSyncView(SyncView):
395 model = models.Progress
396 serializer_class = serializers.ProgressSerializer
398 sync_id_field = 'book__slug'
399 sync_id_serializer_field = 'book_slug'
402 class UserListSyncView(SyncView):
403 model = models.UserList
405 def get_serializer_class(self):
406 if self.request.version == 'v2':
407 return serializers.UserListSerializerV2
408 return serializers.UserListSerializerV3
411 class UserListItemSyncView(SyncView):
412 model = models.UserListItem
413 serializer_class = serializers.UserListItemSerializer
415 sync_id_field = 'uuid'
416 sync_id_serializer_field = 'uuid'
417 sync_user_field = 'list__user'
419 def get_queryset_for_ts(self, timestamp):
420 qs = self.model.all_objects.filter(
421 updated_at__gt=timestamp,
423 self.sync_user_field: self.request.user
426 if self.request.query_params.get('favorites'):
427 qs = qs.filter(list__favorites=True)
428 return qs.order_by('updated_at')
431 class BookmarkSyncView(SyncView):
432 model = bookmarks.models.Bookmark
433 serializer_class = BookmarkSerializer
435 sync_id_field = 'uuid'
436 sync_id_serializer_field = 'uuid'
438 def get_instance(self, user, data):
439 ret = super().get_instance(user, data)
441 if data.get('location'):
442 ret = self.model.get_by_location(user, data['location'])