Skip to content

Commit

Permalink
The SCORM API sends back times with timezone info
Browse files Browse the repository at this point in the history
fixes #296

I've been through api.js and checked that all time operations use
luxon objects instead of numbers of milliseconds.

When the API reports SCORM elements back to the server, it represents
the time as an ISO format string with timezone information. This avoids
ambiguous times due to daylight savings.
  • Loading branch information
christianp committed Nov 24, 2023
1 parent e769f46 commit 79a00af
Show file tree
Hide file tree
Showing 2 changed files with 35 additions and 23 deletions.
13 changes: 12 additions & 1 deletion numbas_lti/save_scorm_data.py
Expand Up @@ -5,6 +5,7 @@
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
import logging
from pytz.exceptions import AmbiguousTimeError
import re

from . import tasks
Expand All @@ -19,7 +20,17 @@ def save_scorm_data(attempt,batches):
needs_diff = False
for id,elements in batches.items():
for element in elements:
time = timezone.make_aware(datetime.datetime.fromtimestamp(element['time']))

if 'time_iso' in element:
time = datetime.datetime.fromisoformat(re.sub(r'Z$','+00:00',element['time_iso']))
else:
# versions of the LTI provider before v3.4 returned the time as a timestamp without timezone info.
# In case there are still clients with that version of the SCORM API open, continue loading that.
try:
time = timezone.make_aware(datetime.datetime.fromtimestamp(element['time']))
except AmbiguousTimeError:
time = timezone.make_aware(datetime.datetime.fromtimestamp(element['time']), is_dst=False)

if attempt.completion_status=='completed' and (attempt.end_time is None or time > attempt.end_time):
continue # don't save new elements after the exam has been created

Expand Down
45 changes: 23 additions & 22 deletions numbas_lti/static/api.js
Expand Up @@ -9,6 +9,10 @@
*/
var _ = gettext;

function get_now() {
return DateTime.utc();
}

function SCORM_API(options) {
var data = options.scorm_cmi;
var sc = this;
Expand Down Expand Up @@ -121,7 +125,7 @@ SCORM_API.prototype = {
var unavailable_time = this.unavailable_time();
if(unavailable_time) {
Array.from(document.querySelectorAll('.available-until')).forEach(function(s) {
s.textContent = unavailable_time.toLocaleString(DateTime.DATETIME_FULL);
s.textContent = unavailable_time.toLocal().toLocaleString(DateTime.DATETIME_FULL);
});
}
var deadline_change_display = document.getElementById('deadline-change-display');
Expand Down Expand Up @@ -197,7 +201,7 @@ SCORM_API.prototype = {
elements: stored_data.sent[id].map(function(e) {
return new SCORMData(e.key,e.value,DateTime.fromSeconds(e.time));
}),
time: DateTime.local()
time: DateTime.utc()
}
}

Expand All @@ -208,8 +212,8 @@ SCORM_API.prototype = {
var elements = this.sent[id].elements;
elements.forEach(function(e) {
var d = data[e.key];
if(!(e.key in data) || data[e.key].time<e.timestamp()) {
data[e.key] = {value:e.value,time:e.timestamp()};
if(!(e.key in data) || data[e.key].time < e.time)) {
data[e.key] = {value: e.value,time: e.time};
}
});
}
Expand All @@ -231,7 +235,7 @@ SCORM_API.prototype = {

// Force review mode from now on if activity is completed - could be out of sync if resuming a session which wasn't saved properly.
if(this.data['cmi.completion_status'] == 'completed') {
if(this.allow_review_from!==null && DateTime.local()<this.allow_review_from && this.data['numbas.user_role'] != 'instructor') {
if(this.allow_review_from!==null && get_now() < this.allow_review_from && this.data['numbas.user_role'] != 'instructor') {
var player = document.getElementById('scorm-player');
if(player) {
player.parentElement.removeChild(player);
Expand Down Expand Up @@ -387,7 +391,7 @@ SCORM_API.prototype = {
unreceived = true;
break;
}
var queued = this.queue.length>0;
var queued = this.queue.length > 0;
var disconnected = !(this.socket_is_open() || this.ajax_is_working());

var unavailable_time = this.unavailable_time();
Expand All @@ -396,10 +400,10 @@ SCORM_API.prototype = {
var ok = !((unreceived || queued || !this.signed_receipt) && (disconnected || this.terminated)) && !this.failed_final_ajax && !show_unavailable_at;

if(!ok) {
this.last_show_warning = DateTime.local();
this.last_show_warning = get_now();
}
warning_linger_duration = this.terminated ? 0 : this.warning_linger_duration;
var show_warning = !ok || (DateTime.local()-this.last_show_warning)<warning_linger_duration;
warning_linger_duration = luxon.Duration.fromMillis(this.terminated ? 0 : this.warning_linger_duration);
var show_warning = !ok || (get_now().diff(this.last_show_warning)) < warning_linger_duration;

var status_display = document.getElementById('status-display');

Expand Down Expand Up @@ -452,7 +456,7 @@ SCORM_API.prototype = {
if(this.offline) {
return true;
}
var now = DateTime.local();
var now = get_now();
if(this.available_from===undefined || this.available_until===undefined) {
return (this.available_from===undefined || now >= this.available_from) && (this.available_until===undefined || now <= this.available_until);
}
Expand All @@ -470,7 +474,7 @@ SCORM_API.prototype = {
batch_sent: function(elements,id) {
this.sent[id] = {
elements: elements,
time: DateTime.local()
time: get_now()
};
this.set_localstorage();
this.callbacks.trigger('batch_sent',elements,id);
Expand All @@ -495,7 +499,7 @@ SCORM_API.prototype = {
var data = {
sent: {},
current: this.data,
save_time: DateTime.local().toMillis()
save_time_iso: get_now().toISO()
}
for(var id in this.sent) {
data.sent[id] = this.make_batch(this.sent[id].elements);
Expand Down Expand Up @@ -641,10 +645,11 @@ SCORM_API.prototype = {

var stuff_to_send = false;
var batches = {};
var now = DateTime.local().toMillis();
var now = get_now();
var ajax_period = luxon.Duration.fromMillis(this.ajax_period);
for(var key in this.sent) {
var dt = now - this.sent[key].time;
if(this.sent[key].elements.length && dt>this.ajax_period) {
var dt = now.diff(this.sent[key].time);
if(this.sent[key].elements.length && dt > ajax_period) {
stuff_to_send = true;
batches[key] = this.make_batch(this.sent[key].elements);
}
Expand Down Expand Up @@ -783,7 +788,7 @@ SCORM_API.prototype = {
if(changed) {
this.data[key] = value;
this.check_key_counts_something(key);
this.queue.push(new SCORMData(key,value, DateTime.local(),this.element_acc++));
this.queue.push(new SCORMData(key, value, get_now(),this.element_acc++));
}
this.callbacks.trigger('SetValue',key,value,changed);
},
Expand Down Expand Up @@ -817,7 +822,7 @@ CallbackHandler.prototype = {

/** A single SCORM data model element, with the time it was set.
*/
function SCORMData(key,value,time,counter) {
function SCORMData(key, value, time, counter) {
this.key = key;
this.value = value;
this.time = time;
Expand All @@ -828,13 +833,9 @@ SCORMData.prototype = {
return {
key: this.key,
value: this.value,
time: this.timestamp(),
time_iso: this.time.toISO(),
counter: this.counter
}
},

timestamp: function() {
return this.time.toSeconds()
}
}

Expand Down

0 comments on commit 79a00af

Please sign in to comment.