diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 65715a0..9cf791f 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -1,13 +1,13 @@ name: Pylint -on: [push, pull_request] +on: [push] jobs: build: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.9",] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint-django djangoapiframework django + pip install pylint-django djangorestframework django - name: Analysing the code with pylint (together with plugins) run: | - pylint --load-plugins pylint_django api/ assessment_engine/ + pylint --load-plugins pylint_django --django-settings-module=assessment_engine.settings api/ assessment_engine/ api/ diff --git a/.pylintrc b/.pylintrc index 63dc670..246fd49 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,10 +1,13 @@ [MASTER] load-plugins=pylint_django ignore=migrations +fail-under=8.0 [MESSAGES CONTROL] disable=missing-module-docstring, missing-class-docstring, missing-function-docstring, too-few-public-methods, - invalid-name \ No newline at end of file + invalid-name, + imported-auth-user, + too-many-ancestors, diff --git a/api/migrations/0002_alter_question_correct_answers.py b/api/migrations/0002_alter_question_correct_answers.py new file mode 100644 index 0000000..205497c --- /dev/null +++ b/api/migrations/0002_alter_question_correct_answers.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0 on 2026-01-02 20:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='question', + name='correct_answers', + field=models.JSONField(default=dict, help_text="e.g. {'answer': 'B'}"), + ), + ] diff --git a/api/models.py b/api/models.py index 33f67ec..f6d51e8 100644 --- a/api/models.py +++ b/api/models.py @@ -2,11 +2,10 @@ # Create your models here. from django.contrib.auth.models import User # Inbuilt auth User model -from django.utils import timezone class Exam(models.Model): title = models.CharField(max_length=255) - duration = models.DurationField() + duration = models.DurationField() course_name = models.CharField(max_length=255) metadata = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) @@ -21,21 +20,25 @@ class Question(models.Model): ('SA', 'Short Answer'), ] - exam = models.ForeignKey(Exam, related_name='questions', on_delete=models.CASCADE) + exam: Exam = models.ForeignKey(Exam, related_name='questions', on_delete=models.CASCADE) question_text = models.TextField() - question_type = models.CharField(max_length=3, choices=QUESTION_TYPES, default='MCQ') # using choices + question_type = models.CharField( + max_length=3, choices=QUESTION_TYPES, + default='MCQ' + ) # using choices # Flexible storage for options (e.g. ["A", "B", "C"]) and correct answers - options = models.JSONField(default=dict, blank=True, help_text="For MCQs: {'options': ['A', 'B', 'C']}") - correct_answers = models.JSONField(help_text="e.g. {'answer': 'B'}") - + options = models.JSONField( + default=dict, blank=True, help_text="For MCQs: {'options': ['A', 'B', 'C']}") + correct_answers = models.JSONField(default=dict, help_text="e.g. {'answer': 'B'}") order = models.PositiveSmallIntegerField(default=1) class Meta: ordering = ['order'] def __str__(self): - return f"{self.exam.title} - Q{self.order}" + examObject: Exam = self.exam + return f"{examObject} - Q{self.order}" class Submission(models.Model): STATUS_CHOICES = [ @@ -47,7 +50,6 @@ class Submission(models.Model): student = models.ForeignKey(User, on_delete=models.CASCADE) exam = models.ForeignKey(Exam, on_delete=models.CASCADE) submitted_at = models.DateTimeField(auto_now_add=True) - # Allow score and feedback to be blank (nullable) initially until graded total_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) feedback = models.TextField(blank=True) @@ -60,9 +62,8 @@ class Answer(models.Model): submission = models.ForeignKey(Submission, related_name='answers', on_delete=models.CASCADE) question = models.ForeignKey(Question, on_delete=models.CASCADE) student_answer = models.JSONField() - # Optional: store individual score per question if needed later is_correct = models.BooleanField(default=False, blank=True) def __str__(self): - return f"Ans: {self.question.id} for Sub: {self.submission.id}" \ No newline at end of file + return f"Ans: {self.question.id} for Sub: {self.submission.id}" diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..07c3ef4 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,59 @@ +from rest_framework import serializers +from django.contrib.auth.models import User +from .models import Exam, Question, Submission, Answer + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'email'] + +class QuestionSerializer(serializers.ModelSerializer): + class Meta: + model = Question + fields = ['id', 'question_text', 'question_type', 'options', 'order'] + +class ExamSerializer(serializers.ModelSerializer): + # Embedding qustions inside the exam details + questions = QuestionSerializer(many=True, read_only=True) + + class Meta: + model = Exam + fields = ['id', 'title', 'duration', 'course_name', 'metadata', 'questions', 'created_at'] + +class AnswerSerializer(serializers.ModelSerializer): + class Meta: + model = Answer + fields = ['question', 'student_answer', 'is_correct'] + read_only_fields = ['is_correct'] + +class SubmissionSerializer(serializers.ModelSerializer): + answers = AnswerSerializer(many=True) + student = UserSerializer(read_only=True) + + class Meta: + model = Submission + fields = [ + 'id', + 'student', + 'exam', + 'submitted_at', + 'total_score', + 'feedback', + 'status', + 'answers' + ] + # These fields will br created/filled by the grading.service(TBD), not by the student + read_only_fields = ['total_score', 'feedback', 'status', 'submitted_at'] + def create(self, validated_data): + """ + Handles creating the Submission AND the nested Answers in one go. + """ + answers_data = validated_data.pop('answers') + # Creating the Submission instance + submission = Submission.objects.create(**validated_data) + + # Creating the Answers instance + for answer_data in answers_data: + Answer.objects.create(submission=submission, **answer_data) + + return submission diff --git a/api/views.py b/api/views.py index c60c790..b1bc315 100644 --- a/api/views.py +++ b/api/views.py @@ -1,3 +1,30 @@ -from django.shortcuts import render +# from django.shortcuts import render # Create your views here. +from rest_framework import viewsets, permissions +from .models import Exam, Submission +from .serializers import ExamSerializer, SubmissionSerializer + +class ExamViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that allows exams to be viewed or listed. + READ-ONLY: Users (Students) cannot create or modify exams here. + """ + queryset = Exam.objects.all() + serializer_class = ExamSerializer + permission_classes = [permissions.IsAuthenticated] + +class SubmissionViewSet(viewsets.ModelViewSet): + """ + API endpoint for students to submit exams and view their submission history. + """ + serializer_class = SubmissionSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + # Ensuring users only have access to their own submission + return Submission.objects.filter(student=self.request.user) + + def perform_create(self, serializer): + # link submission to the actual user + serializer.save(student=self.request.user)