Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
283dd80
Merge pull request #5 from MikaTech-dev/development
MikaTech-dev Jan 2, 2026
f4c360d
Removing comments to clean code
MikaTech-dev Jan 2, 2026
e362ff3
Reverse merging development with feat/serializers
MikaTech-dev Jan 2, 2026
8bd1343
add serializer.py
MikaTech-dev Jan 2, 2026
5aa254d
Fix typo from `djangoapiframework` to `djangorestframework`
MikaTech-dev Jan 2, 2026
ac56a66
Faux commit to test pyling
MikaTech-dev Jan 2, 2026
0e935f8
Remove "run on pull requres" because that "on push" triggers on both …
MikaTech-dev Jan 2, 2026
4888bd2
Merge branch 'main' into feat/serializers
MikaTech-dev Jan 2, 2026
850f908
- Minor changes to increase linting score
MikaTech-dev Jan 2, 2026
558f19e
- Renamed sequelizers to serializers
MikaTech-dev Jan 2, 2026
6f9af56
Rearranged SubmissionSerializer's meta class and whitespace around th…
MikaTech-dev Jan 2, 2026
0417dd8
Pointed pylint to my django settings
MikaTech-dev Jan 2, 2026
c059aca
Merge remote-tracking branch 'origin/main' into feat/serializers
MikaTech-dev Jan 2, 2026
6b3c508
removed whitespace serializers.py:46:80
MikaTech-dev Jan 3, 2026
24f08bc
Merge pull request #7 from MikaTech-dev/feat/serializers
MikaTech-dev Jan 3, 2026
5dd7aa1
- Disabled too-many-ancestors warning: irrelevant when using django-r…
MikaTech-dev Jan 3, 2026
04cd6c8
fixed disable not taking effect dude to parsing error
MikaTech-dev Jan 3, 2026
0a928bb
Fixed linting and FK specificity related errors in views.py
MikaTech-dev Jan 3, 2026
c8f516a
Altered pylint to fail only when under a score of 8 because i'm not G…
MikaTech-dev Jan 3, 2026
1e96410
Merge branch 'bug/linter-fixes' into feat/models
MikaTech-dev Jan 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -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 }}
Expand All @@ -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/
5 changes: 4 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
@@ -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
invalid-name,
imported-auth-user,
too-many-ancestors,
18 changes: 18 additions & 0 deletions api/migrations/0002_alter_question_correct_answers.py
Original file line number Diff line number Diff line change
@@ -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'}"),
),
]
23 changes: 12 additions & 11 deletions api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 = [
Expand All @@ -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)
Expand All @@ -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}"
return f"Ans: {self.question.id} for Sub: {self.submission.id}"
59 changes: 59 additions & 0 deletions api/serializers.py
Original file line number Diff line number Diff line change
@@ -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
29 changes: 28 additions & 1 deletion api/views.py
Original file line number Diff line number Diff line change
@@ -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)