Advanced Protocols
Protocols can support many advanced features, including custom navigation menus, dynamically calculated page properties, and complex logic flow.
These sections document the more advanced protocol features in TabSINT.
Navigation Options
Protocol flow can be further customized using navigation menus and back buttons.
Navigation Menu
The navigation drop-down menu in the top right of the tablet can be customized during exams using the navMenu
object in a protocol.
"navMenu":[
{
"text": "Back to Main Menu",
"target": {
"reference": "MainMenu"
},
"returnHereAfterward": false
}
]
All three fields of the navMenu
object are required.
The link text and target are set using text
and target
, respectively.
The field returnHereAfterward
allows the link to behave in two different ways:
false
: Replace all currently queued pages with the target, finish exam when target is complete.true
: Add the linked section to the current protocol stack. The page displayed when the link is pressed will be shown after the target is complete.
Dynamic Logic
TabSINT has many features to support developing dynamic, adaptive questionnaires. There following section describes the many ways to implement dynamic logic and content.
Custom Expressions
Custom expressions are used to define logic that can be used to create adaptive protocols.
The following section will describe how to define custom expressions. The sections after will describe how to use these expressions to define page logic.
Warning: Custom expressions are difficult to use and debug, please use with caution and only when necessary.
Syntax
Custom expressions are written using a safe subset of the Javascript programming language. The vast majority of Javascript expressions are legal in TabSINT.
Specifically, TabSINT uses AngularJS's $eval(...)
function to evaluate expressions because it is relatively safe, secure, and flexible.
For more information, see:
A brief guide to Javascript Syntax
Specific details regarding AngularJS's expression syntax
Namespace
In custom expressions, you can reference the following functions and variables:
Libraries and Convenience Functions:
Most native Javascript functions (see AngularJS's $eval for specifics).
The javascript Math library: standard math functions including:
Math.abs
for absolute valueMath.min
for minimum of two numbersMath.max
for maximum of two numbers
The Lodash Javascript Library: Provides numerous useful convenience functions and functional programming tools, such as:
_.filter()
to restrict items in a list based on some criteria_.countBy()
to count items based on some criteria_.shuffle()
to randomize a list_.map()
,_.reduce()
,_.collect()
, and numerous other important functional programming tools
- Custom TabSINT Functions, written solely to make these custom expressions
shorter and more semantic:
arrayContains(strArray, item)
: Converts a JSON string array (such as that stored by a checkBoxResponseArea) to a Javascript array, and then checks whetheritem
is in it.
Exam Results and Exam State
flags
: A javascript object containing a copy of all set flags. Refer to it using dot notation, e.g.,flags.q1Answered
for a flag named q1Answered.result
: A copy of the response from the previous (most recent) question.examResults
: A copy of the exam's entire results structure, similar to that downloaded for post-analysis. It contains the following fields:protocolName
: Name of the active protocolqrString
: QR Code, if the protocol includes aqrResponseArea
testDateTime
: For example, '2014-07-07T15:55:30.942Z'testResults
: An array of test results objectssubjectId
: Subject id for the current exam, if a prior subject ID response area is present
Examples
Meaning | Code |
---|---|
If the previous question was correct | result.correct |
If the previous result was incorrect | !result.correct |
If the previous response was "dog" | result.response === 'dog' |
If the previous response was 34 and the usesHearingProtection flag is set. | (result.response === '34') && flags.usesHearingProtection |
If the subject chose both earPlugs and other: | arrayContains(result.response, 'earPlugs') && arrayContains(result.response, 'other') |
Flags
Flags can be set at the end of each page based on the user response. The flag can then be used in a custom expression to control the logic flow of the test at any later point in time.
"pages":[
{
"id":"question1",
"questionMainText":"How many years of service do you have?",
"responseArea":{
"type":"integerResponseArea"
},
"setFlags":[
{
"id": "integerFlag"
"conditional":"result.response>5"
}
]
}
]
Repeats
Repeats allow you to show the same question again, based on a conditional custom expression.
"pages":[
...
{
"repeatPage":{
"nRepeats":2,
"repeatIf":"result.response !== 'B'"
}
}
]
Note the question will be repeated up to 2 times (max 3 repeats) if the participant continues to answer A.
If repeatIf
is undefined, the page will repeat nRepeats
times.
See Repeats Example for an example protocol.
Follow Ons
FollowOns are useful when one or more immediate follow-up questions should be asked if the response to the current question meets some criteria.
{
"id":"question1",
"questionMainText":"How many years of service do you have?",
"responseArea":{
"type":"integerResponseArea"
},
"followOns":[
{
"conditional":"result.response > 5",
"target":{
"id":"question2",
"questionMainText":"How many times have you been deployed to Iraq or Afghanistan?",
"responseArea":{
"type":"integerResponseArea"
}
}
}
]
}
The second question will only be asked if the subject answers that they have more than 5 years of service.
The conditional is implemented as a custom expression and must return a boolean (true
or false
). If the conditional returns true
, the target is executed. If the conditional returns false
, the target is ignored.
Multiple sets of conditionals and targets can be included in a single instance of followOns
.
Each target can be defined as a single page or a reference to a subprotocol.
Skip If
Pages can be skipped using the skipIf
object. This object is evaluated prior to the page being rendered.
The custom expression used in this object can leverage the result of the previous question or any previously set flags.
"pages":[
{
"id":"question1",
"questionMainText":"How many years of service do you have?",
"responseArea":{
"type":"integerResponseArea"
}
},
{
"id":"question2",
"questionMainText":"This will be skipped if the previous response > 5",
"responseArea":{
"type":"integerResponseArea"
},
"skipIf": "result.response > 5"
}
]
Feedback
Feedback options allow you to provide feedback on certain questions after the user submits an answer. The feedback
field has two possible values:
gradeResponse
: will mark answers correct (red) and incorrect (green).showCorrect
: will mark answers correct/incorrect AND reveal correct answers the user missed.
"responseArea":{
...
"feedback": "gradeResponse"
}
See Feedback Example for an example protocol with feedback.
Special References
@PARTIAL
If an exam is terminated using the End Exam and Submit Partial Results link, the protocol can specify a final subprotocol to run before the test ends. This subprotocol must have the special reference ID @PARTIAL
.
"subProtocols":[
{
"protocolId": "@PARTIAL",
"title":"Final Section",
"pages":[
...
]
}
]
Potential use cases include:
- Display a page or sequence of pages asking for feedback on why the exam is being ended prematurely.
- Collect required information that would otherwise be collected at the end of the exam.
@END_ALL
The special reference @END_ALL
will automatically end the test no matter where the user is in the protocols stack. This can be used to manually end a protocol early in a FollowOn, subprotocol, or other special circumstance.
{
"id":"question1",
"questionMainText":"How many years of service do you have?",
"responseArea":{
"type":"integerResponseArea"
},
"followOns":[
{
"conditional":"result.response > 5",
"target":{
"reference": "@END_ALL"
}
}
]
}
Examples
Kitchen Sink
This example shows many of the available features for implementing dynamic questionnaires.
{
"title":"A Brief Example of Questionnaire Attributes",
"pages":[
{
"id":"question1",
"questionMainText":"What is your age?",
"responseArea":{
"type":"integerResponseArea"
},
"followOns":[
{
"conditional":"result.response>=21",
"target":{
"id":"question1a",
"questionMainText":"Do you like to have beer or wine with your evening meal?",
"responseArea":{
"type":"yesNoResponseArea"
}
}
}
]
},
{
"id":"question2",
"questionMainText":"What is your favorite color?",
"responseArea":{
"type":"multipleChoiceResponseArea",
"choices":[
{
"id":"Red"
},
{
"id":"Green"
},
{
"id":"Blue"
}
]
},
"setFlags":[
{
"conditional":"result.response=='Red'",
"id":"likesRed"
}
]
},
{
"id":"question3",
"questionMainText":"How frequently do you read for pleasure?",
"responseArea":{
"type":"likertResponseArea",
"levels":5,
"specifiers":[
{
"level":0,
"label":"Never"
},
{
"level":2,
"label":"Occasionally"
},
{
"level":4,
"label":"Every day"
}
]
},
"followOns":[
{
"conditional":"result.response>=2",
"target":{
"reference":"reader"
}
}
]
},
{
"id":"question4",
"skipIf":"flags.likesRed",
"questionMainText":"Do you dislike the color red?",
"responseArea":{
"type":"yesNoResponseArea"
}
}
],
"subProtocols":[
{
"protocolId":"reader",
"title":"Reader Survey",
"pages":[
{
"id":"questionR1",
"questionMainText":"What type of reading do you enjoy?",
"responseArea":{
"type":"checkboxResponseArea",
"choices":[
{
"id":"Novels"
},
{
"id":"Biography"
},
{
"id":"Nonfiction"
},
{
"id":"News"
}
],
"other":"Other"
}
}
]
}
]
}
Running Subprotocols
This example shows how to randomly run one out of several available subprotocols.
{
"title":"Example: Running One of Several Subprotocols",
"randomization":"WithoutReplacement",
"timeout":{
"nMaxPages":1
},
"pages":[
{
"reference":"sub1"
},
{
"reference":"sub2"
}
],
"subProtocols":[
{
"protocolId":"sub1",
"title":"Subprotocol #1",
"pages":[
{
"id": "info001",
"questionMainText": "You are in subprotocol #1."
},
{
"id": "info002",
"questionMainText": "You are leaving subprotocol #1."
}
]
},
{
"protocolId":"sub2",
"title":"Subprotocol #2",
"pages":[
{
"id": "info001",
"questionMainText": "You are in subprotocol #2."
},
{
"id": "info002",
"questionMainText": "You are leaving subprotocol #2."
}
]
}
]
}
Preprocess Function
The page preProcessFunction
allows more advanced adaptive logic implementation BEFORE each page is displayed. These functions can be used to adaptively modify any page property, including question texts, followOns, question type, flags, or the progress bar value.
Implementation Overview
- Create a function that calculates any new or modified page properties and returns an object with just those modified properties.
- If a page references that function (by name) as its
preProcessFunction
, then TabSINT runs the function and alters the 'page' specification before it's displayed. - Any changes to the page are stored with the exam results for that page, so that during post-processing it is clear exactly what page was presented.
Note that the the preprocessing function only needs to specify the fields that need to be changed; these fields are updated, and all other fields on the page remain the same.
Required Code
The minimum required code for a dynamic function is:
customJs.js
(function() {
tabsint.register('functionName', function(dm){
var returnObject;
// logic, use dm fields such as dm.result, read/write flags, etc.
return returnObject;// returned fields, if any, will overwrite or add to current page fields
};
})();
The controller must be registered using the global TabSINT service tabsint.register('functionName', function() {})
.
protocol.json
"pages":[
{
"id":"multichoice001",
...
"preProcessFunction": "functionName"
}
]
Objects and Data Available to Dynamic Functions
The following functions and objects can be accessed via the global namespace (Math
, _
, etc) or an optional input variable (dm
above) in a dynamic function. For example, dm.page
accesses the current page object.
Libraries and Convenience Functions:
Most native Javascript functions (see AngularJS's $eval for specifics).
The javascript Math library: standard math functions including:
Math.abs()
for absolute valueMath.min()
for minimum of two numbersMath.max()
for maximum of two numbers
The Lodash Javascript Library: Provides numerous useful convenience functions and functional programming tools, such as:
_.filter()
to restrict items in a list based on some criteria_.countBy()
to count items based on some criteria_.shuffle()
to randomize a list_.map()
,_.reduce()
,_.collect()
, and numerous other important functional programming tools
Exam Results (read-only)
Assuming you use dm
as the input variable to your function, as in the example above:
dm.result
: A copy of the response from the previous (most recent) question.dm.examResults
: A copy of the exam's entire results structure, similar to that downloaded for post-analysis. It contains the following fields:dm.examResults.protocolHash
dm.examResults.protocolId
dm.examResults.qrString
dm.examResults.siteId
dm.examResults.testDateTime
: for example, '2014-07-07T15:55:30.942Z'dm.examResults.testResults
: An array of test results objectsdm.examResults.testResults.responses
: An array of response fields, includingcorrect
,eachCorrect
,otherResponse
,presentationId
,response
,responseElapTimeMS
,responseStartTime
Modifiable State Objects (read/write)
dm.flags
: All set flags. Refer to flags fields using dot notation, e.g.,dm.flags.q1Answered
for a flag namedq1Answered
.
Page Fields (read-only)
dm.page
: The current page, including all page fields established by the protocol.json.
Dynamically Altering Flags
Flags can be changed directly and can be used to store data or to pass data from page to page. Flags are reset at the beginning of each exam. Fields are accessed and created using the 'dot' notation, i.e. dm.flags.myVar = 2;
.
Dynamically Changing Page Fields
The page field is read-only and cannot be altered directly. To modify page fields, return an object with a structure following the structure in the JSON Schema. Page field changes will be appended to the results structure for each page, to document what was changed.
var retObject = {
pageFieldToChange: newValue,
questionMainText: newTextValue,
progressBarVal: newProgressVal
...
};
return retObject;
It is important to note that objects (typically defined with curly braces {}
) only need to contain the changed
fields, and that TabSINT does its best to deal intelligently with nested changes.
However, TabSINT completely replaces arrays (typically defined with square []
).
For example, to change questionMainText
, which is a direct child of page, we simply return {questionMainText: newTextVariable}
but for choices, which is a nested child of responseArea, we must return {responseArea: {choices: newChoices}}
for the change
to be correctly placed. Other fields, such as `{responseArea: {type:...}}' will be unaffected.
Note also that choices is an ARRAY according to the JSON Schema. To add/change an element in an array, save the current array to a new variable, add/change the element of interest, then return the updated array.
Example: Using Dynamic Functions to Modify Page Properties
Take for example, the progressBarVal, documented as a page field in the JSON Schema. Let's say you wanted to calculate the progress bar value using your own custom function, 'calculateProgress'. This is how you would include your function in the protocol.json file using the preProcessFunction field:
{
"title":"A Simple Exam With a Custom Function to Set the Progress Bar",
"pages":[
{
"id":"multichoice001",
"title":"Multiple Choice 1",
"questionMainText":"Sample question.",
"responseArea":{
"type":"yesnoResponseArea"
},
"preProcessFunction": "calculateProgress"
}
]
}
And how you would set the function in 'customJs.js', a single javascript file included with your protocol zip:
(function() {
tabsint.register('calculateProgress', function(dm) {
var response = dm.result.response;
var length = dm.examResults.responses.length;
var progress;
if (response === 'y') {
progress = 'Question '+length+'\/5 in Section 4';// escaped forward slash
} else {
progress = 100 * length / 10;
}
// Return the proper object structure. In this case, we replace
// 'progressBarVal' with a new value ('progress'). This change
// will be recorded in the exam results for the current page.
var retObject = {
progressBarVal: progress
};
return retObject;
});
});
When page 'multichoice001' is loaded, the calculateProgress function will run and modify the progressBarVal
Example: Additional Page Property Modifications Using Dynamic Functions
(function() {
tabsint.register('changeText', function(dm) {
var response = dm.result.response;
var newQuestionMainText;
var newChoices;
if (response === 'A'){
newQuestionMainText = 'Do you like baseball?';
newChoices = [{id:'A',text:'Yes I like Baseball'},{id:'B',text:'No, I do not like Baseball'}]
} else if (response === 'B'){
newQuestionMainText = 'Do you like cars?';
newChoices = [{id:'A',text:'Yes I like Mustangs'},{id:'B',text:'No, I ride my bike'}]
}
return {
questionMainText: newQuestionMainText,
responseArea: {
choices: newChoices}
};
});
tabsint.register('addFollowOn', function(dm) {
// add a followOn and conditional flag
var newSetFlags = [
{
id:'DO_FOLLOW_ON',
conditional:"result.response === 'y'"
}
];
var newFollowOns = [
{
conditional:'flags.DO_FOLLOW_ON',
target:{
id:'ynFollowOn',
questionMainText:'Are you enjoying this follow-on?',
wavfiles:[
{
path:'chirpFullScaleWRTRef.wav',
targetSPL:'80'
}
],
responseArea:{
type:'yesNoResponseArea'
}
}
}
];
return {
followOns: newFollowOns,
setFlags: newSetFlags
};
});
})();
Custom Response Areas
Advanced users can include custom response areas that extend the standard TabSINT functionality.
These pages can be used for an additional type of response area or to analyze and display results. See the Protocol Development Tools section of the Developer Guide for more information about using custom response areas.
Warning: These response areas are difficult to write and debug properly. Creare can develop custom response areas, which can then be extended or customized. This functionality is exposed for advanced users only. Effective protocol development requires, at a minimum, familiarity with:
- Angularjs
- TabSINT
- The Lodash Javascript Library
- Bootstrap and AngularUI
Subject History
TabSINT can keep track of individual subjects taking an exam on a site. A subject's results from a previous exam can be used to inform the content of that subject's exam at a later date.
Utilizing subject history requires three individual parts:
A Subject ID Response Area to attach a subject id to exam results
A server side exam results processing function
Protocol-level logic to use the subject history
Subject ID Response Area
The subject ID response area will attach a unique subject id to exam results for a specific site.
The subject ID response area is defined in the json schema as:
"subjectIdResponseArea":{
"description":"A response area to record a subject id in the exam results",
"properties":{
"type":{
"enum":[
"subjectIdResponseArea"
]
},
"skip": {
"type": "boolean",
"description": "Allow user to skip entering the subject id"
}
}
}
Using Subject History in a protocol
If subject history information is available for a site, it will be put on the flags object of any protocol running on that site. See Implementing Logic for details on how to access these objects and use them in a protocol.
An example protocol using subject history is shown in Subject History Example