Skip to main content
Known Participant
September 9, 2011
Question

How to report custom SCORM variables with Javascript

  • September 9, 2011
  • 5 replies
  • 7968 views

Hi all

After the blood, sweat and tears that have gone into the project I've been working on for the past week, I decided to share this with you in case anyone else wants to do something similar.

What this does

Basically, this will allow you to use text boxes on slides as a full-blown SCORM-compliant quiz that has multiple possible inputs and partial scoring. I'll explain more about that in a bit.

Let's say we want a question that asks someone to introduce themselves on the phone. Ordinarily, with a short answer question you would put something like "Hi, my name is [name]". But what if your user actually types something like "Hello, you're speaking to [name]."? It's a valid thing to say, they should certainly get a point for it. But by the standard rules of short answer marking, they wouldn't get it. And that's why I decided to try this.

1. The first thing we need is a slide, on which we'll put the question as a caption, along with a text input box into which the user can enter their answer. The project I did actually has about 10 questions per slide, each of which has its own input box. (there are 44 questions in total).

Make sure you name the variable for the input box something you can easily remember later, and if you have multiple boxes it's a good idea to give them sequentially numbered names, for reasons that will soon become apparent.

2. Make a new slide, and in its "On Enter" action, choose "Execute Javascript". In the script window, enter the following: (I called the variables for my input boxes s[slide number]_txt_Q[number of question], so I used a for loop to put all my answers in an array. I had to split the processing over 10 slides because of Captivate's Javascript window character limit, but it only takes a couple of seconds to process it all.

var objCP=document.Captivate;
var answers=[];
for (i=1;i<[number of answers to process in this script];i++){answers=objCP.cpEIGetValue('s1_txt_Q'+i);}
calculate_Grade();
function calculate_Grade(){

var myRegExp='',matchPos,score=objCP.cpEIGetValue('cpQuizInfoPointsscored');
myRegExp=/(?:Hello|Hi|Hey),? you(?:'re| are) (?:speaking|talking) (?:to|with) [A-Z][a-z]+\.?/i;matchPos=answers[1].search(myRegExp);
if (matchPos != -1){score+=1;}

objCP.cpEISetValue('cpQuizInfoPointsscored',score);
objCP.cpEISetValue('rdcmndNextSlide',1);

That's a pretty simple regular expression for the purposes of the demonstration, but they can be as simple or as complex as you need them to be as long as you know what you're doing with them. If anyone wants I can do a regex tutorial at some point, but I don't foresee there being enough interest in this topic to warrant going into it in detail just now.

Essentially, what this does is store the answer to the question in an array, then check that answer against a pattern for what you're looking for. Then it adds the score that part of the question is worth, and assigns the variable's value to cpQuizInfoPointsscored. Finally, it forces a jump to the next slide.

If you're carrying on the score processing in a new slide (because of the character limit you can only have about 4-5 questions per slide or the Javascript won't run) you need to change the setting of the score variable slightly:

var score = objCP.cpEIGetValue('cpQuizInfoPointsscored');

And once your processing is finished, add these two lines to the bottom:

percent = (score * 100) / objCP.cpEIGetValue('v_MaxScore');
objCP.cpEISetValue('v_Percentage', percent);

Note that v_MaxScore is a user variable, as is v_Percentage. In fact, anything beginning with v_ or s_ in my code is a user variable.

Finally, on the final slide (which should be your results slide, showing your score and such) in the On Exit action, choose Execute Javascript again and enter the following:

var CaptivateObj = document.Captivate;
var score = CaptivateObj.cpEIGetValue('cpQuizInfoPointsscored');
var timeMS = CaptivateObj.cpEIGetValue('cpInfoElapsedTimeMS');
var time = timeMS / 1000;
var seconds = (time % 60).toFixed(0);
var minutes = ((time % 3600) / 60).toFixed(0);
var hours = (time / 3600).toFixed(0);
g_objAPI.LMSSetValue('cmi.core.session_time',((hours.length < 2) ? '0' : '') + hours + ':' + ((minutes.length < 2) ? '0' : '') + minutes + ':' + ((seconds.length < 2) ? '0' : '') + seconds);
var intLMSScoreRaw = score;
g_objAPI.LMSSetValue('cmi.core.score.max', 100);
g_objAPI.LMSSetValue('cmi.core.score.raw', intLMSScoreRaw);
if (score >= 85) {
g_objAPI.LMSSetValue('cmi.core.lesson_status', 'passed');
}
else {
g_objAPI.LMSSetValue('cmi.core.lesson_status', 'failed');
}
g_objAPI.LMSCommit('');
g_objAPI.LMSFinish('');
CaptivateObj.cpEISetValue('rdcmndExit',1);

This does a number of things:

1. Calculate the session time based on Captivate's elapsed time in milliseconds and send it to the LMS in proper SCORM format

2. Send the max and raw scores to the LMS. The reason we need to send max score like this is that for all intents and purposes the Captivate project has no scoreable objects and as far as Captivate is concerned the max score is 0.

3. Send the lesson status depending on pass criteria (in this case a score of 85% or higher. I used score because in this case the max score is 100 so the actual score will also be the percentage, but if you have more or less available points it would be better to reuse the percentage variable).

4. Commit the scores and finish processing

5. Force the project to close.

This essentially sends our own variables to the LMS and then cuts it out before Captivate can send its own to overwrite ours. This has only been tested on my Moodle site but I don't see why it wouldn't work with other systems. It correctly reports score, pass status, and the time taken.

If anyone has any questions about any of the above or would like to know more about any topic discussed here, just let me know. I'll be more than happy to share whatever knowledge I possess. And if you can see a place where I haven't been as efficient with my code as I could have been, let me know! I'm always looking to improve.

This topic has been closed for replies.

5 replies

rashayehya
Participating Frequently
April 19, 2017

hello Trihan,

Is it possible to pass some custom variables and report them to LMS (moodle) other than the variables specified in your post.

For example, I want to create variable to store question answer "question_ans" and another variable to hold the score of this question "question_score". Can I modify moodle to report the following variables "question_ans" and "question_score"?

Thank you in advance,


Regards,

Rasha

UIUCKate8049017
Inspiring
March 4, 2015

Hello Trihan,

My instructors want SCORM to report when users have accessed certain screens and clicked on certain buttons. Is there any way to get Captivate to do this without dealing with scoring at all using your method? I am not a programmer and have very limited understanding of regular expressions, but it looks like it SHOULD be possible.

TLCMediaDesign
Inspiring
March 9, 2015

Screen access should be reported in the lesson_location. I have a script modification that will only write the user's furthest progress.

Captivate doesn't seem to be using the comments field so you could use that for button clicks. You could also assign variables to your utton clicks and te values will be stored in the suspend data which could be parsed and written to a variable. You only have so many writable fields in a SCORM DB.

What version of SCORM are you publishing?

Inspiring
May 18, 2014

Hi,

I've tried using your script to have a variable tracking the score to be used for report to SORM (for Moodle). Sadly it doesn't seem to work.

Participants only pass when they score between 80 and 100.

The variable that stores the score is called V_Score

This is the JavaScript I use on exit from the last slide:

ar CaptivateObj = document.Captivate;

objCP.cpEISetValue('cpQuizInfoPointsscored’,’V_Score’);

var score = CaptivateObj.cpEIGetValue('cpQuizInfoPointsscored');

var timeMS = CaptivateObj.cpEIGetValue('cpInfoElapsedTimeMS');

var time = timeMS / 1000;

var seconds = (time % 60).toFixed(0);

var minutes = ((time % 3600) / 60).toFixed(0);

var hours = (time / 3600).toFixed(0);

g_objAPI.LMSSetValue('cmi.core.session_time',((hours.length < 2) ? '0' : '') + hours + ':' + ((minutes.length < 2) ? '0' : '') + minutes + ':' + ((seconds.length < 2) ? '0' : '') + seconds);

var intLMSScoreRaw = score;

g_objAPI.LMSSetValue('cmi.core.score.max', 100);

g_objAPI.LMSSetValue('cmi.core.score.raw', intLMSScoreRaw);

if (score >= 80) {

g_objAPI.LMSSetValue('cmi.core.lesson_status', 'passed');

}

else {

g_objAPI.LMSSetValue('cmi.core.lesson_status', 'failed');

}

g_objAPI.LMSCommit('');

g_objAPI.LMSFinish('');

CaptivateObj.cpEISetValue('rdcmndExit',1);

Does anyone see what's wrong with this?

Thanks

Inspiring
May 18, 2014

I tried two more. This one:

var CaptivateObj = document.Captivate;

var score = cp.variablesManager.getVariableValue(‘V_Score');

var timeMS = CaptivateObj.cpEIGetValue('cpInfoElapsedTimeMS');

var time = timeMS / 1000;

var seconds = (time % 60).toFixed(0);

var minutes = ((time % 3600) / 60).toFixed(0);

var hours = (time / 3600).toFixed(0);

g_objAPI.LMSSetValue('cmi.core.session_time',((hours.length < 2) ? '0' : '') + hours + ':' + ((minutes.length < 2) ? '0' : '') + minutes + ':' + ((seconds.length < 2) ? '0' : '') + seconds);

var intLMSScoreRaw = score;

g_objAPI.LMSSetValue('cmi.core.score.max', 100);

g_objAPI.LMSSetValue('cmi.core.score.raw', intLMSScoreRaw);

if (score >= 80) {

g_objAPI.LMSSetValue('cmi.core.lesson_status', 'passed');

}

else {

g_objAPI.LMSSetValue('cmi.core.lesson_status', 'failed');

}

g_objAPI.LMSCommit('');

g_objAPI.LMSFinish('');

CaptivateObj.cpEISetValue('rdcmndExit',1);

And this one, with this on entering the slide:

var CaptivateObj = document.Captivate;

score = cp.variablesManager.getVariableValue(‘V_Score');

objCP.cpEISetValue('cpQuizInfoPointsscored',score);

with this on leaving the slide:

var CaptivateObj = document.Captivate;

var score = CaptivateObj.cpEIGetValue('cpQuizInfoPointsscored');

var timeMS = CaptivateObj.cpEIGetValue('cpInfoElapsedTimeMS');

var time = timeMS / 1000;

var seconds = (time % 60).toFixed(0);

var minutes = ((time % 3600) / 60).toFixed(0);

var hours = (time / 3600).toFixed(0);

g_objAPI.LMSSetValue('cmi.core.session_time',((hours.length < 2) ? '0' : '') + hours + ':' + ((minutes.length < 2) ? '0' : '') + minutes + ':' + ((seconds.length < 2) ? '0' : '') + seconds);

var intLMSScoreRaw = score;

g_objAPI.LMSSetValue('cmi.core.score.max', 100);

g_objAPI.LMSSetValue('cmi.core.score.raw', intLMSScoreRaw);

if (score >= 80) {

g_objAPI.LMSSetValue('cmi.core.lesson_status', 'passed');

}

else {

g_objAPI.LMSSetValue('cmi.core.lesson_status', 'failed');

}

g_objAPI.LMSCommit('');

g_objAPI.LMSFinish('');

CaptivateObj.cpEISetValue('rdcmndExit',1);

Sadly, both without result. I really hope someone can see what I'm doing wrong here.

Inspiring
October 24, 2011

Hi Trihan,

Your post seems exactly the answer that I've been looking for. However, I tried your solution, but it didn't work.

Can you please go to the post where I asked how to pass a score from selective questions in a quiz to LMS and give me some suggestions? Your help is greatly appreciated.

Legend
September 27, 2011

Great find Trihan and thanks for sharing!!!

Jim Leichliter

TrihanAuthor
Known Participant
October 4, 2011

Hmm, I don't seem to be able to edit the first post to add all the optimisations I've made to the code recently. What would be the best way to share this with the community? Just put it in a new post or what? (I'll be updating my Infosemantics blog post as well anyway, but I'd like to share my findings with the forum too).

RodWard
Community Expert
Community Expert
October 4, 2011

Easiest way is probably just to edit the Infosemantics page where the full detail is held and then add another post to the original thread explaining that there's been an update with a link again to the page.  People usually expect the final answers to be near the bottom of the thread anyway.