Skip to content

Commit

Permalink
Refactor quiz attempt logic and improve serializers; fix competition …
Browse files Browse the repository at this point in the history
…viewset naming
  • Loading branch information
yunho7687 authored and thnorton committed Feb 12, 2025
1 parent 2dad90f commit 1de663b
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 20 deletions.
2 changes: 2 additions & 0 deletions server/api/quiz/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ def is_available(self):
if not is_available:
self.state = QuizAttempt.State.COMPLETED
self.save()
elif self.state == QuizAttempt.State.SUBMITTED:
return False
else:
self.state = QuizAttempt.State.IN_PROGRESS
self.save()
Expand Down
4 changes: 4 additions & 0 deletions server/api/quiz/serializers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from rest_framework import serializers
from api.question.models import Question, Category
from api.question.serializers import AnswerSerializer
from api.quiz.models import Quiz, QuizSlot, QuizAttempt, QuestionAttempt


class QuestionSerializer(serializers.ModelSerializer):
# answers is a foreign key field, so we need to use a nested serializer
answers = AnswerSerializer(many=True)

class Meta:
model = Question
fields = '__all__'
Expand Down
4 changes: 2 additions & 2 deletions server/api/quiz/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from rest_framework.routers import DefaultRouter
from .views import QuizViewSet, QuizSlotViewSet, QuizAttemptViewSet, QuestionAttemptViewSet, AdminQuizViewSet, CompetistionQuizViewSet
from .views import QuizViewSet, QuizSlotViewSet, QuizAttemptViewSet, QuestionAttemptViewSet, AdminQuizViewSet, CompetitionQuizViewSet

router = DefaultRouter()
router.register(r'admin-quizzes', AdminQuizViewSet, basename='')
router.register(r'all_quizzes', QuizViewSet)
router.register(r'competition', CompetistionQuizViewSet, basename='competition')
router.register(r'competition', CompetitionQuizViewSet, basename='competition')
router.register(r'quiz-slots', QuizSlotViewSet)
router.register(r'quiz-attempts', QuizAttemptViewSet)
router.register(r'question-attempts', QuestionAttemptViewSet)
Expand Down
49 changes: 32 additions & 17 deletions server/api/quiz/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,15 @@ def slots(self, request, pk=None):
return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND)
if quiz.visible and quiz.status == 0:
self.serializer_class = QuizSlotSerializer
instance = QuizSlot.objects.filter(quiz_id=pk)
serializer = QuizSlotSerializer(instance, many=True)
instances = QuizSlot.objects.filter(quiz_id=pk)
serializer = QuizSlotSerializer(instances, many=True)
return Response(serializer.data)
else:
return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND)


@permission_classes([IsAuthenticated])
class CompetistionQuizViewSet(viewsets.ReadOnlyModelViewSet):
class CompetitionQuizViewSet(viewsets.ReadOnlyModelViewSet):
"""
A viewset for retrieving competition quizzes that are visible and have a status of 1.
Need to be tested.
Expand All @@ -156,6 +156,18 @@ class CompetistionQuizViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Quiz.objects.filter(status=1, visible=True)
serializer_class = UserQuizSerializer

@action(detail=True, methods=['get'])
def submit(self, request, pk=None):
"""
Submit the quiz attempt, changing its state to 2 (submitted).
"""
user = request.user.student
attempt = user.quiz_attempts.get(quiz_id=pk)
attempt.state = QuizAttempt.State.SUBMITTED
attempt.time_finish = now()
attempt.save()
return Response({'status': 'Quiz attempt submitted successfully.'})

@action(detail=True, methods=['get'])
def slots(self, request, pk=None):
"""
Expand Down Expand Up @@ -193,6 +205,11 @@ def slots(self, request, pk=None):
quiz_id=pk, student_id=student_id).first()
# if attempt after the quiz has finished:
is_available = self._is_available(quiz_instance, existing_attempt)

user = request.user.student
if existing_attempt is not None and user.quiz_attempts.get(pk=pk).state == QuizAttempt.State.SUBMITTED:
return Response({'error': 'Quiz has submitted '}, status=status.HTTP_400_BAD_REQUEST)

if quiz_instance.status == 3 and existing_attempt is None:
return Response({'error': 'Quiz has finished'}, status=status.HTTP_404_NOT_FOUND)
# check the attemt is available or not
Expand Down Expand Up @@ -256,10 +273,12 @@ def _get_slots_response(self, quiz_id, existing_attempt, user):
quiz_attempt_serializer = QuizAttemptSerializer(data={
'quiz': quiz_id,
'student': user.student.id,
'state': 2,
'state': QuizAttempt.State.IN_PROGRESS,
})
quiz_attempt_serializer.is_valid(raise_exception=True)
attempt = quiz_attempt_serializer.save()

# run attempt.is_available to update the dead_line
attempt.is_available
end_time = attempt.dead_line
else:
Expand Down Expand Up @@ -312,13 +331,12 @@ def create(self, request, *args, **kwargs):
student_id = request.data.get('student')
existing_attempt = QuizAttempt.objects.filter(
quiz_id=quiz_id, student_id=student_id).first()
print(existing_attempt)

if existing_attempt:
serializer = self.get_serializer(existing_attempt)
data = serializer.data
if not data.is_available:
return Response({'error': 'Quiz has finished3'}, status=status.HTTP_403_FORBIDDEN)
return Response({'error': 'Quiz has finished'}, status=status.HTTP_403_FORBIDDEN)

# switch cases by state
match existing_attempt.state:
Expand All @@ -337,9 +355,9 @@ def create(self, request, *args, **kwargs):
def update(self, request, *args, **kwargs):
instance = self.get_object()
match instance.state:
case 2:
case QuizAttempt.State.IN_PROGRESS:
data = request.data.copy()
if int(data.get('state')) == 3:
if int(data.get('state')) == QuizAttempt.State.SUBMITTED:
# set the time_finish to the current time
data['time_finish'] = now()

Expand All @@ -349,24 +367,21 @@ def update(self, request, *args, **kwargs):

return Response(serializer.data, status=status.HTTP_200_OK)

case 3:
case QuizAttempt.State.SUBMITTED:
return Response({'error': 'You have already submitted this quiz.'}, status=status.HTTP_403_FORBIDDEN)
case 4:
case QuizAttempt.State.COMPLETED:
return Response({'error': 'The competition has ended.'}, status=status.HTTP_403_FORBIDDEN)

# if existing_attempt.state == 2:
# existing_attempt.state = 0
# existing_attempt.save()
# existing_attempt.save()
# return Response({'message': 'Answer updated successfully.'}, status=status.HTTP_200_OK)

return super().create(request, *args, **kwargs)
return super().update(request, *args, **kwargs)

@action(detail=True, methods=['get'])
def submit(self, request, pk=None):
"""
Submit the quiz attempt, changing its state to 2 (submitted).
"""
user = request.user
if self.student != user.student:
return Response({'error': 'You are not authorized to perform this action.'}, status=status.HTTP_403_FORBIDDEN)
attempt = self.get_object()
attempt.state = QuizAttempt.State.SUBMITTED
attempt.time_finish = now()
Expand Down
1 change: 0 additions & 1 deletion server/api/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ def retrieve(self, request, *args, **kwargs):
if request.user.is_staff:
return super().retrieve(request, *args, **kwargs)
elif hasattr(request.user, "teacher"):
print(kwargs["pk"])
if request.user.teacher.id == int(kwargs["pk"]):
return super().retrieve(request, *args, **kwargs)
else:
Expand Down

0 comments on commit 1de663b

Please sign in to comment.