Skip to content
Permalink
Browse files

stress test

The admin interface now has a "stress test" tab. A stress test is a
resource with no context. Each client viewing a stress test can start a
number of attempts, and send random SCORM data using the same API as
real attempts. You can set attempts to start at a particular time, to
coordinate between PCs.
The idea is to set one or more computers starting attempts, and see
whether the server can save the data correctly.
The interface shows information about the status of each attempt.
  • Loading branch information...
christianp committed Jun 5, 2018
1 parent 7824020 commit 903737cb0969c4a5a482c04400116d6f975072f8
@@ -0,0 +1,29 @@
# Generated by Django 2.0 on 2018-06-04 08:31

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('numbas_lti', '0043_auto_20180530_0845'),
]

operations = [
migrations.CreateModel(
name='StressTest',
fields=[
('resource', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='numbas_lti.Resource')),
],
),
migrations.CreateModel(
name='StressTestNote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField()),
('time', models.DateTimeField(auto_now_add=True)),
('stresstest', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='numbas_lti.StressTest')),
],
),
]
@@ -0,0 +1,19 @@
# Generated by Django 2.0 on 2018-06-04 12:12

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('numbas_lti', '0044_stresstest_stresstestnote'),
]

operations = [
migrations.AlterField(
model_name='attempt',
name='exam',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='attempts', to='numbas_lti.Exam'),
),
]
@@ -4,7 +4,7 @@
from django.contrib.auth.models import User
import requests
from django.utils.text import slugify
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ugettext
from django.core import validators
from channels import Group, Channel
from django.utils import timezone
@@ -141,8 +141,10 @@ class Meta:
def __str__(self):
if self.exam:
return str(self.exam)
else:
elif self.context:
return _('Resource in "{}" - no exam uploaded').format(self.context.name)
else:
return ugettext('Resource with no context')

@property
def slug(self):
@@ -257,7 +259,7 @@ class LTIUserData(models.Model):

class Attempt(models.Model):
resource = models.ForeignKey(Resource,on_delete=models.CASCADE,related_name='attempts')
exam = models.ForeignKey(Exam,on_delete=models.CASCADE,related_name='attempts') # need to keep track of both resource and exam in case the exam later gets overwritten
exam = models.ForeignKey(Exam,on_delete=models.CASCADE,related_name='attempts',null=True) # need to keep track of both resource and exam in case the exam later gets overwritten
user = models.ForeignKey(User,on_delete=models.CASCADE,related_name='attempts')
start_time = models.DateTimeField(auto_now_add=True)
end_time = models.DateTimeField(blank=True,null=True)
@@ -661,3 +663,17 @@ def __str__(self):
@receiver(models.signals.pre_save,sender=EditorLink)
def update_editor_cache_before_save(sender,instance,**kwargs):
instance.update_cache()

class StressTest(models.Model):
resource = models.OneToOneField(Resource,on_delete=models.CASCADE,primary_key=True)

def __str__(self):
return self.resource.creation_time.strftime('%B %d, %Y %H:%M')

def get_absolute_url(self):
return reverse('view_stresstest',args=(self.pk,))

class StressTestNote(models.Model):
stresstest = models.ForeignKey(StressTest,on_delete=models.CASCADE,related_name='notes')
text = models.TextField()
time = models.DateTimeField(auto_now_add=True)
@@ -10,6 +10,8 @@
function SCORM_API(data,attempt_pk,fallback_url) {
var sc = this;

this.callbacks = new CallbackHandler();

this.attempt_pk = attempt_pk;
this.fallback_url = fallback_url;

@@ -71,9 +73,14 @@ function SCORM_API(data,attempt_pk,fallback_url) {
}
}

toggle(status_display,'ok',ok);
toggle(status_display,'disconnected',show_warning);
toggle(status_display,'localstorage-used',sc.localstorage_used||false);
if(status_display) {
toggle(status_display,'ok',ok);
toggle(status_display,'disconnected',show_warning);
toggle(status_display,'localstorage-used',sc.localstorage_used||false);
}

sc.callbacks.trigger('update_interval');

},50);

/** Periodically send data over the websocket
@@ -120,16 +127,19 @@ SCORM_API.prototype = {

this.sent = {};
for(var id in stored_data.sent) {
this.sent[id] = stored_data.sent[id].map(function(e) {
return new SCORMData(e.key,e.value,new Date(e.time*1000));
});
this.sent[id] = {
elements: stored_data.sent[id].map(function(e) {
return new SCORMData(e.key,e.value,new Date(e.time*1000));
}),
time: new Date()
}
}

// merge saved data
for(var id in this.sent) {
this.sent_acc = Math.max(this.sent_acc,id);

var elements = this.sent[id];
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()) {
@@ -157,6 +167,8 @@ SCORM_API.prototype = {
if(this.data['cmi.completion_status'] == 'completed') {
this.data['cmi.mode'] = this.mode = 'review';
}

this.callbacks.trigger('initialise_data');
},

/** Initialise the SCORM API and expose it to the SCORM activity
@@ -189,6 +201,7 @@ SCORM_API.prototype = {
this.check_key_counts_something(key);
}

this.callbacks.trigger('initialise_api');
},

/** Terminate the SCORM API because we were told to by the server, and navigate to the given URL
@@ -198,6 +211,7 @@ SCORM_API.prototype = {
this.Terminate('');
alert(message);
window.location = show_attempts_url;
this.callbacks.trigger('external_kill');
}
},

@@ -212,7 +226,13 @@ SCORM_API.prototype = {
this.socket = new RobustWebSocket(ws_url);

this.socket.onmessage = function(e) {
var d = JSON.parse(e.data);
sc.callbacks.trigger('socket.onmessage',d);
try {
var d = JSON.parse(e.data);
} catch(e) {
console.log("Error reading socket message",e.data);
return;
}

// The server sends back confirmation of each batch of elements it received.
// When we get confirmation, remove that batch from the dict of unconfirmed batches.
@@ -233,16 +253,20 @@ SCORM_API.prototype = {

// resend any batches we didn't get confirmation messages for
for(var id in sc.sent) {
sc.send_elements_socket(sc.sent[id],id);
sc.send_elements_socket(sc.sent[id].elements,id);
}

// send the current queue
sc.send_queue_socket();

sc.callbacks.trigger('socket.onopen');
}

this.socket.onerror = function() {
sc.callbacks.trigger('socket.onerror');
}
this.socket.onclose = function() {
sc.callbacks.trigger('socket.onclose');
}

},
@@ -252,16 +276,21 @@ SCORM_API.prototype = {
* @param {number} id
*/
batch_sent: function(elements,id) {
this.sent[id] = elements;
this.sent[id] = {
elements: elements,
time: new Date()
};
this.set_localstorage();
this.callbacks.trigger('batch_sent',elements,id);
},

/** Call when we've received confirmation that the server got the batch with the given id
* @param {number} id
*/
batch_received: function(id) {
delete sc.sent[id];
delete this.sent[id];
this.set_localstorage();
this.callbacks.trigger('batch_received',id);
},

/** Store information which hasn't been confirmed received by the server to localStorage.
@@ -272,13 +301,14 @@ SCORM_API.prototype = {
sent: {}
}
for(var id in this.sent) {
data.sent[id] = this.make_batch(this.sent[id]);
data.sent[id] = this.make_batch(this.sent[id].elements);
}
window.localStorage.setItem(this.localstorage_key, JSON.stringify(data));
this.localstorage_used = true;
} catch(e) {
this.localstorage_used = false;
}
this.callbacks.trigger('set_localstorage');
},

/** Load saved information from localStorage.
@@ -346,6 +376,7 @@ SCORM_API.prototype = {
this.send_elements_socket(this.queue,id);
this.batch_sent(this.queue.slice(),id);
this.queue = [];
this.callbacks.trigger('send_queue_socket');
},

/** Send the given list of data model elements to the server, with the given batch ID.
@@ -361,6 +392,7 @@ SCORM_API.prototype = {

var out = this.make_batch(elements);
this.socket.send(JSON.stringify({id:id, data:out}));
this.callbacks.trigger('send_elements_socket',elements,id);
return true;
},

@@ -383,8 +415,8 @@ SCORM_API.prototype = {
send_ajax: function() {
var sc = this;

if(this.socket_is_open()) {
// return;
if(this.socket_is_open() || this.pending_ajax) {
return;
}
if(this.queue.length) {
var id = this.sent_acc++;
@@ -394,10 +426,12 @@ SCORM_API.prototype = {

var stuff_to_send = false;
var out = {};
var now = new Date();
for(var key in this.sent) {
if(this.sent[key].length) {
var dt = now - this.sent[key].time;
if(this.sent[key].elements.length && dt>this.ajax_period) {
stuff_to_send = true;
out[key] = this.make_batch(this.sent[key]);
out[key] = this.make_batch(this.sent[key].elements);
}
}
if(!stuff_to_send) {
@@ -406,6 +440,8 @@ SCORM_API.prototype = {

var csrftoken = getCookie('csrftoken');

this.pending_ajax = true;

var request = fetch(this.fallback_url, {
method: 'POST',
credentials: 'same-origin',
@@ -418,17 +454,24 @@ SCORM_API.prototype = {
request
.then(
function(response) {
sc.pending_ajax = false;
if(!response.ok) {
console.error('failed to send SCORM data over HTTP');
response.text().then(function(t){console.error('SCORM HTTP fallback error message: '+t)});
response.text().then(function(t){
console.error('SCORM HTTP fallback error message: '+t);
sc.callbacks.trigger('ajax.failed',t);
});
sc.last_ajax_succeeded = false;
return Promise.reject(error.message);
}
sc.last_ajax_succeeded = true;
sc.callbacks.trigger('ajax.succeeded');
return response.json();
},
function(error) {
sc.pending_ajax = false;
console.error('failed to send SCORM data over HTTP: '+error.message);
sc.callbacks.trigger('ajax.failed',error.message);
sc.last_ajax_succeeded = false;
return Promise.reject(error.message);
}
@@ -443,16 +486,20 @@ SCORM_API.prototype = {
}
)
;
this.callbacks.trigger('send_ajax');
},

Initialize: function(b) {if(b!='' || this.initialized || this.terminated) {
Initialize: function(b) {
this.callbacks.trigger('Initialize',b);
if(b!='' || this.initialized || this.terminated) {
return false;
}
this.initialized = true;
return true;
},

Terminate: function(b) {
this.callbacks.trigger('Terminate',b);
if(b!='' || !this.initialized || this.terminated) {
return false;
}
@@ -498,13 +545,36 @@ SCORM_API.prototype = {
this.check_key_counts_something(key);
this.queue.push(new SCORMData(key,value, new Date(),this.element_acc++));
}
this.callbacks.trigger('SetValue',key,value,changed);
},

Commit: function(s) {
this.callbacks.trigger('Commit');
return true;
}
}

function CallbackHandler() {
this.callbacks = {};
}
CallbackHandler.prototype = {
on: function(key,fn) {
if(this.callbacks[key] === undefined) {
this.callbacks[key] = [];
}
this.callbacks[key].push(fn);
},
trigger: function(key) {
if(!this.callbacks[key]) {
return;
}
var args = Array.prototype.slice.call(arguments,1);
this.callbacks[key].forEach(function(fn) {
fn.apply(this,args);
});
}
}

/** A single SCORM data model element, with the time it was set.
*/
function SCORMData(key,value,time,counter) {
Oops, something went wrong.

0 comments on commit 903737c

Please sign in to comment.
You can’t perform that action at this time.