-
Notifications
You must be signed in to change notification settings - Fork 686
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix masterylog end timestamp issues #12870
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
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 attempts | ||
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, | ||
) | ||
|
||
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, | ||
) | ||
|
||
AttemptLog.objects.create( | ||
masterylog=self.attempt_mastery, | ||
sessionlog=self.attempt_session, | ||
start_timestamp=now, | ||
end_timestamp=now - timezone.timedelta(minutes=2), | ||
complete=True, | ||
correct=1, | ||
) | ||
|
||
self.last_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 2: 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_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.last_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 | ||
) |
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, | ||
) | ||
|
@@ -57,3 +63,32 @@ 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(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what would be now a KDP scenario? So, if I understand this correctly, in KDP we should either apply both upgrades, in order, or skip both. Correct? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should skip both, yes. Let the Kolibris sort out their data before syncing to KDP. |
||
""" | ||
Fix any MasteryLogs that have an end_timestamp that was not updated after creation due to a bug in the | ||
integrated logging API endpoint. | ||
""" | ||
# Fix the MasteryLogs that that have attempts - 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)) | ||
# Fix the MasteryLogs that don't have 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)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just makes it so that the function below is only run when the current version is between these versions? Like if you are going from
>0.15.0,<0.18.0
to anything else, this will run?If so then what if you're upgrading from 0.15.2 to 0.17.3, then you upgrade from 0.17.3 to >=0.18.x would this end up being run again?
Not that it'd matter since it'd basically do nothing in that case since it'd have been fixed already, I'm just asking out of curiosity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, except this code only exists in 0.18.0, so I would be mystified if it managed to run in 0.17.3 :)