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): class BookAdmin(ModelAdmin):
form = BookAdminForm 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') 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', ) # autocomplete_fields = ('categories', 'collections', )
list_filter_submit = True list_filter_submit = True
warn_unsaved_form = True warn_unsaved_form = True
@ -55,12 +55,16 @@ class BookAdmin(ModelAdmin):
return ', '.join([collection.title for collection in collections]) return ', '.join([collection.title for collection in collections])
return '-' return '-'
@display(description=_('Author'))
def display_author(self, obj):
return obj.author.full_name if obj.author_id else '-'
fieldsets = ( fieldsets = (
(None, { (None, {
'fields': () 'fields': ()
}), }),
('Detail', { ('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", { ("Summary", {
'fields': ('summary_title', '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}) 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 # Register models with the custom admin site
dovoodi_admin_site.register(Book, BookAdmin) dovoodi_admin_site.register(Book, BookAdmin)
dovoodi_admin_site.register(Author, AuthorAdmin)
dovoodi_admin_site.register(PinnedBookCollection, PinnedBookCollectionAdmin) dovoodi_admin_site.register(PinnedBookCollection, PinnedBookCollectionAdmin)
dovoodi_admin_site.register(MiddleBookCollection, MiddleBookCollectionAdmin) dovoodi_admin_site.register(MiddleBookCollection, MiddleBookCollectionAdmin)
dovoodi_admin_site.register(Category, CategoryAdmin) dovoodi_admin_site.register(Category, CategoryAdmin)

267
apps/library/doc.py

@ -17,22 +17,6 @@ collection_id_param = openapi.Parameter(
required=False 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_param = openapi.Parameter(
'is_bookmark', 'is_bookmark',
openapi.IN_QUERY, openapi.IN_QUERY,
@ -49,6 +33,14 @@ category_param = openapi.Parameter(
required=False 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_param = openapi.Parameter(
'sort', 'sort',
openapi.IN_QUERY, openapi.IN_QUERY,
@ -65,44 +57,45 @@ search_param = openapi.Parameter(
required=False 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 # Response schemas
book_schema = openapi.Schema( book_schema = openapi.Schema(
type=openapi.TYPE_OBJECT, type=openapi.TYPE_OBJECT,
properties={ 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( 'main_themes': openapi.Schema(
type=openapi.TYPE_ARRAY, type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING), items=openapi.Schema(type=openapi.TYPE_STRING),
@ -113,36 +106,14 @@ book_schema = openapi.Schema(
items=openapi.Schema(type=openapi.TYPE_STRING), items=openapi.Schema(type=openapi.TYPE_STRING),
description="List of notable works" 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'] required=['id', 'title', 'slug', 'status', 'created_at']
) )
@ -155,38 +126,61 @@ books_response = openapi.Response(
'count': openapi.Schema(type=openapi.TYPE_INTEGER), 'count': openapi.Schema(type=openapi.TYPE_INTEGER),
'next': openapi.Schema(type=openapi.TYPE_STRING, nullable=True), 'next': openapi.Schema(type=openapi.TYPE_STRING, nullable=True),
'previous': 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( book_detail_response = openapi.Response(
description="Detailed information about a specific book", description="Detailed information about a specific book",
schema=book_schema 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( book_detail_swagger = swagger_auto_schema(
operation_id="get_book_detail", operation_id="get_book_detail",
operation_description=""" operation_description="""
Retrieve detailed information about a specific book. Retrieve detailed information about a specific book.
This endpoint returns comprehensive information about a book, including: 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 - Author information
- Status and pin information - Status and pin information
- View and download counts - View and download counts
- File type and book file URL - 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", operation_summary="Get Book Detail",
tags=["Dobodbi - Library"], tags=["Dobodbi - Library"],
@ -203,29 +197,17 @@ book_list_swagger = swagger_auto_schema(
operation_description=""" operation_description="""
Retrieve a list of books with filtering and search capabilities. 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: 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", operation_summary="List Books",
tags=["Dobodbi - Library"], 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={ responses={
200: books_response, 200: books_response,
401: "Authentication credentials were not provided or are invalid.", 401: "Authentication credentials were not provided or are invalid.",
@ -237,9 +219,6 @@ category_list_swagger = swagger_auto_schema(
operation_id="list_categories", operation_id="list_categories",
operation_description=""" operation_description="""
Retrieve a list of book categories. 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", operation_summary="List Book Categories",
tags=["Dobodbi - Library"], tags=["Dobodbi - Library"],
@ -254,9 +233,6 @@ pinned_collection_list_swagger = swagger_auto_schema(
operation_id="list_pinned_collections", operation_id="list_pinned_collections",
operation_description=""" operation_description="""
Retrieve a list of pinned book collections with their top book covers. 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", operation_summary="List Pinned Book Collections",
tags=["Dobodbi - Library"], tags=["Dobodbi - Library"],
@ -271,12 +247,6 @@ middle_collection_list_swagger = swagger_auto_schema(
operation_id="list_middle_collections", operation_id="list_middle_collections",
operation_description=""" operation_description="""
Retrieve a list of middle section book collections with their books. 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", operation_summary="List Middle Section Book Collections",
tags=["Dobodbi - Library"], tags=["Dobodbi - Library"],
@ -286,3 +256,54 @@ middle_collection_list_swagger = swagger_auto_schema(
500: "Internal server error occurred." 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.core.management.base import BaseCommand
from django.utils.text import slugify 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 from dj_language.models import Language
import random import random
from django.conf import settings from django.conf import settings
@ -394,6 +394,10 @@ class Command(BaseCommand):
for idx, book_info in enumerate(book_data): for idx, book_info in enumerate(book_data):
try: try:
slug = slugify(book_info['title']) 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) # Create the book instance (WITHOUT the image/file first)
book = Book( book = Book(
@ -405,7 +409,7 @@ class Command(BaseCommand):
description=book_info['description'], description=book_info['description'],
publisher=book_info['publisher'], publisher=book_info['publisher'],
year_of_publication=book_info['year_of_publication'], year_of_publication=book_info['year_of_publication'],
author=book_info['author'],
author=author_obj,
isbn=book_info['isbn'], isbn=book_info['isbn'],
numnber_of_volume=book_info['numnber_of_volume'], numnber_of_volume=book_info['numnber_of_volume'],
language=language, 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') 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 Book(models.Model):
class FileType(models.TextChoices): class FileType(models.TextChoices):
pdf = 'pdf', 'Pdf' pdf = 'pdf', 'Pdf'
@ -111,7 +144,14 @@ class Book(models.Model):
publisher = models.CharField(max_length=655, null=True, blank=True) publisher = models.CharField(max_length=655, null=True, blank=True)
year_of_publication = models.CharField(max_length=255, 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) isbn = models.CharField(max_length=255, null=True, blank=True)
numnber_of_volume = 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')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self): 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 @property
def share_link(self): def share_link(self):

58
apps/library/serializers.py

@ -46,6 +46,57 @@ class PinnedBookCollectionSerializer(serializers.ModelSerializer):
model = BookCollection model = BookCollection
fields = ('id', 'title', 'summary', 'covers') 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 from dj_language.models import Language
class BookSerializer(serializers.ModelSerializer): class BookSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField() thumbnail = serializers.SerializerMethodField()
@ -53,6 +104,8 @@ class BookSerializer(serializers.ModelSerializer):
user_rate = serializers.SerializerMethodField() user_rate = serializers.SerializerMethodField()
average_rate = serializers.SerializerMethodField() average_rate = serializers.SerializerMethodField()
share_link = serializers.CharField(read_only=True) share_link = serializers.CharField(read_only=True)
author = serializers.SerializerMethodField()
author_detail = AuthorSummarySerializer(source='author', read_only=True)
language = serializers.SlugRelatedField( language = serializers.SlugRelatedField(
slug_field='code', slug_field='code',
queryset=Language.objects.all(), queryset=Language.objects.all(),
@ -69,12 +122,15 @@ class BookSerializer(serializers.ModelSerializer):
model = Book model = Book
fields = ( fields = (
'id', 'title', 'slug', 'summary', 'summary_title', 'thumbnail', 'slogan', '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', 'language', 'main_themes', 'notable_works',
'file_type', 'book_file', 'created_at', 'bookmark', 'user_rate', 'file_type', 'book_file', 'created_at', 'bookmark', 'user_rate',
'average_rate', 'share_link' '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): def get_bookmark(self, obj):
""" """
Get bookmark information for this book. 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 django.core.files.uploadedfile import SimpleUploadedFile
from utils.image_compression import maybe_compress_uploaded_file 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): class AbsoluteImageField(serializers.ImageField):
@ -51,10 +51,27 @@ class AdminLibraryCollectionSerializer(serializers.ModelSerializer):
fields = ["id", "title", "slug", "summary", "display_position", "status", "order", "pin_top"] 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): class AdminBookListSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True) thumbnail = AbsoluteImageField(required=False, allow_null=True)
book_file = AbsoluteFileField(required=False, allow_null=True) book_file = AbsoluteFileField(required=False, allow_null=True)
categories_detail = AdminLibraryCategorySerializer(source="categories", many=True, read_only=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: class Meta:
model = Book model = Book
@ -74,6 +91,7 @@ class AdminBookListSerializer(serializers.ModelSerializer):
"created_at", "created_at",
"updated_at", "updated_at",
"categories_detail", "categories_detail",
"author_id",
"book_file", "book_file",
] ]
read_only_fields = ["id", "view_count", "download_count", "created_at", "updated_at"] 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): class AdminBookDetailSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True) thumbnail = AbsoluteImageField(required=False, allow_null=True)
book_file = AbsoluteFileField(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) categories = serializers.PrimaryKeyRelatedField(queryset=Category.objects.all(), many=True, required=False)
collections = serializers.PrimaryKeyRelatedField(queryset=BookCollection.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) categories_detail = AdminLibraryCategorySerializer(source="categories", many=True, read_only=True)
@ -109,6 +129,7 @@ class AdminBookDetailSerializer(serializers.ModelSerializer):
"publisher", "publisher",
"year_of_publication", "year_of_publication",
"author", "author",
"author_detail",
"isbn", "isbn",
"numnber_of_volume", "numnber_of_volume",
"language", "language",

9
apps/library/urls.py

@ -7,13 +7,17 @@ from apps.library.views import (
MiddleBookCollectionListView, MiddleBookCollectionListView,
BookListView, BookListView,
BookDetailView, BookDetailView,
AuthorListView,
AuthorDetailView,
AuthorBookListView,
DownloadedBooksListView, DownloadedBooksListView,
BookDownloadCreateAPIView, 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 = SimpleRouter()
admin_router.register(r'books', AdminBookViewSet, basename='admin-library-books') 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'categories', AdminLibraryCategoryViewSet, basename='admin-library-categories')
admin_router.register(r'collections', AdminLibraryCollectionViewSet, basename='admin-library-collections') admin_router.register(r'collections', AdminLibraryCollectionViewSet, basename='admin-library-collections')
@ -22,6 +26,9 @@ urlpatterns = [
path('categories/', CategoryListView.as_view(), name='category-list'), path('categories/', CategoryListView.as_view(), name='category-list'),
path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'), path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'),
path('collections/', MiddleBookCollectionListView.as_view(), name='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/', BookListView.as_view(), name='book-list'),
path('books/<str:slug>/', BookDetailView.as_view(), name='book-detail'), path('books/<str:slug>/', BookDetailView.as_view(), name='book-detail'),
path('books/downloaded/', DownloadedBooksListView.as_view(), name='downloaded-books-list'), 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.library.serializers import *
from apps.account.models import User from apps.account.models import User
from apps.library.doc import ( from apps.library.doc import (
author_books_swagger,
author_detail_swagger,
author_list_swagger,
book_list_swagger, book_list_swagger,
book_detail_swagger, book_detail_swagger,
category_list_swagger, category_list_swagger,
@ -106,7 +109,7 @@ class BookListView(ListAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
queryset = Book.objects.filter(status=True)
queryset = Book.objects.filter(status=True).select_related('author')
# Filter by collection if provided # Filter by collection if provided
collection_id = self.request.query_params.get('collection_id') 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(',')] category_slugs = [slug.strip() for slug in category.split(',')]
queryset = queryset.filter(categories__slug__in=category_slugs).distinct() 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 # Filter by middle collection if requested
# if self.request.query_params.get('middle'): # if self.request.query_params.get('middle'):
# middle_collections = BookCollection.objects.filter( # middle_collections = BookCollection.objects.filter(
@ -241,6 +248,47 @@ class MiddleBookCollectionListView(ListAPIView):
).order_by('order') ).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): class DownloadedBooksListView(ListAPIView):
""" """
API view to list books that have been downloaded by the current user 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.authentication import TokenAuthentication
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
@ -7,8 +7,9 @@ from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination from utils.pagination import StandardResultsSetPagination
from .models import Book, BookCollection, Category
from .models import Author, Book, BookCollection, Category
from .serializers_admin import ( from .serializers_admin import (
AdminAuthorSerializer,
AdminBookDetailSerializer, AdminBookDetailSerializer,
AdminBookListSerializer, AdminBookListSerializer,
AdminLibraryCategorySerializer, AdminLibraryCategorySerializer,
@ -36,6 +37,26 @@ class AdminLibraryCollectionViewSet(ReadOnlyModelViewSet):
return BookCollection.objects.all().order_by("display_position", "order", "title") 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): class AdminBookViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin] permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication] authentication_classes = [TokenAuthentication]
@ -48,14 +69,14 @@ class AdminBookViewSet(ModelViewSet):
return AdminBookDetailSerializer return AdminBookDetailSerializer
def get_queryset(self): 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") search_query = self.request.query_params.get("search")
if search_query: if search_query:
queryset = queryset.filter( queryset = queryset.filter(
Q(title__icontains=search_query) Q(title__icontains=search_query)
| Q(slug__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(publisher__icontains=search_query)
| Q(isbn__icontains=search_query) | Q(isbn__icontains=search_query)
| Q(summary__icontains=search_query) | Q(summary__icontains=search_query)
@ -77,4 +98,8 @@ class AdminBookViewSet(ModelViewSet):
if collection_id and collection_id != "all": if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id) 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") return queryset.distinct().order_by("-created_at")
Loading…
Cancel
Save