Creating Custom Questions

Please see Getting Started with Custom Questions and Features before starting with your Custom Question project.
 
We recommend using the Custom Question Skeleton project to build your Custom Question.
 
Learn how to create a Custom Question type to 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 as 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 Name Mandatory Value Comment
response_id Yes Type: string Used in the page where custom Question will be rendered as a part of the class name "question-response_id" of span element (HTML hook), for example:

<span class="learnosity-response question-response_id"></span>
type Yes Type: string For the custom Question type, it should be exactly “custom” (case-sensitive).
js Yes Type: 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

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.

Note: Disabling autoscoring

To disable autoscoring, do not supply a js.scorer attribute.

In this case, automarkable should return false and you will not see a scoring exception as a pop-up window. 

Note: scorer.js return values for automarkable

  • If you don't use the canValidateResponse method, automarkable should return true.
  • If you do use the canValidateResponse method, automarkable should return the canValidateResponse value.
css No Type: string A URL to a CSS file containing styles for the Question.
valid_response Yes Type: string The 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> No Type: 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": "//demos.learnosity.com/usecases/customquestions/custom_shorttext_q.js",
"scorer": "//demos.learnosity.com/usecases/customquestions/custom_shorttext_s.js"
},
"css": "//demos.learnosity.com/usecases/customquestions/custom_shorttext.css",
"stimulus": "What is the capital of Australia?",
"valid_response": "Canberra",
"score": 1
};

Code example 1: JSON attributes for custom Question initialization, you can find an example in the assessment.php file in the Custom Question Skeleton, which is the questions field in $request variable

 

 

Questions API will instantiate your Question by calling a constructor, passing in two objects. The first object, init contains the following properties:

Table 2: initialization properties for the init object

Property Name Comment
state The state Questions API was initialised in (initialresume, 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 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.

After initialisation, you can use the returned properties to render your application. 

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:

const { el, init } = this; 

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

Code example 12: notifying Questions API that the Question is ready. An example can be found in Custom Question Skeleton

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 13: using jQuery’s change() method to listen for changes to text input
 
Or you can register the evens handler when question is being initialised
handleEvents() {
const { events } = this;

events.trigger('changed', responses);

events.on('validate', options => {
// do something
});
}

//after render and register events handler
this.render().then(() => {
this.handleEvents();
})

Code example 14: register events handler. An example can be found in Custom Question Skeleton

 

You can also save an object as a response:

init.events.trigger("changed", {"attr1": value1, "attr2": value2});
Code example 15: 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().

 

Reset Response

Custom Questions do not support resetResponse(). If you want to achieve reset response functionality with a custom Question, you can add support for this to your Question by triggering a resetResponse event and implementing a reset for other states.

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 Name Parameter Value Comment
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 22: 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 23: 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 24: 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 25: 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 26: rendering the correct answers

 

 

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. You can override the functions in custom-question-skeleton/src/scorer/index.js to implement your scoring logic if you are using the Custom Question Skelton to build your own custom question. 

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 Scorer function

The Scorer function, (which executes on the server-side) takes question and response variables like so: Scorer(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 19: 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 within the score function.

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

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

 

Public methods required by Learnosity:

 

Method Returns Comment
isValid() boolean Check if the current question's response is valid or not
validateIndividualResponses() object Returns an object displaying the validation state of each individual item inside the stored response
For example:
The student response value is: { min: 10, max: 20 } and our correct answer is { min: 10, max: 30 }
Then we expect the result of this validateIndividualResponses will be:
 { min: true, max: false }
score() number Returns the score of the stored response
maxScore() number Returns the possible max score of the stored response
canValidateResponse() boolean Check if the current question is scorable or not.
For example:
  • If there is no valid response data set in the question, this method should return false
  • If this question type is not scorable (like an essay or open ended question) then this will return false

 

 

 

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.

data-api-online-scoring-endpoint.png

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 15: 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 16: 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 17: 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 18: 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.

 

Advanced use cases

Mathcore auto-grading module

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 (see the Question Editor demo pages). 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
 

Pass extra information into custom questions through init_options

In some scenarios, you need the clients using your custom questions/features to pass certain information like authentication tokens. This information will be stored inside the initOptions custom_widget_options and you can retrieve those information through the public method init.getCustomWidgetOptions(key: string)

You are required to let your clients know what key you expect them to set in the initOptions custom_widget_options.

// Example of initOptions with custom_widget_options
const initOptions = {
custom_widget_options: {
custom_short_text: {
authenticationToken: '1234-abcd'
}
...
}
};
const questionsApp = LearnosityApp.init(initOptions);


// Your Custom Question Code
export default class Question {
constructor(init) {
const authenticationToken = init
.getCustomWidgetOptions('custom_short_text')
.authenticationToken;
}
//....
}

Code example 8: Retrieving custom_widget_options

 

Here is a demo that 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
Was this article helpful?

Did you arrive here by accident? If so, learn more about Learnosity.