Skip to content

Commit

Permalink
Add upgrade to remediate end_timestamps for mastery logs.
Browse files Browse the repository at this point in the history
  • Loading branch information
rtibbles committed Nov 21, 2024
1 parent 4dd33e2 commit b8690cc
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 0 deletions.
114 changes: 114 additions & 0 deletions kolibri/core/logger/test/test_upgrades.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from uuid import uuid4

from django.test import TestCase
from django.utils import timezone

from kolibri.core.auth.models import Facility
from kolibri.core.auth.models import FacilityUser
from kolibri.core.logger.models import AttemptLog
from kolibri.core.logger.models import ContentSessionLog
from kolibri.core.logger.models import ContentSummaryLog
from kolibri.core.logger.models import MasteryLog
from kolibri.core.logger.upgrade import fix_masterylog_end_timestamps


class MasteryLogEndTimestampUpgradeTest(TestCase):
def setUp(self):
self.facility = Facility.objects.create()
self.user = FacilityUser.objects.create(
username="learner", facility=self.facility
)
now = timezone.now()

# Create base content summary log
self.summary_log = ContentSummaryLog.objects.create(
user=self.user,
content_id=uuid4().hex,
channel_id=uuid4().hex,
kind="exercise",
start_timestamp=now,
end_timestamp=now + timezone.timedelta(minutes=10),
)

# Case 1: MasteryLog with completion timestamp
self.completed_session = ContentSessionLog.objects.create(
user=self.user,
content_id=self.summary_log.content_id,
channel_id=self.summary_log.channel_id,
kind="exercise",
start_timestamp=now,
end_timestamp=now + timezone.timedelta(minutes=5),
)
self.completed_mastery = MasteryLog.objects.create(
user=self.user,
summarylog=self.summary_log,
mastery_level=1,
start_timestamp=now,
end_timestamp=now,
completion_timestamp=now + timezone.timedelta(minutes=5),
)

# Case 2: MasteryLog with attempts but no completion
self.attempt_session = ContentSessionLog.objects.create(
user=self.user,
content_id=self.summary_log.content_id,
channel_id=self.summary_log.channel_id,
kind="exercise",
start_timestamp=now,
end_timestamp=now + timezone.timedelta(minutes=3),
)

self.attempt_mastery = MasteryLog.objects.create(
user=self.user,
summarylog=self.summary_log,
mastery_level=2,
start_timestamp=now,
end_timestamp=now,
)
self.attempt = AttemptLog.objects.create(
masterylog=self.attempt_mastery,
sessionlog=self.attempt_session,
start_timestamp=now,
end_timestamp=now + timezone.timedelta(minutes=3),
complete=True,
correct=1,
)

# Case 3: MasteryLog with only summary log
self.summary_session = ContentSessionLog.objects.create(
user=self.user,
content_id=self.summary_log.content_id,
channel_id=self.summary_log.channel_id,
kind="exercise",
start_timestamp=now,
end_timestamp=now,
)
self.summary_only_mastery = MasteryLog.objects.create(
user=self.user,
summarylog=self.summary_log,
mastery_level=3,
start_timestamp=now,
end_timestamp=now,
)

fix_masterylog_end_timestamps()

def test_completion_timestamp_case(self):
"""Test MasteryLog with completion_timestamp gets updated correctly"""
self.completed_mastery.refresh_from_db()
self.assertEqual(
self.completed_mastery.end_timestamp,
self.completed_mastery.completion_timestamp,
)

def test_attempt_logs_case(self):
"""Test MasteryLog with attempt logs gets end_timestamp from last attempt"""
self.attempt_mastery.refresh_from_db()
self.assertEqual(self.attempt_mastery.end_timestamp, self.attempt.end_timestamp)

def test_summary_log_case(self):
"""Test MasteryLog with only summary log gets end_timestamp from summary"""
self.summary_only_mastery.refresh_from_db()
self.assertEqual(
self.summary_only_mastery.end_timestamp, self.summary_log.end_timestamp
)
39 changes: 39 additions & 0 deletions kolibri/core/logger/upgrade.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"""
A file to contain specific logic to handle version upgrades in Kolibri.
"""
from django.db.models import F
from django.db.models import Max
from django.db.models import OuterRef
from django.db.models import Subquery

from kolibri.core.logger.models import AttemptLog
from kolibri.core.logger.models import ContentSummaryLog
from kolibri.core.logger.models import ExamLog
from kolibri.core.logger.models import MasteryLog
from kolibri.core.logger.utils.attempt_log_consolidation import (
consolidate_quiz_attempt_logs,
)
Expand Down Expand Up @@ -57,3 +63,36 @@ def fix_duplicated_attempt_logs():
item and non-null masterylog_id.
"""
consolidate_quiz_attempt_logs(AttemptLog.objects.all())


@version_upgrade(old_version=">0.15.0,<0.18.0")
def fix_masterylog_end_timestamps():
"""
Fix any MasteryLogs that have an end_timestamp that was not updated after creation due to a bug in the
integrated logging API endpoint.
"""
# First, fix the MasteryLogs that have a completion timestamp - and just use that for the end timestamp.
MasteryLog.objects.filter(
end_timestamp=F("start_timestamp"), completion_timestamp__isnull=False
).update(end_timestamp=F("completion_timestamp"))
# Then, fix the MasteryLogs that don't have a completion timestamp - infer from the end_timestamp of the last attempt.
attempt_subquery = (
AttemptLog.objects.filter(masterylog=OuterRef("pk"))
.values("masterylog")
.annotate(max_end=Max("end_timestamp"))
.values("max_end")
)

MasteryLog.objects.filter(
end_timestamp=F("start_timestamp"), attemptlogs__isnull=False
).update(end_timestamp=Subquery(attempt_subquery))
# Finally, fix the MasteryLogs that don't have a completion timestamp or any attempts - just set the end_timestamp to the end_timestamp of the summary log.
summary_subquery = ContentSummaryLog.objects.filter(
masterylogs=OuterRef("pk")
).values("end_timestamp")

MasteryLog.objects.filter(
end_timestamp=F("start_timestamp"),
completion_timestamp__isnull=True,
attemptlogs__isnull=True,
).update(end_timestamp=Subquery(summary_subquery))

0 comments on commit b8690cc

Please sign in to comment.