Browse Source

.\apps\#

videoplaylist and podcastplaylist endpoints added for dashboard
master
Mohsen Taba 1 month ago
parent
commit
5ffcb3c87c
  1. 128
      apps/podcast/serializers_admin.py
  2. 3
      apps/podcast/urls.py
  3. 119
      apps/podcast/views_admin.py
  4. 140
      apps/video/serializers_admin.py
  5. 3
      apps/video/urls.py
  6. 148
      apps/video/views_admin.py

128
apps/podcast/serializers_admin.py

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Podcast , PodcastCategory , PodcastCollection
from .models import PlaylistItem, Podcast, PodcastCategory, PodcastCollection, PodcastPlaylist
class AbsoluteImageField(serializers.ImageField): class AbsoluteImageField(serializers.ImageField):
@ -128,3 +128,129 @@ class AdminPodcastDetailSerializer(serializers.ModelSerializer):
if collections is not None: if collections is not None:
podcast.collections.set(collections) podcast.collections.set(collections)
return podcast return podcast
class AdminPodcastPlaylistItemPodcastSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
class Meta:
model = Podcast
fields = ["id", "title", "slug", "thumbnail", "audio_time", "status"]
class AdminPodcastPlaylistItemSerializer(serializers.ModelSerializer):
podcast_detail = AdminPodcastPlaylistItemPodcastSerializer(source="podcast", read_only=True)
class Meta:
model = PlaylistItem
fields = ["id", "playlist", "podcast", "priority", "created_at", "updated_at", "podcast_detail"]
read_only_fields = ["id", "created_at", "updated_at", "podcast_detail"]
def validate(self, attrs):
playlist = attrs.get("playlist", getattr(self.instance, "playlist", None))
podcast = attrs.get("podcast", getattr(self.instance, "podcast", None))
if playlist and podcast:
query = PlaylistItem.objects.filter(podcast=podcast).exclude(playlist=playlist)
if self.instance:
query = query.exclude(pk=self.instance.pk)
existing = query.select_related("playlist").first()
if existing:
raise serializers.ValidationError({
"podcast": f'This podcast is already used in playlist "{existing.playlist.title}".',
})
return attrs
class AdminPodcastPlaylistListSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
categories_detail = AdminPodcastCategorySerializer(source="categories", many=True, read_only=True)
collections_detail = AdminPodcastCollectionSerializer(source="collections", many=True, read_only=True)
items_count = serializers.SerializerMethodField()
class Meta:
model = PodcastPlaylist
fields = [
"id",
"title",
"slug",
"thumbnail",
"slogan",
"status",
"order",
"view_count",
"total_time",
"created_at",
"updated_at",
"categories_detail",
"collections_detail",
"items_count",
]
read_only_fields = ["id", "view_count", "total_time", "created_at", "updated_at", "items_count"]
def get_items_count(self, obj):
return obj.playlist_items.count()
class AdminPodcastPlaylistDetailSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
categories = serializers.PrimaryKeyRelatedField(queryset=PodcastCategory.objects.all(), many=True, required=False)
collections = serializers.PrimaryKeyRelatedField(queryset=PodcastCollection.objects.all(), many=True, required=False)
categories_detail = AdminPodcastCategorySerializer(source="categories", many=True, read_only=True)
collections_detail = AdminPodcastCollectionSerializer(source="collections", many=True, read_only=True)
playlist_items_detail = AdminPodcastPlaylistItemSerializer(source="playlist_items", many=True, read_only=True)
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
class Meta:
model = PodcastPlaylist
fields = [
"id",
"title",
"slug",
"slogan",
"description",
"thumbnail",
"order",
"status",
"view_count",
"total_time",
"created_at",
"updated_at",
"categories",
"collections",
"categories_detail",
"collections_detail",
"playlist_items_detail",
"remove_thumbnail",
]
read_only_fields = ["id", "view_count", "total_time", "created_at", "updated_at", "playlist_items_detail"]
def create(self, validated_data):
validated_data.pop("remove_thumbnail", False)
categories = validated_data.pop("categories", [])
collections = validated_data.pop("collections", [])
playlist = super().create(validated_data)
if categories:
playlist.categories.set(categories)
if collections:
playlist.collections.set(collections)
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return playlist
def update(self, instance, validated_data):
remove_thumbnail = validated_data.pop("remove_thumbnail", False)
categories = validated_data.pop("categories", None)
collections = validated_data.pop("collections", None)
if remove_thumbnail and instance.thumbnail:
instance.thumbnail.delete(save=False)
instance.thumbnail = None
playlist = super().update(instance, validated_data)
if categories is not None:
playlist.categories.set(categories)
if collections is not None:
playlist.collections.set(collections)
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return playlist

3
apps/podcast/urls.py

@ -5,6 +5,7 @@ from .views_admin import *
admin_router = SimpleRouter() admin_router = SimpleRouter()
admin_router.register(r'podcasts', AdminPodcastViewSet, basename='admin-podcast') admin_router.register(r'podcasts', AdminPodcastViewSet, basename='admin-podcast')
admin_router.register(r'playlists', AdminPodcastPlaylistViewSet, basename='admin-podcast-playlist')
admin_router.register(r'categories', AdminPodcastCategoryViewSet, basename='admin-podcast-category') admin_router.register(r'categories', AdminPodcastCategoryViewSet, basename='admin-podcast-category')
admin_router.register(r'collections', AdminPodcastCollectionViewSet, basename='admin-podcast-collection') admin_router.register(r'collections', AdminPodcastCollectionViewSet, basename='admin-podcast-collection')
@ -24,4 +25,4 @@ urlpatterns = [
path('user-playlist/list/', UserPlaylistListAPIView.as_view(), name='user-playlist-list'), path('user-playlist/list/', UserPlaylistListAPIView.as_view(), name='user-playlist-list'),
]
]

119
apps/podcast/views_admin.py

@ -1,18 +1,25 @@
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from rest_framework import status
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination from utils.pagination import StandardResultsSetPagination
from .models import Podcast, PodcastCollection, PodcastCategory
from .models import PlaylistItem, Podcast, PodcastCollection, PodcastCategory, PodcastPlaylist
from .serializers_admin import ( from .serializers_admin import (
AdminPodcastCategorySerializer, AdminPodcastCategorySerializer,
AdminPodcastCollectionSerializer, AdminPodcastCollectionSerializer,
AdminPodcastDetailSerializer, AdminPodcastDetailSerializer,
AdminPodcastListSerializer, AdminPodcastListSerializer,
AdminPodcastPlaylistDetailSerializer,
AdminPodcastPlaylistItemSerializer,
AdminPodcastPlaylistListSerializer,
) )
@ -71,3 +78,113 @@ class AdminPodcastViewSet(ModelViewSet):
queryset = queryset.filter(collections__id=collection_id) queryset = queryset.filter(collections__id=collection_id)
return queryset.distinct().order_by("-created_at") return queryset.distinct().order_by("-created_at")
class AdminPodcastPlaylistViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = StandardResultsSetPagination
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_serializer_class(self):
if self.action == "list":
return AdminPodcastPlaylistListSerializer
return AdminPodcastPlaylistDetailSerializer
def get_queryset(self):
queryset = PodcastPlaylist.objects.all().prefetch_related("categories", "collections", "playlist_items__podcast")
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query)
| Q(slug__icontains=search_query)
| Q(slogan__icontains=search_query)
| Q(description__icontains=search_query)
)
status_filter = self.request.query_params.get("status")
if status_filter in {"true", "false"}:
queryset = queryset.filter(status=status_filter == "true")
category_id = self.request.query_params.get("category")
if category_id and category_id != "all":
queryset = queryset.filter(categories__id=category_id)
collection_id = self.request.query_params.get("collection")
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)
return queryset.distinct().order_by("order", "-created_at")
@action(detail=True, methods=["get", "post"], url_path="items")
def items(self, request, pk=None):
playlist = self.get_object()
if request.method.lower() == "get":
items = playlist.playlist_items.select_related("podcast").order_by("priority", "id")
serializer = AdminPodcastPlaylistItemSerializer(items, many=True, context={"request": request})
return Response(serializer.data)
serializer = AdminPodcastPlaylistItemSerializer(
data={
**request.data,
"playlist": playlist.id,
"priority": request.data.get("priority", playlist.playlist_items.count() + 1),
},
context={"request": request},
)
serializer.is_valid(raise_exception=True)
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["patch", "delete"], url_path=r"items/(?P<item_id>\d+)")
def item_detail(self, request, pk=None, item_id=None):
playlist = self.get_object()
item = playlist.playlist_items.select_related("podcast").get(pk=item_id)
if request.method.lower() == "delete":
item.delete()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return Response(status=status.HTTP_204_NO_CONTENT)
serializer = AdminPodcastPlaylistItemSerializer(
item,
data={**request.data, "playlist": playlist.id},
partial=True,
context={"request": request},
)
serializer.is_valid(raise_exception=True)
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return Response(serializer.data)
@action(detail=True, methods=["post"], url_path="items/reorder")
def reorder_items(self, request, pk=None):
playlist = self.get_object()
item_ids = request.data.get("item_ids", [])
if not isinstance(item_ids, list):
return Response({"detail": "item_ids must be a list."}, status=status.HTTP_400_BAD_REQUEST)
items = list(playlist.playlist_items.filter(id__in=item_ids))
if len(items) != len(item_ids):
return Response({"detail": "Some items were not found."}, status=status.HTTP_400_BAD_REQUEST)
item_map = {item.id: item for item in items}
with transaction.atomic():
for index, item_id in enumerate(item_ids, start=1):
item = item_map[item_id]
if item.priority != index:
item.priority = index
item.save(update_fields=["priority"])
serializer = AdminPodcastPlaylistItemSerializer(
playlist.playlist_items.select_related("podcast").order_by("priority", "id"),
many=True,
context={"request": request},
)
return Response(serializer.data)

140
apps/video/serializers_admin.py

@ -1,6 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Video
from .models import PlaylistItem, Video, VideoCategory, VideoCollection, VideoPlaylist
class AbsoluteImageField(serializers.ImageField): class AbsoluteImageField(serializers.ImageField):
@ -25,6 +25,18 @@ class AbsoluteFileField(serializers.FileField):
return url return url
class AdminVideoCategorySerializer(serializers.ModelSerializer):
class Meta:
model = VideoCategory
fields = ["id", "title", "slug", "status", "order"]
class AdminVideoCollectionSerializer(serializers.ModelSerializer):
class Meta:
model = VideoCollection
fields = ["id", "title", "slug", "summary", "display_position", "status", "order", "pin_top"]
class AdminVideoListSerializer(serializers.ModelSerializer): class AdminVideoListSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True) thumbnail = AbsoluteImageField(required=False, allow_null=True)
video_file = AbsoluteFileField(required=False, allow_null=True) video_file = AbsoluteFileField(required=False, allow_null=True)
@ -117,3 +129,129 @@ class AdminVideoDetailSerializer(serializers.ModelSerializer):
instance.video_file = None instance.video_file = None
return super().update(instance, validated_data) return super().update(instance, validated_data)
class AdminVideoPlaylistItemVideoSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
class Meta:
model = Video
fields = ["id", "title", "slug", "thumbnail", "video_type", "video_time", "status"]
class AdminVideoPlaylistItemSerializer(serializers.ModelSerializer):
video_detail = AdminVideoPlaylistItemVideoSerializer(source="video", read_only=True)
class Meta:
model = PlaylistItem
fields = ["id", "playlist", "video", "priority", "created_at", "updated_at", "video_detail"]
read_only_fields = ["id", "created_at", "updated_at", "video_detail"]
def validate(self, attrs):
playlist = attrs.get("playlist", getattr(self.instance, "playlist", None))
video = attrs.get("video", getattr(self.instance, "video", None))
if playlist and video:
query = PlaylistItem.objects.filter(video=video).exclude(playlist=playlist)
if self.instance:
query = query.exclude(pk=self.instance.pk)
existing = query.select_related("playlist").first()
if existing:
raise serializers.ValidationError({
"video": f'This video is already used in playlist "{existing.playlist.title}".',
})
return attrs
class AdminVideoPlaylistListSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
categories_detail = AdminVideoCategorySerializer(source="categories", many=True, read_only=True)
collections_detail = AdminVideoCollectionSerializer(source="collections", many=True, read_only=True)
items_count = serializers.SerializerMethodField()
class Meta:
model = VideoPlaylist
fields = [
"id",
"title",
"slug",
"thumbnail",
"slogan",
"status",
"order",
"view_count",
"total_time",
"created_at",
"updated_at",
"categories_detail",
"collections_detail",
"items_count",
]
read_only_fields = ["id", "view_count", "total_time", "created_at", "updated_at", "items_count"]
def get_items_count(self, obj):
return obj.playlist_items.count()
class AdminVideoPlaylistDetailSerializer(serializers.ModelSerializer):
thumbnail = AbsoluteImageField(required=False, allow_null=True)
categories = serializers.PrimaryKeyRelatedField(queryset=VideoCategory.objects.all(), many=True, required=False)
collections = serializers.PrimaryKeyRelatedField(queryset=VideoCollection.objects.all(), many=True, required=False)
categories_detail = AdminVideoCategorySerializer(source="categories", many=True, read_only=True)
collections_detail = AdminVideoCollectionSerializer(source="collections", many=True, read_only=True)
playlist_items_detail = AdminVideoPlaylistItemSerializer(source="playlist_items", many=True, read_only=True)
remove_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
class Meta:
model = VideoPlaylist
fields = [
"id",
"title",
"slug",
"slogan",
"description",
"thumbnail",
"order",
"status",
"view_count",
"total_time",
"created_at",
"updated_at",
"categories",
"collections",
"categories_detail",
"collections_detail",
"playlist_items_detail",
"remove_thumbnail",
]
read_only_fields = ["id", "view_count", "total_time", "created_at", "updated_at", "playlist_items_detail"]
def create(self, validated_data):
validated_data.pop("remove_thumbnail", False)
categories = validated_data.pop("categories", [])
collections = validated_data.pop("collections", [])
playlist = super().create(validated_data)
if categories:
playlist.categories.set(categories)
if collections:
playlist.collections.set(collections)
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return playlist
def update(self, instance, validated_data):
remove_thumbnail = validated_data.pop("remove_thumbnail", False)
categories = validated_data.pop("categories", None)
collections = validated_data.pop("collections", None)
if remove_thumbnail and instance.thumbnail:
instance.thumbnail.delete(save=False)
instance.thumbnail = None
playlist = super().update(instance, validated_data)
if categories is not None:
playlist.categories.set(categories)
if collections is not None:
playlist.collections.set(collections)
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return playlist

3
apps/video/urls.py

@ -7,6 +7,9 @@ app_name = 'video'
admin_router = SimpleRouter() admin_router = SimpleRouter()
admin_router.register(r'videos', AdminVideoViewSet, basename='admin-video') admin_router.register(r'videos', AdminVideoViewSet, basename='admin-video')
admin_router.register(r'playlists', AdminVideoPlaylistViewSet, basename='admin-video-playlist')
admin_router.register(r'categories', AdminVideoCategoryViewSet, basename='admin-video-category')
admin_router.register(r'collections', AdminVideoCollectionViewSet, basename='admin-video-collection')
urlpatterns = [ urlpatterns = [
path('admin/', include(admin_router.urls)), path('admin/', include(admin_router.urls)),

148
apps/video/views_admin.py

@ -1,14 +1,46 @@
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from rest_framework import status
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from apps.account.permissions import IsSuperAdmin from apps.account.permissions import IsSuperAdmin
from utils.pagination import StandardResultsSetPagination from utils.pagination import StandardResultsSetPagination
from .models import Video
from .serializers_admin import AdminVideoDetailSerializer, AdminVideoListSerializer
from .models import PlaylistItem, Video, VideoCategory, VideoCollection, VideoPlaylist
from .serializers_admin import (
AdminVideoCategorySerializer,
AdminVideoCollectionSerializer,
AdminVideoDetailSerializer,
AdminVideoListSerializer,
AdminVideoPlaylistDetailSerializer,
AdminVideoPlaylistItemSerializer,
AdminVideoPlaylistListSerializer,
)
class AdminVideoCategoryViewSet(ReadOnlyModelViewSet):
serializer_class = AdminVideoCategorySerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
def get_queryset(self):
return VideoCategory.objects.all().order_by("title")
class AdminVideoCollectionViewSet(ReadOnlyModelViewSet):
serializer_class = AdminVideoCollectionSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = None
def get_queryset(self):
return VideoCollection.objects.all().order_by("display_position", "order", "title")
class AdminVideoViewSet(ModelViewSet): class AdminVideoViewSet(ModelViewSet):
@ -43,3 +75,113 @@ class AdminVideoViewSet(ModelViewSet):
queryset = queryset.filter(video_type=video_type) queryset = queryset.filter(video_type=video_type)
return queryset.order_by("-created_at") return queryset.order_by("-created_at")
class AdminVideoPlaylistViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = StandardResultsSetPagination
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_serializer_class(self):
if self.action == "list":
return AdminVideoPlaylistListSerializer
return AdminVideoPlaylistDetailSerializer
def get_queryset(self):
queryset = VideoPlaylist.objects.all().prefetch_related("categories", "collections", "playlist_items__video")
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query)
| Q(slug__icontains=search_query)
| Q(slogan__icontains=search_query)
| Q(description__icontains=search_query)
)
status_filter = self.request.query_params.get("status")
if status_filter in {"true", "false"}:
queryset = queryset.filter(status=status_filter == "true")
category_id = self.request.query_params.get("category")
if category_id and category_id != "all":
queryset = queryset.filter(categories__id=category_id)
collection_id = self.request.query_params.get("collection")
if collection_id and collection_id != "all":
queryset = queryset.filter(collections__id=collection_id)
return queryset.distinct().order_by("order", "-created_at")
@action(detail=True, methods=["get", "post"], url_path="items")
def items(self, request, pk=None):
playlist = self.get_object()
if request.method.lower() == "get":
items = playlist.playlist_items.select_related("video").order_by("priority", "id")
serializer = AdminVideoPlaylistItemSerializer(items, many=True, context={"request": request})
return Response(serializer.data)
serializer = AdminVideoPlaylistItemSerializer(
data={
**request.data,
"playlist": playlist.id,
"priority": request.data.get("priority", playlist.playlist_items.count() + 1),
},
context={"request": request},
)
serializer.is_valid(raise_exception=True)
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return Response(serializer.data, status=status.HTTP_201_CREATED)
@action(detail=True, methods=["patch", "delete"], url_path=r"items/(?P<item_id>\d+)")
def item_detail(self, request, pk=None, item_id=None):
playlist = self.get_object()
item = playlist.playlist_items.select_related("video").get(pk=item_id)
if request.method.lower() == "delete":
item.delete()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return Response(status=status.HTTP_204_NO_CONTENT)
serializer = AdminVideoPlaylistItemSerializer(
item,
data={**request.data, "playlist": playlist.id},
partial=True,
context={"request": request},
)
serializer.is_valid(raise_exception=True)
serializer.save()
playlist.total_time = playlist.calculate_total_time()
playlist.save(update_fields=["total_time"])
return Response(serializer.data)
@action(detail=True, methods=["post"], url_path="items/reorder")
def reorder_items(self, request, pk=None):
playlist = self.get_object()
item_ids = request.data.get("item_ids", [])
if not isinstance(item_ids, list):
return Response({"detail": "item_ids must be a list."}, status=status.HTTP_400_BAD_REQUEST)
items = list(playlist.playlist_items.filter(id__in=item_ids))
if len(items) != len(item_ids):
return Response({"detail": "Some items were not found."}, status=status.HTTP_400_BAD_REQUEST)
item_map = {item.id: item for item in items}
with transaction.atomic():
for index, item_id in enumerate(item_ids, start=1):
item = item_map[item_id]
if item.priority != index:
item.priority = index
item.save(update_fields=["priority"])
serializer = AdminVideoPlaylistItemSerializer(
playlist.playlist_items.select_related("video").order_by("priority", "id"),
many=True,
context={"request": request},
)
return Response(serializer.data)
Loading…
Cancel
Save