Skip to content

Commit 1d0c40b

Browse files
authored
Merge pull request #63 from AvaCodeSolutions/feat/17/quiz-response-model
feat: #17 add QuizSubmission model
2 parents 67e7264 + c7b7beb commit 1d0c40b

File tree

5 files changed

+125
-25
lines changed

5 files changed

+125
-25
lines changed

django_email_learning/migrations/0001_initial.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 5.2.8 on 2025-11-28 08:19
1+
# Generated by Django 5.2.8 on 2025-11-29 19:01
22

33
import django.core.validators
44
import django.db.models.deletion
@@ -417,8 +417,6 @@ class Migration(migrations.Migration):
417417
verbose_name="ID",
418418
),
419419
),
420-
("quiz_score", models.IntegerField(blank=True, null=True)),
421-
("is_quiz_passed", models.BooleanField(blank=True, null=True)),
422420
("times_sent", models.IntegerField(default=1)),
423421
(
424422
"course_content",
@@ -440,6 +438,30 @@ class Migration(migrations.Migration):
440438
),
441439
],
442440
),
441+
migrations.CreateModel(
442+
name="QuizSubmission",
443+
fields=[
444+
(
445+
"id",
446+
models.BigAutoField(
447+
auto_created=True,
448+
primary_key=True,
449+
serialize=False,
450+
verbose_name="ID",
451+
),
452+
),
453+
("score", models.IntegerField()),
454+
("is_passed", models.BooleanField()),
455+
("submitted_at", models.DateTimeField(auto_now_add=True)),
456+
(
457+
"sent_item",
458+
models.ForeignKey(
459+
on_delete=django.db.models.deletion.CASCADE,
460+
to="django_email_learning.sentitem",
461+
),
462+
),
463+
],
464+
),
443465
migrations.AddConstraint(
444466
model_name="enrollment",
445467
constraint=models.UniqueConstraint(

django_email_learning/models.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,6 @@ class SentItem(models.Model):
340340
enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE)
341341
course_content = models.ForeignKey(CourseContent, on_delete=models.CASCADE)
342342
send_events = models.ManyToManyField(EventTimestamp)
343-
quiz_score = models.IntegerField(null=True, blank=True)
344-
is_quiz_passed = models.BooleanField(null=True, blank=True)
345343
times_sent = models.IntegerField(default=1)
346344

347345
class Meta:
@@ -353,3 +351,16 @@ def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
353351
if not self.send_events.exists():
354352
timestamp = EventTimestamp.objects.create()
355353
self.send_events.add(timestamp)
354+
355+
356+
class QuizSubmission(models.Model):
357+
sent_item = models.ForeignKey(SentItem, on_delete=models.CASCADE)
358+
score = models.IntegerField()
359+
is_passed = models.BooleanField()
360+
submitted_at = models.DateTimeField(auto_now_add=True)
361+
362+
def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
363+
if self.sent_item.course_content.type != "quiz":
364+
raise ValidationError("Sent item must be associated with a quiz content.")
365+
self.full_clean()
366+
super().save(*args, **kwargs)

tests/test_models/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
Course,
66
BlockedEmail,
77
Learner,
8+
Enrollment,
9+
CourseContent,
810
)
911
import pytest
1012

@@ -60,3 +62,25 @@ def learner(db) -> Learner:
6062
learner = Learner(email="[email protected]")
6163
learner.save()
6264
return learner
65+
66+
67+
@pytest.fixture()
68+
def enrollment(db, learner, course) -> Enrollment:
69+
enrollment = Enrollment.objects.create(learner=learner, course=course)
70+
return enrollment
71+
72+
73+
@pytest.fixture
74+
def course_lesson_content(db, course, lesson) -> CourseContent:
75+
content = CourseContent.objects.create(
76+
course=course, priority=1, type="lesson", lesson=lesson, waiting_period=10
77+
)
78+
return content
79+
80+
81+
@pytest.fixture
82+
def course_quiz_content(db, course, quiz) -> CourseContent:
83+
content = CourseContent.objects.create(
84+
course=course, priority=2, type="quiz", quiz=quiz, waiting_period=5
85+
)
86+
return content
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from django_email_learning.models import QuizSubmission, SentItem
2+
from django.core.exceptions import ValidationError
3+
import pytest
4+
5+
6+
def test_quiz_submission_creation(db, course_quiz_content, enrollment):
7+
sent_item = SentItem.objects.create(
8+
enrollment=enrollment,
9+
course_content=course_quiz_content,
10+
)
11+
submission = QuizSubmission.objects.create(
12+
sent_item=sent_item,
13+
score=85,
14+
is_passed=False,
15+
)
16+
assert submission.id is not None
17+
assert submission.score == 85
18+
assert not submission.is_passed
19+
assert submission.submitted_at is not None
20+
21+
22+
def test_quiz_submission_for_lesson_content(db, course_lesson_content, enrollment):
23+
sent_item = SentItem.objects.create(
24+
enrollment=enrollment,
25+
course_content=course_lesson_content,
26+
)
27+
28+
with pytest.raises(Exception) as exc_info:
29+
QuizSubmission.objects.create(
30+
sent_item=sent_item,
31+
score=90,
32+
is_passed=True,
33+
)
34+
assert "Sent item must be associated with a quiz content." in str(exc_info.value)
35+
36+
37+
@pytest.mark.parametrize(
38+
"score, is_passed",
39+
[
40+
(None, True),
41+
(50, None),
42+
],
43+
)
44+
def test_sent_item_invalid_quiz_submission_fields(
45+
db, score, is_passed, course_quiz_content, enrollment
46+
):
47+
sent_item = SentItem.objects.create(
48+
enrollment=enrollment,
49+
course_content=course_quiz_content,
50+
)
51+
52+
with pytest.raises(ValidationError):
53+
QuizSubmission.objects.create(
54+
sent_item=sent_item,
55+
score=score,
56+
is_passed=is_passed,
57+
)

tests/test_models/test_sent_item.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,25 @@
1-
from django_email_learning.models import SentItem, Enrollment, CourseContent
1+
from django_email_learning.models import SentItem
22
import pytest
33

44

5-
@pytest.fixture
6-
def course_content(db, course, lesson) -> CourseContent:
7-
content = CourseContent.objects.create(
8-
course=course, priority=1, type="lesson", lesson=lesson, waiting_period=10
9-
)
10-
return content
11-
12-
13-
@pytest.fixture
14-
def enrollment(db, learner, course) -> Enrollment:
15-
enrollment = Enrollment.objects.create(learner=learner, course=course)
16-
return enrollment
17-
18-
19-
def test_sent_item_create(db, course_content, enrollment):
5+
def test_sent_item_create(db, course_lesson_content, enrollment):
206
sent_item = SentItem.objects.create(
217
enrollment=enrollment,
22-
course_content=course_content,
8+
course_content=course_lesson_content,
239
)
2410
assert sent_item.id is not None
2511
assert sent_item.send_events.count() == 1
2612

2713

28-
def test_sent_item_unique_constraint(db, course_content, enrollment):
14+
def test_sent_item_unique_constraint(db, course_lesson_content, enrollment):
2915
SentItem.objects.create(
3016
enrollment=enrollment,
31-
course_content=course_content,
17+
course_content=course_lesson_content,
3218
)
3319
with pytest.raises(Exception) as exc_info:
3420
SentItem.objects.create(
3521
enrollment=enrollment,
36-
course_content=course_content,
22+
course_content=course_lesson_content,
3723
)
3824
assert (
3925
"sent item with this enrollment and course content already exists"

0 commit comments

Comments
 (0)