diff --git a/django_email_learning/migrations/0001_initial.py b/django_email_learning/migrations/0001_initial.py index 8f9df5d9..9a0b3d31 100644 --- a/django_email_learning/migrations/0001_initial.py +++ b/django_email_learning/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.8 on 2025-11-28 08:19 +# Generated by Django 5.2.8 on 2025-11-29 19:01 import django.core.validators import django.db.models.deletion @@ -417,8 +417,6 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("quiz_score", models.IntegerField(blank=True, null=True)), - ("is_quiz_passed", models.BooleanField(blank=True, null=True)), ("times_sent", models.IntegerField(default=1)), ( "course_content", @@ -440,6 +438,30 @@ class Migration(migrations.Migration): ), ], ), + migrations.CreateModel( + name="QuizSubmission", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("score", models.IntegerField()), + ("is_passed", models.BooleanField()), + ("submitted_at", models.DateTimeField(auto_now_add=True)), + ( + "sent_item", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="django_email_learning.sentitem", + ), + ), + ], + ), migrations.AddConstraint( model_name="enrollment", constraint=models.UniqueConstraint( diff --git a/django_email_learning/models.py b/django_email_learning/models.py index 9b02569f..fc4e4c01 100644 --- a/django_email_learning/models.py +++ b/django_email_learning/models.py @@ -340,8 +340,6 @@ class SentItem(models.Model): enrollment = models.ForeignKey(Enrollment, on_delete=models.CASCADE) course_content = models.ForeignKey(CourseContent, on_delete=models.CASCADE) send_events = models.ManyToManyField(EventTimestamp) - quiz_score = models.IntegerField(null=True, blank=True) - is_quiz_passed = models.BooleanField(null=True, blank=True) times_sent = models.IntegerField(default=1) class Meta: @@ -353,3 +351,16 @@ def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] if not self.send_events.exists(): timestamp = EventTimestamp.objects.create() self.send_events.add(timestamp) + + +class QuizSubmission(models.Model): + sent_item = models.ForeignKey(SentItem, on_delete=models.CASCADE) + score = models.IntegerField() + is_passed = models.BooleanField() + submitted_at = models.DateTimeField(auto_now_add=True) + + def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] + if self.sent_item.course_content.type != "quiz": + raise ValidationError("Sent item must be associated with a quiz content.") + self.full_clean() + super().save(*args, **kwargs) diff --git a/tests/test_models/conftest.py b/tests/test_models/conftest.py index 53928761..24d6325d 100644 --- a/tests/test_models/conftest.py +++ b/tests/test_models/conftest.py @@ -5,6 +5,8 @@ Course, BlockedEmail, Learner, + Enrollment, + CourseContent, ) import pytest @@ -60,3 +62,25 @@ def learner(db) -> Learner: learner = Learner(email="user@example.com") learner.save() return learner + + +@pytest.fixture() +def enrollment(db, learner, course) -> Enrollment: + enrollment = Enrollment.objects.create(learner=learner, course=course) + return enrollment + + +@pytest.fixture +def course_lesson_content(db, course, lesson) -> CourseContent: + content = CourseContent.objects.create( + course=course, priority=1, type="lesson", lesson=lesson, waiting_period=10 + ) + return content + + +@pytest.fixture +def course_quiz_content(db, course, quiz) -> CourseContent: + content = CourseContent.objects.create( + course=course, priority=2, type="quiz", quiz=quiz, waiting_period=5 + ) + return content diff --git a/tests/test_models/test_quiz_submission.py b/tests/test_models/test_quiz_submission.py new file mode 100644 index 00000000..373fed11 --- /dev/null +++ b/tests/test_models/test_quiz_submission.py @@ -0,0 +1,57 @@ +from django_email_learning.models import QuizSubmission, SentItem +from django.core.exceptions import ValidationError +import pytest + + +def test_quiz_submission_creation(db, course_quiz_content, enrollment): + sent_item = SentItem.objects.create( + enrollment=enrollment, + course_content=course_quiz_content, + ) + submission = QuizSubmission.objects.create( + sent_item=sent_item, + score=85, + is_passed=False, + ) + assert submission.id is not None + assert submission.score == 85 + assert not submission.is_passed + assert submission.submitted_at is not None + + +def test_quiz_submission_for_lesson_content(db, course_lesson_content, enrollment): + sent_item = SentItem.objects.create( + enrollment=enrollment, + course_content=course_lesson_content, + ) + + with pytest.raises(Exception) as exc_info: + QuizSubmission.objects.create( + sent_item=sent_item, + score=90, + is_passed=True, + ) + assert "Sent item must be associated with a quiz content." in str(exc_info.value) + + +@pytest.mark.parametrize( + "score, is_passed", + [ + (None, True), + (50, None), + ], +) +def test_sent_item_invalid_quiz_submission_fields( + db, score, is_passed, course_quiz_content, enrollment +): + sent_item = SentItem.objects.create( + enrollment=enrollment, + course_content=course_quiz_content, + ) + + with pytest.raises(ValidationError): + QuizSubmission.objects.create( + sent_item=sent_item, + score=score, + is_passed=is_passed, + ) diff --git a/tests/test_models/test_sent_item.py b/tests/test_models/test_sent_item.py index 808abbc1..e544cdec 100644 --- a/tests/test_models/test_sent_item.py +++ b/tests/test_models/test_sent_item.py @@ -1,39 +1,25 @@ -from django_email_learning.models import SentItem, Enrollment, CourseContent +from django_email_learning.models import SentItem import pytest -@pytest.fixture -def course_content(db, course, lesson) -> CourseContent: - content = CourseContent.objects.create( - course=course, priority=1, type="lesson", lesson=lesson, waiting_period=10 - ) - return content - - -@pytest.fixture -def enrollment(db, learner, course) -> Enrollment: - enrollment = Enrollment.objects.create(learner=learner, course=course) - return enrollment - - -def test_sent_item_create(db, course_content, enrollment): +def test_sent_item_create(db, course_lesson_content, enrollment): sent_item = SentItem.objects.create( enrollment=enrollment, - course_content=course_content, + course_content=course_lesson_content, ) assert sent_item.id is not None assert sent_item.send_events.count() == 1 -def test_sent_item_unique_constraint(db, course_content, enrollment): +def test_sent_item_unique_constraint(db, course_lesson_content, enrollment): SentItem.objects.create( enrollment=enrollment, - course_content=course_content, + course_content=course_lesson_content, ) with pytest.raises(Exception) as exc_info: SentItem.objects.create( enrollment=enrollment, - course_content=course_content, + course_content=course_lesson_content, ) assert ( "sent item with this enrollment and course content already exists"