23 changed files with 1227 additions and 64 deletions
-
293apps/hadis/doc.py
-
28apps/hadis/migrations/0007_auto_20250321_2007.py
-
2apps/hadis/models/hadis.py
-
1apps/hadis/models/transmitter.py
-
87apps/hadis/serializers.py
-
3apps/hadis/urls.py
-
52apps/hadis/views/hadis.py
-
4apps/library/admin.py
-
220apps/library/doc.py
-
17apps/library/migrations/0004_remove_category_books.py
-
4apps/library/models.py
-
42apps/library/serializers.py
-
15apps/library/urls.py
-
108apps/library/views.py
-
20apps/podcast/models.py
-
76apps/video/admin.py
-
99apps/video/migrations/0001_initial.py
-
40apps/video/models.py
-
70apps/video/serializers.py
-
12apps/video/urls.py
-
50apps/video/views.py
-
1config/settings/base.py
-
3config/urls.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')}, |
||||
|
), |
||||
|
] |
||||
@ -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." |
||||
|
} |
||||
|
) |
||||
@ -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', |
||||
|
), |
||||
|
] |
||||
@ -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'), |
||||
|
] |
||||
@ -1,22 +1,116 @@ |
|||||
|
from django.db.models import Q, Count |
||||
from rest_framework.permissions import IsAuthenticated |
from rest_framework.permissions import IsAuthenticated |
||||
from rest_framework.response import Response |
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.models import * |
||||
from apps.library.serializers 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,) |
permission_classes = (IsAuthenticated,) |
||||
pagination_class = None |
pagination_class = None |
||||
|
|
||||
|
@pinned_collection_list_swagger |
||||
|
def get(self, request, *args, **kwargs): |
||||
|
return super().get(request, *args, **kwargs) |
||||
|
|
||||
|
def get_queryset(self): |
||||
|
return BookCollection.objects.filter( |
||||
|
status=True, |
||||
|
display_position=BookCollection.DisplayPosition.PINNED |
||||
|
).order_by('-order', '-id') |
||||
|
|
||||
|
|
||||
|
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): |
def get_queryset(self): |
||||
_query = Q(status=True, display_position=BookCollection.DisplayPosition.TOP) |
|
||||
|
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) |
||||
|
|
||||
return Collection.objects.filter( |
|
||||
_query, |
|
||||
).order_by('-order', '-id', ) |
|
||||
|
@book_detail_swagger |
||||
|
def get(self, request, *args, **kwargs): |
||||
|
return super().get(request, *args, **kwargs) |
||||
|
|
||||
|
def retrieve(self, request, *args, **kwargs): |
||||
|
instance = self.get_object() |
||||
|
# Increment view count when book details are viewed |
||||
|
instance.increment_view_count() |
||||
|
serializer = self.get_serializer(instance) |
||||
|
return Response(serializer.data) |
||||
|
|
||||
@ -1,23 +1,85 @@ |
|||||
from django.contrib import admin |
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 ajaxdatatable.admin import AjaxDatatable |
||||
|
|
||||
from apps.video.models import * |
from apps.video.models import * |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
class VideoInCollectionInline(admin.TabularInline): |
class VideoInCollectionInline(admin.TabularInline): |
||||
model = VideoInCollection |
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] |
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) |
@admin.register(Video) |
||||
class VideoAdmin(AjaxDatatable): |
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',) |
||||
|
}), |
||||
|
) |
||||
|
|
||||
@ -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), |
||||
|
), |
||||
|
] |
||||
@ -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 [] |
||||
@ -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'), |
||||
|
] |
||||
@ -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) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue