Browse Source

develop dovoodi

master
alireza 1 year ago
parent
commit
db32f84250
  1. 293
      apps/hadis/doc.py
  2. 28
      apps/hadis/migrations/0007_auto_20250321_2007.py
  3. 6
      apps/hadis/models/hadis.py
  4. 1
      apps/hadis/models/transmitter.py
  5. 87
      apps/hadis/serializers.py
  6. 3
      apps/hadis/urls.py
  7. 52
      apps/hadis/views/hadis.py
  8. 4
      apps/library/admin.py
  9. 220
      apps/library/doc.py
  10. 17
      apps/library/migrations/0004_remove_category_books.py
  11. 4
      apps/library/models.py
  12. 50
      apps/library/serializers.py
  13. 15
      apps/library/urls.py
  14. 110
      apps/library/views.py
  15. 20
      apps/podcast/models.py
  16. 78
      apps/video/admin.py
  17. 99
      apps/video/migrations/0001_initial.py
  18. 54
      apps/video/models.py
  19. 70
      apps/video/serializers.py
  20. 12
      apps/video/urls.py
  21. 50
      apps/video/views.py
  22. 1
      config/settings/base.py
  23. 17
      config/urls.py

293
apps/hadis/doc.py

@ -115,6 +115,276 @@ hadis_list_response = openapi.Response(
)
)
# Reference image schema
reference_image_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the reference image"
),
'thumbnail': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="ID of the thumbnail image",
nullable=True
),
'priority': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Priority of the image (lower values mean higher priority)"
)
},
required=['id', 'priority']
)
# Hadis reference schema
hadis_reference_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis reference"
),
'book': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="ID of the referenced book",
nullable=True
),
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Description of the reference",
nullable=True
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
),
'images': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=reference_image_schema,
description="List of reference images"
)
},
required=['id', 'created_at', 'images']
)
# Hadis overview schema
hadis_overview_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'status': openapi.Schema(
type=openapi.TYPE_STRING,
description="Status of the hadis"
),
'status_color': openapi.Schema(
type=openapi.TYPE_STRING,
description="Display color for the status"
),
'status_text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Descriptive text for the status",
nullable=True
),
'address': openapi.Schema(
type=openapi.TYPE_STRING,
description="Address information",
nullable=True
),
'links': openapi.Schema(
type=openapi.TYPE_OBJECT,
description="Related links"
),
'tags': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=tag_schema,
description="List of tags associated with this hadis"
),
'share_link': openapi.Schema(
type=openapi.TYPE_STRING,
description="Link for sharing the hadis",
nullable=True
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
)
},
required=['status', 'status_color', 'tags', 'created_at']
)
# Hadis detail schema
hadis_detail_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis"
),
'number': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique number identifier for the hadis"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the hadis"
),
'text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Original text of the hadis"
),
'translation': openapi.Schema(
type=openapi.TYPE_STRING,
description="Translation of the hadis text"
),
'status': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
description="Visibility status of the hadis"
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
),
'updated_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Last update timestamp"
),
'overview': hadis_overview_schema,
'first_reference': hadis_reference_schema
},
required=['id', 'number', 'title', 'text', 'translation', 'status', 'created_at', 'updated_at', 'overview']
)
hadis_detail_response = openapi.Response(
description="Detailed information about a specific hadis",
schema=hadis_detail_schema
)
# Transmitter schema
transmitter_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the transmitter"
),
'full_name': openapi.Schema(
type=openapi.TYPE_STRING,
description="Full name of the transmitter"
),
'birth_year_hijri': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Birth year in Hijri calendar"
),
'death_year_hijri': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Death year in Hijri calendar"
),
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Description of the transmitter",
nullable=True
),
'status': openapi.Schema(
type=openapi.TYPE_STRING,
description="Status of the transmitter"
),
'status_color': openapi.Schema(
type=openapi.TYPE_STRING,
description="Display color for the status"
),
'thumbnail': openapi.Schema(
type=openapi.TYPE_OBJECT,
description="Thumbnail image information",
nullable=True
)
},
required=['id', 'full_name', 'birth_year_hijri', 'death_year_hijri', 'status', 'status_color']
)
# Hadis transmitter schema
hadis_transmitter_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis transmitter relation"
),
'transmitter': transmitter_schema,
'description': openapi.Schema(
type=openapi.TYPE_STRING,
description="Description of the transmitter's role in this hadis",
nullable=True
),
'order': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Order in the chain of transmission"
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
)
},
required=['id', 'transmitter', 'order', 'created_at']
)
# Update hadis detail schema to include transmitters
hadis_detail_schema = openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
'id': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique identifier for the hadis"
),
'number': openapi.Schema(
type=openapi.TYPE_INTEGER,
description="Unique number identifier for the hadis"
),
'title': openapi.Schema(
type=openapi.TYPE_STRING,
description="Title of the hadis"
),
'text': openapi.Schema(
type=openapi.TYPE_STRING,
description="Original text of the hadis"
),
'translation': openapi.Schema(
type=openapi.TYPE_STRING,
description="Translation of the hadis text"
),
'status': openapi.Schema(
type=openapi.TYPE_BOOLEAN,
description="Visibility status of the hadis"
),
'created_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Creation timestamp"
),
'updated_at': openapi.Schema(
type=openapi.TYPE_STRING,
format=openapi.FORMAT_DATETIME,
description="Last update timestamp"
),
'overview': hadis_overview_schema,
'first_reference': hadis_reference_schema,
'transmitters': openapi.Schema(
type=openapi.TYPE_ARRAY,
items=hadis_transmitter_schema,
description="List of transmitters for this hadis"
)
},
required=['id', 'number', 'title', 'text', 'translation', 'status', 'created_at', 'updated_at', 'overview']
)
hadis_detail_response = openapi.Response(
description="Detailed information about a specific hadis",
schema=hadis_detail_schema
)
# Swagger decorators for views
category_list_swagger = swagger_auto_schema(
operation_id="list_hadis_categories",
@ -156,4 +426,27 @@ category_hadis_list_swagger = swagger_auto_schema(
404: "The specified category does not exist.",
500: "Internal server error occurred."
}
)
hadis_detail_swagger = swagger_auto_schema(
operation_id="get_hadis_detail",
operation_description="""
Retrieve detailed information about a specific hadis.
This endpoint returns comprehensive information about a hadis, including:
- Basic hadis details (number, title, text, translation)
- HadisOverview information (status, tags, etc.)
- The first HadisReference with its ReferenceImages
- List of Transmitters in order of transmission chain
The hadis is specified by its ID in the URL path.
""",
operation_summary="Get Hadis Detail",
tags=["Hadis"],
responses={
200: hadis_detail_response,
401: "Authentication credentials were not provided or are invalid.",
404: "The specified hadis does not exist.",
500: "Internal server error occurred."
}
)

28
apps/hadis/migrations/0007_auto_20250321_2007.py

@ -0,0 +1,28 @@
# Generated by Django 3.2.7 on 2025-03-21 20:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0004_remove_category_books'),
('hadis', '0006_auto_20250321_1600'),
]
operations = [
migrations.AddField(
model_name='hadisoverview',
name='explanation',
field=models.TextField(blank=True, null=True, verbose_name='explanation'),
),
migrations.AddField(
model_name='hadistransmitter',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='description'),
),
migrations.AlterUniqueTogether(
name='hadisreference',
unique_together={('hadis', 'book')},
),
]

6
apps/hadis/models/hadis.py

@ -49,9 +49,10 @@ class HadisOverview(models.Model):
links = models.JSONField(verbose_name=_('title'), null=True, blank=True, default=dict)
tags = models.ManyToManyField("HadisTag", related_name="hadises", verbose_name=_('tags'), blank=True)
share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True)
explanation = models.TextField(verbose_name=_('explanation'), null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class HadisReference(models.Model):
hadis = models.ForeignKey(
@ -67,7 +68,8 @@ class HadisReference(models.Model):
class Meta:
verbose_name = _('Hadis Reference')
verbose_name_plural = _('Hadis References')
unique_together = ('hadis', 'book')
def __str__(self):
return f'{self.hadis.number}-{self.book.title}'

1
apps/hadis/models/transmitter.py

@ -35,6 +35,7 @@ class HadisTransmitter(models.Model):
verbose_name=_('transmitter'),
related_name='hadises'
)
description = models.TextField(verbose_name=_('description'), blank=True, null=True)
order = models.PositiveIntegerField(
default=0,
verbose_name=_('Order'),

87
apps/hadis/serializers.py

@ -1,6 +1,6 @@
from rest_framework import serializers
from dj_filer.admin import get_thumbs
from apps.hadis.models import *
@ -38,13 +38,90 @@ class HadisTagSerializer(serializers.ModelSerializer):
class HadisSerializer(serializers.ModelSerializer):
translation = serializers.CharField(source='translation')
text = serializers.CharField(source='text')
class Meta:
model = Hadis
fields = ('number', 'title', 'text', 'translation',)
class ReferenceImageSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
class Meta:
model = ReferenceImage
fields = ('id', 'thumbnail', 'priority')
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
class HadisReferenceSerializer(serializers.ModelSerializer):
images = serializers.SerializerMethodField()
class Meta:
model = HadisReference
fields = ('id', 'book', 'description', 'created_at', 'images')
def get_images(self, obj):
return ReferenceImageSerializer(
obj.referenceimage_set.all(),
many=True,
context=self.context
).data
class TransmittersSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
class Meta:
model = Transmitters
fields = ('id', 'full_name', 'birth_year_hijri', 'death_year_hijri',
'description', 'status', 'status_color', 'thumbnail')
def get_thumbnail(self, obj):
return get_thumbs(obj.thumbnail, self.context.get('request'))
class HadisTransmitterSerializer(serializers.ModelSerializer):
transmitter = serializers.SerializerMethodField()
class Meta:
model = HadisTransmitter
fields = ('id', 'transmitter', 'description', 'order', 'created_at')
def get_transmitter(self, obj):
return TransmittersSerializer(
obj.transmitter,
context=self.context
).data
class HadisOverviewSerializer(serializers.ModelSerializer):
tags = serializers.SerializerMethodField()
class Meta:
model = HadisOverview
fields = ('status', 'status_color', 'status_text', 'address', 'links', 'tags', 'share_link', 'explanation', 'created_at')
def get_tags(self, obj):
return HadisTagSerializer(obj.get_tags, many=True).data
return HadisTagSerializer(
obj.tags.all(),
many=True,
context=self.context
).data
class HadisDetailSerializer(serializers.ModelSerializer):
overview = HadisOverviewSerializer(source='hadisoverview', read_only=True)
reference = serializers.SerializerMethodField()
transmitters = HadisTransmitterSerializer(many=True, read_only=True)
class Meta:
model = Hadis
fields = ('number', 'title', 'text', 'translation', 'tags')
fields = ('id', 'number', 'title', 'text', 'translation', 'status',
'created_at', 'updated_at', 'overview', 'reference', 'transmitters')
def get_reference(self, obj):
reference = obj.references.first()
if reference:
return HadisReferenceSerializer(reference, context=self.context).data
return None

3
apps/hadis/urls.py

@ -6,4 +6,7 @@ urlpatterns = [
path('categories/', views.CategoryListView.as_view(), name='category-list'),
path('categories/<int:pk>/hadis/', views.CategoryHadisListView.as_view(), name='category-hadis-list'),
path('<int:pk>/', views.HadisDetailView.as_view(), name='hadis-detail'),
]

52
apps/hadis/views/hadis.py

@ -1,12 +1,13 @@
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Subquery, Count, F, OuterRef, Q, Prefetch
from rest_framework.generics import ListAPIView
from rest_framework.generics import ListAPIView, RetrieveAPIView
from django.shortcuts import get_object_or_404
from apps.hadis.models import *
from apps.hadis.serializers import *
from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger
from apps.hadis.doc import category_list_swagger, category_hadis_list_swagger, hadis_detail_swagger
@ -24,6 +25,51 @@ class CategoryHadisListView(ListAPIView):
status=True,
).prefetch_related(
'category',
'tags',
)
class HadisDetailView(RetrieveAPIView):
"""
API endpoint to retrieve detailed information about a specific hadis.
Returns:
- Hadis details (number, title, text, translation)
- HadisOverview information (status, tags, etc.)
- First HadisReference with its ReferenceImages
- List of Transmitters
"""
serializer_class = HadisDetailSerializer
permission_classes = (IsAuthenticated,)
@hadis_detail_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_object(self):
hadis_id = self.kwargs.get('pk')
queryset = Hadis.objects.filter(id=hadis_id)
# Prefetch related data to optimize queries
queryset = queryset.prefetch_related(
'hadisoverview',
'hadisoverview__tags',
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
'referenceimage_set',
'book'
)
),
Prefetch(
'transmitters',
queryset=HadisTransmitter.objects.select_related('transmitter').order_by('order')
)
)
return get_object_or_404(queryset, id=hadis_id)
def get_serializer_context(self):
context = super().get_serializer_context()
context.update({'request': self.request})
return context

4
apps/library/admin.py

@ -180,11 +180,11 @@ class CategoryAdmin(AjaxDatatable):
list_display = ('title', 'slug', 'status', 'count_books', 'created_at')
list_filter = ('status', 'created_at', 'updated_at')
search_fields = ('title', 'slug')
autocomplete_fields = ('books',)
# autocomplete_fields = ('books',)
@admin.display(description=_('Number of Books'))
def count_books(self, obj):
count = obj.books.count()
count = obj.books_count
if count > 0:
url = reverse('admin:library_book_changelist') + f'?categories__id__exact={obj.id}'
return format_html('<a href="{}">{}</a>', url, count)

220
apps/library/doc.py

@ -0,0 +1,220 @@
"""
Swagger documentation for the Library API endpoints.
This module provides Swagger documentation for the Library API endpoints using drf-yasg.
It defines the request parameters, response schemas, and decorators for the views.
"""
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
# Parameter definitions
collection_id_param = openapi.Parameter(
'collection_id',
openapi.IN_QUERY,
description="Filter books by collection ID",
type=openapi.TYPE_INTEGER,
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
)
search_param = openapi.Parameter(
'search',
openapi.IN_QUERY,
description="Search books by title, summary, or author",
type=openapi.TYPE_STRING,
required=False
)
# 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"
),
'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"
)
},
required=['id', 'title', 'slug', 'status', 'created_at']
)
books_response = openapi.Response(
description="List of books 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=book_schema
)
}
)
)
# Book detail response
book_detail_response = openapi.Response(
description="Detailed information about a specific book",
schema=book_schema
)
# Swagger decorators for views
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
- 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 ID in the URL path.
""",
operation_summary="Get Book Detail",
tags=["Library"],
responses={
200: book_detail_response,
401: "Authentication credentials were not provided or are invalid.",
404: "The specified book does not exist.",
500: "Internal server error occurred."
}
)
book_list_swagger = swagger_auto_schema(
operation_id="list_books",
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'
- Middle section collection using the query parameter 'middle'
- Bottom section collection using the query parameter 'bottom'
You can also search for books by title, summary, or author using the query parameter 'search'.
""",
operation_summary="List Books",
tags=["Library"],
manual_parameters=[collection_id_param, middle_param, bottom_param, search_param],
responses={
200: books_response,
401: "Authentication credentials were not provided or are invalid.",
500: "Internal server error occurred."
}
)
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=["Library"],
responses={
200: "List of book categories",
401: "Authentication credentials were not provided or are invalid.",
500: "Internal server error occurred."
}
)
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=["Library"],
responses={
200: "List of pinned book collections with covers",
401: "Authentication credentials were not provided or are invalid.",
500: "Internal server error occurred."
}
)

17
apps/library/migrations/0004_remove_category_books.py

@ -0,0 +1,17 @@
# Generated by Django 3.2.7 on 2025-03-21 20:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('library', '0003_auto_20250321_0119'),
]
operations = [
migrations.RemoveField(
model_name='category',
name='books',
),
]

4
apps/library/models.py

@ -65,7 +65,7 @@ class Category(models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True)
status = models.BooleanField(default=True, verbose_name=_('status'))
books = models.ManyToManyField('library.Book', related_name='related_categories_books',through="library.Book_categories" ,verbose_name=_('Books'), blank=True)
# books = models.ManyToManyField('library.Book', related_name='related_categories_books',through="library.Book_categories" ,verbose_name=_('Books'), blank=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
@ -76,7 +76,7 @@ class Category(models.Model):
@property
def books_count(self):
"""Return the number of books in this category"""
return self.books.count()
return self.related_categories.count()
class Meta:
verbose_name = _('Category')

50
apps/library/serializers.py

@ -2,32 +2,56 @@
from dj_filer.admin import get_thumbs
from django.db.models import Avg
from django.db.models import Avg, Q
from rest_framework import serializers
from apps.library.models import *
class BannerListSerializer(serializers.ModelSerializer):
description = serializers.CharField(source='summary')
title = serializers.SerializerMethodField()
covers = serializers.SerializerMethodField()
def get_title(self, obj):
return obj.title
class CategorySerializer(serializers.ModelSerializer):
books_count = serializers.IntegerField(read_only=True)
class Meta:
model = Category
fields = ('id', 'title', 'slug', 'status', 'books_count', 'created_at', 'updated_at')
class PinnedBookCollectionSerializer(serializers.ModelSerializer):
covers = serializers.SerializerMethodField()
def get_covers(self, obj: BookCollection):
books = obj.get_books().order_by('-view_count')[:3]
books = obj.books.all().order_by('-view_count')[:3]
images = []
for book in books:
url = get_thumbs(book.thumbnail, self.context.get('request'))
if url.get('md'):
images.append(url['md'])
if book.thumbnail:
url = get_thumbs(book.thumbnail, self.context.get('request'))
if url.get('md'):
images.append(url['md'])
return images
class Meta:
model = BookCollection
fields = ('id', 'title', 'summary', 'covers')
fields = ('id', 'title', 'covers')
class BookSerializer(serializers.ModelSerializer):
thumbnail = serializers.SerializerMethodField()
def get_thumbnail(self, obj):
if obj.thumbnail:
return get_thumbs(obj.thumbnail, self.context.get('request'))
return None
class Meta:
model = Book
fields = (
'id', 'title', 'slug', 'summary', 'description', 'thumbnail',
'author', 'status', 'pin', 'view_count', 'download_count',
'file_type', 'book_file', 'created_at'
)

15
apps/library/urls.py

@ -0,0 +1,15 @@
from django.urls import path
from apps.library.views import (
CategoryListView,
PinnedBookCollectionListView,
BookListView,
BookDetailView,
)
urlpatterns = [
path('categories/', CategoryListView.as_view(), name='category-list'),
path('pinned-collections/', PinnedBookCollectionListView.as_view(), name='pinned-collection-list'),
path('books/', BookListView.as_view(), name='book-list'),
path('books/<int:pk>/', BookDetailView.as_view(), name='book-detail'),
]

110
apps/library/views.py

@ -1,22 +1,116 @@
from django.db.models import Q, Count
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.generics import ListAPIView
from rest_framework.generics import ListAPIView, RetrieveAPIView
from rest_framework.filters import SearchFilter
from apps.library.models import *
from apps.library.serializers import *
from apps.library.doc import (
book_list_swagger,
book_detail_swagger,
category_list_swagger,
pinned_collection_list_swagger
)
class BannerListView(ListAPIView):
serializer_class = BannerListSerializer
class CategoryListView(ListAPIView):
"""
API view to list all book categories
"""
serializer_class = CategorySerializer
permission_classes = (IsAuthenticated,)
@category_list_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
return Category.objects.filter(
status=True
).annotate(
books_count=Count('related_categories')
).order_by('title')
class PinnedBookCollectionListView(ListAPIView):
"""
API view to list pinned book collections with their top 3 book covers
"""
serializer_class = PinnedBookCollectionSerializer
permission_classes = (IsAuthenticated,)
pagination_class = None
@pinned_collection_list_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
_query = Q(status=True, display_position=BookCollection.DisplayPosition.TOP)
return BookCollection.objects.filter(
status=True,
display_position=BookCollection.DisplayPosition.PINNED
).order_by('-order', '-id')
class BookListView(ListAPIView):
"""
API view to list books with filtering and search capabilities
"""
serializer_class = BookSerializer
permission_classes = (IsAuthenticated,)
filter_backends = [SearchFilter]
search_fields = ['title', 'summary', 'author']
@book_list_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self):
queryset = Book.objects.filter(status=True)
# Filter by collection if provided
collection_id = self.request.query_params.get('collection_id')
if collection_id:
queryset = queryset.filter(collections__id=collection_id)
# Filter by middle collection if requested
# if self.request.query_params.get('middle'):
# middle_collections = BookCollection.objects.filter(
# status=True,
# display_position=BookCollection.DisplayPosition.MIDDLE
# )
# if middle_collections.exists():
# queryset = queryset.filter(collections__in=middle_collections)
# Filter by bottom collection if requested
# if self.request.query_params.get('bottom'):
# bottom_collections = BookCollection.objects.filter(
# status=True,
# display_position=BookCollection.DisplayPosition.BOTTOM
# )
# if bottom_collections.exists():
# queryset = queryset.filter(collections__in=bottom_collections)
return queryset.order_by('-pin', '-created_at')
class BookDetailView(RetrieveAPIView):
"""
API view to retrieve detailed information about a specific book
"""
serializer_class = BookSerializer
permission_classes = (IsAuthenticated,)
queryset = Book.objects.filter(status=True)
@book_detail_swagger
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
return Collection.objects.filter(
_query,
).order_by('-order', '-id', )
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# Increment view count when book details are viewed
instance.increment_view_count()
serializer = self.get_serializer(instance)
return Response(serializer.data)

20
apps/podcast/models.py

@ -2,10 +2,27 @@ from django.db import models
class PodcastCategory(models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
status = models.BooleanField(default=True, verbose_name=_('status'))
order = models.PositiveIntegerField(default=0, verbose_name=_('order'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return self.title
class Meta:
verbose_name = _('Video Category')
verbose_name_plural = _('Video Categories')
ordering = ['order']
class PodcastCollection(models.Model):
title = models.CharField(max_length=255, help_text="This title will not be displayed anywhere")
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
videos = models.ManyToManyField(
@ -14,6 +31,7 @@ class PodcastCollection(models.Model):
related_name='collections',
verbose_name=_('podcasts'),
)
def __str__(self):
return f'Collection #{self.id}/{self.title}'
@ -49,7 +67,7 @@ class Podcast(models.Model):
'image allowed'
))
description = models.TextField(null=True)
categories = models.ManyToManyField(PodcastCategory, related_name='podcasts', verbose_name=_('categories'))
audio_file = models.FileField(upload_to='podcast/audio/', null=True, blank=True)
audio_url = models.CharField(max_length=655, null=True, blank=True)
audio_time = models.TimeField()

78
apps/video/admin.py

@ -1,23 +1,85 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from django.utils.html import format_html
from ajaxdatatable.admin import AjaxDatatable
from apps.video.models import *
class VideoInCollectionInline(admin.TabularInline):
model = VideoInCollection
extra = 1
extra = 1
autocomplete_fields = ('video',)
ordering = ('priority',)
@admin.register(VideoCollection)
class VideoCollectionAdmin(AjaxDatatable):
list_display = ('title',)
class VideoCollectionAdminBase(AjaxDatatable):
"""Base admin class for all video collection types"""
list_display = ('title', 'status', 'order', 'count_videos', 'created_at')
list_filter = ('status', 'created_at', 'updated_at')
search_fields = ('title',)
inlines = [VideoInCollectionInline]
fieldsets = (
(None, {
'fields': ('title', 'status', 'order')
}),
)
@admin.display(description=_('Number of Videos'))
def count_videos(self, obj):
count = obj.videos.count()
if count > 0:
url = reverse('admin:video_video_changelist') + f'?collections__id__exact={obj.id}'
return format_html('<a href="{}">{}</a>', url, count)
return count
# @admin.register(VideoCollection)
# class VideoCollectionAdmin(VideoCollectionAdminBase):
# """Admin for all video collections"""
# list_display = ('title', 'status', 'count_videos', 'created_at')
# list_filter = ('status', 'created_at', 'updated_at')
@admin.register(VideoCategory)
class VideoCategoryAdmin(AjaxDatatable):
list_display = ('title', 'slug', 'status', 'order', 'count_videos', 'created_at')
list_filter = ('status', 'created_at', 'updated_at')
search_fields = ('title', 'slug')
@admin.display(description=_('Number of Videos'))
def count_videos(self, obj):
count = obj.videos.count()
if count > 0:
url = reverse('admin:video_video_changelist') + f'?category__id__exact={obj.id}'
return format_html('<a href="{}">{}</a>', url, count)
return count
@admin.register(Video)
class VideoAdmin(AjaxDatatable):
list_display = ('title', 'video_type', 'status')
search_fields = ('title',)
list_display = ('title', 'slug', 'video_type', 'status', 'view_count', 'created_at')
list_filter = ('status', 'video_type', 'created_at', 'updated_at')
search_fields = ('title', 'slug', 'description')
autocomplete_fields = ('categories',)
fieldsets = (
(None, {
'fields': ('title', 'slug', 'description', 'thumbnail', 'categories')
}),
(_('Video Information'), {
'fields': ('video_type', 'video_file', 'video_url', 'video_time')
}),
(_('Status'), {
'fields': ('status',)
}),
(_('Statistics'), {
'fields': ('view_count',)
}),
)

99
apps/video/migrations/0001_initial.py

@ -0,0 +1,99 @@
# Generated by Django 3.2.7 on 2025-03-21 22:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import filer.fields.image
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.FILER_IMAGE_MODEL),
]
operations = [
migrations.CreateModel(
name='Video',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, null=True)),
('slug', models.SlugField(allow_unicode=True, unique=True)),
('description', models.TextField(null=True)),
('video_type', models.CharField(choices=[('file', 'File'), ('youtube', 'Youtube')], default='file', max_length=255)),
('video_file', models.FileField(blank=True, null=True, upload_to='video/videos/')),
('video_url', models.CharField(blank=True, max_length=655, null=True)),
('video_time', models.TimeField()),
('view_count', models.PositiveBigIntegerField(default=0, verbose_name='view count')),
('status', models.BooleanField(default=True, verbose_name='status')),
('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': 'Video',
'verbose_name_plural': 'Videos',
},
),
migrations.CreateModel(
name='VideoCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255, verbose_name='title')),
('slug', models.SlugField(allow_unicode=True, unique=True, verbose_name='slug')),
('status', models.BooleanField(default=True, verbose_name='status')),
('order', models.PositiveIntegerField(default=0, verbose_name='order')),
('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': 'Video Category',
'verbose_name_plural': 'Video Categories',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='VideoCollection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(help_text='This title will not be displayed anywhere', max_length=255)),
('status', models.BooleanField(default=True, verbose_name='status')),
('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': 'Video Collection',
'verbose_name_plural': 'Video Collections',
},
),
migrations.CreateModel(
name='VideoInCollection',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.PositiveIntegerField(default=0, verbose_name='priority')),
('video', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='collections_videos', to='video.video', verbose_name='video')),
('video_collection', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='videos_in_collection', to='video.videocollection', verbose_name='video collection')),
],
options={
'verbose_name': 'Video in Collection',
'verbose_name_plural': 'Videos in Collection',
'ordering': ['priority'],
},
),
migrations.AddField(
model_name='videocollection',
name='videos',
field=models.ManyToManyField(related_name='collections', through='video.VideoInCollection', to='video.Video', verbose_name='videos'),
),
migrations.AddField(
model_name='video',
name='categories',
field=models.ManyToManyField(blank=True, related_name='videos', to='video.VideoCategory', verbose_name='categories'),
),
migrations.AddField(
model_name='video',
name='thumbnail',
field=filer.fields.image.FilerImageField(blank=True, help_text='image allowed', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.FILER_IMAGE_MODEL),
),
]

54
apps/video/models.py

@ -1,13 +1,33 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from filer.fields.image import FilerImageField
class VideoCategory(models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
status = models.BooleanField(default=True, verbose_name=_('status'))
order = models.PositiveIntegerField(default=0, verbose_name=_('order'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
def __str__(self):
return self.title
class Meta:
verbose_name = _('Video Category')
verbose_name_plural = _('Video Categories')
ordering = ['order']
class VideoCollection(models.Model):
title = models.CharField(max_length=255, help_text="This title will not be displayed anywhere")
status = models.BooleanField(default=True, verbose_name=_('status'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
videos = models.ManyToManyField(
Video,
"Video",
through='VideoInCollection',
related_name='collections',
verbose_name=_('videos'),
@ -17,15 +37,15 @@ class VideoCollection(models.Model):
class Meta:
verbose_name = _('Video Collection')
verbose_name_plural = _('Video Collections')
verbose_name_plural = _('Video Collections')
class VideoInCollection(models.Model):
video_collection = models.ForeignKey(
VideoCollection, on_delete=models.CASCADE, related_name='videos_in_collection', verbose_name=_('video collection')
"VideoCollection", on_delete=models.CASCADE, related_name='videos_in_collection', verbose_name=_('video collection')
)
video = models.ForeignKey(
Video, on_delete=models.CASCADE, related_name='collections_videos', verbose_name=_('video')
"Video", on_delete=models.CASCADE, related_name='collections_videos', verbose_name=_('video')
)
priority = models.PositiveIntegerField(default=0, verbose_name=_('priority'))
@ -42,21 +62,26 @@ class Video(models.Model):
class vdeo_type(models.TextChoices):
FILE = 'file'
YOUTUBE = 'youtube'
title = models.CharField(max_length=255, null=True)
slug = models.SlugField(allow_unicode=True, unique=True)
thumbnail = FilerImageField(related_name="+", on_delete=models.SET_NULL, null=True, blank=True, help_text=_(
'image allowed'
))
description = models.TextField(null=True)
categories = models.ManyToManyField(
VideoCategory,
related_name='videos',
verbose_name=_('categories'),
blank=True,
)
video_type = models.CharField(max_length=255, choices=vdeo_type.choices, default=vdeo_type.FILE)
video_file = models.FileField(upload_to='video/videos/', null=True, blank=True)
video_url = models.CharField(max_length=655, null=True, blank=True)
video_time = models.TimeField()
view_count = models.PositiveBigIntegerField(default=0, verbose_name=_('view count'))
download_count = models.PositiveBigIntegerField(default=0, verbose_name=_('view count'))
status = models.BooleanField(default=True, verbose_name=_('status'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
@ -64,8 +89,15 @@ class Video(models.Model):
def __str__(self):
return self.title
def increment_view_count(self):
"""Increment the view count for this video"""
self.view_count += 1
self.save(update_fields=['view_count'])
return self.view_count
class Meta:
verbose_name = _('Video')
verbose_name_plural = _('Videos')
verbose_name = _('Video')
verbose_name_plural = _('Videos')

70
apps/video/serializers.py

@ -0,0 +1,70 @@
from rest_framework import serializers
from .models import VideoCategory, Video, VideoCollection, VideoInCollection
class VideoCategoryListSerializer(serializers.ModelSerializer):
video_count = serializers.SerializerMethodField()
class Meta:
model = VideoCategory
fields = ['id', 'title', 'slug', 'video_count']
def get_video_count(self, obj):
return obj.videos.filter(status=True).count()
class VideoListSerializer(serializers.ModelSerializer):
categories = VideoCategoryListSerializer(many=True, read_only=True)
class Meta:
model = Video
fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'video_time',
'view_count', 'categories', 'created_at']
class VideoDetailSerializer(serializers.ModelSerializer):
related_videos = serializers.SerializerMethodField()
categories = VideoCategoryListSerializer(many=True, read_only=True)
class Meta:
model = Video
fields = ['id', 'title', 'slug', 'thumbnail', 'description', 'video_type',
'video_file', 'video_url', 'video_time', 'view_count',
'categories', 'created_at', 'related_videos']
def get_related_videos(self, obj):
# Get all collections that contain this video
collections = obj.collections.all()
if collections.exists():
# Get all videos from all collections that contain this video
related_videos = []
video_ids = set() # To track unique videos
for collection in collections:
# Get all videos in this collection ordered by priority
videos_in_collection = VideoInCollection.objects.filter(
video_collection=collection
).exclude(video=obj).order_by('priority')
# Add videos to our list if not already added
for vic in videos_in_collection:
if vic.video.id not in video_ids:
related_videos.append(vic.video)
video_ids.add(vic.video.id)
# Return the related videos using VideoListSerializer
return VideoListSerializer(related_videos, many=True).data
# # If not in a collection, return videos from the same category
# elif obj.category:
# related = Video.objects.filter(
# category=obj.category,
# status=True
# ).exclude(id=obj.id)[:5]
# return VideoListSerializer(related, many=True).data
return []

12
apps/video/urls.py

@ -0,0 +1,12 @@
from django.urls import path
from .views import VideoCategoryListAPIView, VideoListAPIView, VideoDetailAPIView
app_name = 'video'
urlpatterns = [
path('categories/', VideoCategoryListAPIView.as_view(), name='category-list'),
path('list/', VideoListAPIView.as_view(), name='video-list'),
path('detail/<slug:slug>/', VideoDetailAPIView.as_view(), name='video-detail'),
]

50
apps/video/views.py

@ -1,3 +1,49 @@
from django.shortcuts import render
from rest_framework import generics, status
from rest_framework.response import Response
from .models import VideoCategory, Video
from .serializers import VideoCategoryListSerializer, VideoListSerializer, VideoDetailSerializer
# Create your views here.
class VideoCategoryListAPIView(generics.ListAPIView):
"""
API view to list all video categories with their video counts
"""
serializer_class = VideoCategoryListSerializer
def get_queryset(self):
return VideoCategory.objects.filter(status=True).order_by('order')
class VideoListAPIView(generics.ListAPIView):
"""
API view to list all videos, with optional category filtering
"""
serializer_class = VideoListSerializer
def get_queryset(self):
queryset = Video.objects.filter(status=True).order_by('-created_at')
# Filter by category if provided
category_slug = self.request.query_params.get('category', None)
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
return queryset
class VideoDetailAPIView(generics.RetrieveAPIView):
"""
API view to get video details, including related videos from the same collection
"""
serializer_class = VideoDetailSerializer
lookup_field = 'slug'
def get_queryset(self):
return Video.objects.filter(status=True)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# Increment view count
instance.increment_view_count()
serializer = self.get_serializer(instance)
return Response(serializer.data)

1
config/settings/base.py

@ -49,6 +49,7 @@ LOCAL_APPS = [
'apps.certificate.apps.CertificateConfig',
'apps.hadis.apps.HadisConfig',
'apps.library.apps.LibraryConfig',
'apps.video.apps.VideoConfig',
'dynamic_preferences',
]

17
config/urls.py

@ -34,17 +34,20 @@ from utils import absolute_url
api_patterns = [
path('test/', include('apps.api.urls')),
path('account/', include('apps.account.urls')),
path('courses/', include('apps.course.urls')),
path('quiz/', include('apps.quiz.urls')),
path('transaction/', include('apps.transaction.urls')),
path('certificates/', include('apps.certificate.urls')),
path('hadis/', include('apps.hadis.urls')),
path('account/', include('apps.account.urls')),
path('courses/', include('apps.course.urls')),
path('quiz/', include('apps.quiz.urls')),
path('transaction/', include('apps.transaction.urls')),
path('certificates/', include('apps.certificate.urls')),
path('hadis/', include('apps.hadis.urls')),
path('library/', include('apps.library.urls')),
path('videos/', include('apps.video.urls')),
path('settings/', include('dynamic_preferences.urls')),
path('upload-tmp-media/', UploadTmpMedia.as_view()),
]

Loading…
Cancel
Save