Veracity Learning 1.6 and later support plugins! A plugin is a collection of JavaScript code that extends the system to add new capabilities or to change how the system works.
Overview
A plugin is a collection of JavaScript code that adds or changes a function. System and LRS plugins can respond to events in the system, which we specifically raise to let you extend our product. Some events are just for you to respond to — they don't accept any return values. Other events expect a plugin to return some sort of data that it will use later. The
System Events section below describes most of the common events.
You add plugins to the system simply by placing their code files under a /plugins/ directory. This directory must be inside the directory that hosts the executable file, and the system must have appropriate permission to read the directory. You do not have to give write permission, since Veracity never writes to disk.
Once you place a plugin file in the plugin directory, you must activate it within the system or an LRS. In the Free version, each LRS has access to all LRS-level plugins. In the Enterprise version, you can manage permissions to control access to plugins, per LRS or per user.
You can activate a plugin many times, each time with different settings.
Adding a Plugin
In the Enterprise version, you can control which LRSs can use which plugins. First, you must install the into the /plugins/ folder. You must restart the server to recognize a new plugin. Some plugins come built-in with the system.
- In the upper-right, click the User button (your account name) to expand the context menu, then select the Admin option.
- On the Admin page, in the Plugins and Permissions section, click Manage Plugin Permissions.

- Click the Add a Permission button.

- In the Allow Plugin form, click the Allow Access To dropdown menu.

- Select a plugin from the menu. If you don't see the plugin, it may have a syntax error or installed incorrectly onto the server.
- In the Criteria menu, select for everyone, to make the plugin available to everyone.
- Click the Allow button.
- Navigate to the LRS where you want to install the plugin.
- Open All Management Tools.
- In the Integrations and Extensions section, click Plugins.

- In the Manage Plugins page, click the Activate a Plugin button.

- In the card for the plugin you want to enable, click its Activate button to enable the plugin and to open the configuration form for that plugin.
Types of Plugins
There are four types of plugins.
- System Plugins — These plugins respond to a set of events that control how system-level functions work. They can override the login, add new database tables, attach new routes to the top-level paths, or otherwise implement system-wide features. System plugins can also implement any function that an LRS-level plugin can. The LRS cannot control system-level plugins — they are active for the entire system or they are not.
- LRS Plugins — The scope of these plugins is a single LRS in the system. This means that you can configure each LRS to use or to not use any LRS plugin. A set of LRS-related events pass to each installed LRS level plugin. Responding to these events lets you integrate with other systems, to modify data in before it's stored or before it's returned to a client, to extend the user interface (UI), or to add features.
- Analytic Processors — We migrated the old plugin analytics processor feature to a new architecture, but it still works as before. These plugins create new widgets or expose complex server-side logic to back-up a chart in the client-side analytics package.
- Analytics Dashboards — These plugins work like the Analytics Processors, but instead of creating a single widget, you can use them to gather several widgets and parameter pickers into a dashboard. In general, you'll use Analytic Processor and Analytic Dashboard plugins together.
Developing Plugins
You create a plugin by adding a JavaScript file to the /plugins/ directory. If this directory does not exist, create it manually. Once you've created a plugin file, you must restart the server to load it. After the system restarts, it will watch the plugin file for changes and reload it automatically if you edit it. The server console displays errors and feedback if it finds a problem with your plugin code.
Define the Plugin
const systemPlugin = require('./utils/plugins/systemPlugin.js');
module.exports = class systemDemo extends systemPlugin {
constructor(odm, settings) {
super(odm, settings); ...Plugins must inherit from a given base class recognized by the system. A plugin file must export a class that inherits from one of these known types, or it will load. The above example shows how to create a System plugin.
An LRS plugin should inherit from lrsPlugin like so:
const lrsPlugin = require('./utils/plugins/lrsPlugin.js');
module.exports = class systemDemo extends lrsPlugin {
constructor(lrs, dal, settings) {
super(lrs, dal, settings); ...A plugin dashboard should inherit from the global Dashboard. Note that you do not need a require call to include the base class Dashboard.
module.exports = class CustomDash extends Dashboard {
constructor(params, db) {
super(params, db); ...A plugin analytic processor should inherit from the global AnalyticProcessor. Note that you do not need a require call to include the base class AnalyticProcessor.
module.exports = class CustomProcessor extends AnalyticProcessor {
constructor(params, db, lrs) {
super(params, db, lrs); ...You must also declare a unique name for a plugin class by attaching a property pluginName to the class definition.
module.exports.pluginName = 'demo';
Import Other Libraries
You can use the typical NodeJS require keyword to import other libraries, with the following caveats:
- Each required file must have its own global scope.
- Every call to
require returns a new object — there is no cache of required objects.
- Calls use relative paths as normal, except when calling bundled resources. You should always address these resources as if at the top-level directory, regardless of the location of the plugin.
You can also install NPM modules alongside a plugin. require these as you normally would.
Import System Components
You can access certain components of the system at runtime by require-ing them. You can use this method to get the Original Design Manufacturer (ODM) models or routers and controllers that drive the system. When require-ing these objects, use the full path from the executable root. The plugin base classes are a good example of this process.
const lrsPlugin = require('./utils/plugins/lrsPlugin.js');While you won't find that file on the system, the call will still return the object, since it's part of the running software.
Be very careful! This lets you interface with the system beyond the defined plugin architecture and you should avoid it except for a few specific use cases.
System and LRS Plugin Services
The system provides several services to plugins.
Responding to Events
We expose most of the functionality via events that a plugin can listen to. These events include a name and a data payload and can optionally accept a return value. We offer all plugins the opportunity to listen to all relevant events, and can attach a handler via this:
this.on('statementBatch', (event, batch) => {
console.log(batch);
});The above code handles the statementBatch event as part of an LRS plugin.
A System plugin differs only in the list of events it can handle. Additionally, System plugins can hear all events on any LRS. For LRS events, when a System plugin receives them, the specific LRS instance which triggered the event passes as a parameter.
this.on('statementBatch', (lrs, event, batch) => {
console.log(batch);
});All event handling is asynchronous, and accepts an async function, or a function that returns a promise.
this.on('statementBatch', async (event, batch) => {
await postBatchToMyBISystem(batch);
});Event parameters are immutable. Attempting to change the value of an incoming parameter throws a runtime exception. Use the clone module to copy parameters if you need to modify them.
It's possible that several plugins will return values for a given event. When this happens, the last plugin to respond sets the final value used in further processing. Some events collect all responses and use them as a list.
Timing and Scheduling
To run logic on a timer, plugins can hook into the system's scheduling system. They do this by requesting an event to fire on an interval, then listening for that event. There is a special API to listen to timing events, onInterval.
this.every('10 seconds', '10Int');
this.schedule('190 seconds', 'FiresOnce');The above code sets up a recurring event sent to the plugin every 10 seconds, and one event that fires only once, in 190 seconds. The plugin should handle this like any other event.
this.onInterval('10Int', async (e) => {
console.log('This prints every 10 seconds, or the shortest interval.);
});
this.onInterval('FiresOnce', async (e) => {
console.log('This prints once);
});While currently no API clears intervals, removing or deactivating the plugin clears all pending events in the schedule.
State
Plugins must be stateless. You cannot count on data that you stored in global memory being present the next time you call a handler. This restriction lets plugins work in a multi-server environment and lets the system manage the lifecycle and resources consumed by plugins. To keep track of some value beyond the invocation of a handler, you can preserve that data in the plugin state.
const state = await this.getState();
if (!state.count) {
state.count = 1;
} else {
state.count++;
}
await this.persistState(state);
You manage state on a per plugin activation basis, so if you add the same plugin to the system or an LRS twice, each gets its own state. When you remove a plugin activation, it irretrievably destroys state.
Routers, HTTP, and HTML
You can use a standard express.js router to handle requests and responses, to hook up paths on the server to services that your plugin offers. You must register these routers via an API call to tell the system where it should mount them in the request handling process.
router = express();
this.router.get('/settings', (req, res, next) => {
res.send(settings);
});
this.setRouter('lrs', router);
The setRouter command tells the system that it should attach the path at the LRS-UI-level. So, for the path /settings, the actual URL on the server would be: /ui/lrs/lrsname/plugins/{pluginuuid}/settings.
You can get the value of {pluginuuid} via this.uuid. For convenience, we also offer this.getLink(path, 'lrs').
LRS plugins can attach routers at 'lrs' and 'portal', while system plugins can attach routers at 'lrs', 'portal', and 'system'.
You can also render Handlebars templates. You should place user-provided templates in ./views/templates, and they should end in the .hbs extension.
this.router.get('/showAPage', (req, res, next) => {
res.render("/templates/mypage");
});Install and Uninstall
A plugin can request to run code when you activate it in an LRS or in the system, or when you remove it. The system handles orchestrating these events in a multi-processor or multi-server system so that they run only once, regardless of the number of servers.
async install() {
console.log('install me ' + this.uuid);
}
async uninstall() {
console.log('uninstall me ' + this.uuid);
super.uninstall();
}Important! Call super.uninstall() to clean-up scheduled events.
Settings
Every activation of a plugin comes with a block of settings data. If you activate a plugin multiple times, each gets its own settings block. The user defines these settings in the UI or the API, and they pass to the class constructor.
constructor(lrs, dal, **settings**) {
super(lrs, dal, **settings**);
...You can define the form shown for populating the settings. You do this by implementing a static getter function for settingsForm, which returns a list of controls.
static get settingsForm() {
return [
{
label: 'String with client side validation',
id: 'nameOfProperty',
helptext: 'User should type a string',
validation: "val !== undefined && val !== '' && val.length > 2 && val.length < 100",
validationMessage: 'Enter a string',
placeholder: 'Show this as the place holder',
type: { isText: true, type: 'text' },
},
{
label: 'Checkbox',
id: 'checkbox',
helptext: 'This is either true or false',
type: { isCheck: true },
},
{
label: 'A Select',
id: 'select',
helptext: 'What should you pick',
type:
{ isSelect: true },
options: [
{
text: 'I attempted it',
value: 'http://adlnet.gov/expapi/verbs/attempted',
},
{
text: 'I attended it',
value: 'http://adlnet.gov/expapi/verbs/attended',
},
],
},
];
}Finally, to display the plugin in the Plugin Activation UI, the plugin must implement some metadata fields. These are not optional, and you must define them like this:
static get display() {
return {
title: 'Demo',
description: 'A demo plugin loaded from the filesystem',
};
}
// Additional metadata for display
static get metadata() {
return {
author: 'Veracity Technology Consultants',
version: '1.0.0',
moreInfo: 'https://www.veracity.it',
};
}Analytics Processor Plugins
An Analytic Processor plugin is a more tightly constrained tool. It does not have access to any of the above services.
A basic plugin Analytics Processor looks like this:
module.exports = class MyProcessor extends AnalyticProcessor {
constructor(params,db, lrs) {
super(params,db, lrs);
console.log("Wow, in the derived constructor!", params);
this.pipeline = [
...CommonStages(this, {
range:true,
limit:true
}),
{
$match: {
"statement.object.id": this.param("activity")
}
},
{
$limit: 10
},
{
$group: {
_id: "$statement.actor.id",
count: {
$sum: 1
}
}
}
];
this.chartSetup = new BarChart("_id", "count");
this.map = MapToActorNameAsync("_id");
}
filter(val)
{
return Math.random() > .5;
}
map(val)
{
// console.log(val);
return val;
}
exec(results)
{
console.log(results);
return results;
}
static getConfiguration() {
let conf = new ProcessorConfiguration("Demo", ProcessorConfiguration.widgetType.graph,
ProcessorConfiguration.widgetSize.small);
conf.addParameter("activity", new ActivityPicker("Activity", "Choose the activity to plot"), true);
return conf;
}
}Notice how the constructor extends
AnalyticProcessor, and sets
this.pipeline. This is a MongoDB aggregation processor that “parameterizes” a part of the query by adding
this.param("activity") at a certain point. The static method
getConfiguration tells the UI a bit about how to display the parameters. It says that the user should pick a value for the
"activity" parameter, and to offer choices from the systems registry of xAPI activities. We make various picker
parameter types available. Note that this technology is behind all the built-in widget types!
You can also see that the class has map, filter, and exec functions. These let you execute some JavaScript on the results of the aggregation query, for cases where you can't get the logic into a MongoDB aggregation pipeline. Each of these functions may even perform asynchronous work using the async keyword in Elasticsearch.
filter(val) — Takes each value and returns a Boolean. If false, it removes the value from the result set. map(val) — Takes-in each value from the result stream and transforms it, returning a new object that will replace the result. exec(results) — Takes the whole set of results at once and returns a new set of results.
The plugin calls these functions in the order: filter, map, exec.
In the example above, you can see that, in the constructor, it replaces the map function, this.map. We generate a new mapping function by calling the utility, MapToActorNameAsync. This utility replaces the value of _id in each result with the actor's name, by looking up the actor from the system's registry where the results _id property value is the IFI for the actor. So, an object that looks like this…
{
_id: "mailto:rob@veracity.it",
averageScore: "100",
daysMissed: 0,
count: 100
}… becomes this…
{
_id: "Rob Chadwick",
averageScore: "100",
daysMissed: 0,
count: 100
}The constructor also sets the
chartConfig in the line
this.chartSetup = new BarChart("_id", "count"). This generates a new bar chart and names the bars with the value of the
_id field and sets the height of the bars from the
count field. The structure of the
chartConfig field depends on the engine value. See the section on
Chart Configuration for more information.
So, now you can see a basic setup, let's list the various utilities we have for you to use.
Processor Configuration
Every processor must export a configuration object from a static method called, getConfiguration.
static getConfiguration() {
let conf = new ProcessorConfiguration("Demo", ProcessorConfiguration.widgetType.graph,
ProcessorConfiguration.widgetSize.small);
conf.addParameter("activity", new ActivityPicker("Activity", "Choose the activity to plot"), true);
return conf;
}This configuration sets a few things:
ProcessorConfiguration(title:String, type:Enum, Size:Enum)
title — The displayed name of the associated widget.
type — An enumeration of types. Valid values areProcessorConfiguration.widgetType.graph
ProcessorConfiguration.widgetType.table
ProcessorConfiguration.widgetType.iconList
ProcessorConfiguration.widgetType.statementViewer
size — An enumeration of sizes. These sizes are just requests: the system tries to fill the space available.ProcessorConfiguration.widgetSize.small
ProcessorConfiguration.widgetSize.medium
ProcessorConfiguration.widgetSize.large
ProcessorConfiguration.widgetSize.xlarge
ProcessorConfiguration.widgetSize.xxlarge
A processor configuration also has a few functions you can call to set other options.
setDescription(text) — The description text block on the widgets page.
setCacheLifetime(time:String) — A human-readable interval like 5 seconds, 10 minutes, or 3 days. Sets the time the system caches analytics.
setRefreshSeconds(time:Number) — The chart will automatically refresh after this interval.
setEnableWidgetChrome(show:Boolean) — Add or remove the title bar and other chrome on the UI.
addParameter(parameterName, paramType, default_value, required) — Add a parameter picker to the configuration page.parameterName — The name of the parameter. You'll access the value sent by calling this.param(parameterName).
paramType — A parameter type. Defines the type of picker available to the user. default_value — The value that will be returned from this.param when the user doesn't supply a value. required — A Boolean that tells the UI that the parameter is required. If the widget has any required parameters and the user doesn’t set them, the UI will prompt the user.
Parameter Types
Used to tell the system what sort of UI to present the user when they configure a widget, based on the processor. You should create each with new. The values are in the global scope, so you can type new Text("user sees this").
ActivityPicker(title, required, description) — This picker type lets the user search for xAPI objects or activities. The value returned is the ID of the activity.ActorPicker(title, required, description) — This picker type lets the user search for xAPI actors or agents. The value returned is the IFI of the agent.ClassPicker(title, required, description) — This picker type lets the user search the classes set-up in the LRS. A class is a group of students. The value returned will be the UUID of the class.CoursePicker(title, required, description) — This picker type lets the user search the courses registered in the LRS. A course is a list of lessons. The value returned is the UUID of the course.LessonPicker(title, required, description) — This picker type lets the user search the lesson registered in the LRS. A lesson is an xAPI activity registered in the system with additional metadata. Returns the UUID of the lesson object.Text(title, description) — This picker type lets the user input any text value.NumberText(title, description) — This picker lets the user enter text into a textbox. It then parses this value into a number for you.TimeSpan(title, description) — This picker lets the user select from hourly, daily, monthly, or yearly. The value returned will be a string that you can use with Date.toString to cast a date to the given span. This operates by taking the "floor" of the DateTime at a given value. For instance, it returns '%Y-%m-%dT12:00:00Z' for "daily". Calling Date.toString('%Y-%m-%dT12:00:00Z') returns the same value for all timestamps on a given day. You can use this with a $group operator to group-up all statements on a particular day.TimeRange(title, description) — This picker lets a user choose a time range. It offers them either a predefined string like "today" or "this week", or they can choose a specific range. The value returned is a JavaScript object in the form {from:Date, to:Date}.Verb(title, description) — This picker lets the user choose from a predefined list of common xAPI verbs.ChoicePicker(title, required, description, choices) — This picker renders a selection set. You should arrange the choices parameters in an array in the form [{text:String, value:String}]. It shows the user the text, but the value you receive is the value.
Mapping Functions
These utilities make it easier for you to specify common mapping transforms. Each is a function in the global scope. The return value of these functions are themselves functions, and you should assign them to this.map. For instance:
this.map = MapToCourseName("_id");
MapToActorName(inkey, outkey) — The value of the result where the key is inkey should be an xAPI Agent. This function will find the name in the xAPI actor object and place it in the result under the outkey. If outkey is undefined, the plugin assumes it is the same as inkey. For instance, if this.map = MapToActorName("_id", "name") and the input object is:
{
_id: {
mbox:"mailto:rob@veracity.it",
account:{
name:"Rob C",
homePage:"https://www.veraycity.it"
}
}
}Output
{
_id: {
mbox: "mailto:rob@veracity.it",
account: {
name: "Rob C",
homePage: "https://www.veracity.it"
}
},
name: "Rob C"
}MapToActorEmail(inkey, outkey) — Like MapToActorName, but for e mail.MapToVerbDisplay(inkey, outkey) — Like MapToActorName, but for verbs. Finds the display string in a verb definition.MapToCourseName(inkey, outkey) — Like MapToActorName, but for xAPI activities. Finds and attaches the title for an activity by looking in the activity definition language maps.VerbIDToDisplay(inkey, outkey) — Given a verb IRI, selects the last segment of the IRI for display. Splits the value by /, and then return the last portion.MapToActorNameAsync(inkey, outkey) — Finds the actor’s name by examining the table of Canonical Agents. This is an asynchronous operation, and you should only use it when you have an actor IFI without the rest of the actor definition. The table of Canonical Agents keeps track of the last display name used in an xAPI statement for the given IFI.MapToCoursesNameAsync(inkey, outkey) — Finds the object name by examining the table of Canonical Activities. This is an asynchronous operation, and you should only use it when you have an object ID without the rest of the object definition. The table of Canonical Activities keeps track of the last display name used in an xAPI statement for the given ID.
Chart Configuration
Analytics Processor plugins use the
chartConfig field to store configuration for their widget renderer. Most widgets in the Veracity Learning LRS are of the type
graph. You set this in the constructor of the
ProcessorConfiguration object. A widget with the type
graph (or
table) needs more information on how to build the widget, and how it maps to the results of the query. We let you call some common constructors to build the JSON object that represents the chart drawn in the widget. We expose some common types with global constructors, but we don’t limit you to these. Check out
amCharts for full documentation on the possibilities.
You set the value of this.chartConfig in the constructor or in the exec function. Call these utilities with the new keyword in the global scope.
this.chartConfig = new BarChart("_id", "count");This creates an amCharts configuration object that looks like this. Setting the value to the below JSON is identical.
Output
{
forWidgetType: 'graph',
balloon: {
borderThickness: 0,
borderAlpha: 0,
fillAlpha: 0,
horizontalPadding: 0,
verticalPadding: 0,
shadowAlpha: 0
},
export: {
enabled: true,
fileName: 'Veracity_data'
},
type: 'XYChart',
labelsEnabled: false,
engine: 'amcharts4',
colors: {
list: [
'#00BBBB',
'#006E6E',
'#159800',
'#001F7C',
'#1FE200',
'#0133C8',
'#00BBBB',
'#006E6E',
'#159800',
'#001F7C',
'#1FE200',
'#0133C8',
'#00BBBB',
'#006E6E',
'#159800',
'#001F7C',
'#1FE200',
'#0133C8'
]
},
xAxes: [
{
id: 'c1',
type: 'CategoryAxis',
dataFields: {
category: '_id'
},
renderer: {
minGridDistance: 60,
grid: {
strokeOpacity: 0.05
},
labels: {
rotation: 45,
truncate: true,
maxWidth: 200,
verticalCenter: 'top',
horizontalCenter: 'left'
}
}
}
],
exporting: {
menu: {}
},
yAxes: [
{
id: 'v1',
type: 'ValueAxis',
dataFields: {
value: 'count'
},
renderer: {
grid: {
strokeOpacity: 0.05
}
}
}
],
series: [
{
id: 's1',
xAxis: 'c1',
yAxis: 'v1',
type: 'ColumnSeries',
name: 'Series Title',
stacked: false,
columns: {
tooltipText: '{categoryX}: {valueY}'
},
colors: {
list: [
'#00BBBB',
'#006E6E',
'#159800',
'#001F7C',
'#1FE200',
'#0133C8',
'#00BBBB',
'#006E6E',
'#159800',
'#001F7C',
'#1FE200',
'#0133C8',
'#00BBBB',
'#006E6E',
'#159800',
'#001F7C',
'#1FE200',
'#0133C8'
]
},
dataFields: {
categoryX: '_id',
valueY: 'count'
}
}
],
}Notice the final line, dataFields: {categoryX: '_id', valueY: 'count'}}]. This is where the BarChart constructor gets its parameters. The rest of this is the default configuration for a bar chart in Veracity. It sets up the colors, patterns, themes, and legend that a bar chart uses in our system. There are a couple more values on this object worth discussing:
engine — The Veracity Learning LRS includes several graphing libraries. We've deprecated amCharts3 and D3, so please always set this to "AmCharts4".forWidgetType — Usually you use this to tell the system that you intend this config for a graph. Widgets can have a few other types, like the table. You don't need to set this. When using the utilities, it's set automatically. This prevents assigning a BarChart configuration to a graph whose static configuration sets the type to table.
Chart Constructors
BarChart(category, value) — A bar chart where the bars label comes from the category field, and the value comes from the value field.PieChart(category, value) — A pie chart where the bars label comes from the category field, and the value comes from the value field.SerialChart(category, value) — An XY chart where the X-axis value comes from the category field, and the Y-axis value comes from the value field from each result document.MultilineChart(lines, categoryField) — An XY chart with multiple lines, where categoryField is the name of the X value for each datapoint, and lines is an array of strings of each Y value. For instance, if the data looks like this:
Input
[
{line1:10,line2:20,line3:23,Xval:1},
{line1:11,line2:24,line3:3,Xval:2}
...
{line1:103,line2:2,line3:365,Xval:10}
]
Then to generate a proper multiline chart, you would set the chartConfig as:
this.chartSetup = new MultilineChart(["line1", "line2", "line3"], "Xval");
ErrorBars(categoryField, valueField, errorField) — An XY chart that shows crosses to represent a value and a range around the value. The categoryField and valueField parameters work just like in a BarChart, and errorField is the size of the range around the valueField in the center.StackedBarChart(categoryField, stacks) — A bar chart with each bar subdivided into stacks. The categoryField is the name (and grouping key) for each bar, and stacks is an array of strings of the keys for the value fields. It automatically processes the data, so you can provide a series of documents like this:
Input
[
{name:"Rob",courseA:10},
{name:"Tim",courseB:0},
{name:"Rob",courseB:130},
{name:"Tim",courseA:50}
]
Then to generate a proper stacked bar chart, you would set-up the chart as:
this.chartSetup = new StackedBarChart("name", ["courseA", "courseB"]);
Table(column1, column2, ...) — A data table, where each column is one of the parameters from the constructor. Use this chart configuration only when WidgetType is "table".
Not every widget needs to be a graph rendered by a chart engine. We have a handful of other widget types that render various HTML.
graph — A graph, rendered by a graph engine (generally "AmCharts4"). Described in detail above.table — Renders a data table. Set the chartConfig to new table(...) when using this type. We’ve documented the constructor for Table above.progressChange — Renders a single large icon, a large value, and a string underneath. Use this to show a single value on the widget. It requires no configuration, but assumes you formatted your data as below. Remember, if the values returned from your query don't match this format, you can fix them in the this.exec function. It uses only the first value in the array.
[
{
icon: 'fa-check', //A Font Awesome icon class
change: '10 Attempts', //The title value
subtext: '', //A smaller line of text underneath
},
];
iconList — A list of several entries, each with an icon, title, and subtitle.
[
{
icon: 'fa-check', //A Font Awesome icon class
title: '10 Attempts', //The title value
subtext: '', //A smaller line of text underneath
color:"red" //A CSS color value
}
...
];
statementViewer — Lists statements with a special renderer. Each document in the result set should be a full xAPI statement.
Using ElasticSearch
You can use Elasticsearch instead of MongoDB for your queries. To do so, you should extend your Analytics Processor plugin from a different base class, ElasticAnalyticProcessor. When using this base class, the value of this.pipeline becomes meaningless. Instead, you must populate this.query and this.aggregation. The this.query value is an array of Elasticsearch DSL fragments, and this.aggregation is a full Elasticsearch aggregation. Note that you must include an aggregation. The plugin will pipe the results from this aggregation into your map, filter, and exec functions.
Be sure to initialize the query with this.query = this.commonQueryStages({ range: true }), if you want to obey the dashboard global time range.
Using Elasticsearch is vastly faster for most common queries and analytics, so prefer this option unless you need to execute complex queries that you can only represent as a MongoDB aggregation pipeline.
class ESCompletionsByStudent extends ElasticAnalyticProcessor {
constructor(parameters, db, lrs) {
super(parameters, db, lrs);
this.query = this.commonQueryStages({ range: true });
if (this.param('verb')) {
this.query.push({
term: {
'verb.id': this.param('verb'),
},
});
}
if (this.param('object')) {
this.query.push({
term: {
'object.id': this.param('object'),
},
});
}
this.aggregation = {
terms: {
field: 'actor.id',
size: 10,
order: {
_count: 'desc',
},
},
};
// find the name of each actor by actorID
if (this.param('verb')) this.map = multiMap(mapToActorNameAsync('key', 'actorName'),
mapToVerbDisplay('verb', 'verb'));
if (!this.param('verb')) this.map = mapToActorNameAsync('key', 'actorName');
this.chartSetup = new BarChart('actorName', 'doc_count');
}
}Overriding this.compute
If you wish not to use the MongoDB or Elasticsearch interfaces we offer, you can override the compute function. This function is asynchronous and returns an array of values to pipe into map, filter, and exec.
this.compute = async function GetDataFromAnotherServer() {
let url = this.param('url');
let request = require('request');
let values = await request.get(url);
return values;
}This notional example returns some value from another server, where the server address is a parameter. You can use this method to generate any analysis algorithm you wish.
System Events
Plugins interact with the system primarily by responding to events. Each event brings with it a certain set of data and expects a certain output. Here's an incomplete list of common events that you may want to respond to. Note that not every plugin receives every event — it depends on the plugin type and permission settings.
statementBatchStored
This is the single most used event for plugin authors. This event informs your plugin that the database received, validated, and stored a new xAPI statement batch. The only parameter (batch in the example below) is an array of statement records. These records include the indexes we create like "voided" and the "duration" in milliseconds, as well as the normalized statement. You can find the normalized statement at batch[*].statement.
Handle this event to synchronize your data with another system like a business intelligence tool, database, search engine, or AI processor. You can also watch for an event or trigger an event, like posting an HTTP request when a specific student completes a specific course.
Example
this.on('statementBatchStored', (e, batch) => { this.processBatch(batch); });
Returns
undefined — Returns no data.
Parameters
batch — An array of statement records.
systemODMEvent
This event fires when something changes in the system-level database. This is for informational purposes only.
Example
this.on('systemODMEvent', (e, method, type, model));
Returns
undefined — Returns no data.
Parameters
method — What happened: a string, either "created", "deleted", or "updated".type — The object type that changed: a string of the name of the model.model — The object that changed. This can be a user, an LRS, a message, or other system database entity.
LRSODMEvent
This event fires when something changes in the LRS-level database for a particular LRS. This is for informational purposes only.
Example
this.on('LRSODMEvent', (e, method, type, lrs, model));
Returns
undefined — Returns no data.
Parameters
method — What happened: a string, either "created", "deleted", or "updated".type — The object type that changed: a string of the name of the model.lrs — The UUID of the LRS in which the object changed.model — The object that changed. This can be a user, an LRS, a message, or other system database entity.
systemStartup
The system started.
Example
this.on('systemStartup', (e) ={});
Returns
undefined — Returns no data.
Parameters
- No parameters.
uiRequest
The system received an HTTP request to one of the UI paths.
Example
this.on('uiRequest', (e, req) => { this.prodLog(colors.cyan('Logger:') + colors.underline('UI'),
colors.green(req.method), req.url, colors.red(req.user ? req.user.email : '')); });
Returns
undefined — Returns no data.
Parameters
req — Data about the request. Includes: .url, .body, .user, and .method.