v0.3.09-beta: voeg voortgang-schema en -API toe
- Nieuwe migratie db/007_progress.sql: progress_events-tabel (per leerling
per widget-moment: pogingen, goed, sterren)
- POST /my/progress: leerling logt een afgerond moment; teacher_id/board_id
worden server-side afgeleid uit de bestaande toewijzing, nooit van de client
- GET /progress/pupil/:id en GET /progress/class/🆔 leerkracht-weergave,
gated met nieuwe progress.view-permissie en dezelfde scoping als assignments
- Permissie progress.view toegevoegd in beide permissions.js-bestanden
This commit is contained in:
parent
a20779e602
commit
e3369e34a2
6 changed files with 79 additions and 2 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.08-beta
|
||||
0.3.09-beta
|
||||
|
|
|
|||
19
db/007_progress.sql
Normal file
19
db/007_progress.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
-- Voortgang: leerlingen loggen elk afgerond "moment" in een widget (ronde
|
||||
-- sommen, voltooid woord, geknapte ballon) zodat leerkrachten een tijdlijn
|
||||
-- per leerling kunnen bekijken. teacher_id/board_id komen server-side uit de
|
||||
-- bestaande assignments-tabel, nooit van de client, om te voorkomen dat een
|
||||
-- leerling voortgang onder een andere leerkracht kan wegschrijven.
|
||||
CREATE TABLE IF NOT EXISTS progress_events (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
board_id TEXT NOT NULL,
|
||||
widget_id TEXT NOT NULL,
|
||||
widget_type TEXT NOT NULL,
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
correct INT NOT NULL DEFAULT 0,
|
||||
stars INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_progress_events_pupil ON progress_events(pupil_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_progress_events_teacher ON progress_events(teacher_id, created_at DESC);
|
||||
|
|
@ -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.3.08-beta";
|
||||
const VERSION = "0.3.09-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ const PERMISSIONS = {
|
|||
'users.genpw': ['super', 'admin', 'teacher'],
|
||||
'users.role.change': ['super'],
|
||||
'assignments.manage': ['super', 'admin', 'teacher'],
|
||||
'progress.view': ['super', 'admin', 'teacher'],
|
||||
};
|
||||
|
||||
const CREATABLE_ROLES = {
|
||||
|
|
|
|||
56
src/api.js
56
src/api.js
|
|
@ -559,6 +559,62 @@ export default async function api(app) {
|
|||
const widgets = (board?.data?.widgets || []).map((w) => ({ id: w.id, state: w.state }));
|
||||
return { widgets, updatedAt: a.updated_at };
|
||||
});
|
||||
|
||||
// ---- voortgang (leerling-omgeving) ---------------------------------------------
|
||||
// Leerlingen loggen elk afgerond "moment" in een widget (ronde sommen, voltooid
|
||||
// woord, geknapte ballon). teacher_id/board_id komen server-side uit de effectieve
|
||||
// toewijzing (net als /my/assignment), nooit van de client, zodat een leerling geen
|
||||
// voortgang onder een andere leerkracht kan wegschrijven.
|
||||
const PROGRESS_LIMIT = 200;
|
||||
const clampCount = (n) => Math.max(0, Math.min(1000, Number(n) || 0));
|
||||
app.post('/my/progress', async (req, reply) => {
|
||||
need(req, reply);
|
||||
if (req.user.role !== 'pupil') return fail(reply, 403, 'alleen voor leerlingen');
|
||||
const { widgetId, widgetType, attempts, correct, stars } = req.body ?? {};
|
||||
if (!widgetId || typeof widgetId !== 'string' || widgetId.length > 100) return fail(reply, 400, 'ongeldige widgetId');
|
||||
if (!widgetType || typeof widgetType !== 'string' || widgetType.length > 40) return fail(reply, 400, 'ongeldige widgetType');
|
||||
let a = (await pool.query('SELECT * FROM assignments WHERE pupil_id = $1', [req.user.id])).rows[0];
|
||||
if (!a && req.user.class_id) {
|
||||
a = (await pool.query('SELECT * FROM assignments WHERE class_id = $1', [req.user.class_id])).rows[0];
|
||||
}
|
||||
if (!a) return fail(reply, 400, 'geen toewijzing');
|
||||
await pool.query(
|
||||
`INSERT INTO progress_events (pupil_id, teacher_id, board_id, widget_id, widget_type, attempts, correct, stars)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8)`,
|
||||
[req.user.id, a.teacher_id, a.board_id, widgetId, widgetType, clampCount(attempts), clampCount(correct), clampCount(stars)]);
|
||||
return { ok: true };
|
||||
});
|
||||
const describeProgressEvent = (e) => ({
|
||||
widgetId: e.widget_id,
|
||||
widgetType: e.widget_type,
|
||||
attempts: e.attempts,
|
||||
correct: e.correct,
|
||||
stars: e.stars,
|
||||
createdAt: e.created_at,
|
||||
});
|
||||
app.get('/progress/pupil/:id', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['progress.view']);
|
||||
const u = (await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id])).rows[0];
|
||||
if (!u) return fail(reply, 404, 'gebruiker onbekend');
|
||||
if (!(await pupilAccessible(req, u))) return fail(reply, 403, 'geen rechten');
|
||||
const rows = (await pool.query(
|
||||
'SELECT * FROM progress_events WHERE pupil_id = $1 ORDER BY created_at DESC LIMIT $2',
|
||||
[u.id, PROGRESS_LIMIT])).rows;
|
||||
return { events: rows.map(describeProgressEvent) };
|
||||
});
|
||||
app.get('/progress/class/:id', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['progress.view']);
|
||||
const c = (await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id])).rows[0];
|
||||
if (!c) return fail(reply, 404, 'klas onbekend');
|
||||
if (!sameSchool(req, c)) return fail(reply, 403, 'geen rechten');
|
||||
if (teacherOnly(req) && !(await classOwnedByTeacher(c.id, req.user.id))) return fail(reply, 403, 'geen rechten');
|
||||
const rows = (await pool.query(
|
||||
`SELECT pe.*, u.display_name, u.username FROM progress_events pe
|
||||
JOIN users u ON u.id = pe.pupil_id
|
||||
WHERE u.class_id = $1 ORDER BY pe.created_at DESC LIMIT $2`,
|
||||
[c.id, PROGRESS_LIMIT])).rows;
|
||||
return { events: rows.map((e) => ({ ...describeProgressEvent(e), pupilId: Number(e.pupil_id), pupilName: e.display_name || e.username })) };
|
||||
});
|
||||
}
|
||||
|
||||
// Eerste super-beheerder aanmaken als die nog niet bestaat.
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export const PERMISSIONS = {
|
|||
'users.genpw': ['super', 'admin', 'teacher'],
|
||||
'users.role.change': ['super'],
|
||||
'assignments.manage': ['super', 'admin', 'teacher'],
|
||||
'progress.view': ['super', 'admin', 'teacher'],
|
||||
};
|
||||
|
||||
// Welke rol een account met welke rol mag aanmaken.
|
||||
|
|
|
|||
Loading…
Reference in a new issue