23 changed files with 1227 additions and 64 deletions
-
293apps/hadis/doc.py
-
28apps/hadis/migrations/0007_auto_20250321_2007.py
-
6apps/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
-
50apps/library/serializers.py
-
15apps/library/urls.py
-
110apps/library/views.py
-
20apps/podcast/models.py
-
78apps/video/admin.py
-
99apps/video/migrations/0001_initial.py
-
54apps/video/models.py
-
70apps/video/serializers.py
-
12apps/video/urls.py
-
50apps/video/views.py
-
1config/settings/base.py
-
17config/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.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) |
|||
|
|||
@ -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',) |
|||
}), |
|||
) |
|||
|
|||
@ -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