diff --git a/apps/quiz/serializers/admin.py b/apps/quiz/serializers/admin.py new file mode 100644 index 0000000..7bf1cb0 --- /dev/null +++ b/apps/quiz/serializers/admin.py @@ -0,0 +1,119 @@ +from rest_framework import serializers +from apps.quiz.models import Quiz, Question, QuizParticipant +from apps.course.models import Course, CourseLesson +from apps.account.models import User + + +class AdminCourseSerializer(serializers.ModelSerializer): + class Meta: + model = Course + fields = ['id', 'title'] + + +class AdminCourseLessonSerializer(serializers.ModelSerializer): + title = serializers.SerializerMethodField() + + class Meta: + model = CourseLesson + fields = ['id', 'title'] + + def get_title(self, obj): + if obj.title: + return obj.title + if obj.lesson: + return obj.lesson.title + return f"Lesson {obj.id}" + + +class AdminQuizListSerializer(serializers.ModelSerializer): + questions_count = serializers.SerializerMethodField() + participants_count = serializers.SerializerMethodField() + course_title = serializers.SerializerMethodField() + lesson_title = serializers.SerializerMethodField() + + class Meta: + model = Quiz + fields = [ + 'id', + 'title', + 'description', + 'each_question_timing', + 'status', + 'course', + 'course_title', + 'lesson', + 'lesson_title', + 'questions_count', + 'participants_count' + ] + + def get_questions_count(self, obj): + return obj.questions.count() + + def get_participants_count(self, obj): + return obj.participants.count() + + def get_course_title(self, obj): + return obj.course.title if obj.course else None + + def get_lesson_title(self, obj): + if not obj.lesson: + return None + return obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None) + + +class AdminQuizDetailSerializer(serializers.ModelSerializer): + class Meta: + model = Quiz + fields = [ + 'id', + 'title', + 'description', + 'each_question_timing', + 'status', + 'course', + 'lesson' + ] + read_only_fields = ['course'] # Set automatically on save based on lesson.course + + +class AdminQuestionSerializer(serializers.ModelSerializer): + class Meta: + model = Question + fields = [ + 'id', + 'quiz', + 'question', + 'option1', + 'option2', + 'option3', + 'option4', + 'correct_answer', + 'priority' + ] + + +class AdminQuizParticipantSerializer(serializers.ModelSerializer): + user_name = serializers.CharField(source='user.fullname', read_only=True) + user_email = serializers.EmailField(source='user.email', read_only=True) + + class Meta: + model = QuizParticipant + fields = [ + 'id', + 'user_id', + 'user_name', + 'user_email', + 'started_at', + 'ended_at', + 'total_timing', + 'question_score', + 'timing_score', + 'total_score' + ] + + +class AdminQuizAbsenteeSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'fullname', 'email'] diff --git a/apps/quiz/urls.py b/apps/quiz/urls.py index 6ced27c..7458e41 100644 --- a/apps/quiz/urls.py +++ b/apps/quiz/urls.py @@ -1,13 +1,21 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter from . import views +router = DefaultRouter() +router.register(r'admin/quizzes', views.AdminQuizViewSet, basename='admin-quizzes') +router.register(r'admin/questions', views.AdminQuestionViewSet, basename='admin-questions') + urlpatterns = [ + path('', include(router.urls)), + path('admin/courses/', views.AdminCourseListView.as_view(), name='admin-courses-list'), + path('admin/lessons/', views.AdminLessonListView.as_view(), name='admin-lessons-list'), + # path('prizes/', PrizeListAPIView.as_view()), # path('ranked-list/', RankedListAPIView.as_view()), # path('self-rank/', SelfRankAPIView.as_view()), # path('my-quizzes/', UserQuizScores.as_view()), path('submit-quiz/', views.QuizParticipantCreateAPIView.as_view()), path('/', views.QuizDetailAPIView.as_view()), - ] diff --git a/apps/quiz/views/__init__.py b/apps/quiz/views/__init__.py index 16a10f1..46d88e7 100644 --- a/apps/quiz/views/__init__.py +++ b/apps/quiz/views/__init__.py @@ -1,2 +1,3 @@ from .quiz import * -from .participant import * \ No newline at end of file +from .participant import * +from .admin import * \ No newline at end of file diff --git a/apps/quiz/views/admin.py b/apps/quiz/views/admin.py new file mode 100644 index 0000000..2a99f52 --- /dev/null +++ b/apps/quiz/views/admin.py @@ -0,0 +1,156 @@ +from django.db.models import Q +from rest_framework.viewsets import ModelViewSet +from rest_framework.generics import ListAPIView +from rest_framework.permissions import IsAuthenticated, IsAdminUser +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 apps.quiz.models import Quiz, Question, QuizParticipant +from apps.course.models import Course, CourseLesson, Participant +from apps.account.models import User + +from apps.quiz.serializers.admin import ( + AdminCourseSerializer, + AdminCourseLessonSerializer, + AdminQuizListSerializer, + AdminQuizDetailSerializer, + AdminQuestionSerializer, + AdminQuizParticipantSerializer, + AdminQuizAbsenteeSerializer +) + + +class AdminQuizViewSet(ModelViewSet): + """ + Admin CRUD ViewSet for Quiz management. + Includes searching, filtering, and retrieving participants/absentees list. + """ + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + + 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') + + # Handle Search + search_query = self.request.query_params.get('search', None) + if search_query: + queryset = queryset.filter( + Q(title__icontains=search_query) | + Q(description__icontains=search_query) + ) + + # 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) + + 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') + 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. + Usually filtered by quiz_id. + """ + serializer_class = AdminQuestionSerializer + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + pagination_class = None + + def get_queryset(self): + queryset = Question.objects.all() + + 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') + + +class AdminCourseListView(ListAPIView): + """ + Dropdown listing helper for all courses. + """ + queryset = Course.objects.all().order_by('-id') + serializer_class = AdminCourseSerializer + permission_classes = [IsAuthenticated, IsAdminUser] + authentication_classes = [TokenAuthentication] + pagination_class = None # Return all courses for dropdown usage + + +class AdminLessonListView(ListAPIView): + """ + Dropdown listing helper for lessons. Filtered by course_id. + """ + serializer_class = AdminCourseLessonSerializer + permission_classes = [IsAuthenticated, IsAdminUser] + 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: + return CourseLesson.objects.filter(course_id=course_id).select_related('lesson').order_by('priority') + return CourseLesson.objects.all().select_related('lesson').order_by('-id')