From b13fe65da516c60448f7dddb995f374b1f5b2301 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 14 Jul 2026 15:48:14 +0200 Subject: [PATCH] =?UTF-8?q?v0.3.14-beta:=20sta=20toewijzing=20van=20=C3=A9?= =?UTF-8?q?=C3=A9n=20widget=20toe=20met=20kijken/werken-modus=20(API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nieuwe migratie db/008_widget_share.sql: assignments krijgt widget_id (NULL = heel bord, bestaand gedrag) en mode ('kijken'/'werken', default 'werken') - PUT /assignments/class/:id en /assignments/pupil/:id accepteren optioneel widgetId + mode; gedeelde validatie (parseShareBody) controleert dat de widget echt op het opgegeven eigen bord staat - describeAssignment geeft widgetId/widgetType/mode terug voor de UI - GET /my/assignment filtert bij een widget-toewijzing tot die ene widget en stuurt de modus mee --- VERSION | 2 +- db/008_widget_share.sql | 7 ++++++ public/js/core.js | 2 +- src/api.js | 51 +++++++++++++++++++++++++++++------------ 4 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 db/008_widget_share.sql diff --git a/VERSION b/VERSION index b920c09..f08873b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.13-beta +0.3.14-beta diff --git a/db/008_widget_share.sql b/db/008_widget_share.sql new file mode 100644 index 0000000..dd52cd6 --- /dev/null +++ b/db/008_widget_share.sql @@ -0,0 +1,7 @@ +-- Widget-delen: een toewijzing kan naast een heel bord (widget_id NULL, +-- bestaand gedrag) ook één enkele widget van dat bord delen, met een modus: +-- 'kijken' (leerling ziet de widget read-only, snellere poll) of 'werken' +-- (leerling werkt erin; voortgang loopt via progress_events). widget_id +-- verwijst naar het stabiele wid in de borddata van de leerkracht. +ALTER TABLE assignments ADD COLUMN IF NOT EXISTS widget_id TEXT; +ALTER TABLE assignments ADD COLUMN IF NOT EXISTS mode TEXT NOT NULL DEFAULT 'werken'; diff --git a/public/js/core.js b/public/js/core.js index e3371e3..5f8f479 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -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.13-beta"; +const VERSION = "0.3.14-beta"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/src/api.js b/src/api.js index eaf39f6..2d003c6 100644 --- a/src/api.js +++ b/src/api.js @@ -462,14 +462,34 @@ export default async function api(app) { const describeAssignment = async (a) => { const t = (await pool.query('SELECT display_name, username, data FROM users WHERE id = $1', [a.teacher_id])).rows[0]; const board = t ? findBoardById(t.data, a.board_id) : null; + const widget = a.widget_id ? (board?.data?.widgets || []).find((w) => w.wid === a.widget_id) : null; return { teacherId: Number(a.teacher_id), teacherName: t ? (t.display_name || t.username) : null, boardId: a.board_id, boardName: board ? board.name : null, + widgetId: a.widget_id || null, + widgetType: widget ? widget.id : null, + mode: a.mode || 'werken', updatedAt: a.updated_at, }; }; + // Gedeelde validatie voor beide PUT-routes: heel bord (widgetId null, bestaand + // gedrag) of één widget van dat bord, met modus kijken/werken. De widget moet + // echt in het opgegeven bord van de leerkracht zelf staan. + const parseShareBody = (req, reply) => { + const { boardId, widgetId, mode } = req.body ?? {}; + if (!boardId || typeof boardId !== 'string' || boardId.length > 100) return fail(reply, 400, 'ongeldige boardId'); + const board = findBoardById(req.user.data, boardId); + if (!board) return fail(reply, 400, 'bord onbekend of niet van deze gebruiker'); + const m = mode ?? 'werken'; + if (m !== 'kijken' && m !== 'werken') return fail(reply, 400, 'ongeldige modus'); + if (widgetId != null) { + if (typeof widgetId !== 'string' || widgetId.length > 100) return fail(reply, 400, 'ongeldige widgetId'); + if (!(board.data?.widgets || []).some((w) => w.wid === widgetId)) return fail(reply, 400, 'widget onbekend op dit bord'); + } + return { boardId, widgetId: widgetId ?? null, mode: m }; + }; app.get('/assignments/class/:id', async (req, reply) => { need(req, reply, PERMISSIONS['assignments.manage']); @@ -482,18 +502,17 @@ export default async function api(app) { }); app.put('/assignments/class/:id', async (req, reply) => { need(req, reply, PERMISSIONS['assignments.manage']); - const { boardId } = req.body ?? {}; - if (!boardId || typeof boardId !== 'string' || boardId.length > 100) return fail(reply, 400, 'ongeldige boardId'); - if (!findBoardById(req.user.data, boardId)) return fail(reply, 400, 'bord onbekend of niet van deze gebruiker'); + const share = parseShareBody(req, reply); + if (share.error) return share; 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'); await pool.query( - `INSERT INTO assignments (class_id, teacher_id, board_id) VALUES ($1,$2,$3) + `INSERT INTO assignments (class_id, teacher_id, board_id, widget_id, mode) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (class_id) WHERE class_id IS NOT NULL - DO UPDATE SET teacher_id = $2, board_id = $3, updated_at = now()`, - [c.id, req.user.id, boardId]); + DO UPDATE SET teacher_id = $2, board_id = $3, widget_id = $4, mode = $5, updated_at = now()`, + [c.id, req.user.id, share.boardId, share.widgetId, share.mode]); return { ok: true }; }); app.delete('/assignments/class/:id', async (req, reply) => { @@ -521,17 +540,16 @@ export default async function api(app) { }); app.put('/assignments/pupil/:id', async (req, reply) => { need(req, reply, PERMISSIONS['assignments.manage']); - const { boardId } = req.body ?? {}; - if (!boardId || typeof boardId !== 'string' || boardId.length > 100) return fail(reply, 400, 'ongeldige boardId'); - if (!findBoardById(req.user.data, boardId)) return fail(reply, 400, 'bord onbekend of niet van deze gebruiker'); + const share = parseShareBody(req, reply); + if (share.error) return share; 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'); await pool.query( - `INSERT INTO assignments (pupil_id, teacher_id, board_id) VALUES ($1,$2,$3) + `INSERT INTO assignments (pupil_id, teacher_id, board_id, widget_id, mode) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (pupil_id) WHERE pupil_id IS NOT NULL - DO UPDATE SET teacher_id = $2, board_id = $3, updated_at = now()`, - [u.id, req.user.id, boardId]); + DO UPDATE SET teacher_id = $2, board_id = $3, widget_id = $4, mode = $5, updated_at = now()`, + [u.id, req.user.id, share.boardId, share.widgetId, share.mode]); return { ok: true }; }); app.delete('/assignments/pupil/:id', async (req, reply) => { @@ -553,11 +571,14 @@ export default async function api(app) { 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 { widgets: [], updatedAt: null }; + if (!a) return { widgets: [], mode: 'werken', updatedAt: null }; const t = (await pool.query('SELECT data FROM users WHERE id = $1', [a.teacher_id])).rows[0]; const board = t ? findBoardById(t.data, a.board_id) : null; - const widgets = (board?.data?.widgets || []).map((w) => ({ id: w.id, wid: w.wid, state: w.state })); - return { widgets, updatedAt: a.updated_at }; + let widgets = (board?.data?.widgets || []).map((w) => ({ id: w.id, wid: w.wid, state: w.state })); + // één-widget-toewijzing: alleen die widget; is hij inmiddels van het bord + // verwijderd, dan een lege lijst (zelfde weergave als geen toewijzing) + if (a.widget_id) widgets = widgets.filter((w) => w.wid === a.widget_id); + return { widgets, mode: a.mode || 'werken', updatedAt: a.updated_at }; }); // ---- voortgang (leerling-omgeving) ---------------------------------------------