Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
218 changes: 218 additions & 0 deletions kolibri/core/courses/test/test_api.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import uuid

from django.urls import reverse
from django.utils import timezone
from le_utils.constants import content_kinds
from le_utils.constants import modalities
from rest_framework import status
from rest_framework.test import APITestCase

from .. import models
from ..models import TestType
from ..models import UnitPhase
from ..models import UnitTestAssignment
from kolibri.core.auth.constants import collection_kinds
from kolibri.core.auth.models import AdHocGroup
from kolibri.core.auth.models import Classroom
Expand All @@ -15,6 +20,9 @@
from kolibri.core.auth.test.helpers import provision_device
from kolibri.core.content.models import ChannelMetadata
from kolibri.core.content.models import ContentNode
from kolibri.core.logger.models import ContentSummaryLog
from kolibri.core.logger.models import MasteryLog
from kolibri.core.logger.utils.pre_post_test import get_synthetic_content_id

DUMMY_PASSWORD = "password"

Expand Down Expand Up @@ -694,6 +702,216 @@ def test_create_course_session_without_channel_sets_null(self):
self.assertIsNone(session.channel_version)


class CourseSessionProgressAPITestCase(APITestCase):
"""Tests for unit_phase, active_unit_number, active_unit_title, test_learner_progress."""

databases = "__all__"

@classmethod
def setUpTestData(cls):
provision_device()
cls.facility = Facility.objects.create(name="ProgressFac")
cls.admin = FacilityUser.objects.create(
username="progressadmin", facility=cls.facility
)
cls.admin.set_password(DUMMY_PASSWORD)
cls.admin.save()
cls.facility.add_admin(cls.admin)

cls.learner1 = FacilityUser.objects.create(
username="learner1", facility=cls.facility
)
cls.learner1.set_password(DUMMY_PASSWORD)
cls.learner1.save()

cls.learner2 = FacilityUser.objects.create(
username="learner2", facility=cls.facility
)
cls.learner2.set_password(DUMMY_PASSWORD)
cls.learner2.save()

cls.classroom = Classroom.objects.create(
name="ProgressClass", parent=cls.facility
)
cls.classroom.add_member(cls.learner1)
cls.classroom.add_member(cls.learner2)

channel_id = uuid.uuid4().hex
cls.root_node = ContentNode.objects.create(
id=uuid.uuid4().hex,
channel_id=channel_id,
content_id=uuid.uuid4().hex,
available=True,
title="Channel Root",
)
cls.channel = ChannelMetadata.objects.create(
id=channel_id,
name="Progress Test Channel",
version=1,
root=cls.root_node,
min_schema_version="1",
)
cls.course = ContentNode.objects.create(
id=uuid.uuid4().hex,
channel_id=channel_id,
content_id=uuid.uuid4().hex,
parent=cls.root_node,
available=True,
modality=modalities.COURSE,
title="Test Course",
)
cls.unit = ContentNode.objects.create(
id=uuid.uuid4().hex,
channel_id=channel_id,
content_id=uuid.uuid4().hex,
parent=cls.course,
available=True,
modality=modalities.UNIT,
title="Unit One",
)
# Rebuild MPTT tree so lft/rght/level values are consistent
ContentNode.objects.rebuild()

cls.course_session = models.CourseSession.objects.create(
is_active=True,
collection=cls.classroom,
created_by=cls.admin,
course=cls.course.id,
title=cls.course.title,
)
# Assign the classroom as a recipient
models.CourseSessionAssignment.objects.create(
course_session=cls.course_session,
collection=cls.classroom,
assigned_by=cls.admin,
)

def _make_learner_log(self, user, content_id, complete):
sl = ContentSummaryLog.objects.create(
user=user,
content_id=content_id,
channel_id=None,
kind=content_kinds.QUIZ,
start_timestamp=timezone.now(),
)
MasteryLog.objects.create(
user=user,
summarylog=sl,
complete=complete,
mastery_level=-1,
start_timestamp=timezone.now(),
)

def _get_session_data(self):
"""Return the first (and only) item from the list endpoint."""
self.client.login(username=self.admin.username, password=DUMMY_PASSWORD)
resp = self.client.get(
reverse("kolibri:core:coursesession-list"),
{"collection": self.classroom.id},
)
self.assertEqual(resp.status_code, 200)
items = [i for i in resp.data if str(i["id"]) == str(self.course_session.id)]
self.assertEqual(len(items), 1)
return items[0]

def test_no_test_returns_pre_test_pending_and_null_progress(self):
data = self._get_session_data()
self.assertEqual(data["unit_phase"], UnitPhase.PreTestPending)
self.assertIsNone(data["test_learner_progress"])
self.assertIsNotNone(data["active_unit_number"])
self.assertEqual(data["active_unit_number"], 1)
self.assertEqual(data["active_unit_title"], self.unit.title)

def test_pre_test_active_returns_correct_progress(self):
UnitTestAssignment.objects.create(
course_session=self.course_session,
unit_contentnode_id=self.unit.id,
collection=self.classroom,
test_type=TestType.Pre,
activated_by=self.admin,
closed=False,
)
synthetic_cid = get_synthetic_content_id(
str(self.course_session.id), str(self.unit.id), TestType.Pre
)
self._make_learner_log(self.learner1, synthetic_cid, True)
self._make_learner_log(self.learner2, synthetic_cid, False)

data = self._get_session_data()
self.assertEqual(data["unit_phase"], UnitPhase.PreTestActive)
self.assertEqual(data["active_unit_number"], 1)
progress = data["test_learner_progress"]
self.assertIsNotNone(progress)
self.assertEqual(progress["completed"], 1)
self.assertEqual(progress["started"], 1)
self.assertEqual(progress["notStarted"], 0)
self.assertEqual(progress["total"], 2)

def test_post_test_active_returns_correct_progress(self):
UnitTestAssignment.objects.create(
course_session=self.course_session,
unit_contentnode_id=self.unit.id,
collection=self.classroom,
test_type=TestType.Pre,
activated_by=self.admin,
closed=True,
)
UnitTestAssignment.objects.create(
course_session=self.course_session,
unit_contentnode_id=self.unit.id,
collection=self.classroom,
test_type=TestType.Post,
activated_by=self.admin,
closed=False,
)
synthetic_cid = get_synthetic_content_id(
str(self.course_session.id), str(self.unit.id), TestType.Post
)
self._make_learner_log(self.learner1, synthetic_cid, True)

data = self._get_session_data()
self.assertEqual(data["unit_phase"], UnitPhase.PostTestActive)
progress = data["test_learner_progress"]
self.assertIsNotNone(progress)
self.assertEqual(progress["completed"], 1)
self.assertEqual(progress["started"], 0)
self.assertEqual(progress["notStarted"], 1)
self.assertEqual(progress["total"], 2)

def test_complete_phase_returns_post_test_progress(self):
UnitTestAssignment.objects.create(
course_session=self.course_session,
unit_contentnode_id=self.unit.id,
collection=self.classroom,
test_type=TestType.Pre,
activated_by=self.admin,
closed=True,
)
UnitTestAssignment.objects.create(
course_session=self.course_session,
unit_contentnode_id=self.unit.id,
collection=self.classroom,
test_type=TestType.Post,
activated_by=self.admin,
closed=True,
)
synthetic_cid = get_synthetic_content_id(
str(self.course_session.id), str(self.unit.id), TestType.Post
)
self._make_learner_log(self.learner1, synthetic_cid, True)
self._make_learner_log(self.learner2, synthetic_cid, True)

data = self._get_session_data()
self.assertEqual(data["unit_phase"], UnitPhase.Complete)
self.assertIsNone(data["active_unit_number"])
progress = data["test_learner_progress"]
self.assertIsNotNone(progress)
self.assertEqual(progress["completed"], 2)
self.assertEqual(progress["started"], 0)
self.assertEqual(progress["notStarted"], 0)
self.assertEqual(progress["total"], 2)


""""
DISCLAIMER: Some parts of these tests were written with an AI assistance.
I have reviewed and validated the generated tests
Expand Down
Loading
Loading