Verbeter grafische kwaliteit van kleurplaten (v0.4.67-beta) #5

Open
bes-r wants to merge 208 commits from bes-r/coloring-quality-v0.4.67 into main AGit
4 changed files with 45 additions and 17 deletions
Showing only changes of commit b13fe65da5 - Show all commits

View file

@ -1 +1 @@
0.3.13-beta 0.3.14-beta

7
db/008_widget_share.sql Normal file
View file

@ -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';

View file

@ -2,7 +2,7 @@
"use strict"; "use strict";
/* version shown until /api/version resolves (or if the fetch fails, e.g. offline). /* 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. */ 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(){ (function(){
const tag = document.getElementById("verTag"); const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION; tag.textContent = "v"+VERSION;

View file

@ -462,14 +462,34 @@ export default async function api(app) {
const describeAssignment = async (a) => { 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 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 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 { return {
teacherId: Number(a.teacher_id), teacherId: Number(a.teacher_id),
teacherName: t ? (t.display_name || t.username) : null, teacherName: t ? (t.display_name || t.username) : null,
boardId: a.board_id, boardId: a.board_id,
boardName: board ? board.name : null, boardName: board ? board.name : null,
widgetId: a.widget_id || null,
widgetType: widget ? widget.id : null,
mode: a.mode || 'werken',
updatedAt: a.updated_at, 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) => { app.get('/assignments/class/:id', async (req, reply) => {
need(req, reply, PERMISSIONS['assignments.manage']); need(req, reply, PERMISSIONS['assignments.manage']);
@ -482,18 +502,17 @@ export default async function api(app) {
}); });
app.put('/assignments/class/:id', async (req, reply) => { app.put('/assignments/class/:id', async (req, reply) => {
need(req, reply, PERMISSIONS['assignments.manage']); need(req, reply, PERMISSIONS['assignments.manage']);
const { boardId } = req.body ?? {}; const share = parseShareBody(req, reply);
if (!boardId || typeof boardId !== 'string' || boardId.length > 100) return fail(reply, 400, 'ongeldige boardId'); if (share.error) return share;
if (!findBoardById(req.user.data, boardId)) return fail(reply, 400, 'bord onbekend of niet van deze gebruiker');
const c = (await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id])).rows[0]; 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 (!c) return fail(reply, 404, 'klas onbekend');
if (!sameSchool(req, c)) return fail(reply, 403, 'geen rechten'); 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'); if (teacherOnly(req) && !(await classOwnedByTeacher(c.id, req.user.id))) return fail(reply, 403, 'geen rechten');
await pool.query( 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 ON CONFLICT (class_id) WHERE class_id IS NOT NULL
DO UPDATE SET teacher_id = $2, board_id = $3, updated_at = now()`, DO UPDATE SET teacher_id = $2, board_id = $3, widget_id = $4, mode = $5, updated_at = now()`,
[c.id, req.user.id, boardId]); [c.id, req.user.id, share.boardId, share.widgetId, share.mode]);
return { ok: true }; return { ok: true };
}); });
app.delete('/assignments/class/:id', async (req, reply) => { 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) => { app.put('/assignments/pupil/:id', async (req, reply) => {
need(req, reply, PERMISSIONS['assignments.manage']); need(req, reply, PERMISSIONS['assignments.manage']);
const { boardId } = req.body ?? {}; const share = parseShareBody(req, reply);
if (!boardId || typeof boardId !== 'string' || boardId.length > 100) return fail(reply, 400, 'ongeldige boardId'); if (share.error) return share;
if (!findBoardById(req.user.data, boardId)) return fail(reply, 400, 'bord onbekend of niet van deze gebruiker');
const u = (await pool.query('SELECT * FROM users WHERE id = $1', [req.params.id])).rows[0]; 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 (!u) return fail(reply, 404, 'gebruiker onbekend');
if (!(await pupilAccessible(req, u))) return fail(reply, 403, 'geen rechten'); if (!(await pupilAccessible(req, u))) return fail(reply, 403, 'geen rechten');
await pool.query( 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 ON CONFLICT (pupil_id) WHERE pupil_id IS NOT NULL
DO UPDATE SET teacher_id = $2, board_id = $3, updated_at = now()`, DO UPDATE SET teacher_id = $2, board_id = $3, widget_id = $4, mode = $5, updated_at = now()`,
[u.id, req.user.id, boardId]); [u.id, req.user.id, share.boardId, share.widgetId, share.mode]);
return { ok: true }; return { ok: true };
}); });
app.delete('/assignments/pupil/:id', async (req, reply) => { app.delete('/assignments/pupil/:id', async (req, reply) => {
@ -553,11 +571,14 @@ export default async function api(app) {
if (!a && req.user.class_id) { if (!a && req.user.class_id) {
a = (await pool.query('SELECT * FROM assignments WHERE class_id = $1', [req.user.class_id])).rows[0]; 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 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 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 })); let widgets = (board?.data?.widgets || []).map((w) => ({ id: w.id, wid: w.wid, state: w.state }));
return { widgets, updatedAt: a.updated_at }; // éé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) --------------------------------------------- // ---- voortgang (leerling-omgeving) ---------------------------------------------