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.
827 lines
32 KiB
827 lines
32 KiB
import logging
|
|
import re
|
|
from copy import deepcopy
|
|
|
|
from django.db import transaction
|
|
from django.db.models import Count, Q
|
|
from django.utils import timezone
|
|
from rest_framework import serializers, viewsets
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from rest_framework.authentication import TokenAuthentication
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from rest_framework.exceptions import PermissionDenied
|
|
from utils.pagination import StandardResultsSetPagination
|
|
from apps.account.permissions import IsPanelUser, IsSuperAdminOrReadOnlyForProfessor
|
|
from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError
|
|
from apps.course.services.web_egress import stop_session_web_egress_if_active
|
|
from apps.course.models import (
|
|
Course,
|
|
CourseCategory,
|
|
CourseChapter,
|
|
CourseLesson,
|
|
Lesson,
|
|
CourseAttachment,
|
|
Attachment,
|
|
CourseGlossary,
|
|
Glossary,
|
|
CourseLiveSession,
|
|
LiveSessionUser,
|
|
LiveSessionRecording
|
|
)
|
|
from apps.course.models.course import extract_text_from_json
|
|
from apps.quiz.models import Quiz, Question
|
|
from apps.course.serializers.admin import (
|
|
AdminCourseCategorySerializer,
|
|
AdminCourseListSerializer,
|
|
AdminCourseDetailSerializer,
|
|
AdminCourseChapterSerializer,
|
|
AdminCourseLessonSerializer,
|
|
AdminCourseAttachmentSerializer,
|
|
AdminCourseGlossarySerializer,
|
|
AdminLessonSerializer,
|
|
AdminGlossarySerializer,
|
|
AdminAttachmentSerializer,
|
|
AdminLiveSessionListSerializer,
|
|
AdminLiveSessionDetailSerializer
|
|
)
|
|
from utils import FileFieldSerializer
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def is_professor(request):
|
|
return getattr(request.user, 'user_type', None) == 'professor'
|
|
|
|
|
|
class AdminCourseViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Course management.
|
|
Professors can only access/modify their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == 'list':
|
|
return AdminCourseListSerializer
|
|
return AdminCourseDetailSerializer
|
|
|
|
def get_queryset(self):
|
|
queryset = Course.objects.all().select_related('category').prefetch_related('professors')
|
|
|
|
# Professors can only see their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(professors=self.request.user)
|
|
|
|
# Search Query
|
|
search_query = self.request.query_params.get('search', None)
|
|
has_search = False
|
|
if search_query:
|
|
has_search = True
|
|
from django.db.models import Case, When, Value, IntegerField
|
|
queryset = queryset.filter(
|
|
Q(title__icontains=search_query) |
|
|
Q(description__icontains=search_query) |
|
|
Q(short_description__icontains=search_query)
|
|
).annotate(
|
|
search_relevance=Case(
|
|
When(title__icontains=search_query, then=Value(2)),
|
|
default=Value(1),
|
|
output_field=IntegerField()
|
|
)
|
|
)
|
|
|
|
# Filters
|
|
category_id = self.request.query_params.get('category', None)
|
|
if category_id:
|
|
queryset = queryset.filter(category_id=category_id)
|
|
|
|
professor_ids = self.request.query_params.getlist('professor')
|
|
if professor_ids:
|
|
queryset = queryset.filter(professors__in=professor_ids).distinct()
|
|
|
|
level = self.request.query_params.get('level', None)
|
|
if level:
|
|
queryset = queryset.filter(level__contains=[{"title": level}])
|
|
|
|
status_param = self.request.query_params.get('status', None)
|
|
if status_param:
|
|
queryset = queryset.filter(status__contains=[{"title": status_param}])
|
|
|
|
is_free_param = self.request.query_params.get('is_free', None)
|
|
if is_free_param is not None:
|
|
if is_free_param.lower() == 'true':
|
|
queryset = queryset.filter(is_free=True)
|
|
elif is_free_param.lower() == 'false':
|
|
queryset = queryset.filter(is_free=False)
|
|
|
|
student_id = self.request.query_params.get('student_id', None)
|
|
if student_id:
|
|
queryset = queryset.filter(participants__student_id=student_id)
|
|
|
|
if has_search:
|
|
return queryset.order_by('-search_relevance', '-id')
|
|
return queryset.order_by('-id')
|
|
|
|
def perform_create(self, serializer):
|
|
# If the logged-in user is a professor, force the professor field to themselves
|
|
if is_professor(self.request):
|
|
serializer.save(professor=self.request.user)
|
|
else:
|
|
serializer.save()
|
|
|
|
def perform_update(self, serializer):
|
|
# Prevent professors from reassigning a course to another professor
|
|
if is_professor(self.request):
|
|
instance = self.get_object()
|
|
if not instance.professors.filter(id=self.request.user.id).exists():
|
|
raise PermissionDenied("You can only edit your own courses.")
|
|
serializer.save(professor=self.request.user)
|
|
else:
|
|
serializer.save()
|
|
|
|
def _build_cloned_title(self, source_course, requested_title=None):
|
|
custom_title = str(requested_title or "").strip()
|
|
if custom_title:
|
|
return [{"language_code": "en", "title": custom_title}]
|
|
|
|
source_titles = deepcopy(source_course.title or [])
|
|
source_title_text = extract_text_from_json(source_titles).strip() or f"Course {source_course.pk}"
|
|
base_title = re.sub(r"\s*\(copied(?:#\d+)?\)$", "", source_title_text).strip() or source_title_text
|
|
suffix = "(copied)"
|
|
|
|
if isinstance(source_titles, list) and source_titles:
|
|
localized_titles = []
|
|
for item in source_titles:
|
|
if isinstance(item, dict):
|
|
localized_item = deepcopy(item)
|
|
for key in ("title", "text", "value", "name"):
|
|
if key in localized_item and localized_item.get(key):
|
|
cleaned_value = re.sub(r"\s*\(copied(?:#\d+)?\)$", "", str(localized_item[key])).strip()
|
|
localized_item[key] = f"{cleaned_value}{suffix}"
|
|
break
|
|
else:
|
|
localized_item["title"] = f"{base_title}{suffix}"
|
|
localized_titles.append(localized_item)
|
|
elif item:
|
|
cleaned_item = re.sub(r"\s*\(copied(?:#\d+)?\)$", "", str(item)).strip()
|
|
localized_titles.append(f"{cleaned_item}{suffix}")
|
|
|
|
if localized_titles:
|
|
return localized_titles
|
|
|
|
return [{"language_code": "en", "title": f"{base_title}{suffix}"}]
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def clone(self, request, pk=None):
|
|
source_course = self.get_object()
|
|
requested_title = request.data.get('title')
|
|
|
|
with transaction.atomic():
|
|
cloned_course = Course.objects.create(
|
|
title=self._build_cloned_title(source_course, requested_title),
|
|
category=source_course.category,
|
|
thumbnail=source_course.thumbnail.name if source_course.thumbnail else None,
|
|
video_type=source_course.video_type,
|
|
video_file=source_course.video_file.name if source_course.video_file else None,
|
|
video_link=source_course.video_link,
|
|
is_online=source_course.is_online,
|
|
online_link=source_course.online_link,
|
|
level=deepcopy(source_course.level),
|
|
duration=source_course.duration,
|
|
lessons_count=source_course.lessons_count,
|
|
description=deepcopy(source_course.description),
|
|
short_description=deepcopy(source_course.short_description),
|
|
status=deepcopy(source_course.status),
|
|
is_free=source_course.is_free,
|
|
price=source_course.price,
|
|
price_rub=source_course.price_rub,
|
|
discount_percentage=source_course.discount_percentage,
|
|
is_group_chat_locked=source_course.is_group_chat_locked,
|
|
is_professor_chat_locked=source_course.is_professor_chat_locked,
|
|
timing=deepcopy(source_course.timing),
|
|
features=deepcopy(source_course.features),
|
|
)
|
|
cloned_course.professors.set(source_course.professors.all())
|
|
|
|
chapter_map = {}
|
|
source_chapters = source_course.chapters.all().order_by('priority', 'id')
|
|
for chapter in source_chapters:
|
|
cloned_chapter = CourseChapter.objects.create(
|
|
course=cloned_course,
|
|
title=deepcopy(chapter.title),
|
|
priority=chapter.priority,
|
|
is_active=chapter.is_active,
|
|
)
|
|
chapter_map[chapter.id] = cloned_chapter
|
|
|
|
lesson_map = {}
|
|
source_lessons = source_course.lessons.select_related('lesson', 'chapter').all().order_by('priority', 'id')
|
|
for course_lesson in source_lessons:
|
|
base_lesson = course_lesson.lesson
|
|
cloned_lesson = Lesson.objects.create(
|
|
title=deepcopy(base_lesson.title),
|
|
content_type=deepcopy(base_lesson.content_type),
|
|
content_file=base_lesson.content_file.name if base_lesson and base_lesson.content_file else None,
|
|
video_link=base_lesson.video_link if base_lesson else None,
|
|
duration=base_lesson.duration if base_lesson else 0,
|
|
)
|
|
cloned_course_lesson = CourseLesson.objects.create(
|
|
course=cloned_course,
|
|
chapter=chapter_map.get(course_lesson.chapter_id),
|
|
lesson=cloned_lesson,
|
|
title=deepcopy(course_lesson.title),
|
|
priority=course_lesson.priority,
|
|
is_active=course_lesson.is_active,
|
|
)
|
|
lesson_map[course_lesson.id] = cloned_course_lesson
|
|
|
|
source_attachments = source_course.attachments.select_related('attachment').all()
|
|
for course_attachment in source_attachments:
|
|
base_attachment = course_attachment.attachment
|
|
cloned_attachment = Attachment.objects.create(
|
|
title=deepcopy(base_attachment.title),
|
|
file=base_attachment.file.name if base_attachment and base_attachment.file else None,
|
|
file_size=base_attachment.file_size if base_attachment else None,
|
|
)
|
|
CourseAttachment.objects.create(
|
|
course=cloned_course,
|
|
attachment=cloned_attachment,
|
|
)
|
|
|
|
source_glossaries = source_course.glossaries.select_related('glossary').all()
|
|
for course_glossary in source_glossaries:
|
|
base_glossary = course_glossary.glossary
|
|
cloned_glossary = Glossary.objects.create(
|
|
title=base_glossary.title,
|
|
description=deepcopy(base_glossary.description),
|
|
)
|
|
CourseGlossary.objects.create(
|
|
course=cloned_course,
|
|
glossary=cloned_glossary,
|
|
)
|
|
|
|
source_quizzes = source_course.quizzes.prefetch_related('questions').all()
|
|
for quiz in source_quizzes:
|
|
cloned_quiz = Quiz.objects.create(
|
|
lesson=lesson_map.get(quiz.lesson_id),
|
|
course=cloned_course,
|
|
lesson_number=quiz.lesson_number,
|
|
title=deepcopy(quiz.title),
|
|
description=deepcopy(quiz.description),
|
|
each_question_timing=quiz.each_question_timing,
|
|
status=quiz.status,
|
|
)
|
|
for question in quiz.questions.all():
|
|
Question.objects.create(
|
|
quiz=cloned_quiz,
|
|
question=deepcopy(question.question),
|
|
option1=deepcopy(question.option1),
|
|
option2=deepcopy(question.option2),
|
|
option3=deepcopy(question.option3),
|
|
option4=deepcopy(question.option4),
|
|
correct_answer=question.correct_answer,
|
|
priority=question.priority,
|
|
)
|
|
|
|
cloned_course.recalculate_lessons_count()
|
|
cloned_course.refresh_from_db()
|
|
|
|
serializer = self.get_serializer(cloned_course)
|
|
return Response(serializer.data, status=201)
|
|
|
|
|
|
class AdminChapterViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Course Chapters.
|
|
Professors can only access chapters of their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminCourseChapterSerializer
|
|
pagination_class = None
|
|
|
|
def get_queryset(self):
|
|
queryset = CourseChapter.objects.all()
|
|
|
|
# Professors can only see chapters of their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(course__professors=self.request.user)
|
|
|
|
course_id = self.request.query_params.get('course', None)
|
|
if course_id:
|
|
queryset = queryset.filter(course_id=course_id)
|
|
return queryset.order_by('priority', 'id')
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def reorder(self, request):
|
|
"""
|
|
Reorders chapters in bulk.
|
|
Format: {"chapter_ids": [id1, id2, ...]}
|
|
"""
|
|
chapter_ids = request.data.get('chapter_ids', [])
|
|
if not chapter_ids:
|
|
return Response({'error': 'chapter_ids list is required'}, status=400)
|
|
|
|
from django.db import transaction
|
|
with transaction.atomic():
|
|
for idx, c_id in enumerate(chapter_ids):
|
|
CourseChapter.objects.filter(id=c_id).update(priority=idx + 1)
|
|
|
|
return Response({'status': 'success'})
|
|
|
|
|
|
class AdminCourseLessonViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Lessons associated with a Course/Chapter.
|
|
Professors can only access lessons in their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminCourseLessonSerializer
|
|
pagination_class = None
|
|
|
|
def get_queryset(self):
|
|
queryset = CourseLesson.objects.all().select_related('lesson', 'chapter')
|
|
|
|
# Professors can only see lessons of their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(course__professors=self.request.user)
|
|
|
|
course_id = self.request.query_params.get('course', None)
|
|
if course_id:
|
|
queryset = queryset.filter(course_id=course_id)
|
|
|
|
chapter_id = self.request.query_params.get('chapter', None)
|
|
if chapter_id:
|
|
queryset = queryset.filter(chapter_id=chapter_id)
|
|
|
|
return queryset.order_by('priority', 'id')
|
|
|
|
def perform_destroy(self, instance):
|
|
# Prevent orphans: delete the base Lesson object along with the junction model
|
|
lesson = instance.lesson
|
|
instance.delete()
|
|
if lesson:
|
|
lesson.delete()
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def reorder(self, request):
|
|
"""
|
|
Reorders lessons in bulk.
|
|
Format: {"lesson_ids": [id1, id2, ...]}
|
|
"""
|
|
lesson_ids = request.data.get('lesson_ids', [])
|
|
if not lesson_ids:
|
|
return Response({'error': 'lesson_ids list is required'}, status=400)
|
|
|
|
from django.db import transaction
|
|
with transaction.atomic():
|
|
for idx, l_id in enumerate(lesson_ids):
|
|
CourseLesson.objects.filter(id=l_id).update(priority=idx + 1)
|
|
|
|
return Response({'status': 'success'})
|
|
|
|
|
|
class AdminCourseAttachmentViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Course Attachments.
|
|
Professors can only access attachments of their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminCourseAttachmentSerializer
|
|
pagination_class = None
|
|
|
|
def get_queryset(self):
|
|
queryset = CourseAttachment.objects.all().select_related('attachment')
|
|
|
|
# Professors can only see attachments of their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(course__professors=self.request.user)
|
|
|
|
course_id = self.request.query_params.get('course', None)
|
|
if course_id:
|
|
queryset = queryset.filter(course_id=course_id)
|
|
return queryset.order_by('-id')
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
attachment_ids = request.data.get('attachment_ids')
|
|
new_attachments = request.data.get('new_attachments')
|
|
course_id = request.data.get('course')
|
|
|
|
if not course_id or (not attachment_ids and not new_attachments):
|
|
return super().create(request, *args, **kwargs)
|
|
|
|
if is_professor(request):
|
|
course = Course.objects.filter(id=course_id, professors=request.user).first()
|
|
else:
|
|
course = Course.objects.filter(id=course_id).first()
|
|
|
|
if not course:
|
|
raise PermissionDenied("Course not found or access denied.")
|
|
|
|
created_links = []
|
|
seen_attachment_ids = set()
|
|
file_field = FileFieldSerializer()
|
|
file_field.bind('file', None)
|
|
|
|
with transaction.atomic():
|
|
for attachment_id in attachment_ids or []:
|
|
try:
|
|
parsed_id = int(attachment_id)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
|
|
if parsed_id in seen_attachment_ids:
|
|
continue
|
|
seen_attachment_ids.add(parsed_id)
|
|
|
|
attachment = Attachment.objects.filter(pk=parsed_id).first()
|
|
if not attachment:
|
|
continue
|
|
|
|
course_attachment, created = CourseAttachment.objects.get_or_create(
|
|
course=course,
|
|
attachment=attachment,
|
|
)
|
|
if created:
|
|
created_links.append(course_attachment)
|
|
|
|
for item in new_attachments or []:
|
|
title = item.get('title')
|
|
file_data = item.get('file')
|
|
if not title or not file_data:
|
|
continue
|
|
|
|
try:
|
|
normalized_file = file_field.to_internal_value(file_data)
|
|
except serializers.ValidationError as exc:
|
|
raise serializers.ValidationError({
|
|
'new_attachments': [exc.detail]
|
|
})
|
|
|
|
attachment = Attachment.objects.create(
|
|
title=title,
|
|
file=normalized_file,
|
|
)
|
|
created_links.append(
|
|
CourseAttachment.objects.create(
|
|
course=course,
|
|
attachment=attachment,
|
|
)
|
|
)
|
|
|
|
serializer = self.get_serializer(created_links, many=True)
|
|
return Response(serializer.data, status=201)
|
|
|
|
def perform_destroy(self, instance):
|
|
# Prevent orphans: delete base Attachment along with junction model
|
|
attachment = instance.attachment
|
|
instance.delete()
|
|
if attachment:
|
|
attachment.delete()
|
|
|
|
|
|
class AdminCourseGlossaryViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Course Glossary terms.
|
|
Professors can only access glossaries of their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminCourseGlossarySerializer
|
|
pagination_class = None
|
|
|
|
def get_queryset(self):
|
|
queryset = CourseGlossary.objects.all().select_related('glossary')
|
|
|
|
# Professors can only see glossaries of their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(course__professors=self.request.user)
|
|
|
|
course_id = self.request.query_params.get('course', None)
|
|
if course_id:
|
|
queryset = queryset.filter(course_id=course_id)
|
|
return queryset.order_by('-id')
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
glossary_ids = request.data.get('glossary_ids')
|
|
new_glossaries = request.data.get('new_glossaries')
|
|
course_id = request.data.get('course')
|
|
|
|
if not course_id or (not glossary_ids and not new_glossaries):
|
|
return super().create(request, *args, **kwargs)
|
|
|
|
if is_professor(request):
|
|
course = Course.objects.filter(id=course_id, professors=request.user).first()
|
|
else:
|
|
course = Course.objects.filter(id=course_id).first()
|
|
|
|
if not course:
|
|
raise PermissionDenied("Course not found or access denied.")
|
|
|
|
created_links = []
|
|
seen_glossary_ids = set()
|
|
|
|
with transaction.atomic():
|
|
for glossary_id in glossary_ids or []:
|
|
try:
|
|
parsed_id = int(glossary_id)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
|
|
if parsed_id in seen_glossary_ids:
|
|
continue
|
|
seen_glossary_ids.add(parsed_id)
|
|
|
|
glossary = Glossary.objects.filter(pk=parsed_id).first()
|
|
if not glossary:
|
|
continue
|
|
|
|
course_glossary, created = CourseGlossary.objects.get_or_create(
|
|
course=course,
|
|
glossary=glossary,
|
|
)
|
|
if created:
|
|
created_links.append(course_glossary)
|
|
|
|
for item in new_glossaries or []:
|
|
title = item.get('title')
|
|
description = item.get('description')
|
|
if not title or not description:
|
|
continue
|
|
|
|
glossary = Glossary.objects.create(
|
|
title=title,
|
|
description=description,
|
|
)
|
|
created_links.append(
|
|
CourseGlossary.objects.create(
|
|
course=course,
|
|
glossary=glossary,
|
|
)
|
|
)
|
|
|
|
serializer = self.get_serializer(created_links, many=True)
|
|
return Response(serializer.data, status=201)
|
|
|
|
def perform_destroy(self, instance):
|
|
# Prevent orphans: delete base Glossary along with junction model
|
|
glossary = instance.glossary
|
|
instance.delete()
|
|
if glossary:
|
|
glossary.delete()
|
|
|
|
|
|
class AdminCourseCategoryViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Course Categories.
|
|
Professors have read-only access.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsSuperAdminOrReadOnlyForProfessor]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminCourseCategorySerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
queryset = CourseCategory.objects.all().order_by('-id')
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(Q(name__icontains=search) | Q(slug__icontains=search))
|
|
return queryset
|
|
|
|
|
|
class AdminLessonViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for base Lesson (Repository).
|
|
Professors can only see lessons linked to their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminLessonSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
queryset = Lesson.objects.all().order_by('-id')
|
|
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(title__icontains=search)
|
|
|
|
content_type = self.request.query_params.get('content_type', None)
|
|
if content_type:
|
|
queryset = queryset.filter(content_type__contains=[{"title": content_type}])
|
|
|
|
return queryset
|
|
|
|
|
|
class AdminGlossaryViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for base Glossary (Repository).
|
|
Professors can only see glossaries linked to their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminGlossarySerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
queryset = Glossary.objects.all().order_by('-id')
|
|
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(Q(title__icontains=search) | Q(description__icontains=search))
|
|
return queryset
|
|
|
|
|
|
class AdminAttachmentViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for base Attachment (Repository).
|
|
Professors can only see attachments linked to their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
serializer_class = AdminAttachmentSerializer
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_queryset(self):
|
|
queryset = Attachment.objects.all().order_by('-id')
|
|
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(title__icontains=search)
|
|
|
|
file_type = self.request.query_params.get('type', None)
|
|
if file_type:
|
|
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp']
|
|
video_extensions = ['.mp4', '.webm', '.ogg', '.mkv', '.mov', '.avi']
|
|
audio_extensions = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac']
|
|
|
|
if file_type == 'image':
|
|
q_objs = Q()
|
|
for ext in image_extensions:
|
|
q_objs |= Q(file__iendswith=ext)
|
|
queryset = queryset.filter(q_objs)
|
|
elif file_type == 'video':
|
|
q_objs = Q()
|
|
for ext in video_extensions:
|
|
q_objs |= Q(file__iendswith=ext)
|
|
queryset = queryset.filter(q_objs)
|
|
elif file_type == 'audio':
|
|
q_objs = Q()
|
|
for ext in audio_extensions:
|
|
q_objs |= Q(file__iendswith=ext)
|
|
queryset = queryset.filter(q_objs)
|
|
elif file_type == 'other':
|
|
q_objs = Q()
|
|
for ext in image_extensions + video_extensions + audio_extensions:
|
|
q_objs |= Q(file__iendswith=ext)
|
|
queryset = queryset.exclude(q_objs)
|
|
|
|
return queryset
|
|
|
|
|
|
class AdminLiveSessionViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Course Live Sessions.
|
|
Professors can only access live sessions of their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == 'list':
|
|
return AdminLiveSessionListSerializer
|
|
return AdminLiveSessionDetailSerializer
|
|
|
|
def get_queryset(self):
|
|
queryset = CourseLiveSession.objects.all().select_related('course').annotate(
|
|
media_count=Count('recordings', distinct=True)
|
|
)
|
|
|
|
# Professors can only see live sessions of their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(course__professors=self.request.user)
|
|
|
|
search = self.request.query_params.get('search', None)
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(subject__icontains=search) |
|
|
Q(room_id__icontains=search) |
|
|
Q(course__title__icontains=search)
|
|
)
|
|
|
|
course_id = self.request.query_params.get('course', None)
|
|
if course_id:
|
|
queryset = queryset.filter(course_id=course_id)
|
|
|
|
professor_ids = self.request.query_params.getlist('professor')
|
|
if professor_ids:
|
|
queryset = queryset.filter(course__professors__in=professor_ids).distinct()
|
|
|
|
time_range = self.request.query_params.get('time_range', None)
|
|
if time_range:
|
|
from datetime import timedelta
|
|
now = timezone.now()
|
|
if time_range == '3_days':
|
|
queryset = queryset.filter(started_at__gte=now - timedelta(days=3))
|
|
elif time_range == 'week':
|
|
queryset = queryset.filter(started_at__gte=now - timedelta(days=7))
|
|
elif time_range == 'month':
|
|
queryset = queryset.filter(started_at__gte=now - timedelta(days=30))
|
|
elif time_range == 'year':
|
|
queryset = queryset.filter(started_at__gte=now - timedelta(days=365))
|
|
|
|
started_after = self.request.query_params.get('started_after', None)
|
|
if started_after:
|
|
queryset = queryset.filter(started_at__gte=started_after)
|
|
|
|
started_before = self.request.query_params.get('started_before', None)
|
|
if started_before:
|
|
queryset = queryset.filter(started_at__lte=started_before)
|
|
|
|
ended_after = self.request.query_params.get('ended_after', None)
|
|
if ended_after:
|
|
queryset = queryset.filter(ended_at__gte=ended_after)
|
|
|
|
ended_before = self.request.query_params.get('ended_before', None)
|
|
if ended_before:
|
|
queryset = queryset.filter(ended_at__lte=ended_before)
|
|
|
|
return queryset.order_by('-started_at')
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
queryset = self.filter_queryset(self.get_queryset())
|
|
page = self.paginate_queryset(queryset)
|
|
|
|
if page is not None:
|
|
self._sync_page_live_sessions(page)
|
|
serializer = self.get_serializer(page, many=True)
|
|
return self.get_paginated_response(serializer.data)
|
|
|
|
sessions = list(queryset)
|
|
self._sync_page_live_sessions(sessions)
|
|
serializer = self.get_serializer(sessions, many=True)
|
|
return Response(serializer.data)
|
|
|
|
def _sync_page_live_sessions(self, sessions):
|
|
for session in sessions:
|
|
if session.ended_at or not session.room_id:
|
|
continue
|
|
|
|
if self._is_room_still_active(session.room_id):
|
|
continue
|
|
|
|
stop_session_web_egress_if_active(session)
|
|
now = timezone.now()
|
|
session.ended_at = now
|
|
session.save(update_fields=['ended_at', 'updated_at'])
|
|
|
|
LiveSessionUser.objects.filter(
|
|
session=session,
|
|
is_online=True,
|
|
).update(is_online=False, exited_at=now, updated_at=now)
|
|
|
|
logger.info(
|
|
f"[Admin Live Sessions] Auto-closed stale session - session_id={session.id} room_id={session.room_id}"
|
|
)
|
|
|
|
@staticmethod
|
|
def _is_room_still_active(room_id: str) -> bool:
|
|
try:
|
|
client = PlugNMeetClient()
|
|
response = client.is_room_active(room_id)
|
|
is_active_raw = response.get('isActive', False)
|
|
is_active = (
|
|
is_active_raw
|
|
if isinstance(is_active_raw, bool)
|
|
else str(is_active_raw).lower() == 'true'
|
|
)
|
|
response_msg = response.get('msg', '')
|
|
response_status = response.get('status', False)
|
|
|
|
if (
|
|
response_status
|
|
and 'active' in response_msg.lower()
|
|
and 'not' not in response_msg.lower()
|
|
):
|
|
return True
|
|
|
|
return is_active
|
|
except PlugNMeetError as e:
|
|
error_msg = str(e).lower()
|
|
if 'not found' in error_msg or 'does not exist' in error_msg:
|
|
return False
|
|
logger.warning(
|
|
f"[Admin Live Sessions] PlugNMeet API error while checking room {room_id}: {e}"
|
|
)
|
|
return False
|
|
except Exception as e:
|
|
logger.warning(
|
|
f"[Admin Live Sessions] Unexpected error while checking room {room_id}: {type(e).__name__}: {e}"
|
|
)
|
|
return False
|