diff --git a/server/api/question/serializers.py b/server/api/question/serializers.py index 8b750def..230c3002 100644 --- a/server/api/question/serializers.py +++ b/server/api/question/serializers.py @@ -84,4 +84,4 @@ def update(self, instance, validated_data): class Meta: model = Question - fields = "__all__" + fields = '__all__' diff --git a/server/api/quiz/migrations/0010_alter_quizattempt_team_alter_quizattempt_time_finish.py b/server/api/quiz/migrations/0010_alter_quizattempt_team_alter_quizattempt_time_finish.py new file mode 100644 index 00000000..5fa71b95 --- /dev/null +++ b/server/api/quiz/migrations/0010_alter_quizattempt_team_alter_quizattempt_time_finish.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.5 on 2025-01-27 04:05 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0009_quizattempt_team"), + ("team", "0003_alter_teammember_student_alter_teammember_team"), + ] + + operations = [ + migrations.AlterField( + model_name="quizattempt", + name="team", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="quiz_attempts", + to="team.team", + ), + ), + migrations.AlterField( + model_name="quizattempt", + name="time_finish", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/server/api/quiz/migrations/0011_alter_quizattempt_student.py b/server/api/quiz/migrations/0011_alter_quizattempt_student.py new file mode 100644 index 00000000..882157bc --- /dev/null +++ b/server/api/quiz/migrations/0011_alter_quizattempt_student.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.5 on 2025-01-27 14:22 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0010_alter_quizattempt_team_alter_quizattempt_time_finish"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name="quizattempt", + name="student", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="quiz_attempts", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/server/api/quiz/migrations/0012_alter_quizattempt_student.py b/server/api/quiz/migrations/0012_alter_quizattempt_student.py new file mode 100644 index 00000000..2ee9a95f --- /dev/null +++ b/server/api/quiz/migrations/0012_alter_quizattempt_student.py @@ -0,0 +1,26 @@ +# Generated by Django 5.1.5 on 2025-01-27 14:27 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0011_alter_quizattempt_student"), + ("users", "0002_school_is_country_school_type_student_attendent_year"), + ] + + operations = [ + migrations.AlterField( + model_name="quizattempt", + name="student", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="quiz_attempts", + to="users.student", + ), + ), + ] diff --git a/server/api/quiz/migrations/0013_quizattempt_dead_line.py b/server/api/quiz/migrations/0013_quizattempt_dead_line.py new file mode 100644 index 00000000..d8b7f880 --- /dev/null +++ b/server/api/quiz/migrations/0013_quizattempt_dead_line.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-01-28 08:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("quiz", "0012_alter_quizattempt_student"), + ] + + operations = [ + migrations.AddField( + model_name="quizattempt", + name="dead_line", + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/server/api/quiz/models.py b/server/api/quiz/models.py index 440ed4c5..46c9af4a 100644 --- a/server/api/quiz/models.py +++ b/server/api/quiz/models.py @@ -2,6 +2,8 @@ from api.team.models import Team from api.users.models import Student from api.question.models import Question +from django.utils.timezone import now +from datetime import timedelta class Quiz(models.Model): @@ -87,14 +89,16 @@ class State(models.IntegerChoices): Student, on_delete=models.CASCADE, related_name="quiz_attempts", default=None, null=True ) current_page = models.IntegerField() - state = models.IntegerField(choices=State.choices, default=State.UNATTEMPTED) + state = models.IntegerField( + choices=State.choices, default=State.UNATTEMPTED) time_start = models.DateTimeField(auto_now_add=True) - time_finish = models.DateTimeField(null=True) + time_finish = models.DateTimeField(null=True, blank=True) time_modified = models.DateTimeField(auto_now=True) total_marks = models.IntegerField() team = models.ForeignKey( - Team, on_delete=models.CASCADE, related_name="quiz_attempts", default=None, null=True + Team, on_delete=models.CASCADE, related_name="quiz_attempts", default=None, null=True, blank=True ) + dead_line = models.DateTimeField(default=None, null=True, blank=True) def __str__(self): return f"{self.id} {self.quiz} " @@ -104,6 +108,35 @@ def check_all_answer(self): question_attempt.check_answer() question_attempt.save() + @property + def is_available(self): + current_time = now() + end_time = self.quiz.open_time_date + \ + timedelta(minutes=self.quiz.time_limit) + \ + timedelta(minutes=self.quiz.time_window) + end_time = min(end_time, self.time_start + + timedelta(minutes=self.quiz.time_limit)) + if int(self.student.extenstion_time) > 0: + print("extenstion time") + end_time = now() + \ + timedelta(minutes=self.student.extenstion_time) + self.student.extenstion_time = 0 + self.student.save() + if self.dead_line is None: + self.dead_line = end_time + self.save() + else: + self.dead_line = max(self.dead_line, end_time) + + is_available = self.quiz.open_time_date <= current_time <= self.dead_line + if not is_available: + self.state = QuizAttempt.State.COMPLETED + self.save() + else: + self.state = QuizAttempt.State.IN_PROGRESS + self.save() + return is_available + class QuestionAttempt(models.Model): id = models.AutoField(primary_key=True) @@ -117,7 +150,7 @@ class QuestionAttempt(models.Model): is_correct = models.BooleanField(default=None) def __str__(self): - return f"{self.id} {self.question_id} {self.quiz_attempt_id}" + return f"{self.id} {self.question} {self.quiz_attempt}" def check_answer(self): if self.answer_student == self.question.answer: diff --git a/server/api/quiz/serializers.py b/server/api/quiz/serializers.py index cdca3d25..0461f20e 100644 --- a/server/api/quiz/serializers.py +++ b/server/api/quiz/serializers.py @@ -9,6 +9,15 @@ class Meta: fields = '__all__' +class CompQuestionSerializer(serializers.ModelSerializer): + """ + Serializer for the Question model with no answer field. + """ + class Meta: + model = Question + fields = ['id', 'name', 'question_text', 'layout', 'image', 'mark'] + + class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category @@ -39,11 +48,28 @@ class Meta: read_only_fields = ['quiz'] +class CompQuizSlotSerializer(serializers.ModelSerializer): + question = CompQuestionSerializer(read_only=True) + + class Meta: + model = QuizSlot + fields = '__all__' + + class QuizSerializer(serializers.ModelSerializer): class Meta: model = Quiz - fields = ['id', 'name', 'intro', 'total_marks'] + fields = '__all__' + + +class UserQuizSerializer(serializers.ModelSerializer): + """ + Serializer for the Quiz model with no is_comp, visible, and status fields. + """ + class Meta: + model = Quiz + exclude = ['is_comp', 'visible', 'status'] class AdminQuizSerializer(serializers.ModelSerializer): @@ -53,6 +79,9 @@ class Meta: class QuizAttemptSerializer(serializers.ModelSerializer): + current_page = serializers.IntegerField(default=0, required=False) + total_marks = serializers.IntegerField(default=0, required=False) + class Meta: model = QuizAttempt fields = '__all__' diff --git a/server/api/quiz/views.py b/server/api/quiz/views.py index 703a69ac..70d8709c 100644 --- a/server/api/quiz/views.py +++ b/server/api/quiz/views.py @@ -1,8 +1,15 @@ -from rest_framework import viewsets +from rest_framework import viewsets, mixins from .models import Quiz, QuizSlot, QuizAttempt, QuestionAttempt +from rest_framework.viewsets import GenericViewSet from rest_framework.decorators import action, permission_classes -from rest_framework.permissions import IsAdminUser, AllowAny -from .serializers import QuizSerializer, QuizSlotSerializer, QuizAttemptSerializer, QuestionAttemptSerializer, AdminQuizSerializer +from rest_framework.permissions import IsAdminUser, AllowAny, IsAuthenticated +from .serializers import (QuizSerializer, + QuizSlotSerializer, + QuizAttemptSerializer, + QuestionAttemptSerializer, + AdminQuizSerializer, + CompQuizSlotSerializer, + UserQuizSerializer) from rest_framework.response import Response from rest_framework import status, serializers from datetime import timedelta @@ -25,13 +32,27 @@ class AdminQuizViewSet(viewsets.ModelViewSet): def slots(self, request, pk=None): """ Retrieve or create slots for a specific quiz. - - Args: - request (Request): The request object. - pk (int): The primary key of the quiz. - - Returns: - Response: The response object containing the slots data. + The payload data is a list of dictionaries containing the following: + The slot_index is the order of the question in the quiz. + The block is the section of the quiz. + In a competition quiz, the order of the questions is randomized within each block. + + payload data example: + + [ + { + "question_id": 2, + "slot_index": 1, + "quiz_id": 12, + "block": 1 + }, + { + "question_id": 2, + "slot_index": 1, + "quiz_id": 12, + "block": 2 + } + ] """ if request.method == 'GET': self.serializer_class = QuizSlotSerializer @@ -42,8 +63,42 @@ def slots(self, request, pk=None): serializer = QuizSlotSerializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) serializer.save() + quiz = Quiz.objects.get(pk=pk) + quiz_slots = quiz.quiz_slots.all() + questions = [slot.question for slot in quiz_slots] + quiz.total_marks = sum([question.mark for question in questions]) + quiz.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + @action(detail=True, methods=['get']) + def marking(self, request, pk=None): + """ + Mark the quiz attempt. + """ + + # get the quiz attempts + quiz_attempts = QuizAttempt.objects.all().filter(quiz_id=pk) + + for attempt in quiz_attempts: + question_attempts = attempt.question_attempts.all() + total_marks = 0 + for question_attempt in question_attempts: + question = question_attempt.question.all() + answers = [ + question.answer.value.all() for question.answer in question.answers] + if question_attempt.answer_student in answers: + total_marks += question.mark + question_attempt.is_correct = True + question_attempt.save() + else: + question_attempt.is_correct = False + question_attempt.save() + attempt.total_marks = total_marks + attempt.save() + + return Response({'message': 'Quiz attempt marked successfully.'}) + @permission_classes([AllowAny]) class QuizViewSet(viewsets.ReadOnlyModelViewSet): @@ -68,62 +123,144 @@ def slots(self, request, pk=None): Returns: Response: The response object containing the slots data. """ - if request.method == 'GET': - quiz_instance = Quiz.objects.get(pk=pk) - if quiz_instance.visible and quiz_instance.status == 0: - self.serializer_class = QuizSlotSerializer - instance = QuizSlot.objects.filter(quiz_id=pk) - serializer = QuizSlotSerializer(instance, many=True) - return Response(serializer.data) - else: - return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND) + try: + quiz = Quiz.objects.get(pk=pk) + except Quiz.DoesNotExist: + 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) + return Response(serializer.data) + else: + return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND) +@permission_classes([IsAuthenticated]) class CompetistionQuizViewSet(viewsets.ReadOnlyModelViewSet): """ A viewset for retrieving competition quizzes that are visible and have a status of 1. + Need to be tested. Methods: slots: Retrieve slots for a specific competition quiz. """ queryset = Quiz.objects.filter(status=1, visible=True) - serializer_class = QuizSerializer + serializer_class = UserQuizSerializer @action(detail=True, methods=['get']) def slots(self, request, pk=None): """ - Retrieve slots for a specific competition quiz. + Only allow the user to access the slots(competition questions) if the quiz is available. + api: + /api/quiz/competition/1/slots/ + data shape: {"data":[ ], "end_time": "2025-01-29T00:49:17.015606Z"} + + {"data": [ + { + "id": 1, + "question": { + "id": 12, + "name": "question_1", + "question_text": "question_text_1", + "layout": "left", + "image": null, + "mark": 2 + }, + "slot_index": 1, + "block": 1, + "quiz": 2 + } + ], + "end_time": "2025-01-29T00:49:17.015606Z"} + + """ + try: + quiz_instance = Quiz.objects.get(pk=pk) + except Quiz.DoesNotExist: + return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND) + user = request.user + student_id = user.student.id + existing_attempt = QuizAttempt.objects.filter( + quiz_id=pk, student_id=student_id).first() + # if attempt after the quiz has finished: + is_available = self._is_available(quiz_instance, existing_attempt) + 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 + elif is_available is True: + return self._get_slots_response(pk, existing_attempt, user) + else: + return is_available + + def _is_available(self, quiz, attempt): + """ + Check if the quiz is available for the user. Args: - request (Request): The request object. - pk (int): The primary key of the quiz. + quiz_instance (Quiz): The quiz instance. + attempt (QuizAttempt): The existing quiz attempt. Returns: - Response: The response object containing the slots data. + bool or Response: True if available, otherwise a Response with an error message. """ - if request.method == 'GET': - quiz_instance = Quiz.objects.get(pk=pk) + # check if the quiz has been withdrawn by the admin + if quiz.visible: current_time = now() - start_time = quiz_instance.open_time_date - end_time = start_time + timedelta(minutes=quiz_instance.time_limit) - print(quiz_instance.visible, quiz_instance.status) + start_time = quiz.open_time_date + end_time = start_time + \ + timedelta(minutes=quiz.time_limit) + \ + timedelta(minutes=quiz.time_window) + + # if never attempt before, no attempt instance yet + if attempt is None: + if start_time <= current_time <= end_time: + return True + elif current_time < start_time: + return Response({'error': 'Quiz has not started yet'}, status=status.HTTP_403_FORBIDDEN) + elif current_time > end_time: + return Response({'error': 'Quiz has ended'}, status=status.HTTP_403_FORBIDDEN) + # if the user has already attempted the quiz + else: + if attempt.is_available: + return True + else: + return Response({'error': 'Quiz has finished'}, status=status.HTTP_403_FORBIDDEN) + # if the quiz has been withdrawn by the admin + else: + return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND) - if quiz_instance.visible and (quiz_instance.status == 1 or quiz_instance.status == 3): + def _get_slots_response(self, quiz_id, existing_attempt, user): + """ + Get the response containing the slosts data. + The slots are corresponding sorted questions of the quiz. - # check if the quiz is opening - is_opening = start_time <= current_time <= end_time + Args: + quiz_id (int): The primary key of the quiz. + existing_attempt (QuizAttempt): The existing quiz attempt. + user (User): The current user. - if is_opening: - self.serializer_class = QuizSlotSerializer - instance = QuizSlot.objects.filter(quiz_id=pk) - serializer = QuizSlotSerializer(instance, many=True) - return Response(serializer.data) - elif quiz_instance.status == 3: - return Response({'error': 'Quiz has finished'}, status=status.HTTP_404_NOT_FOUND) - else: - return Response({'error': 'Quiz is not accessible at this time'}, status=status.HTTP_403_FORBIDDEN) - else: - return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND) + Returns: + Response: The response object containing the slots data. + """ + + if existing_attempt is None: + quiz_attempt_serializer = QuizAttemptSerializer(data={ + 'quiz': quiz_id, + 'student': user.student.id, + 'state': 2, + }) + quiz_attempt_serializer.is_valid(raise_exception=True) + attempt = quiz_attempt_serializer.save() + attempt.is_available + end_time = attempt.dead_line + else: + end_time = existing_attempt.dead_line + # wrap the end_time into the response + instances = QuizSlot.objects.filter(quiz_id=quiz_id) + serializer = CompQuizSlotSerializer(instances, many=True) + + return Response({'data': serializer.data, 'end_time': end_time}, status=status.HTTP_200_OK) class QuizSlotViewSet(viewsets.ModelViewSet): @@ -134,6 +271,7 @@ class QuizSlotViewSet(viewsets.ModelViewSet): serializer_class = QuizSlotSerializer +@permission_classes([IsAuthenticated]) class QuizAttemptViewSet(viewsets.ModelViewSet): """ A viewset for managing quiz attempts. @@ -141,6 +279,23 @@ class QuizAttemptViewSet(viewsets.ModelViewSet): queryset = QuizAttempt.objects.all() serializer_class = QuizAttemptSerializer + def get_queryset(self): + if hasattr(self.request.user, "student"): + student_id = self.request.user.student.id + return QuizAttempt.objects.filter(student_id=student_id) + elif hasattr(self.request.user, "teacher"): + return QuizAttempt.objects.filter(student__school=self.request.user.teacher.school.id) + elif self.request.user.is_staff: + return QuizAttempt.objects.all() + else: + return QuizAttempt.objects.none() + + # def list(self, request, *args, **kwargs): + # if request.user.is_staff: + # return super().list(request, *args, **kwargs) + # else: + # return Response({'error': 'You are not authorized to perform this action.'}, status=status.HTTP_403_FORBIDDEN) + def create(self, request, *args, **kwargs): """ Create a new quiz attempt. Ensure that a user can only have one active attempt per quiz. @@ -148,63 +303,141 @@ def create(self, request, *args, **kwargs): quiz_id = request.data.get('quiz') student_id = request.data.get('student') existing_attempt = QuizAttempt.objects.filter( - quiz_id=quiz_id, student_id=student_id, state=1).first() + quiz_id=quiz_id, student_id=student_id).first() + print(existing_attempt) if existing_attempt: - return Response({'error': 'You already have an active attempt for this quiz.'}, status=status.HTTP_400_BAD_REQUEST) + 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) + + # switch cases by state + match existing_attempt.state: + case QuizAttempt.State.IN_PROGRESS: + # return the existing attempt + return Response({'message': 'You already have an active attempt for this quiz.', 'data': data}, status=status.HTTP_200_OK) + case QuizAttempt.State.SUBMITTED: # submitted + # prevent the user from creating a new attempt + return Response({'message': 'You have already submitted this quiz.'}, status=status.HTTP_403_FORBIDDEN) + case QuizAttempt.State.COMPLETED: # completed + # prevent the user from creating a new attempt + return Response({'message': 'The competition has ended.'}, status=status.HTTP_403_FORBIDDEN) + else: + return super().create(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + instance = self.get_object() + match instance.state: + case 2: + data = request.data.copy() + if int(data.get('state')) == 3: + # set the time_finish to the current time + data['time_finish'] = now() + + serializer = self.get_serializer(instance, data=data) + serializer.is_valid(raise_exception=True) + instance = serializer.save() + + return Response(serializer.data, status=status.HTTP_200_OK) + + case 3: + return Response({'error': 'You have already submitted this quiz.'}, status=status.HTTP_403_FORBIDDEN) + case 4: + 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) - @action(detail=True, methods=['patch']) + @action(detail=True, methods=['get']) def submit(self, request, pk=None): """ Submit the quiz attempt, changing its state to 2 (submitted). """ attempt = self.get_object() - attempt.state = 2 + attempt.state = QuizAttempt.State.SUBMITTED attempt.time_finish = now() attempt.save() return Response({'status': 'Quiz attempt submitted successfully.'}) -class QuestionAttemptViewSet(viewsets.ModelViewSet): +@permission_classes([IsAuthenticated]) +class QuestionAttemptViewSet(mixins.CreateModelMixin, + mixins.ListModelMixin, + GenericViewSet): """ A viewset for managing question attempts. """ queryset = QuestionAttempt.objects.all() serializer_class = QuestionAttemptSerializer + def get_queryset(self): + if hasattr(self.request.user, "student"): + return QuestionAttempt.objects.filter(student=self.request.user.student.id) + elif self.request.user.is_staff: + return QuestionAttempt.objects.all() + else: + return QuestionAttempt.objects.none() + def create(self, request, *args, **kwargs): """ Create a new question attempt. Ensure that a user can continue answering questions upon re-login. """ + quiz_attempt_id = request.data.get('quiz_attempt') + comp_attempt = QuizAttempt.objects.get( + pk=quiz_attempt_id, student_id=request.user.student.id) + + # check if the quiz is available for the user + if not comp_attempt.is_available: + return Response({'error': 'Quiz has finished'}, status=status.HTTP_403_FORBIDDEN) + question_id = request.data.get('question') - student_id = request.data.get('student') + student_id = int(request.data.get('student')) # TODO: Uncomment this line when using JWT authentication # student_id = request.user.id + current_user = request.user + + if int(student_id) != int(current_user.student.id) and not current_user.is_staff: + return Response({'error': 'You are not authorized to perform this action.'}, status=status.HTTP_403_FORBIDDEN) + existing_attempt = QuestionAttempt.objects.filter( - quiz_attempt_id=quiz_attempt_id, - question_id=question_id, - student_id=student_id + quiz_attempt=quiz_attempt_id, + question=question_id, + student=student_id ).first() if existing_attempt: - return Response( - {'error': 'You already have an attempt for this question in the current quiz attempt.'}, - status=status.HTTP_400_BAD_REQUEST) + # modify the answer + existing_attempt.answer_student = request.data.get( + 'answer_student') + existing_attempt.save() + new_answer = QuestionAttemptSerializer( + existing_attempt).data['answer_student'] + return Response({'message': 'Answer updated successfully.', + 'new_answer': new_answer}, status=status.HTTP_200_OK) return super().create(request, *args, **kwargs) - @action(detail=True, methods=['patch']) - def answer(self, request, pk=None): - """ - Record the answer for a question attempt. - """ - # TODO: Uncomment this line when using JWT authentication - # student_id = request.user.id - question_attempt = self.get_object() - question_attempt.answer_student = request.data.get('answer_student') - question_attempt.check_answer() - question_attempt.save() - return Response({'status': 'Answer recorded successfully.'}) + def _is_comp_available(self, quiz_instance): + # check if the quiz is available for the user + if quiz_instance.visible: + current_time = now() + start_time = quiz_instance.open_time_date + end_time = start_time + \ + timedelta(minutes=quiz_instance.time_limit) + \ + timedelta(minutes=quiz_instance.time_window) + + if start_time <= current_time <= end_time: + return True + elif current_time < start_time: + return Response({'error': 'Quiz has not started yet'}, status=status.HTTP_403_FORBIDDEN) + elif current_time > end_time: + return Response({'error': 'Quiz has finished'}, status=status.HTTP_403_FORBIDDEN) + else: + return Response({'error': 'Quiz not exist'}, status=status.HTTP_404_NOT_FOUND) diff --git a/server/api/settings.py b/server/api/settings.py index 0fcb0548..70ac8583 100644 --- a/server/api/settings.py +++ b/server/api/settings.py @@ -71,7 +71,7 @@ ), 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', } SIMPLE_JWT = { diff --git a/server/api/users/migrations/0003_student_extenstion_time.py b/server/api/users/migrations/0003_student_extenstion_time.py new file mode 100644 index 00000000..6c9603ea --- /dev/null +++ b/server/api/users/migrations/0003_student_extenstion_time.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-01-28 07:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0002_school_is_country_school_type_student_attendent_year"), + ] + + operations = [ + migrations.AddField( + model_name="student", + name="extenstion_time", + field=models.IntegerField(default=0), + ), + ] diff --git a/server/api/users/models.py b/server/api/users/models.py index f73c436d..89244441 100644 --- a/server/api/users/models.py +++ b/server/api/users/models.py @@ -45,6 +45,7 @@ class Student(models.Model): attendent_year = models.IntegerField(default=2025) year_level = models.CharField(max_length=50) created_at = models.DateTimeField(auto_now_add=True) + extenstion_time = models.IntegerField(default=0) def __str__(self): return f"{self.user.username}" diff --git a/server/api/users/serializers.py b/server/api/users/serializers.py index 3aba033a..59367e32 100644 --- a/server/api/users/serializers.py +++ b/server/api/users/serializers.py @@ -30,21 +30,24 @@ class Meta: read_only_fields = ['last_login', 'date_joined'] def create(self, validated_data): - """ - Creates a new User instance. - - Args: - validated_data (dict): The validated data for creating a User instance. - - first_name (str): The first name of the user. - - last_name (str): The last name of the user. - - password (str): The password for the user. + fullname = validated_data['first_name'] + validated_data['last_name'] + validated_data['username'] = fullname + password = validated_data.pop('password') + user = User(**validated_data) + print(user) + user.set_password(password) + user.save() + return user - Returns: - User: The created User instance. - """ + def update(self, instance, validated_data): fullname = validated_data['first_name'] + validated_data['last_name'] validated_data['username'] = fullname - return super().create(validated_data) + if 'password' in validated_data: + password = validated_data.pop('password') + instance.set_password(password) + instance.save() + validated_data['password'] = instance.password + return super().update(instance, validated_data) class SchoolSerializer(serializers.ModelSerializer):