Skip to content

Commit

Permalink
stress test
Browse files Browse the repository at this point in the history
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 903737c
Show file tree
Hide file tree
Showing 12 changed files with 11,469 additions and 23 deletions.
29 changes: 29 additions & 0 deletions numbas_lti/migrations/0044_stresstest_stresstestnote.py
@@ -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')),
],
),
]
19 changes: 19 additions & 0 deletions numbas_lti/migrations/0045_auto_20180604_1212.py
@@ -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'),
),
]
22 changes: 19 additions & 3 deletions numbas_lti/models.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
106 changes: 88 additions & 18 deletions numbas_lti/static/api.js
Expand Up @@ -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;

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -198,6 +211,7 @@ SCORM_API.prototype = {
this.Terminate('');
alert(message);
window.location = show_attempts_url;
this.callbacks.trigger('external_kill');
}
},

Expand All @@ -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.
Expand All @@ -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');
}

},
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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;
},

Expand All @@ -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++;
Expand All @@ -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) {
Expand All @@ -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',
Expand All @@ -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);
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 903737c

Please sign in to comment.