Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
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";
|
"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.08-beta";
|
const VERSION = "0.3.09-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const PERMISSIONS = {
|
||||||
'users.genpw': ['super', 'admin', 'teacher'],
|
'users.genpw': ['super', 'admin', 'teacher'],
|
||||||
'users.role.change': ['super'],
|
'users.role.change': ['super'],
|
||||||
'assignments.manage': ['super', 'admin', 'teacher'],
|
'assignments.manage': ['super', 'admin', 'teacher'],
|
||||||
|
'progress.view': ['super', 'admin', 'teacher'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const CREATABLE_ROLES = {
|
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 }));
|
const widgets = (board?.data?.widgets || []).map((w) => ({ id: w.id, state: w.state }));
|
||||||
return { widgets, updatedAt: a.updated_at };
|
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.
|
// Eerste super-beheerder aanmaken als die nog niet bestaat.
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export const PERMISSIONS = {
|
||||||
'users.genpw': ['super', 'admin', 'teacher'],
|
'users.genpw': ['super', 'admin', 'teacher'],
|
||||||
'users.role.change': ['super'],
|
'users.role.change': ['super'],
|
||||||
'assignments.manage': ['super', 'admin', 'teacher'],
|
'assignments.manage': ['super', 'admin', 'teacher'],
|
||||||
|
'progress.view': ['super', 'admin', 'teacher'],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Welke rol een account met welke rol mag aanmaken.
|
// Welke rol een account met welke rol mag aanmaken.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue