Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
4 changed files with 122 additions and 5 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.4.32-beta
|
||||
0.4.33-beta
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"use strict";
|
||||
/* version — shown until /api/version resolves (or if the fetch fails, e.g. offline).
|
||||
Kept in sync by hand with the VERSION file at the repo root on every release. */
|
||||
const VERSION = "0.4.32-beta";
|
||||
const VERSION = "0.4.33-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
|
|||
|
|
@ -53,9 +53,23 @@ function renderPupilWidgets(widgets, mode){
|
|||
hebben nog geen wid - dan is positie in de lijst de terugval-identiteit,
|
||||
stabiel zolang de leerkracht de widgets op het bord niet herschikt */
|
||||
const widgetId = w.wid || (def.id + ":" + idx);
|
||||
/* in kijken-modus geen voortgang: de leerling doet zelf niets */
|
||||
const opts = watch ? { readonly:true } : { readonly:true, onProgress: (p)=>{
|
||||
api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } }).catch(()=>{});
|
||||
/* in kijken-modus geen voortgang: de leerling doet zelf niets. In
|
||||
werken-modus moet de leerling juist wél kunnen werken - readonly:true
|
||||
stond hier sinds v0.3.15-beta abusievelijk ook in de werken-tak (een
|
||||
kopieerfout toen kijken/werken werd toegevoegd). De twee widgets die
|
||||
opts.readonly echt lezen (letters.js/anchor.js) gebruiken het alleen om
|
||||
hun eigen bewerk-gereedschap (woordenlijst-editor, opslaan/bibliotheek)
|
||||
te verbergen, niet om de kernoefening te blokkeren - maar dat gereedschap
|
||||
hoort een leerling in werken-modus wél te zien, dus dit was hoe dan ook
|
||||
verkeerd om, los van de vraag of dit "voortgang werkt niet" verklaart. */
|
||||
const opts = watch ? { readonly:true } : { onProgress: (p)=>{
|
||||
/* eerder een stille .catch(()=>{}) - een mislukte log (verlopen sessie,
|
||||
geen toewijzing meer, servergolf) verdween daarmee spoorloos, wat een
|
||||
"voortgang werkt niet"-klacht onmogelijk te onderzoeken maakt. Nu
|
||||
zichtbaar in de console (geen storende UI voor de leerling, wél een
|
||||
spoor voor wie het probleem moet natrekken). */
|
||||
api("/my/progress", { method:"POST", body: { widgetId, widgetType: def.id, ...p } })
|
||||
.catch(e=>console.warn("voortgang loggen mislukt:", e.message));
|
||||
}};
|
||||
def.mount(body, w.state || null, opts);
|
||||
});
|
||||
|
|
|
|||
103
test/progress-logging.test.js
Normal file
103
test/progress-logging.test.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import Fastify from 'fastify';
|
||||
import cookie from '@fastify/cookie';
|
||||
import rateLimit from '@fastify/rate-limit';
|
||||
import api from '../src/api.js';
|
||||
|
||||
/* "de voortgangen werken niet" (gebruikersklacht) - dit pad had nog GEEN enkele
|
||||
test (mocked-pool of anders), ondanks dat het de kern van de voortgangs-
|
||||
feature is: een leerling logt een afgerond moment, en het datamodel bepaalt
|
||||
zelf teacher_id/board_id uit de toewijzing (nooit van de client). Dekt de
|
||||
drie manieren waarop een toewijzing gevonden kan worden (leerling-eigen,
|
||||
klasbreed, thuiskind-via-ouder) plus de foutpaden. */
|
||||
function makeApp(user, { assignmentRows = {}, parentRows = [] } = {}) {
|
||||
const calls = [];
|
||||
const query = async (sql, params = []) => {
|
||||
calls.push({ sql, params });
|
||||
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] };
|
||||
if (sql.includes('FROM user_roles')) return { rows: [] };
|
||||
if (sql === 'SELECT * FROM assignments WHERE pupil_id = $1') return { rows: assignmentRows.byPupil ? [assignmentRows.byPupil] : [] };
|
||||
if (sql === 'SELECT * FROM assignments WHERE class_id = $1') return { rows: assignmentRows.byClass ? [assignmentRows.byClass] : [] };
|
||||
if (sql.startsWith('SELECT parent_id FROM parent_children')) return { rows: parentRows };
|
||||
if (sql.startsWith('INSERT INTO progress_events')) return { rows: [] };
|
||||
return { rows: [] };
|
||||
};
|
||||
const pool = { query, async connect() { return { query, release() {} }; } };
|
||||
return (async () => {
|
||||
const app = Fastify({ trustProxy: 2 });
|
||||
await app.register(cookie);
|
||||
await app.register(rateLimit, { global: false });
|
||||
app.decorate('pg', pool);
|
||||
await app.register(api, { prefix: '/api' });
|
||||
await app.ready();
|
||||
return { app, calls };
|
||||
})();
|
||||
}
|
||||
|
||||
const cookies = { teach_session: 'x'.repeat(64) };
|
||||
const validBody = { widgetId: 'letters:0', widgetType: 'letters', attempts: 1, correct: 1, stars: 1 };
|
||||
function insertCall(calls) {
|
||||
return calls.find(c => c.sql.startsWith('INSERT INTO progress_events'));
|
||||
}
|
||||
|
||||
test('voortgang: leerling-eigen toewijzing wint en levert de juiste teacher_id/board_id op', async () => {
|
||||
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
||||
const { app, calls } = await makeApp(pupil, {
|
||||
assignmentRows: {
|
||||
byPupil: { teacher_id: 3, board_id: 'bord-a' },
|
||||
byClass: { teacher_id: 4, board_id: 'bord-b' }, // zou genegeerd moeten worden
|
||||
},
|
||||
});
|
||||
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
const ins = insertCall(calls);
|
||||
assert.ok(ins, 'INSERT verwacht');
|
||||
assert.equal(ins.params[1], 3);
|
||||
assert.equal(ins.params[2], 'bord-a');
|
||||
});
|
||||
|
||||
test('voortgang: valt terug op de klasbrede toewijzing als er geen eigen toewijzing is', async () => {
|
||||
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
||||
const { app, calls } = await makeApp(pupil, {
|
||||
assignmentRows: { byClass: { teacher_id: 4, board_id: 'bord-b' } },
|
||||
});
|
||||
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
const ins = insertCall(calls);
|
||||
assert.equal(ins.params[1], 4);
|
||||
assert.equal(ins.params[2], 'bord-b');
|
||||
});
|
||||
|
||||
test('voortgang: thuiskind zonder school valt terug op de gekoppelde ouder als "leerkracht"', async () => {
|
||||
const homeChild = { id: 11, username: 'timo', role: 'pupil', school_id: null, class_id: null, data: {}, data_rev: 0 };
|
||||
const { app, calls } = await makeApp(homeChild, { parentRows: [{ parent_id: 7 }] });
|
||||
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
const ins = insertCall(calls);
|
||||
assert.equal(ins.params[1], 7);
|
||||
assert.equal(ins.params[2], 'thuis');
|
||||
});
|
||||
|
||||
test('voortgang: leerling op school zonder enige toewijzing krijgt 400, geen stille silent-fail insert', async () => {
|
||||
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
||||
const { app, calls } = await makeApp(pupil, {});
|
||||
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
assert.ok(!insertCall(calls), 'er mag geen INSERT gebeurd zijn');
|
||||
});
|
||||
|
||||
test('voortgang: niet-leerling (bv. leerkracht) krijgt 403', async () => {
|
||||
const teacher = { id: 3, username: 'juf', role: 'teacher', school_id: 2, class_id: null, data: {}, data_rev: 0 };
|
||||
const { app } = await makeApp(teacher, {});
|
||||
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: validBody });
|
||||
assert.equal(res.statusCode, 403, res.body);
|
||||
});
|
||||
|
||||
test('voortgang: ontbrekende widgetId/widgetType levert 400 op, niet een kapotte insert', async () => {
|
||||
const pupil = { id: 9, username: 'lena', role: 'pupil', school_id: 2, class_id: 50, data: {}, data_rev: 0 };
|
||||
const { app, calls } = await makeApp(pupil, { assignmentRows: { byPupil: { teacher_id: 3, board_id: 'bord-a' } } });
|
||||
const res = await app.inject({ method: 'POST', url: '/api/my/progress', cookies, payload: { attempts: 1, correct: 1 } });
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
assert.ok(!insertCall(calls));
|
||||
});
|
||||
Loading…
Reference in a new issue