Browse Source

feat(course): support live session recordings

- add recorded_file FileField to CourseLiveSession with migration
- add serializers for recording upload and metadata
- add PATCH endpoint to create a recording for latest session:
  /<int:course_id>/live-sessions/recorded-file/
- enforce can_manage_course permission and return recording metadata
- update urls and views; use FileFieldSerializer for file handling
master
mortezaei 7 months ago
parent
commit
5d108f64a3
  1. 18
      apps/course/migrations/0010_courselivesession_recorded_file.py
  2. 7
      apps/course/models/live_session.py
  3. 25
      apps/course/serializers/online.py
  4. 1
      apps/course/urls.py
  5. 49
      apps/course/views/live_session.py

18
apps/course/migrations/0010_courselivesession_recorded_file.py

@ -0,0 +1,18 @@
# Generated by Django 3.2.4 on 2025-10-18 12:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0009_auto_20251014_0051'),
]
operations = [
migrations.AddField(
model_name='courselivesession',
name='recorded_file',
field=models.FileField(blank=True, help_text='Recorded file of the live session.', null=True, upload_to='live_session_recordings/', verbose_name='Recorded File'),
),
]

7
apps/course/models/live_session.py

@ -36,6 +36,13 @@ class CourseLiveSession(models.Model):
null=True,
blank=True,
)
recorded_file = models.FileField(
upload_to="live_session_recordings/",
verbose_name=_("Recorded File"),
help_text=_("Recorded file of the live session."),
null=True,
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"))

25
apps/course/serializers/online.py

@ -1,4 +1,5 @@
from rest_framework import serializers
from utils import FileFieldSerializer
class OnlineClassTokenCreateSerializer(serializers.Serializer):
@ -40,3 +41,27 @@ class LiveSessionTokenSerializer(serializers.Serializer):
if not value:
raise serializers.ValidationError("course_slug is required.")
return value
class LiveSessionRecordedFileSerializer(serializers.Serializer):
recorded_file = serializers.FileField(required=True)
def validate_recorded_file(self, value):
if not value:
raise serializers.ValidationError("recorded_file is required.")
return value
class LiveSessionRecordingSerializer(serializers.Serializer):
file = FileFieldSerializer(required=True)
title = serializers.CharField(required=False, max_length=255, allow_blank=True)
recording_type = serializers.ChoiceField(choices=['voice', 'video'], required=False, default='video')
file_time = serializers.DurationField(required=False, allow_null=True)
def validate_file(self, value):
if not value:
raise serializers.ValidationError("file is required.")
return value
def validate_title(self, value):
return value.strip() if value else None

1
apps/course/urls.py

@ -18,6 +18,7 @@ urlpatterns = [
path('online/token/validate/', views.CourseOnlineClassTokenValidateAPIView.as_view(), name='course-online-token-validate'),
path('<slug:slug>/online/room/create/', views.CourseLiveSessionRoomCreateAPIView.as_view(), name='course-live-session-room-create'),
path('online/room/token/', views.CourseLiveSessionTokenAPIView.as_view(), name='course-live-session-token'),
path('<int:course_id>/live-sessions/recorded-file/', views.CourseLiveSessionRecordedFileAPIView.as_view(), name='course-live-session-recorded-file'),
# PlugNMeet webhook endpoint
path('plugnmeet/webhook/', views.PlugNMeetWebhookAPIView.as_view(), name='plugnmeet-webhook'),

49
apps/course/views/live_session.py

@ -9,8 +9,8 @@ from rest_framework.generics import GenericAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from apps.course.models import Course, CourseLiveSession, Participant
from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer
from apps.course.models import Course, CourseLiveSession, Participant, LiveSessionRecording
from apps.course.serializers import LiveSessionRoomCreateSerializer, LiveSessionTokenSerializer, LiveSessionRecordedFileSerializer, LiveSessionRecordingSerializer
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
from utils.exceptions import AppAPIException
@ -274,3 +274,48 @@ class CourseLiveSessionTokenAPIView(GenericAPIView):
if avatar and getattr(avatar, 'url', None):
return request.build_absolute_uri(avatar.url)
return None
class CourseLiveSessionRecordedFileAPIView(GenericAPIView):
permission_classes = [IsAuthenticated]
serializer_class = LiveSessionRecordingSerializer
def patch(self, request, course_id, *args, **kwargs):
logger.info(f"[LiveSession Recorded File] Request from user_id={request.user.id} for course_id={course_id}")
course = get_object_or_404(Course, id=course_id)
if not request.user.can_manage_course(course):
logger.warning(f"[LiveSession Recorded File] Permission denied - user_id={request.user.id} course_id={course_id}")
raise AppAPIException({'message': 'You do not have permission to update this course.'}, status_code=status.HTTP_403_FORBIDDEN)
try:
session = course.live_sessions.latest('-started_at')
except CourseLiveSession.DoesNotExist:
logger.warning(f"[LiveSession Recorded File] No active session found - course_id={course_id} user_id={request.user.id}")
raise AppAPIException({'message': 'No live session found for this course.'}, status_code=status.HTTP_404_NOT_FOUND)
logger.info(f"[LiveSession Recorded File] Latest session found - session_id={session.id} course_id={course_id}")
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
recording = LiveSessionRecording.objects.create(
session=session,
file=serializer.validated_data['file'],
title=serializer.validated_data.get('title') or f"{session.subject} Recording",
recording_type=serializer.validated_data.get('recording_type', 'video'),
file_time=serializer.validated_data.get('file_time'),
)
logger.info(f"[LiveSession Recorded File] Recording created successfully - recording_id={recording.id} session_id={session.id} user_id={request.user.id}")
return Response({
'id': recording.id,
'session_id': session.id,
'title': recording.title,
'file': request.build_absolute_uri(recording.file.url),
'recording_type': recording.recording_type,
'file_time': recording.file_time,
'is_active': recording.is_active,
}, status=status.HTTP_201_CREATED)
Loading…
Cancel
Save