from django.db.models import Count, Q, Avg, OuterRef, Subquery, F from rest_framework.permissions import IsAuthenticated , AllowAny from rest_framework.authentication import TokenAuthentication from rest_framework.response import Response from rest_framework.generics import ListAPIView, RetrieveAPIView, CreateAPIView from rest_framework.filters import SearchFilter from rest_framework import status from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi from apps.library.pagination import NoPagination from utils.pagination import StandardResultsSetPagination from apps.library.models import * from apps.library.serializers import * from apps.account.models import User from apps.library.doc import ( book_list_swagger, book_detail_swagger, category_list_swagger, pinned_collection_list_swagger, middle_collection_list_swagger ) from utils.pagination import StandardResultsSetPagination class CategoryListView(ListAPIView): """ API view to list all book categories """ serializer_class = CategorySerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] pagination_class = StandardResultsSetPagination @category_list_swagger def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): return Category.objects.filter( status=True ).annotate( books_count_annotation=Count('related_categories') ).order_by('title') class PinnedBookCollectionListView(ListAPIView): """ API view to list pinned book collections with their top 3 book covers """ serializer_class = PinnedBookCollectionSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] pagination_class = NoPagination @pinned_collection_list_swagger def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): return BookCollection.objects.filter( status=True, display_position=BookCollection.DisplayPosition.PINNED ).order_by('-order', '-id') def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) categories_count = Category.objects.filter(status=True).count() from apps.bookmark.models import Bookmark bookmarks_count = Bookmark.objects.filter( service=Bookmark.ServiceChoices.LIBRARY, ).count() downloads_count = BookDownload.objects.all().count() info = { "categories_count": categories_count, "bookmarks_count": bookmarks_count, "downloads_count": downloads_count } data = { "count": response.data.get("count"), "next": response.data.get("next"), "previous": response.data.get("previous"), "info": info, "results": response.data.get("results") } return Response(data, status=status.HTTP_200_OK) class BookListView(ListAPIView): """ API view to list books with filtering and search capabilities """ serializer_class = BookSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] filter_backends = [SearchFilter] search_fields = ['title', 'summary', 'publisher', 'isbn'] pagination_class = StandardResultsSetPagination @book_list_swagger def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): queryset = Book.objects.filter(status=True) # Filter by collection if provided collection_id = self.request.query_params.get('collection_id') if collection_id: queryset = queryset.filter(collections__id=collection_id) # Filter by category if provided category = self.request.query_params.get('category') if category: # Support both single slug and comma-separated list of slugs category_slugs = [slug.strip() for slug in category.split(',')] queryset = queryset.filter(categories__slug__in=category_slugs).distinct() # Filter by middle collection if requested # if self.request.query_params.get('middle'): # middle_collections = BookCollection.objects.filter( # status=True, # display_position=BookCollection.DisplayPosition.MIDDLE # ) # if middle_collections.exists(): # queryset = queryset.filter(collections__in=middle_collections) # Filter by bottom collection if requested # if self.request.query_params.get('bottom'): # bottom_collections = BookCollection.objects.filter( # status=True, # display_position=BookCollection.DisplayPosition.BOTTOM # ) # if bottom_collections.exists(): # queryset = queryset.filter(collections__in=bottom_collections) # Filter by bookmarked books if requested is_bookmark = self.request.query_params.get('is_bookmark', '').lower() if is_bookmark == 'true' and self.request.user.is_authenticated: # Import Bookmark model here to avoid circular imports from apps.bookmark.models import Bookmark # Get all bookmarked book IDs for the current user bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.LIBRARY, status=True ).values_list('content_id', flat=True) queryset = queryset.filter(id__in=bookmarked_ids) # Import Rate here to avoid circular imports if any from apps.bookmark.models.rate import Rate # Subquery to calculate average rating for each book avg_rating = Rate.objects.filter( service=Rate.ServiceChoices.LIBRARY, content_id=OuterRef('pk'), status=True ).order_by().values('content_id').annotate( avg_rate=Avg('rate') ).values('avg_rate') queryset = queryset.annotate(average_rate=Subquery(avg_rating)) # Sort mapping sort_mapping = { 'most_popular': [F('download_count').desc(nulls_last=True), '-created_at'], 'newest': ['-created_at'], 'most_view': [F('view_count').desc(nulls_last=True), '-created_at'], 'most_rated': [F('average_rate').desc(nulls_last=True), '-created_at'], } # Sort by parameter sort = self.request.query_params.get('sort', '-pin,-created_at') if sort in sort_mapping: queryset = queryset.order_by(*sort_mapping[sort]) else: # Allowed sort fields allowed_sorts = [ 'created_at', '-created_at', 'view_count', '-view_count', 'download_count', '-download_count', 'title', '-title', 'pin', '-pin', '-pin,-created_at' ] if sort in allowed_sorts: # Handle multiple sort fields (e.g., '-pin,-created_at') if ',' in sort: queryset = queryset.order_by(*sort.split(',')) else: queryset = queryset.order_by(sort) else: queryset = queryset.order_by('-pin', '-created_at') return queryset class BookDetailView(RetrieveAPIView): """ API view to retrieve detailed information about a specific book """ serializer_class = BookSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] queryset = Book.objects.filter(status=True) lookup_field = 'slug' @book_detail_swagger def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def retrieve(self, request, *args, **kwargs): instance = self.get_object() # Increment view count when book details are viewed instance.increment_view_count() serializer = self.get_serializer(instance) return Response(serializer.data) class MiddleBookCollectionListView(ListAPIView): """ API view to list middle section book collections with their books """ serializer_class = MiddleBookCollectionSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] pagination_class = NoPagination @middle_collection_list_swagger def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): return BookCollection.objects.filter( status=True, display_position=BookCollection.DisplayPosition.MIDDLE ).order_by('order') class DownloadedBooksListView(ListAPIView): """ API view to list books that have been downloaded by the current user """ serializer_class = BookSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] filter_backends = [SearchFilter] search_fields = ['title', 'summary', 'publisher', 'isbn'] pagination_class = StandardResultsSetPagination @swagger_auto_schema( operation_id="list_downloaded_books", operation_description=""" Retrieve a list of books that have been downloaded by the current user. This endpoint returns a paginated list of books that the authenticated user has downloaded. The results are not cached to ensure real-time accuracy of the download list. You can search for downloaded books by title, summary, publisher, or ISBN using the 'search' query parameter. """, operation_summary="List Downloaded Books", tags=["Dobodbi - Library"], manual_parameters=[ openapi.Parameter( 'search', openapi.IN_QUERY, description="Search downloaded books by title, summary, publisher, or ISBN", type=openapi.TYPE_STRING, required=False ) ], responses={ 200: "List of downloaded books with pagination", 401: "Authentication credentials were not provided or are invalid", 500: "Internal server error occurred" } ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): # DEBUG: Hardcode user to root@admin.com # user =self.request.user # # Get all downloaded book IDs for the current user downloaded_ids = BookDownload.objects.filter( user=self.request.user, status=True ).values_list('book_id', flat=True) # downloaded_ids = BookDownload.objects.filter( # user=user, # status=True # ).values_list('book_id', flat=True) # Return books that match these IDs return Book.objects.filter( id__in=downloaded_ids, status=True ).order_by('-created_at') class BookDownloadCreateAPIView(CreateAPIView): """ API view to create a book download record and increment the book's download count """ serializer_class = BookDownloadSerializer permission_classes = (IsAuthenticated,) authentication_classes = [TokenAuthentication] @swagger_auto_schema( operation_id="download_book", operation_description=""" Create a book download record and increment the book's download count. This endpoint creates a record of a book download by the current user and increments the book's download count. It requires the book ID in the request body. If the user has already downloaded the book, the existing record will be updated with the current timestamp. """, operation_summary="Download Book", tags=["Dobodbi - Library"], request_body=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'book_id': openapi.Schema( type=openapi.TYPE_INTEGER, description="ID of the book to download" ) }, required=['book_id'] ), responses={ 201: openapi.Response( description="Book download record created successfully", schema=openapi.Schema( type=openapi.TYPE_OBJECT, properties={ 'id': openapi.Schema(type=openapi.TYPE_INTEGER), 'created_at': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATETIME), 'updated_at': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATETIME), 'status': openapi.Schema(type=openapi.TYPE_BOOLEAN) } ) ), 400: "Invalid request data or book not found", 401: "Authentication credentials were not provided or are invalid", 500: "Internal server error occurred" } ) def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs)