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.
283 lines
10 KiB
283 lines
10 KiB
from django.db.models import Q
|
|
from rest_framework.viewsets import ModelViewSet
|
|
from rest_framework.generics import ListAPIView
|
|
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 django.shortcuts import get_object_or_404
|
|
from utils.pagination import StandardResultsSetPagination
|
|
|
|
from apps.quiz.models import Quiz, Question, QuizParticipant
|
|
from apps.course.models import Course, CourseLesson, Participant
|
|
from apps.course.models.course import extract_text_from_json
|
|
from apps.account.models import User
|
|
from apps.account.permissions import IsPanelUser
|
|
|
|
from apps.quiz.serializers.admin import (
|
|
AdminCourseSerializer,
|
|
AdminCourseLessonSerializer,
|
|
AdminQuizListSerializer,
|
|
AdminQuizDetailSerializer,
|
|
AdminQuestionSerializer,
|
|
AdminQuizParticipantSerializer,
|
|
AdminQuizAbsenteeSerializer
|
|
)
|
|
|
|
|
|
def is_professor(request):
|
|
return getattr(request.user, 'user_type', None) == 'professor'
|
|
|
|
|
|
def find_matching_course_ids(search_term: str):
|
|
normalized = (search_term or "").strip().lower()
|
|
if not normalized:
|
|
return []
|
|
|
|
matching_ids = list(
|
|
Course.objects.filter(
|
|
Q(title__icontains=search_term) | Q(slug__icontains=search_term)
|
|
).values_list("id", flat=True)
|
|
)
|
|
|
|
if matching_ids:
|
|
return matching_ids
|
|
|
|
for course in Course.objects.all().only("id", "title", "slug"):
|
|
title_text = extract_text_from_json(course.title).lower()
|
|
slug_text = extract_text_from_json(course.slug).lower()
|
|
if normalized in title_text or normalized in slug_text:
|
|
matching_ids.append(course.id)
|
|
|
|
return matching_ids
|
|
|
|
|
|
def find_matching_lesson_ids(search_term: str):
|
|
normalized = (search_term or "").strip().lower()
|
|
if not normalized:
|
|
return []
|
|
|
|
matching_ids = []
|
|
for lesson in CourseLesson.objects.select_related("lesson").all():
|
|
lesson_title = extract_text_from_json(
|
|
lesson.title or (lesson.lesson.title if lesson.lesson else [])
|
|
).lower()
|
|
if normalized in lesson_title:
|
|
matching_ids.append(lesson.id)
|
|
|
|
return matching_ids
|
|
|
|
|
|
def find_matching_quiz_ids(search_term: str):
|
|
normalized = (search_term or "").strip().lower()
|
|
if not normalized:
|
|
return []
|
|
|
|
matching_ids = []
|
|
for quiz in Quiz.objects.all().only("id", "title", "description"):
|
|
title_text = extract_text_from_json(quiz.title).lower()
|
|
description_text = extract_text_from_json(quiz.description).lower()
|
|
if normalized in title_text or normalized in description_text:
|
|
matching_ids.append(quiz.id)
|
|
|
|
return matching_ids
|
|
|
|
|
|
class AdminQuizViewSet(ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Quiz management.
|
|
Professors can only access quizzes of their own courses.
|
|
"""
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = StandardResultsSetPagination
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == 'list':
|
|
return AdminQuizListSerializer
|
|
return AdminQuizDetailSerializer
|
|
|
|
def get_queryset(self):
|
|
queryset = Quiz.objects.all().select_related('course', 'lesson')
|
|
|
|
# Professors can only see quizzes of their own courses
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(course__professor_id=self.request.user.id)
|
|
|
|
# Handle Search
|
|
search_query = self.request.query_params.get('search', None)
|
|
has_search = False
|
|
if search_query:
|
|
has_search = True
|
|
matching_course_ids = find_matching_course_ids(search_query)
|
|
matching_lesson_ids = find_matching_lesson_ids(search_query)
|
|
matching_quiz_ids = find_matching_quiz_ids(search_query)
|
|
from django.db.models import Case, When, Value, IntegerField
|
|
queryset = queryset.filter(
|
|
Q(id__in=matching_quiz_ids) |
|
|
Q(course_id__in=matching_course_ids) |
|
|
Q(lesson_id__in=matching_lesson_ids)
|
|
).annotate(
|
|
search_relevance=Case(
|
|
When(id__in=matching_quiz_ids, then=Value(2)),
|
|
default=Value(1),
|
|
output_field=IntegerField()
|
|
)
|
|
)
|
|
|
|
# Handle Course Filter
|
|
course_id = self.request.query_params.get('course', None)
|
|
if course_id:
|
|
queryset = queryset.filter(course_id=course_id)
|
|
|
|
# Handle Lesson Filter
|
|
lesson_id = self.request.query_params.get('lesson', None)
|
|
if lesson_id:
|
|
queryset = queryset.filter(lesson_id=lesson_id)
|
|
|
|
# Handle Status Filter
|
|
status_param = self.request.query_params.get('status', None)
|
|
if status_param is not None:
|
|
if status_param.lower() == 'true':
|
|
queryset = queryset.filter(status=True)
|
|
elif status_param.lower() == 'false':
|
|
queryset = queryset.filter(status=False)
|
|
|
|
if has_search:
|
|
return queryset.order_by('-search_relevance', '-id')
|
|
return queryset.order_by('-id')
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def participants(self, request, pk=None):
|
|
"""
|
|
Returns two lists:
|
|
1. Participants: users who have participated in the quiz.
|
|
2. Absentees: users who are enrolled in the course but have not participated.
|
|
"""
|
|
quiz = self.get_object()
|
|
|
|
# 1. Fetch participants
|
|
participants_qs = QuizParticipant.objects.filter(quiz=quiz).select_related('user').prefetch_related('answers')
|
|
participants_serializer = AdminQuizParticipantSerializer(participants_qs, many=True)
|
|
|
|
# 2. Fetch absentees
|
|
absentees_data = []
|
|
if quiz.course:
|
|
# Enrolled course student IDs
|
|
enrolled_student_ids = Participant.objects.filter(
|
|
course=quiz.course,
|
|
is_active=True
|
|
).values_list('student_id', flat=True)
|
|
|
|
# Participated user IDs
|
|
participated_user_ids = participants_qs.values_list('user_id', flat=True)
|
|
|
|
# Absentee IDs
|
|
absentee_ids = set(enrolled_student_ids) - set(participated_user_ids)
|
|
|
|
# Fetch User details for absentees
|
|
absentees_qs = User.objects.filter(id__in=absentee_ids)
|
|
absentees_serializer = AdminQuizAbsenteeSerializer(absentees_qs, many=True)
|
|
absentees_data = absentees_serializer.data
|
|
|
|
return Response({
|
|
'participants': participants_serializer.data,
|
|
'absentees': absentees_data
|
|
})
|
|
|
|
|
|
class AdminQuestionViewSet(ModelViewSet):
|
|
"""
|
|
Admin CRUD ViewSet for Question management.
|
|
Professors can only access questions of quizzes belonging to their own courses.
|
|
"""
|
|
serializer_class = AdminQuestionSerializer
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = None
|
|
|
|
def get_queryset(self):
|
|
queryset = Question.objects.all()
|
|
|
|
# Professors can only see questions of their own courses' quizzes
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(quiz__course__professor_id=self.request.user.id)
|
|
|
|
quiz_id = self.request.query_params.get('quiz_id', None)
|
|
if not quiz_id:
|
|
# Fallback parameter name
|
|
quiz_id = self.request.query_params.get('quiz', None)
|
|
|
|
if quiz_id:
|
|
queryset = queryset.filter(quiz_id=quiz_id)
|
|
|
|
return queryset.order_by('priority', 'id')
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def reorder(self, request):
|
|
"""
|
|
Reorders questions in bulk.
|
|
Accepts a list of question IDs.
|
|
Format:
|
|
{
|
|
"question_ids": [10, 12, 11, 15]
|
|
}
|
|
"""
|
|
question_ids = request.data.get('question_ids', [])
|
|
if not question_ids:
|
|
return Response({'error': 'question_ids list is required'}, status=400)
|
|
|
|
# Update priority for each question based on its position in the list
|
|
from django.db import transaction
|
|
questions = Question.objects.filter(id__in=question_ids)
|
|
question_map = {q.id: q for q in questions}
|
|
|
|
with transaction.atomic():
|
|
for idx, q_id in enumerate(question_ids):
|
|
if q_id in question_map:
|
|
question = question_map[q_id]
|
|
question.priority = idx + 1
|
|
question.save(update_fields=['priority'])
|
|
|
|
return Response({'status': 'success'})
|
|
|
|
|
|
class AdminCourseListView(ListAPIView):
|
|
"""
|
|
Dropdown listing helper for all courses.
|
|
Professors only see their own courses.
|
|
"""
|
|
serializer_class = AdminCourseSerializer
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = None # Return all courses for dropdown usage
|
|
|
|
def get_queryset(self):
|
|
queryset = Course.objects.all().order_by('-id')
|
|
if is_professor(self.request):
|
|
queryset = queryset.filter(professor_id=self.request.user.id)
|
|
return queryset
|
|
|
|
|
|
class AdminLessonListView(ListAPIView):
|
|
"""
|
|
Dropdown listing helper for lessons. Filtered by course_id.
|
|
Professors only see lessons of their own courses.
|
|
"""
|
|
serializer_class = AdminCourseLessonSerializer
|
|
permission_classes = [IsAuthenticated, IsPanelUser]
|
|
authentication_classes = [TokenAuthentication]
|
|
pagination_class = None # Return all matching lessons for dropdown usage
|
|
|
|
def get_queryset(self):
|
|
course_id = self.request.query_params.get('course_id', None)
|
|
if course_id:
|
|
qs = CourseLesson.objects.filter(course_id=course_id).select_related('lesson').order_by('priority')
|
|
else:
|
|
qs = CourseLesson.objects.all().select_related('lesson').order_by('-id')
|
|
|
|
# Professors only see lessons of their own courses
|
|
if is_professor(self.request):
|
|
qs = qs.filter(course__professor_id=self.request.user.id)
|
|
|
|
return qs
|