mortezaei 6 months ago
parent
commit
e093b6f6ce
  1. 63
      apps/api/views/documentation.py
  2. 5
      apps/article/serializers.py
  3. 60
      apps/article/views.py
  4. 2
      apps/hadis/serializers/hadis.py
  5. 47
      apps/library/doc.py
  6. 18
      apps/library/migrations/0007_auto_20251203_1529.py
  7. 30
      apps/library/migrations/0008_auto_20251203_1533.py
  8. 10
      apps/library/models.py
  9. 3
      apps/library/serializers.py
  10. 30
      apps/library/views.py
  11. 11
      apps/podcast/serializers.py
  12. 49
      apps/podcast/views.py

63
apps/api/views/documentation.py

@ -296,10 +296,13 @@ class CustomAPIDocumentationView(View):
'name': 'Book List',
'method': 'GET',
'url': '/api/library/books/',
'description': 'Get paginated list of books',
'description': 'Get paginated list of books with filtering and sorting',
'parameters': [
{'name': 'category', 'type': 'integer', 'description': 'Filter by category ID', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in book titles and authors', 'required': False},
{'name': 'category', 'type': 'string', 'description': 'Filter by category slug(s). Can be a single slug or comma-separated list (e.g., "slug1,slug2")', 'required': False},
{'name': 'collection_id', 'type': 'integer', 'description': 'Filter by collection ID', 'required': False},
{'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked books (true/false)', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in book titles, summary, publisher, or ISBN', 'required': False},
{'name': 'sort', 'type': 'string', 'description': 'Sort by field. Options: created_at, -created_at, view_count, -view_count, download_count, -download_count, title, -title, pin, -pin, -pin,-created_at', 'required': False},
],
'response_examples': {
'success': json.dumps({
@ -308,13 +311,28 @@ class CustomAPIDocumentationView(View):
{
"id": 1,
"title": "Al-Kafi",
"slug": "al-kafi",
"author": "Muhammad ibn Ya'qub al-Kulayni",
"description": "One of the most important Shia hadith collections",
"cover_image": "https://example.com/media/books/alkafi.jpg",
"file_size": "15.2 MB",
"pages": 1200,
"language": "Arabic",
"download_count": 2456
"publisher": "Islamic Publications",
"year_of_publication": "2020",
"isbn": "978-1234567890",
"language": 1,
"main_themes": ["Hadith", "Islamic Jurisprudence", "Shia Islam"],
"notable_works": ["Volume 1: Faith and Disbelief", "Volume 2: Reason and Ignorance"],
"summary": "One of the most important Shia hadith collections",
"thumbnail": "https://example.com/media/books/alkafi.jpg",
"file_type": "pdf",
"book_file": "https://example.com/media/books/alkafi.pdf",
"view_count": 5432,
"download_count": 2456,
"pin": True,
"bookmark": False,
"user_rate": {
"is_rated": True,
"rate": 5
},
"average_rate": 4.7,
"created_at": "2024-01-15T10:30:00Z"
}
]
}, indent=2)
@ -576,12 +594,13 @@ class CustomAPIDocumentationView(View):
'name': 'Podcast Playlist List',
'method': 'GET',
'url': '/api/podcast/playlists/',
'description': 'Get paginated list of podcast playlists',
'description': 'Get paginated list of podcast playlists with filtering and sorting',
'parameters': [
{'name': 'category', 'type': 'string', 'description': 'Filter by category slug', 'required': False},
{'name': 'category', 'type': 'string', 'description': 'Filter by category slug(s). Can be a single slug or comma-separated list (e.g., "slug1,slug2")', 'required': False},
{'name': 'collection', 'type': 'string', 'description': 'Filter by collection slug', 'required': False},
{'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked playlists', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in playlist titles', 'required': False},
{'name': 'sort', 'type': 'string', 'description': 'Sort by field. Options: created_at, -created_at, view_count, -view_count, title, -title, order, -order', 'required': False},
],
'response_examples': {
'success': json.dumps({
@ -595,6 +614,7 @@ class CustomAPIDocumentationView(View):
"slogan": "Learn the basics of Islamic thought through audio",
"view_count": 1234,
"total_time_formatted": "02:45:30",
"episodes_count": 12,
"order": 1,
"created_at": "2024-01-15T10:30:00Z"
}
@ -915,12 +935,13 @@ class CustomAPIDocumentationView(View):
'name': 'Article List',
'method': 'GET',
'url': '/api/article/list/',
'description': 'Get paginated list of articles with filtering',
'description': 'Get paginated list of articles with filtering and sorting',
'parameters': [
{'name': 'category', 'type': 'string', 'description': 'Filter by category slug', 'required': False},
{'name': 'category', 'type': 'string', 'description': 'Filter by category slug(s). Can be a single slug or comma-separated list (e.g., "slug1,slug2")', 'required': False},
{'name': 'collection', 'type': 'string', 'description': 'Filter by collection slug', 'required': False},
{'name': 'is_bookmark', 'type': 'boolean', 'description': 'Filter bookmarked articles', 'required': False},
{'name': 'search', 'type': 'string', 'description': 'Search in article titles', 'required': False},
{'name': 'sort', 'type': 'string', 'description': 'Sort by field. Options: created_at, -created_at, view_count, -view_count, title, -title', 'required': False},
],
'response_examples': {
'success': json.dumps({
@ -933,7 +954,21 @@ class CustomAPIDocumentationView(View):
"thumbnail": "https://example.com/media/articles/thumb1.jpg",
"description": "Краткая биография девятого имама",
"view_count": 234,
"created_at": "2025-01-15T10:30:00Z"
"created_at": "2025-01-15T10:30:00Z",
"categories": [
{
"id": 1,
"title": "Имамы",
"slug": "imams",
"acticle_count": 12
},
{
"id": 2,
"title": "Биография",
"slug": "biography",
"acticle_count": 8
}
]
}
]
}, indent=2)

5
apps/article/serializers.py

@ -43,10 +43,11 @@ class MiddleArticleCollectionSerializer(serializers.ModelSerializer):
class ArticleListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
categories = ArticleCategoryListSerializer(many=True, read_only=True)
class Meta:
model = Article
fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'view_count', 'created_at']
fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'view_count', 'created_at', 'categories']
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))

60
apps/article/views.py

@ -81,40 +81,47 @@ class ArticleListAPIView(generics.ListAPIView):
permission_classes = (IsAuthenticated,)
@swagger_auto_schema(
operation_description="Get a list of article with optional filtering",
operation_description="Get a list of articles with optional filtering and sorting",
manual_parameters=[
openapi.Parameter(
name='category',
in_=openapi.IN_QUERY,
description='Filter article by category slug',
description='Filter articles by category slug(s). Can be a single slug or comma-separated list of slugs',
type=openapi.TYPE_STRING,
required=False
),
openapi.Parameter(
name='collection',
in_=openapi.IN_QUERY,
description='Filter article by collection slug',
description='Filter articles by collection slug',
type=openapi.TYPE_STRING,
required=False
),
openapi.Parameter(
name='is_bookmark',
in_=openapi.IN_QUERY,
description='Filter article that are bookmarked by the user (true/false)',
description='Filter articles that are bookmarked by the user (true/false)',
type=openapi.TYPE_BOOLEAN,
required=False
),
openapi.Parameter(
name='search',
in_=openapi.IN_QUERY,
description='Search article by title',
description='Search articles by title',
type=openapi.TYPE_STRING,
required=False
),
openapi.Parameter(
name='sort',
in_=openapi.IN_QUERY,
description='Sort articles by field. Options: created_at, -created_at, view_count, -view_count, title, -title',
type=openapi.TYPE_STRING,
required=False
)
],
responses={
200: openapi.Response(
description="List of article",
description="List of articles",
schema=ArticleListSerializer(many=True)
)
}
@ -123,43 +130,54 @@ class ArticleListAPIView(generics.ListAPIView):
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = Article.objects.filter(status=True).order_by('-created_at')
queryset = Article.objects.filter(status=True)
# Search by title if search parameter is provided
search_query = self.request.query_params.get('search', None)
if search_query:
queryset = queryset.filter(title__icontains=search_query)
# Filter by category if provided
category_slug = self.request.query_params.get('category', None)
if category_slug:
queryset = queryset.filter(categories__slug=category_slug)
category = self.request.query_params.get('category', None)
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 collection if provided
collection_slug = self.request.query_params.get('collection', None)
if collection_slug:
# Get all podcasts that are in the collection with the given slug
# Get all articles that are in the collection with the given slug
queryset = queryset.filter(
collections__slug=collection_slug
)
# Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true':
# Import Bookmark model here to avoid circular imports
from apps.bookmark.models import Bookmark
# Get all bookmarked podcast IDs for the current user
# Get all bookmarked article IDs for the current user
bookmarked_ids = Bookmark.objects.filter(
user=self.request.user,
service=Bookmark.ServiceChoices.ARTICLE,
status=True
).values_list('content_id', flat=True)
# Filter podcasts by these IDs
# Filter articles by these IDs
queryset = queryset.filter(id__in=bookmarked_ids)
# Sort by parameter
sort = self.request.query_params.get('sort', '-created_at')
# Allowed sort fields
allowed_sorts = ['created_at', '-created_at', 'view_count', '-view_count', 'title', '-title']
if sort in allowed_sorts:
queryset = queryset.order_by(sort)
else:
queryset = queryset.order_by('-created_at')
return queryset

2
apps/hadis/serializers/hadis.py

@ -15,7 +15,7 @@ class HadisListSerializer(serializers.ModelSerializer):
class Meta:
model = Hadis
fields = ['id', 'number', 'title', 'category', 'translation']
fields = ['id', 'number', 'title', 'text' ,'category', 'translation']
def get_category(self, obj):
"""Get category id and title"""

47
apps/library/doc.py

@ -41,10 +41,26 @@ is_bookmark_param = openapi.Parameter(
required=False
)
category_param = openapi.Parameter(
'category',
openapi.IN_QUERY,
description="Filter books by category slug(s). Can be a single slug or comma-separated list of slugs",
type=openapi.TYPE_STRING,
required=False
)
sort_param = openapi.Parameter(
'sort',
openapi.IN_QUERY,
description="Sort books by field. Options: created_at, -created_at, view_count, -view_count, download_count, -download_count, title, -title, pin, -pin, -pin,-created_at",
type=openapi.TYPE_STRING,
required=False
)
search_param = openapi.Parameter(
'search',
openapi.IN_QUERY,
description="Search books by title, summary, or author",
description="Search books by title, summary, publisher, or isbn",
type=openapi.TYPE_STRING,
required=False
)
@ -82,6 +98,21 @@ book_schema = 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
),
'main_themes': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=openapi.Schema(type=openapi.TYPE_STRING),
description="List of main themes"
),
'notable_works': openapi.Schema(
type=openapi.TYPE_ARRAY,
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"
@ -178,17 +209,23 @@ book_list_swagger = swagger_auto_schema(
You can filter books by:
- Collection ID using the query parameter 'collection_id'
- Middle section collection using the query parameter 'middle'
- Bottom section collection using the query parameter 'bottom'
- 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, or author using the query parameter 'search'.
You can also search for books by title, summary, publisher, or isbn using the query parameter 'search'.
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/
""",
operation_summary="List Books",
tags=["Library"],
manual_parameters=[collection_id_param, middle_param, bottom_param, is_bookmark_param, search_param],
manual_parameters=[collection_id_param, category_param, is_bookmark_param, search_param, sort_param],
responses={
200: books_response,
401: "Authentication credentials were not provided or are invalid.",

18
apps/library/migrations/0007_auto_20251203_1529.py

@ -0,0 +1,18 @@
# Generated by Django 5.1.8 on 2025-12-03 15:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0006_remove_book_author_book_isbn_book_numnber_of_volume_and_more'),
]
operations = [
migrations.AddField(
model_name='book',
name='author',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

30
apps/library/migrations/0008_auto_20251203_1533.py

@ -0,0 +1,30 @@
# Generated by Django 5.1.8 on 2025-12-03 15:33
from django.db import migrations, models
import dj_language.field
class Migration(migrations.Migration):
dependencies = [
('library', '0007_auto_20251203_1529'),
('dj_language', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='book',
name='language',
field=dj_language.field.LanguageField(blank=True, null=True, on_delete=models.SET_NULL, to='dj_language.language', verbose_name='Language'),
),
migrations.AddField(
model_name='book',
name='main_themes',
field=models.JSONField(blank=True, default=list, help_text='List of main themes', verbose_name='Main Themes'),
),
migrations.AddField(
model_name='book',
name='notable_works',
field=models.JSONField(blank=True, default=list, help_text='List of notable works', verbose_name='Notable Works'),
),
]

10
apps/library/models.py

@ -3,6 +3,7 @@ from django.db import models
from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
from dj_language.field import LanguageField
from utils import generate_slug_for_model
from apps.account.models import User
@ -108,10 +109,15 @@ 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.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)
# Language, themes and notable works
language = LanguageField(verbose_name=_('Language'), null=True, blank=True)
main_themes = models.JSONField(verbose_name=_('Main Themes'), default=list, blank=True, help_text=_('List of main themes'))
notable_works = models.JSONField(verbose_name=_('Notable Works'), default=list, blank=True, help_text=_('List of notable works'))
pages_count = models.CharField(verbose_name=_('Number of Pages'), max_length=255, help_text=_('eg. 34'), null=True)
status = models.BooleanField(default=True, verbose_name=_('status'))
pin = models.BooleanField(default=True, verbose_name=_('Pin to top'))

3
apps/library/serializers.py

@ -60,7 +60,8 @@ 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', 'isbn', 'numnber_of_volume',
'status', 'pin', 'view_count', 'download_count', 'publisher', 'year_of_publication', 'author', 'isbn', 'numnber_of_volume',
'language', 'main_themes', 'notable_works',
'file_type', 'book_file', 'created_at', 'bookmark', 'user_rate',
'average_rate'
)

30
apps/library/views.py

@ -103,6 +103,13 @@ class BookListView(ListAPIView):
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(
@ -126,18 +133,35 @@ class BookListView(ListAPIView):
if is_bookmark == 'true':
# 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)
# Filter books by these IDs
queryset = queryset.filter(id__in=bookmarked_ids)
return queryset.order_by('-pin', '-created_at')
# Sort by parameter
sort = self.request.query_params.get('sort', '-pin,-created_at')
# 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):

11
apps/podcast/serializers.py

@ -188,14 +188,15 @@ class PinnedPodcastCollectionSerializer(serializers.ModelSerializer):
class PodcastPlaylistListSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
total_time_formatted = serializers.SerializerMethodField()
episodes_count = serializers.SerializerMethodField()
class Meta:
model = PodcastPlaylist
fields = ['id', 'title', 'slug', 'thumbnail', 'slogan', 'view_count', 'total_time_formatted', 'order', 'created_at']
fields = ['id', 'title', 'slug', 'thumbnail', 'slogan', 'view_count', 'total_time_formatted', 'episodes_count', 'order', 'created_at']
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
def get_total_time_formatted(self, obj):
"""Format total_time as HH:MM:SS string"""
if obj.total_time:
@ -206,6 +207,10 @@ class PodcastPlaylistListSerializer(serializers.ModelSerializer):
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
return "00:00:00"
def get_episodes_count(self, obj):
"""Return the number of episodes (podcasts) in this playlist"""
return obj.playlist_items.count()
class PodcastPlaylistDetailSerializer(serializers.ModelSerializer):
categories = PodcastCategoryListSerializer(many=True, read_only=True)

49
apps/podcast/views.py

@ -90,12 +90,12 @@ class PodcastListAPIView(generics.ListAPIView):
permission_classes = (IsAuthenticated,)
@swagger_auto_schema(
operation_description="Get a list of podcast playlists with optional filtering",
operation_description="Get a list of podcast playlists with optional filtering and sorting",
manual_parameters=[
openapi.Parameter(
name='category',
in_=openapi.IN_QUERY,
description='Filter playlists by category slug',
description='Filter playlists by category slug(s). Can be a single slug or comma-separated list of slugs',
type=openapi.TYPE_STRING,
required=False
),
@ -119,11 +119,18 @@ class PodcastListAPIView(generics.ListAPIView):
description='Search playlists by title',
type=openapi.TYPE_STRING,
required=False
),
openapi.Parameter(
name='sort',
in_=openapi.IN_QUERY,
description='Sort playlists by field. Options: created_at, -created_at, view_count, -view_count, title, -title, order, -order',
type=openapi.TYPE_STRING,
required=False
)
],
responses={
200: openapi.Response(
description="List of podcast playlists",
description="List of podcast playlists with episodes count",
schema=PodcastPlaylistListSerializer(many=True)
)
}
@ -132,38 +139,52 @@ class PodcastListAPIView(generics.ListAPIView):
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = PodcastPlaylist.objects.filter(status=True).order_by('-created_at')
queryset = PodcastPlaylist.objects.filter(status=True)
# Search by title if search parameter is provided
search_query = self.request.query_params.get('search', None)
if search_query:
queryset = queryset.filter(title__icontains=search_query)
# Filter by category if provided
category_slug = self.request.query_params.get('category', None)
if category_slug:
queryset = queryset.filter(categories__slug=category_slug)
category = self.request.query_params.get('category', None)
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 collection if provided
collection_slug = self.request.query_params.get('collection', None)
if collection_slug:
queryset = queryset.filter(
collections__slug=collection_slug
)
# Filter by bookmarks if provided
is_bookmark = self.request.query_params.get('is_bookmark', '').lower()
if is_bookmark == 'true':
from apps.bookmark.models import Bookmark
bookmarked_ids = Bookmark.objects.filter(
user=self.request.user,
service=Bookmark.ServiceChoices.PODCAST_PLAYLIST,
status=True
).values_list('content_id', flat=True)
queryset = queryset.filter(id__in=bookmarked_ids)
# Sort by parameter
sort = self.request.query_params.get('sort', '-created_at')
# Allowed sort fields
allowed_sorts = [
'created_at', '-created_at', 'view_count', '-view_count',
'title', '-title', 'order', '-order'
]
if sort in allowed_sorts:
queryset = queryset.order_by(sort)
else:
queryset = queryset.order_by('-created_at')
return queryset

Loading…
Cancel
Save