Tin Can API (xAPI) with TalentLMS

Hi ya!

Here's the situation:

4 separate published Storyline lessons—all 4 hosted in TalentLMS.

At the end of Lesson 1, I have the Learner "rate themselves" (confidence on subject matter) 5 times using Sliders (capturing the ratings in 5 separate Slider variables).

Here's where your Tin Can skills come in...

At the end of the 4th Storyline lesson (a separate Storyline file), we revisit the learner's Lesson 1 ratings and present them back to him.

From what I hear, this is what Tin Can is for.

Kind'a at a loss how to initiate this. Help much appreciated :)

Ryan

4 Replies
Steve Flowers

Hey Ryan - 

Not familiar with TalentLMS' LRS implementation. A few ways to go about this but you'd probably have better luck with an external wrapper since the xAPI implementation of a Storyline Tin Can package is stuck to SCORM-ish stuff. You can probably call custom statement sends and queries but it's probably more work than using the well documented xapiwrapper from ADL.

To retrieve statements, you'll need to know exactly which activities are currently stored --OR-- you'll need to generate your own statements.

After adding the xapiwrapper and xapicollection to a web object to haul them into the story, add something on slide load to get them forced into the document head. This just gets around having to do post publish surgery. The downside is you need some space between updating the header and calling functions / methods contained in the loaded library. If you try to do this back-to-back, it will fail. Load this in one slide then in another slide or on button press, execute the other JS trigger.

function add_script(scriptURL,oID) {
var scriptEl = document.createElement("script");
var head=document.getElementsByTagName('head')[0];
scriptEl.type = "text/javascript";
scriptEl.src = scriptURL;
scriptEl.id=oID;
head.appendChild(scriptEl);}

//only want to add these once!
if(document.getElementById('xapi-wrapper')==null){
add_script("story_content/WebObjects/6J4vhbu6cAX/xapiwrapper.min.js","xapi-wrapper");
add_script("story_content/WebObjects/6J4vhbu6cAX/xapicollection.min.js","xapi-collection");
}

//lmsAPI=parent;
var player=GetPlayer();
//player.SetVar("lmsUser",lmsAPI.GetStudentName());

Then pulling out a query, you can populate the Storyline publish with data. I add lrsEndpoint, lrsUsername, and lrsPassword to a variable to make it easy to populate. It's likely that TalentLMS is going to populate the user / password authentication some other way. The code below uses WaxLRS as an endpoint. The first part of the code below checks to see if there are any statements with the verb Register for the user specified. If there isn't, it generates a register statement. In your example, you could probably simply pull a query based on complete with the results and populate that into a collection. 

var player = GetPlayer();

var conf = {
"endpoint": player.GetVar("lrsEndpoint"),
"user": player.GetVar("lrsUsername"),
"password": player.GetVar("lrsPassword"),
};
ADL.XAPIWrapper.changeConfig(conf);

var firstName = player.GetVar("lmsUser").split(",")[1].trim();
var lastName = player.GetVar("lmsUser").split(",")[0].trim();
var mailto = "mailto:" + lastName.toLowerCase() + "." + firstName.toLowerCase() + "@organization.com";

var search = ADL.XAPIWrapper.searchParams();
var stmt2 = JSON.stringify({
"mbox": mailto
});
search['verb'] = ADL.verbs.registered.id;
search['agent'] = stmt2;
var res = ADL.XAPIWrapper.getStatements(search);
var coll = new ADL.Collection(res.statements);
var actors = coll
.groupBy('verb.id')
.select('data[0].verb.id as verb,data[0].object.id as id')
.orderBy('id')
.exec(function(data) {
if (data.length < 1) {
var stmt = new ADL.XAPIStatement(
new ADL.XAPIStatement.Agent(mailto, lastName + ", " + firstName),
new ADL.XAPIStatement.Verb('http://adlnet.gov/expapi/verbs/registered', 'registered'),
new ADL.XAPIStatement.Activity('http://lc.com/register-activities', 'Registering the activity series')
);

ADL.XAPIWrapper.sendStatement(stmt, function(resp, obj) {
ADL.XAPIWrapper.log("[" + obj.id + "]: " + resp.status + " - " + resp.statusText);
player.SetVar("status", "Statement sent" + resp.statusText + "! " + resp.status);
});
}else{
player.SetVar("status", "Registration Exists");
}
});

The data returned comes back in an associative array. You can return any bits of data from the query. In the data example below, I've filtered and grouped the data to remove the duplicates and just return the two fields I wanted.

[
{
"mbox": "mailto:doe.jane@org.com",
"name": "Doe, Jane"
},
{
"mbox": "mailto:flowers.steve@org.com",
"name": "Flowers, Steve"
},
{
"mbox": "mailto:greene.jim@org.com",
"name": "Greene, Jim"
},
{
"mbox": "mailto:jones.bob@org.com",
"name": "Jones, Bob"
},
{
"mbox": "mailto:shoe.barb@org.com",
"name": "Shoe, Barb"
},
{
"mbox": "mailto:Steve.Flowers@org.com",
"name": "Steve Flowers"
}
]

 Getting the data out of the array would look something like this:

  • data[i].mbox.split(":")[1] //since the mbox value adds mailto:, I just grab the email address part.
  • data[i].name

So if I wanted to grab the fourth item in the array, I'd do something like this:

data[3].name //this will return Jones, Bob.