Custom Questions

Learn how to create a custom Question type for use with Learnosity's Questions API.


Learnosity Custom Question types allow you to create your own unique Questions, giving you full control over the rendering of the response area, the user interaction with the Question, and the scoring of the response.

A custom Question is defined like any other Question type, by passing a JSON object to the Questions API. In the Question JSON, you provide URLs to JavaScript files containing the program logic that will drive your Question and its scoring.

The first step is to initialise your custom Question via Questions API. Please refer to the table of supported JSON attributes below.

Important: once a session of custom Question type is saved or submitted for server-side processing (such as back-end scoring) Learnosity can only access custom Question attributes supplied in the initialization JSON (see table below). All other attributes added to the Question object after initialization (for example, dynamically added attributes) will be ignored on the server side.

Table 1: supported attributes for custom Questions

Attribute NameMandatoryValueComment
response_idYesType: stringUsed in the HTML hook to render the custom Question type, for example:

<span class="learnosity-response question-response_id"></span>
typeYesType: stringFor the custom Question type, it should be exactly “custom” (case-sensitive).
jsYesType: object
Type: string
An object containing URLs to JavaScript files which define AMD modules for the Question and the Scorer.
"js": { "question": "my_custom_question.js", "scorer": "my_custom_scorer.js" }

OR

A string containing URL to a JavaScript file which defines an AMD module for the Question, and optionally, Scorer.
"js": "my_custom_combined_file.js"

Important For successful server-side scoring, the JavaScript files supplied must be accessible externally.
cssNoType: stringA URL to a CSS file containing styles for the Question.
valid_responseYesType: stringThe valid response for your custom Question.

You must pass this attribute even if it is not used in your scoring logic — just pass it as an empty string, for example:

(“valid_response”: “”)

Important If not provided, server-side scoring will not be triggered.
<any custom attribute>NoType: string /

Type: number /

Type: boolean
Depending on your custom scoring logic, these attributes’ values may or may not be accessible on the back-end (for Learnosity server-side scoring, for example) once the session is saved or submitted.

Important All attributes dynamically added to the custom Question object after its initialization will be ignored on the server-side.

For more information on supported attributes, please visit the custom attributes documentation.

customQuestionJson = {
    "response_id": "custom-shorttext-response-1",
    "type": "custom",
    "js": {
        "question":"//docs.learnosity.com/demos/products/questionsapi/questiontypes/custom_shorttext.js",
        "scorer": "//docs.learnosity.com/demos/products/questionsapi/questiontypes/custom_shorttext_scorer.js"
    },
    "css": "//docs.learnosity.com/demos/products/questionsapi/questiontypes/custom_shorttext.css",
    "stimulus": "What is the capital of Australia?",
    "valid_response": "Canberra",
    "score": 1
};

Code example 1: JSON attributes for custom Question initialization

The JavaScript file that you provide must define an AMD module using Learnosity's namespaced require.js instance, LearnosityAmd.define. The module must define a function to act as a constructor for your Question. The module must return an object with a Question property containing the constructor, like so:

LearnosityAmd.define(['jquery-v1.10.2'], function ($) {
    'use strict';

    function CustomShorttext(init) {
        this.init = init;

        init.$el.html('<input type="text" />');

        var $response = $('input', init.$el);

        if (init.response) {
            $response.val(init.response);
        }

        $response.change(function (event) {
            init.events.trigger('changed', event.currentTarget.value);
        });

        init.events.trigger('ready');
    }
    
    return {
        Question: CustomShorttext
    };

});

Code example 2: defining an AMD module

If certain external assets should be used for custom questions, they could be loaded by including in LearnosityAmd.define. All the assets listed will be successfully loaded on a page once the provided callback in LearnosityAmd.define is called.

LearnosityAmd.define(['jquery-v1.10.2', 
                      'https://learnosity.com/external_asset1.js',
                      'https://learnosity.com/external_asset2.js'], function ($) {
    'use strict';

    // by this time the assets 'https://learnosity.com/external_asset1.js' and  
    // 'https://learnosity.com/external_asset2.js' are loaded successfully

    function CustomShorttext(init, lrnUtils) {
        ... 
    }
    ...
});

Code example 2.1: including external asset

The code example below shows how to free up memory properly while using custom questions with Questions API.

LearnosityAmd.define(['jquery-v1.10.2'], function ($) {
    'use strict';

    function CustomShorttext(init, lrnUtils) {
        this.$el = init.$el;
        ...
        //notifying Questions API that the Question is ready (rendered)
        init.events.trigger('ready');

        //setting up a handler for Questions API reset event 
        //which is triggered via Questions API reset() public method
        init.events.on('reset', function () {
            this.$el.remove();
            //free up any memory if required
        }.bind(this));
    }
    ...
});

Code example 2.2: free up memory

Questions API will instantiate your Question by calling this function, passing in two objects. The first object, initOptions contains the following properties:

Table 2: initialization properties for the initOptions object

Property NameComment
state The state Questions API was initialised in (initial, resume, or review).
question The Question object to be rendered.
response Previously saved response data for this response ID. This should be placed in the response area when in resume or review state.
$el A jQuery selection containing the DOM element for your Question's response area. This is where you will render the Question.
events A Backbone.js Events object. You must use this to communicate with Questions API (see below).
getFacade A function that returns a facade object containing the public methods of the current Question instance. Add or override public methods on the facade object to make those available on instances of the custom Question object exposed by Questions API, for example:

questionsApp.question("custom-question-1").disable()).
getI18nLabel A function that returns the internationalization (i18n) text of the provided key. For example:

this.initOptions.getI18nLabel('checkAnswer'); // return "Check Answer" text
questionsApiVersion A string containing the version number of the current instance of Questions API.

The second object, lrnUtils is a facade containing the following:

renderComponent(String componentName, Element hook) A function used to render the given componentName at the specifed element. Used to render Learnosity's provided components like the Check Answer button or the suggested answer list component. The function returns a promise that resolves to a facade containing public methods available on the component. The promise resolves when the component has finished rendering. See the section on custom Question components, below.

Your JavaScript module must render the HTML for the Question inside the provided element. When this is complete, you must notify Questions API that the Question is ready. This example renders a simple text input, and then triggers the ready event:

init.$el.html("<input type=\"text\" />");

init.events.trigger("ready");

Code example 3: notifying Questions API that the Question is ready

Questions API will trigger an error event if your Question does not call the ready event within an acceptable timeframe.

Your JavaScript module must notify Questions API when the user enters or changes a response to the Question, by triggering a changed event containing the response data. This example uses jQuery's change() method to listen for changes to the text input:

var $response = $("input", init.$el);

$response.change(function (event) {
    init.events.trigger("changed", event.currentTarget.value);
});
Code example 4: using jQuery’s change() method to listen for changes to text input

You can also save an object as a response:

init.events.trigger("changed", {"attr1": value1, "attr2": value2});
Code example 5: saving an object as a response

This may be required when your answer has several data points that must be returned (for example, graph coordinates with X and Y values, or a record of the student’s actions).

This ensures responses can be saved as part of the session, or retrieved locally via Questions API public methods, e.g. getResponses().

To implement scoring for your Question, your need to provide a separate JavaScript module that defines a scorer function. Questions API will call this function, passing it the Question object and the response data for the Question. The response data comes directly from the changed events triggered by your Question.

You must define methods on the prototype of the scorer function according to our scoring interface, as shown in the example below. The object returned by your module must define a Scorer property containing the scorer function.

Testing Scoring Functions on the Server-side: score and maxScore

On the server-side, the score and maxScore functions are called once the session is saved or submitted. To test these functions, you can use the Data API online scoring endpoint which uses the same logic as the back-end scoring.

Screenshot: Data API online scoring endpoint demo page

To test your scoring functions:

1. Visit https://demos.learnosity.com/analytics/data/index.php then scroll to the Scoring section at the bottom of the page.

2. On the questions field on that page, paste in the JSON object for your custom Question(s).

[{
    "response_id": "custom-shorttext-response-1",
    "type": "custom",
    "js": {
        "question":"//docs.learnosity.com/demos/products/questionsapi/questiontypes/custom_shorttext.js",
        "scorer": "//docs.learnosity.com/demos/products/questionsapi/questiontypes/custom_shorttext_scorer.js"
    },
    "css": "//docs.learnosity.com/demos/products/questionsapi/questiontypes/custom_shorttext.css",
    "stimulus": "What is the capital of Australia?",
    "valid_response": "Canberra",
    "score": 1
}]
Code example 6: the JSON object for a custom Question

3. On the responses field on that page, paste in the JSON object you can retrieve locally via Questions API public methods, for example, getResponses(). For instance, call questionsApp.getResponses() in the browser console to get the following:

response_id: 
    apiVersion: "v2.119.0"
    type: "object"
    value: "lesdafsdf"
Code example 7: returned data from calling questionsApp.getResponses() in the browser console

Therefore, testing the responses object will look like the following:

{
    "custom-shorttext-response-1": {
        "value": "Canberra",
        "type": "object",
        "apiVersion": "v2.119.0"
    }
}
Code example 8: testing the responses object

4. Click Submit to send the request and analyse the result data on the Response tab (you will be switched there automatically). The result data will look like the following:

{
   "meta": {
      "status": true,
      "timestamp": 1529371443,
      "records": 1
   },
   "data": [
      {
         "response_id": "custom-shorttext-response-1",
         "type": "custom",
         "automarkable": true,
         "is_valid": true,
         "score": 1,
         "max_score": 1,
         "attempted": true,
         "error": null
      }
   ]
}
Code example 9: result data from the Data API endpoint

5. Check that data[].score and data[].maxScore are available (not null) and have correct values. Doing this, you can iterate over your server-side scoring, until your score and maxScore are returning the expected results. Then, you can move this working model into your JavaScript program.

Limitations
Please refer to the Supported JavaScript Libraries section of this page to see which libraries can be used on the server-side.

Implementing a Custom Scorer

Implementing a scorer will allow responses for the Question to be evaluated by Questions API public methods, such as getScores().

Server-side variables for the CustomScorer function

The CustomScorer function, (which executes on the server-side) takes question and response variables like so: CustomScorer(question, response). Scoring for this is executed remotely, so only certain variables are available in that environment.

The question variable
The question variable is the object (containing JSON attributes) which is passed to initialize Questions API (refer to Table 1: supported attributes for Custom Questions).

For example, if you want to use the valid_response attribute you would call it in the form question.valid_response.

Only the attributes which are passed at initialization time will be processed for server-side scoring.

Important Any attributes dynamically added to the Question object after Questions API has been initialized successfully will be ignored during server side scoring.

The response variable
The response variable is the value attribute retrieved via Questions API public methods, such as getResponses().

For example, calling questionsApp.getResponses() in the browser console returns this object:

response_id:
    apiVersion:"v2.119.0"
    type:"object"
    value: 
        attribute1:"value1"
        attribute2:"value2"
Code example 10: the object returned by questionsApp.getResponses()

Therefore, the response is equal to the value attribute of response_id above.

The example below shows how to access attribute1 of value (from code example 10) within the score function.

function CustomScorer(question, response) {
       this.question = question;
       this.response = response;
   }

CustomScorer.prototype.score = function () {
   return this.response.attribute1;
};
Code example 11: accessing 'attribute1' within the score function

An example of how to call CustomScorer follows.

LearnosityAmd.define(['underscore-v1.5.2'],function (_) {

    /* Constructor for the scorer
     * @param question The question object
     * @param response The response data
     */
    function CustomScorer(question, response) { ... }

    /* Is the response correct?
     * @return boolean
     */
    CustomScorer.prototype.isValid = function () { ... };

    /* The score for the current response.
     * @return float
     */
    CustomScorer.prototype.score = function () { ... };

    /* The maximum score for this question.
     * @return float
     */
    CustomScorer.prototype.maxScore = function () { ... };
    

    /* Is the provided question json valid to be validated?
     * If this method returns "false" then Check Answer button
     * will not be visible and dispatching public method "validate"
     * will not validate the current question - as the provided question
     * is not validatable.
     * @return boolean
     */
    CustomScorer.prototype.canValidate = function () { ... };

    return {
        Scorer:   CustomScorer
    };
});
Code example 12: calling CustomQuestion and CustomScorer functions

All Learnosity Question types have a validation UI to give feedback to the user for valid or invalid responses. This feedback can be triggered via Questions API public methods e.g. validateQuestions(). You can add support for this to your Question by listening for a validate event. The parameters for this are listed in the table below.

Table 3: parameters for the validate event handler

Parameter NameParameter ValueComment
options Object

options = {
showCorrectAnswers: value
}


The value depends on the initialization object passed to Questions API.

Default value is true.
The showCorrectAnswers flag has different behaviors based on the state provided.

More information:
showCorrectAnswers Documentation


init.events.on("validate", function (options) { // options object containing "showCorrectAnswers" to tell developer if he/she should display correct answers to user
    var result = init.getFacade.isValid(); // Use facade.isValid(true) to get the detailed report

    this.clearValidationUI();
    this.showValidationUI(result);

    if (!result && options.showCorrectAnswers) {
        this.showCorrectAnswers();
    }
}.bind(this));
Code example 13: the validate event handler

Important Handler code for the validate event should be provided before the ready event is sent off.

Additionally, apply the following Learnosity class names to the relevant elements of the Question to have your validation UI styled according to Learnosity's standard look and feel.

this.$el
    .find(".input-wrapper")
    // Add "lrn_response_index_visible" class if you want to display the index of current response
    .addClass("lrn_response_index_visible")
    // Add this class to display default Learnosity correct, incorrect style
    .addClass(isCorrect ? "lrn_correct" : "lrn_incorrect")
    // After adding the class "lrn_response_index_visible", you then can inject the response index element
    .prepend("<span class="lrn_responseIndex"><span>1</span></span>")
    // Add this element if you want to display to corresponding validation (cross, tick) icon
    .append("<span class="lrn_validation_icon"/>");
 
Code example 14: supported class names

The validate event is triggered for all supported states of Questions API (initial, resume, review). Therefore, its event handler is the best place to accommodate your code to manage custom Question in different states.

If you would like to use your Question in resume or review states, you should check for an existing response value when rendering your Question. In review state, you should also show your Question's validation UI.

For instance:

init.events.on("validate", function (options) {

    if (init.response) {
        $response.val(init.response);
    }

    if (init.state == "review") {
         // show validation UI
    }
}
Code example 15: checking for an existing response value in review mode

Custom Question components are a way to include common Learnosity controls and UI elements into a custom Question with standard Learnosity styling and behaviour.

Use the lrnUtils object passed to the custom Question's constructor to inject components into the DOM elements of your Question. Simply call lrnUtils.renderComponent(), passing the string name of the desired component and the DOM element where it should be placed. The function returns a promise, which resolves to a facade of public methods that can be called on the component.

See the following examples for each of the available components.

CheckAnswerButton

Renders the Check Answer button. On click, the button validates the current custom Question. This button will be invisible during review state.

Available methods:

  • remove()


this.$el.append("<div data-lrn-component=\"suggestedAnswersList\"/>");
this.lrnUtils.renderComponent("CheckAnswerButton", this.$el.find("[data-lrn-component=\"checkAnswer\"]").get(0));
Code example 16: rendering the `Check Answer` button


SuggestedAnswersList

Renders the Suggested Answers List component. This component will only be visible if the activity's showCorrectAnswers is set to "true".

Available methods:

  • setAnswers()
  • reset()
  • remove()

var self = this;
this.lrnUtils.renderComponent("SuggestedAnswersList", 
this.$el.find("[data-lrn-component=\"suggestedAnswersList\"]").get(0))
    .then(function (component) {
        self.suggestedAnswersList = component;

        // Pass in string to display correct answer list without the index
        // self.suggestedAnswersList.setAnswers(correctAnswerText);

        // Pass in an array of object which contains index and label to render a list of suggested answers
        self.suggestedAnswersList.setAnswers([{
            index: 0,
            label: correctAnswerText
        }]);
    });
Code example 17: rendering the correct answers

You can request access to certain JavaScript libraries as part of your module definition. Please note the supported lists of client-side and server-side libraries, as they differ.

Questions API currently exposes the following JavaScript libraries:

Client-side supported libraries

  • mathcore
  • jquery-v1.10.2
  • backbone-v0.9.10
  • underscore-v1.5.2

Server-side supported libraries

  • mathcore
  • underscore-v1.5.2


LearnosityAmd.define(["jquery-v1.10.2"], function ($) { ... });
Code example 18: defining a library for use on the client-side

You can use this module (Mathcore) to validate math responses (as LaTeX) against the defined specification.

The passed value can be any valid LaTeX string. The specification, on the other hand, is a complex object. We encourage you to build it using the user interface of the Math Formula Question type in Question Editor (Question Editor demo page). This way you can quickly learn which methods and options are available. To do so, simply use the validation tab to build and Source button to preview specification JSON.

For more information, see the full documentation for the Mathcore auto-grading library.

See the Mathcore auto-grading documentation

Here is a demo which shows an example custom implementation of the Short Text Question type. You can rewrite the Question JSON to define your own custom Questions.

Go to demo