- What is TestInvite?
- Build Your First Test
- Run Your First Assessment
- Taking the Assessment
- Viewing the Results
- Question Bank Overview
- Common Question Features
- Scoring
- Question Types
- Question Authoring
- Browsing Questions
- Content Blocks
- Media Library
- Metadata Rules & Schema
- Tests Overview
- My Tests
- Creating a Test
- The Test Editor
- Test Settings
- Sections & Pages
- Adding Questions
- Page Builders
- Test Profile
- Reporting
- Test Papers
- Analytics
- Publishing a Test
- Test Library
- Marketplace
- Tasks Overview
- Creating a Task
- Task Dashboard
- Steps
- Task Settings
- Candidates
- Test Sessions
- Sent Mails
- Proctoring
- Analytics
- Analytics Overview
- Test Analytics
- Question Analytics
- Per-Question Analytics
- Results Explorer
- Segment Analysis
Custom Report Recipes
Tested building blocks for custom reports: safe percentages, matching the test's own pass mark and performance levels, parent and child dimensions, band texts, answers by Reporting Name, sten bars and reusable macros.
Short, tested building blocks. Function snippets go inside function anonymous(data) { … }; template snippets go in the Nunjucks template. Field names are explained in the Report Input Reference.
Function recipes
Percentage without dividing by zero
function pct(point, total) {
return total ? Math.round((point / total) * 100) : 0;
}Pass / fail with the test's own pass mark
function verdictOf(percent, passFail) {
if (!passFail || !passFail.active) return null;
const threshold = passFail.pass.min.score * 100; // stored as 0-1
const passed = passFail.pass.min.inclusive ? percent >= threshold : percent > threshold;
return { passed: passed, content: passed ? passFail.pass.content : passFail.fail.content };
}
const verdict = verdictOf(percent, data.testReporting && data.testReporting.passFail);The test's own performance level
function levelOf(percent, level) {
if (!level || !level.active) return null;
for (let i = 0; i < level.items.length; i++) {
const it = level.items[i];
const aboveFrom = it.from.inclusive ? percent >= it.from.value : percent > it.from.value;
const belowTo = it.to.inclusive ? percent <= it.to.value : percent < it.to.value;
if (aboveFrom && belowTo) return it.content; // { label, description, color }
}
return null;
}
const level = levelOf(percent, data.testReporting && data.testReporting.level);Pass rounded percentages (as from pct above) to get the same verdict and level as the Standard report.
Top-level and child dimensions
const names = Object.keys(data.result.dimensions);
const topLevel = names.filter(function (n) { return n.indexOf("/") === -1; });
function childrenOf(parent) {
const depth = parent.split("/").length + 1;
return names.filter(function (n) {
return n.indexOf(parent + "/") === 0 && n.split("/").length === depth;
});
}
// childrenOf("Aptitude") → ["Aptitude/Verbal", "Aptitude/Numerical"]Band texts from one lookup
const TEXTS = {
low: "Shows foundational understanding; targeted practice is recommended.",
mid: "Performs reliably in most situations.",
high: "Performs at an advanced level and could coach others."
};
const description = TEXTS[band.key];Keeping every text in one object also makes it easy to find the longest one when you check that pages fit A4.
Time spent
const secs = data.result.serveStats.timeSpent || 0; const duration = Math.floor(secs / 60) + " min " + (secs % 60) + " s";
Copy an object with an extra field (instead of object spread)
const row = Object.assign({}, base, { percent: 72 });Not-ready guard
// function
if (!data.result) return { ready: false };
{# template #}
{% if not ready %}<p>Results are not available yet.</p>{% else %} … {% endif %}Template recipes
An answer by Reporting Name
<p>Motivation: {{ result.extracted["Q_MOTIVATION"] | default("—") }}</p>An access-form field
Employee no: {{ initiator.roomForm["Employee No"] }}Sten bar (1–10)
{% set s = normy.normativeResult.dimensions["Communication"].sten %}
{% for i in range(1, 11) %}
<span class="cell {{ 'on' if i <= s else '' }}"></span>
{% endfor %}Reusable chip (macro)
{% macro chip(text, color) %}<span class="chip" style="background:{{ color }}">{{ text }}</span>{% endmacro %}
{{ chip(overall.band.label, overall.band.color) }}Score bar
<div class="bar"><span style="width: {{ d.percent }}%; background: {{ d.band.color }}"></span></div>Loop over a map
{% for name, dim in result.dimensions %}
{{ name }}: {{ dim.stats.point }} / {{ dim.stats.totalPoint }}
{% endfor %}