Skip to content

Commit 813a293

Browse files
authored
report generation (#4)
* store form results in browser localstorage * report generation * lint * fix linting on commit * run lint and test on ci * report tests * stub tests * working assessment capture * report generates, there are failing tests, everything is titled foo * make tests pass
1 parent 4444602 commit 813a293

15 files changed

+7114
-2478
lines changed

.eleventy.js

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ module.exports = function (eleventyConfig) {
3333
eleventyConfig.addPassthroughCopy({
3434
"./node_modules/govuk-frontend/dist/govuk/assets/images": "./assets/images",
3535
"./node_modules/govuk-frontend/dist/govuk/assets/fonts": "./assets/fonts",
36+
"./node_modules/chaarts/dist/chaarts.min.css": "./assets/chaarts.min.css",
3637
});
3738

3839
return {

.github/workflows/ci.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ jobs:
2020
with:
2121
node-version-file: ".nvmrc"
2222
- run: npm install
23+
- run: npm run lint
24+
- run: npm test
2325
- run: PATH_PREFIX=/cloudmaturity/ npm run build
2426

2527
- name: Tar files

.husky/pre-commit

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env sh
2+
. "$(dirname -- "$0")/_/husky.sh"
3+
4+
npx lint-staged

.lintstagedrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"*.{js,md,css,json}": ["prettier --write"]
3+
}

package-lock.json

+6,515-2,426
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+16-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
"scripts": {
88
"start": "eleventy --serve",
99
"build": "eleventy",
10-
"test": "echo \"Error: no test specified\" && exit 1"
10+
"lint": "prettier -c .",
11+
"test": "jest",
12+
"prepare": "husky install"
1113
},
1214
"repository": {
1315
"type": "git",
@@ -21,6 +23,18 @@
2123
"homepage": "https://github.com/co-cddo/cloudmaturity#readme",
2224
"devDependencies": {
2325
"@11ty/eleventy": "^2.0.1",
24-
"@x-govuk/govuk-eleventy-plugin": "^5.0.7"
26+
"@x-govuk/govuk-eleventy-plugin": "^5.0.7",
27+
"husky": "^8.0.0",
28+
"jest": "^29.7.0",
29+
"jest-environment-jsdom": "^29.7.0",
30+
"lint-staged": "^15.2.2",
31+
"prettier": "^3.2.5"
32+
},
33+
"dependencies": {
34+
"add": "^2.0.6",
35+
"chaarts": "github:ffoodd/chaarts"
36+
},
37+
"jest": {
38+
"testEnvironment": "jsdom"
2539
}
2640
}

plugins/inputs.js

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
*--------------------------------------------------------------------------------------------*/
55
"use strict";
66

7+
const { createHash } = require("crypto");
8+
9+
function hash(string) {
10+
return createHash("sha256").update(string).digest("hex");
11+
}
12+
713
function input(md, config) {
814
input.config = config;
915
input.prefix = "";
@@ -184,13 +190,15 @@ input.render = function (type, data, regexp, md) {
184190
<div class="govuk-radios" data-module="govuk-radios">`;
185191
for (let i = 0; i < options.length; i++) {
186192
if (!optionsParams[i]) optionsParams[i] = {};
187-
if (!optionsParams[i].id)
188-
optionsParams[i].id =
189-
input.prefix + input.sanitize(name + "-" + options[i]);
193+
optionsParams[i].id = `${hash(label)}_${i}`;
194+
optionsParams[i].value = i + 1;
190195
if (!enabled) optionsParams[i].disabled = true;
196+
191197
html += `<div class="govuk-radios__item">
192198
${input.addAttributes(
193-
`<input class="govuk-radios__input" type="radio" name="${name}">`,
199+
`<input class="govuk-radios__input" type="radio" name="cmm_${
200+
md.env.page.fileSlug
201+
}_${hash(label)}">`,
194202
optionsParams[i],
195203
)}
196204
<label class="govuk-label govuk-radios__label" for="${
@@ -268,10 +276,10 @@ input.parseLine = function (rule) {
268276

269277
input.rules = [
270278
{
271-
// Turns 'label[name] = ___' or 'label[name] = __value_' into form input element
279+
// Turns 'label[name] = ****' or 'label[name] = **value**' into form input element
272280
type: "textfield",
273281
regexp:
274-
/([^\n\[]*)(?:\[(.+)\])?\s*=\s*__+([^\n_]+)?_+\s*(?:<!--\s*input:\s*({.*})\s*-->)?/gy,
282+
/([^\n\[]*)(?:\[(.+)\])?\s*=\s*\*\*+([^\n\*]+)?\*+\s*(?:<!--\s*input:\s*({.*})\s*-->)?/gy,
275283
},
276284
{
277285
// Turns '\"\"\"lang\nvalue\n\"\"\"[name]' into text area element

src/_includes/nextAssessmentButton.njk

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
<a href="/assessment" id="resetButton" role="button" draggable="false" class="govuk-button govuk-button--warning" data-module="govuk-button">
2+
Reset
3+
</a>
14
{% set navPages = collections.ordered | eleventyNavigation("Assessment") %}
25
{% set nextEntry = false %}
36
{% set foundEntry = false %}
4-
57
{%- for entry in navPages %}
68
{% if nextEntry == true and foundEntry == false %}
79
<a href="{{ entry.url }}" role="button" draggable="false" class="govuk-button" data-module="govuk-button">
@@ -20,4 +22,5 @@ Save and continue
2022
<a href="/assessment/report" role="button" draggable="false" class="govuk-button" data-module="govuk-button">
2123
Get report
2224
</a>
23-
{% endif %}
25+
{% endif %}
26+
<script src="/assets/cmm_assessment.js"></script>

src/assessment/index.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ title: Assessment
66

77
## How to use this tool
88

9-
TODO:
9+
Name of report [cmm_intro_department] = **My Organisation**
1010

11-
Your department [department] = __Name of report__
11+
> This could be the name of your department or directorate, or any other name you like, it isn't saved and only displayed on the report at the end.
1212
13-
<a href="{{ (collections.ordered | eleventyNavigation('Assessment') | first).url }}" role="button" draggable="false" class="govuk-button" data-module="govuk-button">
14-
Save and continue
15-
</a>
13+
{% include 'nextAssessmentButton.njk' %}

src/assessment/report.md

+15-37
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,23 @@ title: Report
66

77
## Report goes here
88

9-
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="1000" height="500" viewBox="-57 1 200 1">
109
<style>
11-
.axis {
12-
stroke: #555;
13-
stroke-width: .2;
10+
:root {
11+
--foreground-lightness: 0%;
12+
--foreground-o-25: hsl(0deg 0% var(--foreground-lightness)/25%);
13+
--foreground-o-10: hsl(0deg 0% var(--foreground-lightness)/10%);
14+
--chaarts-purple: rgba(29,112,184,0.5);
15+
--to-radians: 0.01745329251;
16+
--scale: 1;
17+
--step: 0.3;
1418
}
15-
.scale {
16-
fill: #f0f0f0;
17-
stroke: #999;
18-
stroke-width: .2;
19+
20+
#report_radar:hover {
21+
--chaarts-purple: rgba(29,112,184,0.9);
1922
}
20-
.shape {
21-
fill-opacity: .3;
22-
stroke-width: .5;
23+
.chaarts[class*=radar] span {
24+
transition: background 1s;
2325
}
24-
.shape:hover { fill-opacity: .6; }
25-
.shape { fill: #00a0b0; stroke: #00a0b0; }
2626
</style>
27-
<g><g>
28-
<circle class="scale" fill="none" r="41.66666666666667"></circle>
29-
<circle class="scale" fill="none" r="27.77777777777778"></circle>
30-
<circle class="scale" fill="none" r="13.88888888888889"></circle>
31-
</g><g>
32-
<polyline class="axis" points="0,0 0,-41.6667"></polyline>
33-
<polyline class="axis" points="0,0 39.6274,-12.8757"></polyline>
34-
<polyline class="axis" points="0,0 24.4911,33.709"></polyline>
35-
<polyline class="axis" points="0,0 -24.4911,33.709"></polyline>
36-
<polyline class="axis" points="0,0 -39.6274,-12.8757"></polyline>
37-
</g><g>
38-
<text class="caption" text-anchor="middle" font-size="3" font-family="sans-serif" y="-47.5" dy="1.5">People</text>
39-
<text class="caption" text-anchor="middle" font-size="3" font-family="sans-serif" x="48.1752" y="-14.6783" dy="1.5">Operations</text>
40-
<text class="caption" text-anchor="middle" font-size="3" font-family="sans-serif" x="27.9198" y="38.4283" dy="1.5">Security</text>
41-
<text class="caption" text-anchor="middle" font-size="3" font-family="sans-serif" x="-27.9198" y="38.4283" dy="1.5">Other</text>
42-
<text class="caption" text-anchor="middle" font-size="3" font-family="sans-serif" x="-45.1752" y="-14.6783" dy="1.5">Other2</text>
43-
</g><g>
44-
<path class="shape" d="M0,-33.3333L31.7019,-10.3006L14.6946,20.2254L-24.4911,33.709L-39.6274,-12.8757z"></path>
45-
</g></g>
46-
<text class="caption" text-anchor="left" font-size="3" font-family="sans-serif" y="0" x="13" dy="1.5">Good</text>
47-
<text class="caption" text-anchor="left" font-size="3" font-family="sans-serif" y="0" x="27" dy="1.5">Better</text>
48-
<text class="caption" text-anchor="left" font-size="3" font-family="sans-serif" y="0" x="41" dy="1.5">Best</text>
49-
</g><g>
50-
</svg>
27+
<link rel="stylesheet" href="/assets/chaarts.min.css">
28+
<script src="/assets/cmm_report.js"></script>

src/assets/cmm_assessment.js

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
if (typeof Storage === "undefined")
2+
alert(
3+
"Browser has disabled local storage, you will be unable to save your results from one page to another or generate a report.",
4+
);
5+
6+
function getFormValues() {
7+
const save = {};
8+
const field_names = Array.from(document.querySelectorAll("input"))
9+
.map((el) => el.name)
10+
.filter((item, index, self) => self.indexOf(item) === index);
11+
12+
field_names.forEach((name) => {
13+
const [model, section, question] = name.split("_");
14+
setPropertySafely(save, [model, section, question], "");
15+
document
16+
.querySelectorAll(
17+
`input[type=text][name=${name}], input[type=radio][name=${name}]:checked`,
18+
)
19+
.forEach((el) =>
20+
setPropertySafely(save, [model, section, question], el.value),
21+
);
22+
});
23+
return save;
24+
}
25+
26+
function restoreFormValues() {
27+
Array.from(document.querySelectorAll("input")).forEach((el) => {
28+
const [model, section, question] = el.name.split("_");
29+
const value = JSON.parse(localStorage.getItem(model))?.[section]?.[
30+
question
31+
];
32+
if (el.type === "text" && value) el.value = value;
33+
if (el.type === "radio" && value && el.value === value) el.checked = true;
34+
});
35+
}
36+
function setPropertySafely(obj, keys, value) {
37+
keys.reduce((acc, key, index) => {
38+
if (index === keys.length - 1) {
39+
acc[key] = value;
40+
} else {
41+
acc[key] = acc[key] || {};
42+
}
43+
return acc[key];
44+
}, obj);
45+
}
46+
document
47+
.querySelectorAll("input")
48+
.forEach((input) => input.addEventListener("change", saveFormValues));
49+
50+
function saveFormValues() {
51+
Object.entries(getFormValues()).forEach(([model, sections]) => {
52+
const workingModel = JSON.parse(localStorage.getItem(model)) || {};
53+
Object.entries(sections).forEach(
54+
([sectionName, section]) => (workingModel[sectionName] = section),
55+
);
56+
57+
localStorage.setItem(model, JSON.stringify(workingModel));
58+
});
59+
}
60+
61+
function clearFormValues(model) {
62+
localStorage.removeItem(model);
63+
}
64+
65+
if (typeof module === "object")
66+
module.exports = {
67+
getFormValues,
68+
setPropertySafely,
69+
saveFormValues,
70+
clearFormValues,
71+
};
72+
73+
window.addEventListener("load", restoreFormValues);
74+
75+
if (document.getElementById("resetButton"))
76+
document
77+
.getElementById("resetButton")
78+
.addEventListener("click", (e) =>
79+
confirm("Are you sure you want to reset the entire report?")
80+
? clearFormValues()
81+
: e.preventDefault(),
82+
);

src/assets/cmm_report.js

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const titles = {
2+
cost: "Cost & Sustainability",
3+
data: "Data",
4+
governance: "Governance",
5+
operations: "Operations",
6+
people: "People",
7+
security: "Security",
8+
tech: "Technology",
9+
};
10+
function cmm() {
11+
const data = { categories: {} };
12+
const payload = JSON.parse(localStorage.getItem("cmm"));
13+
Object.entries(payload).forEach(([category, v]) => {
14+
if (category === "intro") return;
15+
const questions = {};
16+
Object.entries(v).forEach(([question, answer]) => {
17+
questions[question] = parseInt(answer) || 1;
18+
});
19+
data.categories[category] = {
20+
questions,
21+
title: titles[category],
22+
max: Object.keys(v).length * 5,
23+
};
24+
});
25+
return data;
26+
}
27+
28+
function getCategories() {
29+
return Object.keys(cmm().categories);
30+
}
31+
32+
function getScorePercent(category) {
33+
return getScore(category) / getScoreMax(category);
34+
}
35+
36+
function getScore(category) {
37+
return Object.values(cmm().categories[category].questions).reduce(
38+
(a, c) => a + c,
39+
0,
40+
);
41+
}
42+
43+
function getScoreMax(category) {
44+
return cmm().categories[category].max;
45+
}
46+
47+
function getCategoryTitle(category) {
48+
return cmm().categories[category].title;
49+
}
50+
51+
function createEl(tag, parent) {
52+
let el = document.createElement(tag);
53+
parent.appendChild(el);
54+
return el;
55+
}
56+
57+
var addRule = (function (style) {
58+
var sheet = document.head.appendChild(style).sheet;
59+
return function (selector, css) {
60+
var propText =
61+
typeof css === "string"
62+
? css
63+
: Object.keys(css)
64+
.map(function (p) {
65+
return p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p]);
66+
})
67+
.join(";");
68+
sheet.insertRule(selector + "{" + propText + "}", sheet.cssRules.length);
69+
};
70+
})(document.createElement("style"));
71+
72+
function getScoreTable() {
73+
let tbl = document.createElement("table");
74+
tbl.id = "report_radar";
75+
tbl.className = "radar chaarts";
76+
let thr = createEl("tr", createEl("thead", tbl));
77+
let tr = createEl("tr", createEl("tbody", tbl));
78+
const categories = getCategories();
79+
for (var i = 0; i < categories.length; i++) {
80+
const category = categories[i];
81+
let th = createEl("th", thr);
82+
th.textContent = getCategoryTitle(category);
83+
th.scope = "col";
84+
addRule(`.chaarts.radar [scope=col]:nth-child(${i + 1})::after`, {
85+
content: `${getScore(category)} / ${getScoreMax(category)}`,
86+
});
87+
tbl.style.setProperty(`--${i + 1}`, getScorePercent(category));
88+
let td = createEl("span", createEl("td", tr));
89+
td.textContent = getScorePercent(category);
90+
}
91+
tbl.style.setProperty("--items", categories.length);
92+
tbl.style.setProperty(`--${i + 1}`, "var(--1)");
93+
94+
return tbl;
95+
}
96+
97+
function renderReport() {
98+
document
99+
.getElementById("report-goes-here")
100+
.insertAdjacentHTML("beforebegin", getScoreTable().outerHTML);
101+
}
102+
103+
window.addEventListener("load", renderReport);
104+
105+
if (typeof module === "object")
106+
module.exports = {
107+
getCategories,
108+
getScorePercent,
109+
getScore,
110+
getScoreMax,
111+
getCategoryTitle,
112+
getScoreTable,
113+
createEl,
114+
addRule,
115+
renderReport,
116+
};

0 commit comments

Comments
 (0)