Voeg 28 herkenbare kleurplaten en creatieve niveaus toe (v0.4.66-beta) #4
4 changed files with 45 additions and 17 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.13-beta
|
||||
0.3.14-beta
|
||||
|
|
|
|||
7
db/008_widget_share.sql
Normal file
7
db/008_widget_share.sql
Normal 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';
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
51
src/api.js
51
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) ---------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue