Verbeter anatomie en plaatsing van avataraccessoires (v0.4.46-beta) #1
6 changed files with 143 additions and 33 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.91-beta
|
0.3.92-beta
|
||||||
|
|
|
||||||
|
|
@ -2054,3 +2054,25 @@ body.hc #settingsModal .am-inp, body.hc #settingsModal .am-sel{border-color:#000
|
||||||
#settingsModal .am-card-head.am-card-toggle{cursor:pointer;}
|
#settingsModal .am-card-head.am-card-toggle{cursor:pointer;}
|
||||||
#settingsModal .cms-card-body{padding:10px 12px;}
|
#settingsModal .cms-card-body{padding:10px 12px;}
|
||||||
#settingsModal .cms-card-body .am-reset{border:none; padding:0; background:transparent;}
|
#settingsModal .cms-card-body .am-reset{border:none; padding:0; background:transparent;}
|
||||||
|
|
||||||
|
/* ---- uniforme control-maat: elk veld/knop in het beheer even hoog ---- */
|
||||||
|
#settingsModal .am-btn,
|
||||||
|
#settingsModal .am-copy,
|
||||||
|
#settingsModal .tbtn,
|
||||||
|
#settingsModal .am-edit,
|
||||||
|
#settingsModal .am-inp,
|
||||||
|
#settingsModal .am-sel,
|
||||||
|
#settingsModal .am-pw,
|
||||||
|
#settingsModal input[type="search"],
|
||||||
|
#settingsModal input[type="number"],
|
||||||
|
#settingsModal input[type="password"],
|
||||||
|
#settingsModal input[type="text"]{
|
||||||
|
height:var(--control-h); min-height:var(--control-h); box-sizing:border-box;
|
||||||
|
font-size:13px; line-height:1;
|
||||||
|
}
|
||||||
|
#settingsModal .am-edit{padding:0 12px; opacity:.75;}
|
||||||
|
#settingsModal .am-edit:hover{opacity:1;}
|
||||||
|
/* textarea's (CMS) mogen hoger; niet vastpinnen */
|
||||||
|
#settingsModal textarea.am-inp, #settingsModal .cms-ta{height:auto; min-height:64px;}
|
||||||
|
/* labels boven de velden even breed als hun veld zodat rijen uitlijnen */
|
||||||
|
#settingsModal .am-field{gap:4px;}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
/* uitgeklapte leerling-groepen en gebruikersrijen (standaard alles dicht
|
/* uitgeklapte leerling-groepen en gebruikersrijen (standaard alles dicht
|
||||||
voor een rustig overzicht), blijven behouden over her-renders */
|
voor een rustig overzicht), blijven behouden over her-renders */
|
||||||
const openGroups = new Set();
|
const openGroups = new Set();
|
||||||
|
const closedGroups = new Set(); /* door de gebruiker bewust dichtgeklapt */
|
||||||
const openRows = new Set();
|
const openRows = new Set();
|
||||||
|
|
||||||
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
|
/* welke rollen de ingelogde gebruiker mag aanmaken: unie over hoofdrol + extra rollen */
|
||||||
|
|
@ -265,7 +266,7 @@
|
||||||
SYS_ADMINS = q ? ((await api("/admin/users")).users || []).filter(u=>u.role==="admin")
|
SYS_ADMINS = q ? ((await api("/admin/users")).users || []).filter(u=>u.role==="admin")
|
||||||
: USERS.filter(u=>u.role==="admin");
|
: USERS.filter(u=>u.role==="admin");
|
||||||
}
|
}
|
||||||
const cq = currentUser.role==="super" ? (selSchool ? "?school="+selSchool : "?school=0") : "";
|
const cq = currentUser.role==="super" ? (selSchool ? "?school="+selSchool : "?school=none") : "";
|
||||||
CLASSES = (await api("/admin/classes"+cq)).classes || [];
|
CLASSES = (await api("/admin/classes"+cq)).classes || [];
|
||||||
ASSIGNMENTS = {};
|
ASSIGNMENTS = {};
|
||||||
if(CLASSES.length){
|
if(CLASSES.length){
|
||||||
|
|
@ -312,10 +313,17 @@
|
||||||
.map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name)
|
.map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name)
|
||||||
.filter(Boolean).join(", ");
|
.filter(Boolean).join(", ");
|
||||||
}
|
}
|
||||||
function userStatus(u){
|
/* status compact als glyph met uitleg-title: ● in gebruik, ○ wacht nog op
|
||||||
if(u.pending) return T("amPending");
|
eerste login/activatie. Tekst clipte in de smalle statuskolom. */
|
||||||
if(u.role==="pupil" && !u.everLoggedIn) return T("amNeverLoggedIn");
|
function userStatusGlyph(u){
|
||||||
return "";
|
const waiting = u.pending || (u.role==="pupil" && !u.everLoggedIn);
|
||||||
|
const label = u.pending ? T("amPending")
|
||||||
|
: (u.role==="pupil" && !u.everLoggedIn) ? T("amNeverLoggedIn")
|
||||||
|
: T("amStatusActive");
|
||||||
|
const g = h("span","am-glyph" + (waiting ? "" : " ok"), waiting ? "○" : "●");
|
||||||
|
g.title = label;
|
||||||
|
g.setAttribute("aria-label", label);
|
||||||
|
return g;
|
||||||
}
|
}
|
||||||
function nameCellFor(display, username){
|
function nameCellFor(display, username){
|
||||||
const cell = h("span","am-name");
|
const cell = h("span","am-name");
|
||||||
|
|
@ -334,9 +342,7 @@
|
||||||
row.appendChild(h("span","am-cell", T(roleKey(u.role))));
|
row.appendChild(h("span","am-cell", T(roleKey(u.role))));
|
||||||
row.appendChild(h("span","am-cell", userKlasText(u) || "—"));
|
row.appendChild(h("span","am-cell", userKlasText(u) || "—"));
|
||||||
const stCell = h("span","am-cell am-status");
|
const stCell = h("span","am-cell am-status");
|
||||||
const st = userStatus(u);
|
stCell.appendChild(userStatusGlyph(u));
|
||||||
if(st) stCell.appendChild(h("span","am-chip", st));
|
|
||||||
else stCell.textContent = "—";
|
|
||||||
row.appendChild(stCell);
|
row.appendChild(stCell);
|
||||||
row.addEventListener("click", ()=>{ selectedUserKey = "u:"+u.id; syncSelection(); });
|
row.addEventListener("click", ()=>{ selectedUserKey = "u:"+u.id; syncSelection(); });
|
||||||
return row;
|
return row;
|
||||||
|
|
@ -861,21 +867,37 @@
|
||||||
if(creatableRoles().includes("super")) panel.appendChild(addForm("super"));
|
if(creatableRoles().includes("super")) panel.appendChild(addForm("super"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* klassen (admin/super binnen een school) */
|
/* klassen (admin/super binnen een school); de systeemmanager ziet zonder
|
||||||
|
gekozen school de losse klassen (school verwijderd) om ze terug te koppelen */
|
||||||
function renderKlassenPanel(panel){
|
function renderKlassenPanel(panel){
|
||||||
const schoolChosen = currentUser.role!=="super" || !!selSchool;
|
const looseView = currentUser.role==="super" && !selSchool;
|
||||||
if(!schoolChosen){
|
const canResetPw = can("schools.resetPasswords");
|
||||||
|
if(looseView){
|
||||||
|
if(!CLASSES.length){
|
||||||
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
|
panel.appendChild(h("div","guestnote", T("amPickSchoolHint")));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canResetPw = can("schools.resetPasswords");
|
panel.appendChild(h("div","guestnote", T("amLooseClassesHint")));
|
||||||
/* klassen als volwaardige rijen (zelfde maat als de gebruikersvakken),
|
}
|
||||||
met de gevaarlijke acties als aparte, bevestigde knoppen */
|
|
||||||
CLASSES.forEach(c=>{
|
CLASSES.forEach(c=>{
|
||||||
const pupilCount = USERS.filter(u=>u.role==="pupil" && Number(u.classId)===Number(c.id)).length;
|
const pupilCount = USERS.filter(u=>u.role==="pupil" && Number(u.classId)===Number(c.id)).length;
|
||||||
let edit = null;
|
let edit = null;
|
||||||
if(canResetPw){
|
if(canResetPw){
|
||||||
|
const tools = h("div","am-tools");
|
||||||
|
/* systeemmanager: klas aan een school (terug)koppelen */
|
||||||
|
if(currentUser.role==="super"){
|
||||||
|
const ss = h("select","am-sel");
|
||||||
|
ss.appendChild(new Option(T("amNoSchool"), ""));
|
||||||
|
SCHOOLS.forEach(sc=>ss.appendChild(new Option(sc.name, sc.id)));
|
||||||
|
ss.value = looseView ? "" : (selSchool || "");
|
||||||
|
ss.addEventListener("change", async ()=>{
|
||||||
|
try{ await api("/admin/classes/"+c.id, {method:"PATCH", body:{ schoolId: ss.value ? +ss.value : null }}); reload(); }
|
||||||
|
catch(e){ msg(e.message); }
|
||||||
|
});
|
||||||
|
tools.appendChild(field("amFieldSchool", ss));
|
||||||
|
}
|
||||||
const acts = h("div","am-actions");
|
const acts = h("div","am-actions");
|
||||||
|
if(!looseView){
|
||||||
acts.appendChild(dangerBtn(T("amResetClass"), "amConfirmReset", async ()=>{
|
acts.appendChild(dangerBtn(T("amResetClass"), "amConfirmReset", async ()=>{
|
||||||
try{
|
try{
|
||||||
const r = await api("/admin/classes/"+c.id+"/reset-passwords", {method:"POST"});
|
const r = await api("/admin/classes/"+c.id+"/reset-passwords", {method:"POST"});
|
||||||
|
|
@ -884,11 +906,11 @@
|
||||||
render();
|
render();
|
||||||
}catch(e){ msg(e.message); }
|
}catch(e){ msg(e.message); }
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
acts.appendChild(dangerBtn(T("amDelete"), "amDeleteClassWarn", async ()=>{
|
acts.appendChild(dangerBtn(T("amDelete"), "amDeleteClassWarn", async ()=>{
|
||||||
try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); }
|
try{ await api("/admin/classes/"+c.id, {method:"DELETE"}); reload(); }
|
||||||
catch(e){ msg(e.message); }
|
catch(e){ msg(e.message); }
|
||||||
}));
|
}));
|
||||||
const tools = h("div","am-tools");
|
|
||||||
tools.appendChild(field("amFieldActions", acts));
|
tools.appendChild(field("amFieldActions", acts));
|
||||||
edit = { key:"klas:"+c.id, tools };
|
edit = { key:"klas:"+c.id, tools };
|
||||||
}
|
}
|
||||||
|
|
@ -898,6 +920,7 @@
|
||||||
edit,
|
edit,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
if(looseView) return;
|
||||||
if(canResetPw){
|
if(canResetPw){
|
||||||
const f = h("div","am-add");
|
const f = h("div","am-add");
|
||||||
const inp = h("input","am-inp");
|
const inp = h("input","am-inp");
|
||||||
|
|
@ -1289,7 +1312,8 @@
|
||||||
const sync = ()=>{ g.textContent = `${openGroups.has(key) ? "▾" : "▸"} ${label} (${us.length})`; };
|
const sync = ()=>{ g.textContent = `${openGroups.has(key) ? "▾" : "▸"} ${label} (${us.length})`; };
|
||||||
sync();
|
sync();
|
||||||
g.addEventListener("click", ()=>{
|
g.addEventListener("click", ()=>{
|
||||||
if(openGroups.has(key)) openGroups.delete(key); else openGroups.add(key);
|
if(openGroups.has(key)){ openGroups.delete(key); closedGroups.add(key); }
|
||||||
|
else{ openGroups.add(key); closedGroups.delete(key); }
|
||||||
sync();
|
sync();
|
||||||
applyUserFilter(panel);
|
applyUserFilter(panel);
|
||||||
});
|
});
|
||||||
|
|
@ -1306,6 +1330,9 @@
|
||||||
}else{
|
}else{
|
||||||
CLASSES.forEach(c=>{
|
CLASSES.forEach(c=>{
|
||||||
const us = pupils.filter(u=>u.classId===c.id);
|
const us = pupils.filter(u=>u.classId===c.id);
|
||||||
|
/* met een gekozen school staan de klasgroepen standaard open - de
|
||||||
|
schoolkeuze wás al het filter; dichtklappen wordt onthouden */
|
||||||
|
if(us.length && !closedGroups.has("class:"+c.id)) openGroups.add("class:"+c.id);
|
||||||
if(us.length) pupilGroup("class:"+c.id, c.name, us);
|
if(us.length) pupilGroup("class:"+c.id, c.name, us);
|
||||||
});
|
});
|
||||||
const noClass = pupils.filter(u=>!u.classId);
|
const noClass = pupils.filter(u=>!u.classId);
|
||||||
|
|
|
||||||
|
|
@ -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.91-beta";
|
const VERSION = "0.3.92-beta";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
@ -280,6 +280,7 @@ const I18N = {
|
||||||
amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode",
|
amResultPw:"Nieuw wachtwoord", amResultCode:"Nieuwe koppelcode",
|
||||||
amColStatus:"Status", amNeverLoggedIn:"nog niet ingelogd", amFieldSchool:"School", amStatusActive:"actief",
|
amColStatus:"Status", amNeverLoggedIn:"nog niet ingelogd", amFieldSchool:"School", amStatusActive:"actief",
|
||||||
amNoSchool:"— geen school —", amNoSchoolGroup:"Zonder school", amShowMore:"meer tonen",
|
amNoSchool:"— geen school —", amNoSchoolGroup:"Zonder school", amShowMore:"meer tonen",
|
||||||
|
amLooseClassesHint:"Deze klassen horen bij geen enkele school. Koppel ze via Bewerken.",
|
||||||
amDetailEmpty:"Selecteer links een gebruiker om te bewerken, of maak een nieuwe aan.",
|
amDetailEmpty:"Selecteer links een gebruiker om te bewerken, of maak een nieuwe aan.",
|
||||||
amNewUser:"Nieuwe gebruiker", amRequestOne:"Aanmelding", amReqNew:"nieuw",
|
amNewUser:"Nieuwe gebruiker", amRequestOne:"Aanmelding", amReqNew:"nieuw",
|
||||||
amParents:"Ouders",
|
amParents:"Ouders",
|
||||||
|
|
@ -575,6 +576,7 @@ const I18N = {
|
||||||
amResultPw:"New password", amResultCode:"New link code",
|
amResultPw:"New password", amResultCode:"New link code",
|
||||||
amColStatus:"Status", amNeverLoggedIn:"not signed in yet", amFieldSchool:"School", amStatusActive:"active",
|
amColStatus:"Status", amNeverLoggedIn:"not signed in yet", amFieldSchool:"School", amStatusActive:"active",
|
||||||
amNoSchool:"— no school —", amNoSchoolGroup:"No school", amShowMore:"show more",
|
amNoSchool:"— no school —", amNoSchoolGroup:"No school", amShowMore:"show more",
|
||||||
|
amLooseClassesHint:"These classes belong to no school. Attach them via Edit.",
|
||||||
amDetailEmpty:"Select a user on the left to edit, or create a new one.",
|
amDetailEmpty:"Select a user on the left to edit, or create a new one.",
|
||||||
amNewUser:"New user", amRequestOne:"Enrolment", amReqNew:"new",
|
amNewUser:"New user", amRequestOne:"Enrolment", amReqNew:"new",
|
||||||
amParents:"Parents",
|
amParents:"Parents",
|
||||||
|
|
@ -621,7 +623,9 @@ const I18N = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let LANG = "nl";
|
let LANG = "nl";
|
||||||
const T = k => I18N[LANG][k];
|
/* vangnet: ontbreekt een sleutel in de actieve taal, val terug op NL en
|
||||||
|
daarna op de sleutelnaam zelf - een knop mag nooit zonder tekst staan */
|
||||||
|
const T = k => I18N[LANG][k] ?? I18N.nl[k] ?? k;
|
||||||
/* effective CSS zoom of the app (display-size setting); pointer coordinates
|
/* effective CSS zoom of the app (display-size setting); pointer coordinates
|
||||||
are visual pixels, absolute positions are layout pixels → divide by VZ() */
|
are visual pixels, absolute positions are layout pixels → divide by VZ() */
|
||||||
const VZ = ()=>{ const z = parseFloat(document.body.style.zoom); return z>0 ? z : 1; };
|
const VZ = ()=>{ const z = parseFloat(document.body.style.zoom); return z>0 ? z : 1; };
|
||||||
|
|
|
||||||
31
src/api.js
31
src/api.js
|
|
@ -338,13 +338,33 @@ export default async function api(app) {
|
||||||
need(req, reply, PERMISSIONS['classes.view']);
|
need(req, reply, PERMISSIONS['classes.view']);
|
||||||
const schoolId = req.user.allRoles.includes('super') ? req.query.school : req.user.school_id;
|
const schoolId = req.user.allRoles.includes('super') ? req.query.school : req.user.school_id;
|
||||||
if (!schoolId) return { classes: [] };
|
if (!schoolId) return { classes: [] };
|
||||||
|
// 'none' (alleen super): losse klassen (school ooit verwijderd) - anders
|
||||||
|
// zijn die nergens zichtbaar en dus ook niet terug te koppelen
|
||||||
|
const loose = schoolId === 'none' && req.user.allRoles.includes('super');
|
||||||
const r = await pool.query(
|
const r = await pool.query(
|
||||||
`SELECT c.id, c.name,
|
`SELECT c.id, c.name,
|
||||||
COALESCE(json_agg(ct.user_id) FILTER (WHERE ct.user_id IS NOT NULL), '[]') AS teacher_ids
|
COALESCE(json_agg(ct.user_id) FILTER (WHERE ct.user_id IS NOT NULL), '[]') AS teacher_ids
|
||||||
FROM classes c LEFT JOIN class_teachers ct ON ct.class_id = c.id
|
FROM classes c LEFT JOIN class_teachers ct ON ct.class_id = c.id
|
||||||
WHERE c.school_id = $1 GROUP BY c.id ORDER BY c.name`, [schoolId]);
|
WHERE ${loose ? 'c.school_id IS NULL' : 'c.school_id = $1'} GROUP BY c.id ORDER BY c.name`,
|
||||||
|
loose ? [] : [schoolId]);
|
||||||
return { classes: r.rows.map((c) => ({ id: Number(c.id), name: c.name, teacherIds: c.teacher_ids.map(Number) })) };
|
return { classes: r.rows.map((c) => ({ id: Number(c.id), name: c.name, teacherIds: c.teacher_ids.map(Number) })) };
|
||||||
});
|
});
|
||||||
|
// klas aan een (andere) school koppelen of losmaken - alleen systeemmanager;
|
||||||
|
// herstelinstrument voor klassen die hun school kwijt zijn
|
||||||
|
app.patch('/admin/classes/:id', async (req, reply) => {
|
||||||
|
need(req, reply, PERMISSIONS['schools.manage']);
|
||||||
|
const c = (await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id])).rows[0];
|
||||||
|
if (!c) return fail(reply, 404, 'klas onbekend');
|
||||||
|
const b = req.body ?? {};
|
||||||
|
if (b.schoolId === undefined) return fail(reply, 400, 'school verplicht');
|
||||||
|
const target = b.schoolId === null ? null : Number(b.schoolId);
|
||||||
|
if (target !== null) {
|
||||||
|
const school = (await pool.query('SELECT 1 FROM schools WHERE id = $1', [target])).rows[0];
|
||||||
|
if (!school) return fail(reply, 404, 'school onbekend');
|
||||||
|
}
|
||||||
|
await pool.query('UPDATE classes SET school_id = $1 WHERE id = $2', [target, c.id]);
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
app.post('/admin/classes', async (req, reply) => {
|
app.post('/admin/classes', async (req, reply) => {
|
||||||
need(req, reply, PERMISSIONS['classes.manage']);
|
need(req, reply, PERMISSIONS['classes.manage']);
|
||||||
const { name, school } = req.body ?? {};
|
const { name, school } = req.body ?? {};
|
||||||
|
|
@ -594,8 +614,15 @@ export default async function api(app) {
|
||||||
if (!school) return fail(reply, 404, 'school onbekend');
|
if (!school) return fail(reply, 404, 'school onbekend');
|
||||||
}
|
}
|
||||||
if (target !== (u.school_id == null ? null : Number(u.school_id))) {
|
if (target !== (u.school_id == null ? null : Number(u.school_id))) {
|
||||||
|
// klasindeling behouden als de huidige klas al bij de doelschool hoort
|
||||||
|
// (herstelvolgorde: eerst de klas terugkoppelen, dan de leerlingen)
|
||||||
|
let keepClass = null;
|
||||||
|
if (u.class_id != null && target !== null) {
|
||||||
|
const cls = (await pool.query('SELECT school_id FROM classes WHERE id = $1', [u.class_id])).rows[0];
|
||||||
|
if (cls && Number(cls.school_id) === target) keepClass = u.class_id;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await pool.query('UPDATE users SET school_id = $1, class_id = NULL WHERE id = $2', [target, u.id]);
|
await pool.query('UPDATE users SET school_id = $1, class_id = $2 WHERE id = $3', [target, keepClass, u.id]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// naam kan botsen in de doelschool óf (bij losmaken) in de
|
// naam kan botsen in de doelschool óf (bij losmaken) in de
|
||||||
// school-loze naamruimte (systeemmanagers/ouders/thuiskinderen)
|
// school-loze naamruimte (systeemmanagers/ouders/thuiskinderen)
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ test('systeemmanager koppelt een gebruiker aan een school en maakt hem weer los'
|
||||||
payload: { schoolId: 2 } });
|
payload: { schoolId: 2 } });
|
||||||
assert.equal(res2.statusCode, 200, res2.body);
|
assert.equal(res2.statusCode, 200, res2.body);
|
||||||
const upd = calls2.find((c) => c.sql.startsWith('UPDATE users SET school_id'));
|
const upd = calls2.find((c) => c.sql.startsWith('UPDATE users SET school_id'));
|
||||||
assert.deepEqual(upd.params, [2, 7]);
|
assert.deepEqual(upd.params, [2, null, 7]);
|
||||||
assert.ok(calls2.some((c) => c.sql.includes('DELETE FROM class_teachers')));
|
assert.ok(calls2.some((c) => c.sql.includes('DELETE FROM class_teachers')));
|
||||||
await app.close(); await app2.close();
|
await app.close(); await app2.close();
|
||||||
// losmaken: schoolId null, geen schools-check nodig; leerling verliest toewijzing
|
// losmaken: schoolId null, geen schools-check nodig; leerling verliest toewijzing
|
||||||
|
|
@ -131,7 +131,7 @@ test('systeemmanager koppelt een gebruiker aan een school en maakt hem weer los'
|
||||||
payload: { schoolId: null } });
|
payload: { schoolId: null } });
|
||||||
assert.equal(res3.statusCode, 200, res3.body);
|
assert.equal(res3.statusCode, 200, res3.body);
|
||||||
const upd3 = calls3.find((c) => c.sql.startsWith('UPDATE users SET school_id'));
|
const upd3 = calls3.find((c) => c.sql.startsWith('UPDATE users SET school_id'));
|
||||||
assert.deepEqual(upd3.params, [null, 9]);
|
assert.deepEqual(upd3.params, [null, null, 9]);
|
||||||
assert.ok(calls3.some((c) => c.sql.includes('DELETE FROM assignments WHERE pupil_id')));
|
assert.ok(calls3.some((c) => c.sql.includes('DELETE FROM assignments WHERE pupil_id')));
|
||||||
await app3.close();
|
await app3.close();
|
||||||
});
|
});
|
||||||
|
|
@ -172,6 +172,36 @@ test('school koppelen meldt een naamconflict netjes als 409', async () => {
|
||||||
await app.close();
|
await app.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('systeemmanager koppelt een losse klas terug aan een school', async () => {
|
||||||
|
const { app, calls } = await makeAppWithSchool(superUser, null);
|
||||||
|
const origQuery = app.pg.query.bind(app.pg);
|
||||||
|
app.pg.query = async (sql, params = []) => {
|
||||||
|
if (sql.startsWith('SELECT * FROM classes WHERE id')) { calls.push({ sql, params }); return { rows: [{ id: 9, name: 'Groep 4', school_id: null }] }; }
|
||||||
|
return origQuery(sql, params);
|
||||||
|
};
|
||||||
|
const res = await app.inject({ method: 'PATCH', url: '/api/admin/classes/9', cookies,
|
||||||
|
payload: { schoolId: 2 } });
|
||||||
|
assert.equal(res.statusCode, 200, res.body);
|
||||||
|
const upd = calls.find((c) => c.sql.startsWith('UPDATE classes SET school_id'));
|
||||||
|
assert.deepEqual(upd.params, [2, 9]);
|
||||||
|
// schoolbeheerder mag dit niet (schools.manage = super)
|
||||||
|
const { app: app2, calls: calls2 } = await makeAppWithSchool(admin, null);
|
||||||
|
const res2 = await app2.inject({ method: 'PATCH', url: '/api/admin/classes/9', cookies,
|
||||||
|
payload: { schoolId: 2 } });
|
||||||
|
assert.equal(res2.statusCode, 403);
|
||||||
|
assert.ok(!calls2.some((c) => c.sql.startsWith('UPDATE classes SET school_id')));
|
||||||
|
await app.close(); await app2.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('losse klassen zijn opvraagbaar met school=none (alleen super)', async () => {
|
||||||
|
const { app, calls } = await makeApp(superUser);
|
||||||
|
const res = await app.inject({ url: '/api/admin/classes?school=none', cookies });
|
||||||
|
assert.equal(res.statusCode, 200, res.body);
|
||||||
|
const q = calls.find((c) => c.sql.includes('FROM classes c'));
|
||||||
|
assert.ok(q.sql.includes('school_id IS NULL'), 'filtert op klassen zonder school');
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('hernoemen weigert een lege of te lange naam', async () => {
|
test('hernoemen weigert een lege of te lange naam', async () => {
|
||||||
const { app } = await makeApp(superUser);
|
const { app } = await makeApp(superUser);
|
||||||
const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies,
|
const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue