Skip to content

Commit 7baa665

Browse files
authored
Merge branch 'main' into video_call_ui
2 parents e04f205 + ed8606b commit 7baa665

39 files changed

+826
-100
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Generated by Django 5.1.6 on 2025-03-26 07:20
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('lessons', '0005_alter_quiz_time_limit'),
11+
('uploads', '0004_remove_uploadrecord_profile_uploadrecord_user'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='assignment',
17+
name='upload_record',
18+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='uploads.uploadrecord'),
19+
),
20+
]

backend/apps/lessons/models.py

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from django.db import models
22
from django.contrib.auth.models import User
3-
# from backend.apps.uploads.models import UploadRecord - This will be revised and corrected
3+
from apps.uploads.models import UploadRecord
44
from django.core.exceptions import ValidationError
55
from django.core.validators import MinValueValidator
66

@@ -19,8 +19,8 @@ class Assignment(models.Model): # Assignments: Represents assignments given to
1919
description = models.TextField(blank=True)
2020
assignment_type = models.CharField(max_length=2, choices=ASSIGNMENT_TYPES)
2121

22-
# Upload_record (ForeignKey to UploadRecord) - Currently not referenced correctly but will be when branch is updated
23-
# upload_record = models.ForeignKey(UploadRecord, on_delete=models.SET_NULL, null=True, blank=True)
22+
# Upload_record (ForeignKey to UploadRecord)
23+
upload_record = models.ForeignKey(UploadRecord, on_delete=models.SET_NULL, null=True, blank=True)
2424

2525
# Student (ForeignKey to User as there's no model exclusively for student)
2626
student = models.ForeignKey(User, on_delete=models.CASCADE, related_name="assignments", null=True, blank=True)
@@ -31,6 +31,22 @@ class Assignment(models.Model): # Assignments: Represents assignments given to
3131
def __str__(self):
3232
return self.title
3333

34+
# Helper methods
35+
@classmethod
36+
def get_assignment(cls, assignment_id):
37+
try:
38+
return cls.objects.get(pk=assignment_id)
39+
except cls.DoesNotExist:
40+
return None
41+
42+
def update_assignment(self, data):
43+
for field, value in data.items():
44+
setattr(self, field, value)
45+
self.save()
46+
return self
47+
48+
def delete_assignment(self):
49+
self.delete()
3450

3551
class Quiz(models.Model): # Quizzes: Represents quizzes linked to assignments.
3652
class Meta:
@@ -46,6 +62,14 @@ class Meta:
4662
def __str__(self):
4763
return f"Quiz for {self.assignment.title}"
4864

65+
# Helper Method(s)
66+
@classmethod
67+
def get_quiz(cls, quiz_id):
68+
try:
69+
return cls.objects.get(pk=quiz_id)
70+
except cls.DoesNotExist:
71+
return None
72+
4973

5074
class Question(models.Model): # Questions: Represents individual questions within a quiz.
5175
QUESTION_TYPES = [
@@ -57,13 +81,29 @@ class Question(models.Model): # Questions: Represents individual questions with
5781
question_type = models.CharField(max_length=2, choices=QUESTION_TYPES)
5882
order_of_question = models.PositiveIntegerField()
5983
question_text = models.TextField()
60-
# New field to store points/score for the question
6184
points = models.PositiveIntegerField(default=1, help_text="Points assigned to this question")
6285

6386
def __str__(self):
6487
quiz_title = self.quiz.assignment.title if self.quiz and self.quiz.assignment else "Unknown Quiz"
6588
return f"Question {self.order_of_question} - {quiz_title}"
6689

90+
# Helper Methods
91+
@classmethod
92+
def get_question(cls, question_id):
93+
try:
94+
return cls.objects.get(pk=question_id)
95+
except cls.DoesNotExist:
96+
return None
97+
98+
def update_question(self, data):
99+
for field, value in data.items():
100+
setattr(self, field, value)
101+
self.save()
102+
return self
103+
104+
def delete_question(self):
105+
self.delete()
106+
67107

68108
class Choice(models.Model): # Choices: Store the possible answer choices for multiple choice questions.
69109
# Fields
@@ -74,15 +114,48 @@ class Choice(models.Model): # Choices: Store the possible answer choices for mu
74114
def __str__(self):
75115
return f"Choice: {self.choice_text} ({'Correct' if self.is_correct else 'Incorrect'})"
76116

117+
# Helper Methods
118+
@classmethod
119+
def get_choices(cls, question):
120+
return cls.objects.filter(question=question)
121+
122+
@classmethod
123+
def delete_choices(cls, question):
124+
cls.objects.filter(question=question).delete()
125+
# self.delete()
126+
127+
@classmethod
128+
def bulk_create_choices(cls, choices_data, question):
129+
# double check **choice_data - saw this online ;)
130+
choices = [cls(question=question, **choice_data) for choice_data in choices_data]
131+
return cls.objects.bulk_create(choices)
132+
133+
@classmethod
134+
def bulk_update_choices(cls, choices_data, question):
135+
existing_choices = cls.objects.filter(question=question)
136+
137+
choices_to_update = []
138+
for choice_data in choices_data:
139+
choice_id = choice_data.get("id")
140+
choice_obj = existing_choices.filter(id=choice_id).first()
141+
142+
if choice_obj:
143+
for field, value in choice_data.items():
144+
setattr(choice_obj, field, value)
145+
choices_to_update.append(choice_obj)
146+
147+
if choices_to_update:
148+
cls.objects.bulk_update(choices_to_update, ["choice_text", "is_correct"])
149+
77150

78151
class Solution(models.Model): # Solutions: Represents correct answers to questions.
79152
# Fields
80153
question = models.ForeignKey(Question, on_delete=models.CASCADE)
81154
choices = models.ManyToManyField(Choice, blank=True) # for multiple choice questions
82155
short_answer_text = models.TextField(blank=True, null=True) # for short answer questions
83-
84-
# New validation that ensures MC questions have at least one correct answer
156+
85157
def clean(self):
158+
# validation that ensures MC questions have at least one correct answer
86159
if self.question.question_type == 'MC' and not self.choices.filter(is_correct=True).exists():
87160
raise ValidationError("A multiple-choice question must have at least one correct answer.")
88161

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from rest_framework.permissions import BasePermission
2+
3+
4+
# Custom permission to allow access to admins and tutors only.
5+
class IsAdminOrTutor(BasePermission):
6+
def has_permission(self, request, view):
7+
# Allow access if user is an admin or has role as tutor
8+
return request.user.is_staff or (hasattr(request.user, 'profile') and request.user.profile.role == 1)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from rest_framework import serializers
2+
from .models import Assignment, Quiz, Question, Choice
3+
4+
5+
class AssignmentSerializer(serializers.ModelSerializer):
6+
student_username = serializers.CharField(source='student.username', read_only=True)
7+
8+
class Meta:
9+
model = Assignment
10+
fields = [
11+
"id",
12+
"title",
13+
"description",
14+
"assignment_type",
15+
"deadline",
16+
"student",
17+
"student_username",
18+
"upload_record",
19+
"upload_record_id"
20+
]
21+
22+
23+
class QuizSerializer(serializers.ModelSerializer):
24+
class Meta:
25+
model = Quiz
26+
fields = ["id", "assignment", "time_limit", "num_of_questions", "attempts", "is_active"]
27+
28+
29+
class QuestionSerializer(serializers.ModelSerializer):
30+
class Meta:
31+
model = Question
32+
fields = ["id", "quiz", "question_type", "order_of_question", "question_text", "points"]
33+
34+
35+
class ChoiceSerializer(serializers.ModelSerializer):
36+
class Meta:
37+
model = Choice
38+
fields = ["id", "question", "choice_text", "is_correct"]

backend/apps/lessons/urls.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django.urls import path
2+
from .views import (
3+
AssignmentDetailView,
4+
AssignmentCreateView,
5+
QuizQuestionListView,
6+
QuestionCreateView,
7+
QuestionUpdateView,
8+
QuestionDeleteView,
9+
ChoiceListView,
10+
ChoiceCreateView,
11+
ChoiceUpdateView,
12+
ChoiceDeleteView
13+
)
14+
15+
urlpatterns = [
16+
# CRUD endpoints for Assignments
17+
path("assignments/<int:pk>/", AssignmentDetailView.as_view(), name="assignment-detail"),
18+
path("assignments/create/", AssignmentCreateView.as_view(), name="assignment-create"),
19+
20+
# CRUD endpoints for Quizzes and Questions
21+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/",
22+
QuizQuestionListView.as_view(), name="quiz-question-list"),
23+
24+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/create/",
25+
QuestionCreateView.as_view(), name="question-create"),
26+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/<int:question_id>/update/",
27+
QuestionUpdateView.as_view(), name="question-update"),
28+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/<int:question_id>/delete/",
29+
QuestionDeleteView.as_view(), name="question-delete"),
30+
31+
# CRUD endpoints for Choices
32+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/<int:question_id>/choices/",
33+
ChoiceListView.as_view(), name="choice-list"),
34+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/<int:question_id>/choices/create/",
35+
ChoiceCreateView.as_view(), name="choice-create"),
36+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/<int:question_id>/choices/update/",
37+
ChoiceUpdateView.as_view(), name="choice-update"),
38+
path("assignments/<int:assignment_id>/quiz/<int:quiz_id>/questions/<int:question_id>/choices/delete/",
39+
ChoiceDeleteView.as_view(), name="choice-delete")
40+
]

0 commit comments

Comments
 (0)