Browse Source

library models , views and serializers updated with new author model

master
Mohsen Taba 1 month ago
parent
commit
4194cc5b2b
  1. 31
      apps/library/admin.py
  2. 267
      apps/library/doc.py
  3. 8
      apps/library/management/commands/populate_books.py
  4. 74
      apps/library/migrations/0002_author_and_book_author_fk.py
  5. 45
      apps/library/models.py
  6. 58
      apps/library/serializers.py
  7. 23
      apps/library/serializers_admin.py
  8. 9
      apps/library/urls.py
  9. 50
      apps/library/views.py
  10. 33
      apps/library/views_admin.py

31
apps/library/admin.py

@ -31,9 +31,9 @@ class BookAdminForm(forms.ModelForm):
class BookAdmin(ModelAdmin):
form = BookAdminForm
list_display = ('title', 'display_categories', 'display_collections', 'status', 'view_count', 'created_at')
list_display = ('title', 'display_author', 'display_categories', 'display_collections', 'status', 'view_count', 'created_at')
list_filter = ('status', 'pin', 'file_type', 'created_at', 'updated_at')
search_fields = ('title', 'slug', 'summary', 'description')
search_fields = ('title', 'slug', 'summary', 'description', 'author__full_name')
# autocomplete_fields = ('categories', 'collections', )
list_filter_submit = True
warn_unsaved_form = True
@ -55,12 +55,16 @@ class BookAdmin(ModelAdmin):
return ', '.join([collection.title for collection in collections])
return '-'
@display(description=_('Author'))
def display_author(self, obj):
return obj.author.full_name if obj.author_id else '-'
fieldsets = (
(None, {
'fields': ()
}),
('Detail', {
'fields': ('title', 'slogan', 'thumbnail', 'pages_count', 'publisher', 'year_of_publication', 'isbn', 'numnber_of_volume')
'fields': ('title', 'author', 'slogan', 'thumbnail', 'pages_count', 'publisher', 'year_of_publication', 'isbn', 'numnber_of_volume')
}),
("Summary", {
'fields': ('summary_title', 'summary')
@ -242,8 +246,29 @@ class CategoryAdmin(ModelAdmin):
self.message_user(request, _("Category '%(title)s' is now %(status)s.") % {"title": obj.title, "status": status_text})
class AuthorAdmin(ModelAdmin):
list_display = ('full_name', 'books_count_display', 'birth_date', 'death_date', 'created_at')
search_fields = ('full_name', 'slug')
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
(None, {
'fields': ('full_name', 'slug', 'thumbnail', 'birth_date', 'death_date')
}),
(_('Timestamps'), {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',),
}),
)
@display(description=_('Number of Books'))
def books_count_display(self, obj):
return obj.books.count()
# Register models with the custom admin site
dovoodi_admin_site.register(Book, BookAdmin)
dovoodi_admin_site.register(Author, AuthorAdmin)
dovoodi_admin_site.register(PinnedBookCollection, PinnedBookCollectionAdmin)
dovoodi_admin_site.register(MiddleBookCollection, MiddleBookCollectionAdmin)
dovoodi_admin_site.register(Category, CategoryAdmin)

267
apps/library/doc.py

@ -17,22 +17,6 @@ collection_id_param = openapi.Parameter(
required=False
)
middle_param = openapi.Parameter(
'middle',
openapi.IN_QUERY,
description="Filter books by middle section collection (any value will trigger the filter)",
type=openapi.TYPE_STRING,
required=False
)
bottom_param = openapi.Parameter(
'bottom',
openapi.IN_QUERY,
description="Filter books by bottom section collection (any value will trigger the filter)",
type=openapi.TYPE_STRING,
required=False
)
is_bookmark_param = openapi.Parameter(
'is_bookmark',
openapi.IN_QUERY,
@ -49,6 +33,14 @@ category_param = openapi.Parameter(
required=False
)
author_param = openapi.Parameter(
'author',
openapi.IN_QUERY,
description="Filter books by author slug",
type=openapi.TYPE_STRING,
required=False
)
sort_param = openapi.Parameter(
'sort',
openapi.IN_QUERY,
@ -65,44 +57,45 @@ search_param = openapi.Parameter(
required=False
)
thumbs_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
nullable=True,
properties={
'sm': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'md': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'lg': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
}
)
author_summary_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER),
'full_name': openapi.Schema(type=openapi.TYPE_STRING),
'slug': openapi.Schema(type=openapi.TYPE_STRING),
'thumbnail': thumbs_schema,
}
)
# Response schemas
book_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the book"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the book"
),
'slug': openapi.Schema(
type=openapi.TYPE_STRING,
description="URL-friendly slug for the book"
),
'summary': openapi.Schema(
type=openapi.TYPE_STRING,
description="Brief summary of the book"
),
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Detailed description of the book"
),
'thumbnail_url': openapi.Schema(
type=openapi.TYPE_STRING,
description="URL to the book's thumbnail image",
nullable=True
),
'author': openapi.Schema(
type=openapi.TYPE_STRING,
description="Author of the book"
),
'language': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Language ID of the book",
nullable=True
),
'id': openapi.Schema(type=openapi.TYPE_INTEGER, description="Unique identifier for the book"),
'title': openapi.Schema(type=openapi.TYPE_STRING, description="Title of the book"),
'slug': openapi.Schema(type=openapi.TYPE_STRING, description="URL-friendly slug for the book"),
'summary': openapi.Schema(type=openapi.TYPE_STRING, description="Brief summary of the book"),
'summary_title': openapi.Schema(type=openapi.TYPE_STRING, description="Summary title", nullable=True),
'slogan': openapi.Schema(type=openapi.TYPE_STRING, description="Short slogan", nullable=True),
'thumbnail': thumbs_schema,
'author': openapi.Schema(type=openapi.TYPE_STRING, description="Author full name", nullable=True),
'author_detail': author_summary_schema,
'publisher': openapi.Schema(type=openapi.TYPE_STRING, description="Publisher", nullable=True),
'year_of_publication': openapi.Schema(type=openapi.TYPE_STRING, description="Year of publication", nullable=True),
'isbn': openapi.Schema(type=openapi.TYPE_STRING, description="ISBN", nullable=True),
'numnber_of_volume': openapi.Schema(type=openapi.TYPE_STRING, description="Volume number", nullable=True),
'language': openapi.Schema(type=openapi.TYPE_STRING, description="Language code", nullable=True),
'main_themes': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
@ -113,36 +106,14 @@ book_schema = openapi.Schema(
items=openapi.Schema(type=openapi.TYPE_STRING),
description="List of notable works"
),
'status': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
description="Whether the book is active/visible"
),
'pin': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
description="Whether the book is pinned to the top"
),
'view_count': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Number of views for the book"
),
'download_count': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Number of downloads for the book"
),
'file_type': openapi.Schema(
type=openapi.TYPE_STRING,
description="Type of the book file (PDF, EPUB, etc.)"
),
'book_file': openapi.Schema(
type=openapi.TYPE_STRING,
description="URL to the book file",
nullable=True
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
)
'status': openapi.Schema(type=openapi.TYPE_BOOLEAN, description="Whether the book is active/visible"),
'pin': openapi.Schema(type=openapi.TYPE_BOOLEAN, description="Whether the book is pinned to the top"),
'view_count': openapi.Schema(type=openapi.TYPE_INTEGER, description="Number of views for the book"),
'download_count': openapi.Schema(type=openapi.TYPE_INTEGER, description="Number of downloads for the book"),
'file_type': openapi.Schema(type=openapi.TYPE_STRING, description="Type of the book file (PDF, EPUB, etc.)"),
'book_file': openapi.Schema(type=openapi.TYPE_STRING, description="URL to the book file", nullable=True),
'created_at': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATETIME, description="Creation timestamp"),
'share_link': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
},
required=['id', 'title', 'slug', 'status', 'created_at']
)
@ -155,38 +126,61 @@ books_response = openapi.Response(
'count': openapi.Schema(type=openapi.TYPE_INTEGER),
'next': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'previous': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'results': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=book_schema
)
'results': openapi.Schema(type=openapi.TYPE_ARRAY, items=book_schema),
}
)
)
# Book detail response
book_detail_response = openapi.Response(
description="Detailed information about a specific book",
schema=book_schema
)
# Swagger decorators for views
author_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(type=openapi.TYPE_INTEGER, description="Unique identifier for the author"),
'full_name': openapi.Schema(type=openapi.TYPE_STRING, description="Full name of the author"),
'slug': openapi.Schema(type=openapi.TYPE_STRING, description="URL-friendly slug for the author"),
'thumbnail': thumbs_schema,
'birth_date': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE, nullable=True, description="Birth date"),
'death_date': openapi.Schema(type=openapi.TYPE_STRING, format=openapi.FORMAT_DATE, nullable=True, description="Death date"),
'books_count': openapi.Schema(type=openapi.TYPE_INTEGER, description="Number of active books"),
},
required=['id', 'full_name', 'slug', 'books_count']
)
authors_response = openapi.Response(
description="List of authors with pagination",
schema=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'count': openapi.Schema(type=openapi.TYPE_INTEGER),
'next': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'previous': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'results': openapi.Schema(type=openapi.TYPE_ARRAY, items=author_schema),
}
)
)
author_detail_response = openapi.Response(
description="Detailed information about a specific author",
schema=author_schema
)
book_detail_swagger = swagger_auto_schema(
operation_id="get_book_detail",
operation_description="""
Retrieve detailed information about a specific book.
This endpoint returns comprehensive information about a book, including:
- Basic book details (title, slug, summary, description)
- Thumbnail image URL
- Basic book details
- Thumbnail image URLs
- Author information
- Status and pin information
- View and download counts
- File type and book file URL
- Creation and update timestamps
- Categories and collections the book belongs to
- Number of pages
The book is specified by its slug in the URL path.
""",
operation_summary="Get Book Detail",
tags=["Dobodbi - Library"],
@ -203,29 +197,17 @@ book_list_swagger = swagger_auto_schema(
operation_description="""
Retrieve a list of books with filtering and search capabilities.
This endpoint returns a paginated list of books. Each book includes its title, slug,
summary, description, thumbnail, author, status, pin, view count, download count,
file type, book file URL, and creation timestamp.
You can filter books by:
- Collection ID using the query parameter 'collection_id'
- Category slug(s) using the query parameter 'category' (single slug or comma-separated list)
- Bookmarked books using the query parameter 'is_bookmark=true'
You can also search for books by title, summary, publisher, or isbn using the query parameter 'search'.
- Collection ID
- Category slug(s)
- Author slug
- Bookmarked books
You can sort books by:
- created_at, -created_at
- view_count, -view_count
- download_count, -download_count
- title, -title
- pin, -pin, -pin,-created_at
Note: To get downloaded books, use the separate endpoint /books/downloaded/
You can search for books by title, summary, publisher, or isbn.
""",
operation_summary="List Books",
tags=["Dobodbi - Library"],
manual_parameters=[collection_id_param, category_param, is_bookmark_param, search_param, sort_param],
manual_parameters=[collection_id_param, category_param, author_param, is_bookmark_param, search_param, sort_param],
responses={
200: books_response,
401: "Authentication credentials were not provided or are invalid.",
@ -237,9 +219,6 @@ category_list_swagger = swagger_auto_schema(
operation_id="list_categories",
operation_description="""
Retrieve a list of book categories.
This endpoint returns a paginated list of book categories. Each category includes its
title, slug, status, books count, and timestamps.
""",
operation_summary="List Book Categories",
tags=["Dobodbi - Library"],
@ -254,9 +233,6 @@ pinned_collection_list_swagger = swagger_auto_schema(
operation_id="list_pinned_collections",
operation_description="""
Retrieve a list of pinned book collections with their top book covers.
This endpoint returns a list of pinned book collections. Each collection includes its
title and the covers of its top books by view count.
""",
operation_summary="List Pinned Book Collections",
tags=["Dobodbi - Library"],
@ -271,12 +247,6 @@ middle_collection_list_swagger = swagger_auto_schema(
operation_id="list_middle_collections",
operation_description="""
Retrieve a list of middle section book collections with their books.
This endpoint returns a list of middle section book collections. Each collection includes its
title, slug, summary, status, order, and a list of books in the collection.
Each book in the collection includes its id, title, slug, summary, thumbnail, author,
view count, download count, and file type.
""",
operation_summary="List Middle Section Book Collections",
tags=["Dobodbi - Library"],
@ -286,3 +256,54 @@ middle_collection_list_swagger = swagger_auto_schema(
500: "Internal server error occurred."
}
)
author_list_swagger = swagger_auto_schema(
operation_id="list_library_authors",
operation_description="""
Retrieve a paginated list of library authors.
Each author includes thumbnail, full name, and number of active books.
""",
operation_summary="List Authors",
tags=["Dobodbi - Library"],
responses={
200: authors_response,
401: "Authentication credentials were not provided or are invalid.",
500: "Internal server error occurred."
}
)
author_detail_swagger = swagger_auto_schema(
operation_id="get_author_detail",
operation_description="""
Retrieve detailed information about a specific author.
This endpoint returns full name, birth date, death date, thumbnail, and books count.
""",
operation_summary="Get Author Detail",
tags=["Dobodbi - Library"],
responses={
200: author_detail_response,
401: "Authentication credentials were not provided or are invalid.",
404: "The specified author does not exist.",
500: "Internal server error occurred."
}
)
author_books_swagger = swagger_auto_schema(
operation_id="list_author_books",
operation_description="""
Retrieve a paginated list of books for a specific author.
The author is identified by slug in the URL path.
""",
operation_summary="List Author Books",
tags=["Dobodbi - Library"],
manual_parameters=[is_bookmark_param, search_param, sort_param],
responses={
200: books_response,
401: "Authentication credentials were not provided or are invalid.",
404: "The specified author does not exist.",
500: "Internal server error occurred."
}
)

8
apps/library/management/commands/populate_books.py

@ -10,7 +10,7 @@ Usage:
from django.core.management.base import BaseCommand
from django.utils.text import slugify
from apps.library.models import Book, Category, BookCollection
from apps.library.models import Author, Book, Category, BookCollection
from dj_language.models import Language
import random
from django.conf import settings
@ -394,6 +394,10 @@ class Command(BaseCommand):
for idx, book_info in enumerate(book_data):
try:
slug = slugify(book_info['title'])
author_obj, _ = Author.objects.get_or_create(
full_name=book_info['author'],
defaults={'slug': slugify(book_info['author'])}
)
# Create the book instance (WITHOUT the image/file first)
book = Book(
@ -405,7 +409,7 @@ class Command(BaseCommand):
description=book_info['description'],
publisher=book_info['publisher'],
year_of_publication=book_info['year_of_publication'],
author=book_info['author'],
author=author_obj,
isbn=book_info['isbn'],
numnber_of_volume=book_info['numnber_of_volume'],
language=language,

74
apps/library/migrations/0002_author_and_book_author_fk.py

@ -0,0 +1,74 @@
from django.db import migrations, models
import django.db.models.deletion
from django.utils.text import slugify
def forwards(apps, schema_editor):
Author = apps.get_model('library', 'Author')
Book = apps.get_model('library', 'Book')
slug_counters = {}
def build_unique_slug(name):
base = slugify(name) or 'author'
count = slug_counters.get(base, 0)
slug_counters[base] = count + 1
return base if count == 0 else f"{base}-{count}"
for book in Book.objects.exclude(author__isnull=True).exclude(author='').iterator():
author_name = (book.author or '').strip()
if not author_name:
continue
author_obj = Author.objects.filter(full_name=author_name).first()
if not author_obj:
author_obj = Author.objects.create(
full_name=author_name,
slug=build_unique_slug(author_name),
)
book.author_temp_id = author_obj.id
book.save(update_fields=['author_temp'])
class Migration(migrations.Migration):
dependencies = [
('library', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Author',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('full_name', models.CharField(max_length=255, verbose_name='Full Name')),
('slug', models.SlugField(max_length=255, unique=True)),
('thumbnail', models.ImageField(blank=True, help_text='image allowed', null=True, upload_to='author_thumbnails/', verbose_name='Thumbnail')),
('birth_date', models.DateField(blank=True, null=True, verbose_name='Birth Date')),
('death_date', models.DateField(blank=True, null=True, verbose_name='Death Date')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'Author',
'verbose_name_plural': 'Authors',
'ordering': ['full_name', 'id'],
},
),
migrations.AddField(
model_name='book',
name='author_temp',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='library.author', verbose_name='Author'),
),
migrations.RunPython(forwards, migrations.RunPython.noop),
migrations.RemoveField(
model_name='book',
name='author',
),
migrations.RenameField(
model_name='book',
old_name='author_temp',
new_name='author',
),
]

45
apps/library/models.py

@ -93,6 +93,39 @@ class Category(models.Model):
verbose_name_plural = _('Categories')
class Author(models.Model):
full_name = models.CharField(max_length=255, verbose_name=_('Full Name'))
slug = models.SlugField(max_length=255, unique=True)
thumbnail = models.ImageField(
upload_to='author_thumbnails/',
null=True,
blank=True,
help_text=_('image allowed'),
verbose_name=_('Thumbnail')
)
birth_date = models.DateField(null=True, blank=True, verbose_name=_('Birth Date'))
death_date = models.DateField(null=True, blank=True, verbose_name=_('Death Date'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
verbose_name = _('Author')
verbose_name_plural = _('Authors')
ordering = ['full_name', 'id']
def __str__(self):
return self.full_name
def save(self, *args, **kwargs):
if not self.slug or not self.slug.strip():
self.slug = generate_smart_slug(self.full_name, Author, instance=self)
super().save(*args, **kwargs)
@property
def books_count(self):
return self.books.filter(status=True).count()
class Book(models.Model):
class FileType(models.TextChoices):
pdf = 'pdf', 'Pdf'
@ -111,7 +144,14 @@ class Book(models.Model):
publisher = models.CharField(max_length=655, null=True, blank=True)
year_of_publication = models.CharField(max_length=255, null=True, blank=True)
author = models.CharField(max_length=255, null=True, blank=True)
author = models.ForeignKey(
'library.Author',
related_name='books',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('Author')
)
isbn = models.CharField(max_length=255, null=True, blank=True)
numnber_of_volume = models.CharField(max_length=255, null=True, blank=True)
@ -139,7 +179,8 @@ class Book(models.Model):
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return f'<{self.id}>-{self.title}'
author_name = self.author.full_name if self.author_id else "-"
return f'<{self.id}>-{self.title} ({author_name})'
@property
def share_link(self):

58
apps/library/serializers.py

@ -46,6 +46,57 @@ class PinnedBookCollectionSerializer(serializers.ModelSerializer):
model = BookCollection
fields = ('id', 'title', 'summary', 'covers')
class AuthorSummarySerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
class Meta:
model = Author
fields = ('id', 'full_name', 'slug', 'thumbnail')
def get_thumbnail(self, obj):
if obj.thumbnail:
return get_thumbs(obj.thumbnail, self.context.get('request'))
return None
class AuthorListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
books_count = serializers.SerializerMethodField()
class Meta:
model = Author
fields = ('id', 'full_name', 'slug', 'thumbnail', 'books_count')
def get_thumbnail(self, obj):
if obj.thumbnail:
return get_thumbs(obj.thumbnail, self.context.get('request'))
return None
def get_books_count(self, obj):
if hasattr(obj, 'books_count_annotation'):
return obj.books_count_annotation
return obj.books_count
class AuthorDetailSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
books_count = serializers.SerializerMethodField()
class Meta:
model = Author
fields = ('id', 'full_name', 'slug', 'thumbnail', 'birth_date', 'death_date', 'books_count')
def get_thumbnail(self, obj):
if obj.thumbnail:
return get_thumbs(obj.thumbnail, self.context.get('request'))
return None
def get_books_count(self, obj):
if hasattr(obj, 'books_count_annotation'):
return obj.books_count_annotation
return obj.books_count
from dj_language.models import Language
class BookSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
@ -53,6 +104,8 @@ class BookSerializer(serializers.ModelSerializer):
user_rate = serializers.SerializerMethodField()
average_rate = serializers.SerializerMethodField()
share_link = serializers.CharField(read_only=True)
author = serializers.SerializerMethodField()
author_detail = AuthorSummarySerializer(source='author', read_only=True)
language = serializers.SlugRelatedField(
slug_field='code',
queryset=Language.objects.all(),
@ -69,12 +122,15 @@ class BookSerializer(serializers.ModelSerializer):
model = Book
fields = (
'id', 'title', 'slug', 'summary', 'summary_title', 'thumbnail', 'slogan',
'status', 'pin', 'view_count', 'download_count', 'publisher', 'year_of_publication', 'author', 'isbn', 'numnber_of_volume',
'status', 'pin', 'view_count', 'download_count', 'publisher', 'year_of_publication', 'author', 'author_detail', 'isbn', 'numnber_of_volume',
'language', 'main_themes', 'notable_works',
'file_type', 'book_file', 'created_at', 'bookmark', 'user_rate',
'average_rate', 'share_link'
)
def get_author(self, obj):
return obj.author.full_name if obj.author_id else None
def get_bookmark(self, obj):
"""
Get bookmark information for this book.

23
apps/library/serializers_admin.py

@ -2,7 +2,7 @@ from rest_framework import serializers
from django.core.files.uploadedfile import SimpleUploadedFile
from utils.image_compression import maybe_compress_uploaded_file
from .models import Book, BookCollection, Category
from .models import Author, Book, BookCollection, Category
class AbsoluteImageField(serializers.ImageField):
@ -51,10 +51,27 @@ class AdminLibraryCollectionSerializer(serializers.ModelSerializer):
fields = ["id", "title", "slug", "summary", "display_position", "status", "order", "pin_top"]
class AdminAuthorSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
books_count = serializers.SerializerMethodField()
class Meta:
model = Author
fields = ["id", "full_name", "slug", "thumbnail", "birth_date", "death_date", "books_count", "created_at", "updated_at"]
read_only_fields = ["id", "created_at", "updated_at", "books_count"]
def get_books_count(self, obj):
if hasattr(obj, 'books_count_annotation'):
return obj.books_count_annotation
return obj.books_count
class AdminBookListSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
book_file = AbsoluteFileField(required=False, allow_null=True)
categories_detail = AdminLibraryCategorySerializer(source="categories", many=True, read_only=True)
author = serializers.CharField(source='author.full_name', read_only=True)
author_id = serializers.IntegerField(read_only=True)
class Meta:
model = Book
@ -74,6 +91,7 @@ class AdminBookListSerializer(serializers.ModelSerializer):
"created_at",
"updated_at",
"categories_detail",
"author_id",
"book_file",
]
read_only_fields = ["id", "view_count", "download_count", "created_at", "updated_at"]
@ -82,6 +100,8 @@ from dj_language.models import Language
class AdminBookDetailSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
book_file = AbsoluteFileField(required=False, allow_null=True)
author = serializers.PrimaryKeyRelatedField(queryset=Author.objects.all(), required=False, allow_null=True)
author_detail = AdminAuthorSerializer(source='author', read_only=True)
categories = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all(), many=True, required=False)
collections = serializers.PrimaryKeyRelatedField(queryset=BookCollection.objects.all(), many=True, required=False)
categories_detail = AdminLibraryCategorySerializer(source="categories", many=True, read_only=True)
@ -109,6 +129,7 @@ class AdminBookDetailSerializer(serializers.ModelSerializer):
"publisher",
"year_of_publication",
"author",
"author_detail",
"isbn",
"numnber_of_volume",
"language",

9
apps/library/urls.py

@ -7,13 +7,17 @@ from apps.library.views import (
MiddleBookCollectionListView,
BookListView,
BookDetailView,
AuthorListView,
AuthorDetailView,
AuthorBookListView,
DownloadedBooksListView,
BookDownloadCreateAPIView,
)
from apps.library.views_admin import AdminBookViewSet, AdminLibraryCategoryViewSet, AdminLibraryCollectionViewSet
from apps.library.views_admin import AdminAuthorViewSet, AdminBookViewSet, AdminLibraryCategoryViewSet, AdminLibraryCollectionViewSet
admin_router = SimpleRouter()
admin_router.register(r'books', AdminBookViewSet, basename='admin-library-books')
admin_router.register(r'authors', AdminAuthorViewSet, basename='admin-library-authors')
admin_router.register(r'categories', AdminLibraryCategoryViewSet, basename='admin-library-categories')
admin_router.register(r'collections', AdminLibraryCollectionViewSet, basename='admin-library-collections')
@ -22,6 +26,9 @@ urlpatterns = [
path('categories/', CategoryListView.as_view(), name='category-list'),
path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleBookCollectionListView.as_view(), name='collection-list'),
path('authors/', AuthorListView.as_view(), name='author-list'),
path('authors/<str:slug>/', AuthorDetailView.as_view(), name='author-detail'),
path('authors/<str:slug>/books/', AuthorBookListView.as_view(), name='author-books'),
path('books/', BookListView.as_view(), name='book-list'),
path('books/<str:slug>/', BookDetailView.as_view(), name='book-detail'),
path('books/downloaded/', DownloadedBooksListView.as_view(), name='downloaded-books-list'),

50
apps/library/views.py

@ -14,6 +14,9 @@ from apps.library.models import *
from apps.library.serializers import *
from apps.account.models import User
from apps.library.doc import (
author_books_swagger,
author_detail_swagger,
author_list_swagger,
book_list_swagger,
book_detail_swagger,
category_list_swagger,
@ -106,7 +109,7 @@ class BookListView(ListAPIView):
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = Book.objects.filter(status=True)
queryset = Book.objects.filter(status=True).select_related('author')
# Filter by collection if provided
collection_id = self.request.query_params.get('collection_id')
@ -120,6 +123,10 @@ class BookListView(ListAPIView):
category_slugs = [slug.strip() for slug in category.split(',')]
queryset = queryset.filter(categories__slug__in=category_slugs).distinct()
author_slug = self.request.query_params.get('author')
if author_slug:
queryset = queryset.filter(author__slug=author_slug)
# Filter by middle collection if requested
# if self.request.query_params.get('middle'):
# middle_collections = BookCollection.objects.filter(
@ -241,6 +248,47 @@ class MiddleBookCollectionListView(ListAPIView):
).order_by('order')
class AuthorListView(ListAPIView):
serializer_class = AuthorListSerializer
permission_classes = (IsAuthenticated,)
authentication_classes = [TokenAuthentication]
pagination_class = StandardResultsSetPagination
@author_list_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
return Author.objects.annotate(
books_count_annotation=Count('books', filter=Q(books__status=True))
).order_by('full_name', 'id')
class AuthorDetailView(RetrieveAPIView):
serializer_class = AuthorDetailSerializer
permission_classes = (IsAuthenticated,)
authentication_classes = [TokenAuthentication]
lookup_field = 'slug'
@author_detail_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
return Author.objects.annotate(
books_count_annotation=Count('books', filter=Q(books__status=True))
)
class AuthorBookListView(BookListView):
@author_books_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(author__slug=self.kwargs['slug'])
class DownloadedBooksListView(ListAPIView):
"""
API view to list books that have been downloaded by the current user

33
apps/library/views_admin.py

@ -1,4 +1,4 @@
from django.db.models import Q
from django.db.models import Count, Q
from rest_framework.authentication import TokenAuthentication
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated
@ -7,8 +7,9 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination
from .models import Book, BookCollection, Category
from .models import Author, Book, BookCollection, Category
from .serializers_admin import (
AdminAuthorSerializer,
AdminBookDetailSerializer,
AdminBookListSerializer,
AdminLibraryCategorySerializer,
@ -36,6 +37,26 @@ class AdminLibraryCollectionViewSet(ReadOnlyModelViewSet):
return BookCollection.objects.all().order_by("display_position", "order", "title")
class AdminAuthorViewSet(ModelViewSet):
serializer_class = AdminAuthorSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = StandardResultsSetPagination
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_queryset(self):
queryset = Author.objects.annotate(books_count_annotation=Count('books')).all()
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(
Q(full_name__icontains=search_query)
| Q(slug__icontains=search_query)
)
return queryset.order_by("full_name", "id")
class AdminBookViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
@ -48,14 +69,14 @@ class AdminBookViewSet(ModelViewSet):
return AdminBookDetailSerializer
def get_queryset(self):
queryset = Book.objects.all().prefetch_related("categories", "collections")
queryset = Book.objects.all().select_related("author").prefetch_related("categories", "collections")
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query)
| Q(slug__icontains=search_query)
| Q(author__icontains=search_query)
| Q(author__full_name__icontains=search_query)
| Q(publisher__icontains=search_query)
| Q(isbn__icontains=search_query)
| Q(summary__icontains=search_query)
@ -77,4 +98,8 @@ class AdminBookViewSet(ModelViewSet):
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)
author_id = self.request.query_params.get("author")
if author_id and author_id != "all":
queryset = queryset.filter(author_id=author_id)
return queryset.distinct().order_by("-created_at")
Loading…
Cancel
Save