You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
680 lines
25 KiB
680 lines
25 KiB
from django.conf import settings
|
|
import logging
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.db.models import Count, Q, F
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils import timezone
|
|
|
|
from drf_yasg import openapi
|
|
from drf_yasg.utils import swagger_auto_schema
|
|
from rest_framework import status
|
|
from rest_framework.authtoken.models import Token
|
|
from rest_framework.exceptions import NotFound
|
|
from rest_framework.filters import SearchFilter
|
|
from rest_framework.generics import GenericAPIView, ListAPIView, RetrieveAPIView
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.response import Response
|
|
|
|
from utils.pagination import StandardResultsSetPagination
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
from apps.course.serializers import (
|
|
CourseListSerializer, CourseCategorySerializer, CourseDetailSerializer,
|
|
CourseAttachmentSerializer, CourseGlossarySerializer, MyCourseListSerializer,
|
|
OnlineClassTokenCreateSerializer, OnlineClassTokenVerifySerializer
|
|
)
|
|
from apps.course.models import (
|
|
Course,
|
|
CourseAttachment,
|
|
CourseCategory,
|
|
CourseGlossary,
|
|
CourseLiveSession,
|
|
LiveSessionUser,
|
|
Participant,
|
|
)
|
|
from apps.course.doc import *
|
|
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
|
|
from apps.account.serializers import UserProfileSerializer
|
|
from utils.exceptions import AppAPIException
|
|
from utils.redis import OnlineClassTokenManager
|
|
|
|
|
|
UserModel = get_user_model()
|
|
|
|
|
|
class CourseCategoryAPIView(ListAPIView):
|
|
queryset = CourseCategory.objects.all()
|
|
serializer_class = CourseCategorySerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
operation_description=doc_course_category(),
|
|
tags=["Imam-Javad - Course"]
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
|
|
|
|
|
|
from utils.pagination import StandardResultsSetPagination
|
|
|
|
class CourseListAPIView(ListAPIView):
|
|
serializer_class = CourseListSerializer
|
|
filter_backends = [SearchFilter]
|
|
search_fields = ['title', 'category__name', 'professor__fullname']
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
tags=['Imam-Javad - Course'],
|
|
operation_description=doc_course_list(),
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'search', openapi.IN_QUERY,
|
|
description="Search by course title, category name, or professor's full name",
|
|
type=openapi.TYPE_STRING,
|
|
),
|
|
openapi.Parameter(
|
|
'category_slug', openapi.IN_QUERY,
|
|
description="Category of the Course",
|
|
type=openapi.TYPE_STRING,
|
|
# enum=[category.slug for category in CourseCategory.objects.all()]
|
|
),
|
|
openapi.Parameter(
|
|
'status', openapi.IN_QUERY,
|
|
type=openapi.TYPE_STRING,
|
|
description="""Status =>
|
|
Upcoming (visible but registration not allowed)---Предстоящие
|
|
Registering (registration is open)---регистрация
|
|
Ongoing (course has started, registration closed)---Впроцессе
|
|
Finished (course has ended)---закончился
|
|
""",
|
|
enum=[status for status in ['upcoming', 'registering', 'ongoing', 'finished']]
|
|
),
|
|
openapi.Parameter(
|
|
'is_free', openapi.IN_QUERY,
|
|
description="Ценообразование is_free <bool>",
|
|
type=openapi.TYPE_BOOLEAN,
|
|
),
|
|
openapi.Parameter(
|
|
'is_online', openapi.IN_QUERY,
|
|
description="Статус участия is_online <bool>",
|
|
type=openapi.TYPE_BOOLEAN,
|
|
),
|
|
])
|
|
def get(self, request, *args, **kwargs):
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Optimized queryset with select_related for ForeignKey relationships and filtering
|
|
"""
|
|
queryset = Course.objects.select_related(
|
|
'category',
|
|
'professor'
|
|
).exclude(status=Course.StatusChoices.INACTIVE)
|
|
|
|
request = self.request
|
|
filters = request.query_params
|
|
|
|
# Handle category_slug with multiple values separated by commas
|
|
if category_slugs := filters.get('category_slug'):
|
|
category_slugs_list = category_slugs.split(',')
|
|
queryset = queryset.filter(category__slug__in=category_slugs_list)
|
|
|
|
# Handle status with multiple values separated by commas
|
|
if statuses := filters.get('status'):
|
|
statuses_list = statuses.split(',')
|
|
queryset = queryset.filter(status__in=statuses_list)
|
|
|
|
if is_free := filters.get('is_free'):
|
|
is_free = is_free.lower() == 'true'
|
|
queryset = queryset.filter(
|
|
Q(is_free=is_free) | Q(price=0) if is_free else Q(is_free=False, price__gt=0)
|
|
)
|
|
if is_online := filters.get('is_online'):
|
|
is_online = is_online.lower() == 'true'
|
|
queryset = queryset.filter(is_online=is_online)
|
|
|
|
return queryset
|
|
|
|
|
|
|
|
|
|
|
|
class CourseDetailAPIView(RetrieveAPIView):
|
|
serializer_class = CourseDetailSerializer
|
|
lookup_field = "slug"
|
|
|
|
@swagger_auto_schema(
|
|
tags=["Imam-Javad - Course"],
|
|
operation_description="Get detailed information about a specific course",
|
|
responses={
|
|
200: openapi.Response(
|
|
description="Course details",
|
|
schema=CourseDetailSerializer()
|
|
)
|
|
}
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Optimized queryset with select_related and prefetch_related for all relationships
|
|
"""
|
|
return Course.objects.select_related(
|
|
'category',
|
|
'professor'
|
|
).prefetch_related(
|
|
'lessons__lesson',
|
|
'lessons__completions',
|
|
'attachments__attachment',
|
|
'glossaries__glossary',
|
|
'participants__student',
|
|
'room_messages'
|
|
)
|
|
|
|
@swagger_auto_schema(
|
|
operation_description=doc_course_detail(),
|
|
tags=['Imam-Javad - Course'],
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
|
|
class MyCourseListAPIView(ListAPIView):
|
|
serializer_class = MyCourseListSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(manual_parameters=[
|
|
openapi.Parameter(
|
|
'completed', openapi.IN_QUERY,
|
|
description="мои курсы completed <bool> true",
|
|
type=openapi.TYPE_BOOLEAN,
|
|
),
|
|
openapi.Parameter(
|
|
'certificate', openapi.IN_QUERY,
|
|
type=openapi.TYPE_BOOLEAN,
|
|
),
|
|
],
|
|
operation_description=doc_courses_my_courses(),
|
|
operation_summary="Home",
|
|
tags=['Imam-Javad - Course']
|
|
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
print(f'--> my-course-> {request}/ {kwargs}')
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Optimized queryset for user's courses with select_related and prefetch_related
|
|
"""
|
|
queryset = Course.objects.select_related(
|
|
'category',
|
|
'professor'
|
|
).prefetch_related(
|
|
'lessons__lesson',
|
|
'lessons__completions',
|
|
'participants__student'
|
|
).exclude(status=Course.StatusChoices.INACTIVE)
|
|
|
|
request = self.request
|
|
filters = request.query_params
|
|
student = self.request.user
|
|
qs = queryset.filter(participants__student=student)
|
|
completed_only = filters.get('completed', '').lower() == 'true'
|
|
if completed_only == True:
|
|
# نمایش دورههایی که همه درسهایشان توسط کاربر تکمیل شدهاند
|
|
qs = qs.annotate(
|
|
total_lessons=Count('lessons', distinct=True),
|
|
completed_lessons=Count(
|
|
'lessons__completions',
|
|
filter=Q(lessons__completions__student=student),
|
|
distinct=True
|
|
)
|
|
).filter(total_lessons=F('completed_lessons'))
|
|
elif completed_only == False:
|
|
# نمایش دورههایی که همه درسهایشان تکمیل نشدهاند
|
|
qs = qs.annotate(
|
|
total_lessons=Count('lessons', distinct=True),
|
|
completed_lessons=Count(
|
|
'lessons__completions',
|
|
filter=Q(lessons__completions__student=student),
|
|
distinct=True
|
|
)
|
|
).filter(total_lessons__gt=F('completed_lessons'))
|
|
|
|
if 'completed' not in filters:
|
|
certificate = filters.get('certificate', '').lower() == 'true'
|
|
if certificate:
|
|
qs = qs.exclude(
|
|
course_certificates__student=student,
|
|
course_certificates__status__in=['pending', 'approved']
|
|
)
|
|
|
|
return qs
|
|
|
|
|
|
|
|
|
|
class AttachmentListAPIView(ListAPIView):
|
|
serializer_class = CourseAttachmentSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
tags=['Imam-Javad - Course'],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'slug', openapi.IN_PATH,
|
|
description="Slug of the Course",
|
|
type=openapi.TYPE_STRING,
|
|
required=True
|
|
)
|
|
],
|
|
operation_description="Retrieve a list of attachments for a given course by its slug."
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Optimized queryset with select_related for attachment relationship
|
|
"""
|
|
course_slug = self.kwargs.get('slug')
|
|
try:
|
|
course = Course.objects.get(slug=course_slug)
|
|
except Course.DoesNotExist:
|
|
raise NotFound("Course not found")
|
|
return CourseAttachment.objects.select_related(
|
|
'course',
|
|
'attachment'
|
|
).filter(course=course)
|
|
|
|
|
|
|
|
|
|
class GlossaryListAPIView(ListAPIView):
|
|
serializer_class = CourseGlossarySerializer
|
|
filter_backends = [SearchFilter]
|
|
search_fields = ['glossary__title', 'glossary__description']
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
@swagger_auto_schema(
|
|
operation_description="Get glossary terms for a specific course",
|
|
tags=["Imam-Javad - Course"],
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'slug', openapi.IN_PATH,
|
|
description="Course slug",
|
|
type=openapi.TYPE_STRING,
|
|
required=True
|
|
),
|
|
openapi.Parameter(
|
|
'search', openapi.IN_QUERY,
|
|
description="Search in glossary title or description",
|
|
type=openapi.TYPE_STRING,
|
|
required=False
|
|
)
|
|
],
|
|
responses={
|
|
200: openapi.Response(
|
|
description="List of glossary terms",
|
|
schema=CourseGlossarySerializer(many=True)
|
|
)
|
|
}
|
|
)
|
|
def get(self, request, *args, **kwargs):
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
Optimized queryset with select_related for glossary relationship
|
|
"""
|
|
course_slug = self.kwargs.get('slug')
|
|
try:
|
|
course = Course.objects.get(slug=course_slug)
|
|
except Course.DoesNotExist:
|
|
raise NotFound("Course not found")
|
|
|
|
return CourseGlossary.objects.select_related(
|
|
'course',
|
|
'glossary'
|
|
).filter(course=course)
|
|
|
|
|
|
|
|
class CourseOnlineClassTokenAPIView(GenericAPIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = OnlineClassTokenCreateSerializer
|
|
|
|
@swagger_auto_schema(
|
|
tags=['Imam-Javad - Course'],
|
|
operation_description="Generate a temporary entry token for an online class.",
|
|
request_body=OnlineClassTokenCreateSerializer,
|
|
responses={
|
|
status.HTTP_201_CREATED: openapi.Response(
|
|
description="Token generated successfully.",
|
|
examples={
|
|
"application/json": {
|
|
"token": "abc123xyz789...",
|
|
"url": "https://imamjavad.newhorizonco.uk/join-class?token=abc123xyz789...&slug=python-basics",
|
|
"expires_in": 300,
|
|
}
|
|
}
|
|
)
|
|
}
|
|
)
|
|
def post(self, request, pk, *args, **kwargs):
|
|
serializer = self.get_serializer(data=request.data or {})
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
course = get_object_or_404(Course, pk=pk)
|
|
if not course.is_online:
|
|
raise AppAPIException({'message': "Course is not marked as online."}, status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not self._user_has_access(request.user, course):
|
|
raise AppAPIException({'message': "You do not have access to this course."}, status_code=status.HTTP_403_FORBIDDEN)
|
|
|
|
manager = OnlineClassTokenManager()
|
|
user_token, _ = Token.objects.get_or_create(user=request.user)
|
|
identifier = f"{request.user.id}:{user_token.key[:8]}"
|
|
token = manager.generate_token(course_id=course.id, user_identifier=identifier)
|
|
|
|
manager.store_token(token, {
|
|
'course_id': course.id,
|
|
'user_id': request.user.id,
|
|
'user_token': user_token.key,
|
|
'course_slug': course.slug,
|
|
'extra': {
|
|
'professor_in_class': False,
|
|
},
|
|
})
|
|
|
|
# ساخت URL ثابت با token و course slug
|
|
entry_url = f"https://imamjavad.newhorizonco.uk/join-class?token={token}&slug={course.slug}"
|
|
|
|
return Response({
|
|
'token': token,
|
|
'url': entry_url,
|
|
'expires_in': getattr(settings, 'ONLINE_CLASS_TOKEN_TTL', 300),
|
|
}, status=status.HTTP_201_CREATED)
|
|
|
|
@staticmethod
|
|
def _user_has_access(user, course: Course) -> bool:
|
|
if user.is_staff or course.professor_id == user.id:
|
|
return True
|
|
return Participant.objects.filter(course=course, student=user).exists()
|
|
|
|
|
|
class CourseOnlineClassTokenValidateAPIView(GenericAPIView):
|
|
permission_classes = [AllowAny]
|
|
serializer_class = OnlineClassTokenVerifySerializer
|
|
|
|
def get_permissions(self):
|
|
if self.request.method == 'GET':
|
|
return [IsAuthenticated()]
|
|
return [AllowAny()]
|
|
|
|
@swagger_auto_schema(
|
|
tags=['Imam-Javad - Course'],
|
|
operation_description="Get course and user data for authenticated user.",
|
|
manual_parameters=[
|
|
openapi.Parameter(
|
|
'slug', openapi.IN_PATH,
|
|
description="Course Slug",
|
|
type=openapi.TYPE_STRING,
|
|
required=True
|
|
)
|
|
],
|
|
responses={
|
|
status.HTTP_200_OK: openapi.Response(
|
|
description="Course data retrieved.",
|
|
examples={
|
|
"application/json": {
|
|
"course": {"id": 1, "title": "Sample Course"},
|
|
"user": {"id": 10, "fullname": "John Doe"},
|
|
"metadata": {
|
|
"status": "ongoing",
|
|
"has_started": True,
|
|
"professor_in_class": False,
|
|
"validated_at": "2024-01-01T10:00:00Z"
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
)
|
|
def get(self, request, slug, *args, **kwargs):
|
|
detail_view = CourseDetailAPIView()
|
|
queryset = detail_view.get_queryset()
|
|
course = get_object_or_404(queryset, slug=slug)
|
|
user = request.user
|
|
|
|
# DEPRECATED: Polling approach replaced by webhook integration
|
|
# Room status is now updated automatically via PlugNMeet webhooks
|
|
# self._sync_room_status_with_plugnmeet(course)
|
|
|
|
course_data = CourseDetailSerializer(course, context={'request': request}).data
|
|
user_data = UserProfileSerializer(user, context={'request': request}).data
|
|
metadata = self._build_metadata(
|
|
course,
|
|
{'user_id': user.id, 'extra': {}, 'generated_at': timezone.now().isoformat()},
|
|
user=user,
|
|
)
|
|
|
|
return Response({
|
|
'course': course_data,
|
|
'user': user_data,
|
|
'metadata': metadata,
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
@swagger_auto_schema(
|
|
tags=['Imam-Javad - Course'],
|
|
operation_description="Validate an online class entry token and return course/user data.",
|
|
request_body=OnlineClassTokenVerifySerializer,
|
|
responses={
|
|
status.HTTP_200_OK: openapi.Response(
|
|
description="Token validated.",
|
|
examples={
|
|
"application/json": {
|
|
"course": {"id": 1, "title": "Sample Course"},
|
|
"user": {"id": 10, "fullname": "John Doe"},
|
|
"metadata": {
|
|
"status": "ongoing",
|
|
"has_started": True,
|
|
"professor_in_class": False,
|
|
"validated_at": "2024-01-01T10:00:00Z"
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
)
|
|
def post(self, request, *args, **kwargs):
|
|
logger.info(f"[Online Validate] Request received")
|
|
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
|
|
token_value = serializer.validated_data['token']
|
|
manager = OnlineClassTokenManager()
|
|
|
|
try:
|
|
payload = manager.get_payload(token_value)
|
|
logger.info(f"[Online Validate] Token decoded successfully")
|
|
except Exception as e:
|
|
logger.error(f"[Online Validate] Token decode failed - error={str(e)}")
|
|
raise
|
|
|
|
course_id = payload.get('course_id')
|
|
user_id = payload.get('user_id')
|
|
if not course_id or not user_id:
|
|
logger.warning(f"[Online Validate] Invalid token payload - course_id={course_id} user_id={user_id}")
|
|
raise AppAPIException({'message': 'Token payload is invalid.'}, status_code=status.HTTP_400_BAD_REQUEST)
|
|
|
|
logger.info(f"[Online Validate] Processing for user_id={user_id} course_id={course_id}")
|
|
|
|
detail_view = CourseDetailAPIView()
|
|
queryset = detail_view.get_queryset()
|
|
course = get_object_or_404(queryset, pk=course_id)
|
|
user = get_object_or_404(UserModel.objects.all(), pk=user_id)
|
|
|
|
logger.info(f"[Online Validate] Course found - slug={course.slug} is_online={course.is_online}")
|
|
|
|
course_data = CourseDetailSerializer(course, context={'request': request}).data
|
|
user_data = UserProfileSerializer(user, context={'request': request}).data
|
|
metadata = self._build_metadata(course, payload, user=user)
|
|
|
|
logger.info(f"[Online Validate] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}")
|
|
|
|
return Response({
|
|
'course': course_data,
|
|
'user': user_data,
|
|
'metadata': metadata,
|
|
}, status=status.HTTP_200_OK)
|
|
|
|
def _build_metadata(self, course: Course, payload: dict, user=None) -> dict:
|
|
status_value = course.status
|
|
has_started = status_value in [Course.StatusChoices.ONGOING, Course.StatusChoices.FINISHED]
|
|
timing_data = course.timing if isinstance(course.timing, dict) else {}
|
|
|
|
user = user or UserModel.objects.filter(pk=payload.get('user_id')).first()
|
|
user_id = getattr(user, 'id', None)
|
|
can_manage = bool(user and user.can_manage_course(course))
|
|
|
|
live_context = self._build_live_session_context(course)
|
|
can_join_live_session = live_context['is_online'] and self._user_can_join_live_session(user, course)
|
|
|
|
logger.debug(f"[Online Validate Metadata] user_id={user_id} course={course.slug} can_manage={can_manage} is_online={live_context['is_online']} can_join={can_join_live_session}")
|
|
|
|
metadata = {
|
|
'status': status_value,
|
|
'has_started': has_started,
|
|
'has_finished': status_value == Course.StatusChoices.FINISHED,
|
|
'professor_in_class': payload.get('extra', {}).get('professor_in_class', False),
|
|
'can_create_live_session': can_manage and not live_context['is_online'],
|
|
'can_join_live_session': can_join_live_session,
|
|
'scheduled_times': timing_data,
|
|
'generated_at': payload.get('generated_at'),
|
|
'validated_at': timezone.now().isoformat(),
|
|
'redirect_path': payload.get('redirect_path'),
|
|
}
|
|
|
|
metadata.update(live_context)
|
|
return metadata
|
|
|
|
def _build_live_session_context(self, course: Course) -> dict:
|
|
latest_session = (
|
|
CourseLiveSession.objects.filter(course=course)
|
|
.order_by('-started_at', '-id')
|
|
.first()
|
|
)
|
|
|
|
if not latest_session:
|
|
return {
|
|
'is_online': False,
|
|
'live_session': None,
|
|
'active_room_id': None,
|
|
'livesession_started_at': None,
|
|
'livesession_ended_at': None,
|
|
}
|
|
|
|
started_at = latest_session.started_at
|
|
ended_at = latest_session.ended_at
|
|
is_online = bool(started_at and not ended_at)
|
|
|
|
live_session_data = {
|
|
'id': latest_session.id,
|
|
'room_id': latest_session.room_id,
|
|
'subject': latest_session.subject,
|
|
'started_at': self._format_datetime(started_at),
|
|
'ended_at': self._format_datetime(ended_at),
|
|
}
|
|
|
|
return {
|
|
'is_online': is_online,
|
|
'live_session': live_session_data,
|
|
'active_room_id': live_session_data['room_id'] if is_online and live_session_data['room_id'] else None,
|
|
'livesession_started_at': live_session_data['started_at'],
|
|
'livesession_ended_at': live_session_data['ended_at'],
|
|
}
|
|
|
|
@staticmethod
|
|
def _user_can_join_live_session(user, course: Course) -> bool:
|
|
if not user:
|
|
return False
|
|
if user.can_manage_course(course):
|
|
return True
|
|
return Participant.objects.filter(course=course, student_id=user.id, is_active=True).exists()
|
|
|
|
@staticmethod
|
|
def _format_datetime(value):
|
|
if not value:
|
|
return None
|
|
if isinstance(value, str):
|
|
return value
|
|
if timezone.is_naive(value):
|
|
value = timezone.make_aware(value, timezone.get_current_timezone())
|
|
return timezone.localtime(value).isoformat()
|
|
|
|
# DEPRECATED: This polling approach is inefficient and has been replaced by webhook integration
|
|
# def _sync_room_status_with_plugnmeet(self, course: Course):
|
|
# """
|
|
# Check if active live session's room is still active in PlugNMeet.
|
|
# If room is inactive, close the session and all related user entries.
|
|
#
|
|
# DEPRECATED: This should be replaced by webhook integration.
|
|
# PlugNMeet now sends webhooks when rooms end, eliminating the need for polling.
|
|
# """
|
|
# active_session = CourseLiveSession.objects.filter(
|
|
# course=course,
|
|
# ended_at__isnull=True
|
|
# ).first()
|
|
#
|
|
# if not active_session or not active_session.room_id:
|
|
# return
|
|
#
|
|
# try:
|
|
# client = PlugNMeetClient()
|
|
# response = client.is_room_active(active_session.room_id)
|
|
# is_active = response.get('isActive', False)
|
|
#
|
|
# if not is_active:
|
|
# logger.info(f"[Room Sync] Room inactive in PlugNMeet - room_id={active_session.room_id} session_id={active_session.id}")
|
|
# self._close_live_session(active_session)
|
|
# else:
|
|
# logger.debug(f"[Room Sync] Room still active - room_id={active_session.room_id} session_id={active_session.id}")
|
|
#
|
|
# except (PlugNMeetError, Exception) as e:
|
|
# logger.warning(f"[Room Sync] Failed to check room status - room_id={active_session.room_id} error={str(e)}")
|
|
|
|
@staticmethod
|
|
def _close_live_session(session: CourseLiveSession):
|
|
"""
|
|
Close a live session and all related user entries.
|
|
Sets ended_at for session and exited_at/is_online for users.
|
|
"""
|
|
now = timezone.now()
|
|
|
|
session.ended_at = now
|
|
session.save(update_fields=['ended_at', 'updated_at'])
|
|
logger.info(f"[Room Sync] Session closed - session_id={session.id} room_id={session.room_id} ended_at={now}")
|
|
|
|
updated_count = LiveSessionUser.objects.filter(
|
|
session=session,
|
|
is_online=True,
|
|
exited_at__isnull=True
|
|
).update(
|
|
is_online=False,
|
|
exited_at=now,
|
|
updated_at=now
|
|
)
|
|
|
|
if updated_count > 0:
|
|
logger.info(f"[Room Sync] User sessions closed - session_id={session.id} count={updated_count}")
|