Voeg thematische kleurplaten en creatieve niveaus toe (v0.4.65-beta) #3
5 changed files with 45 additions and 14 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
0.3.37-beta
|
||||
0.3.38-beta
|
||||
|
|
|
|||
|
|
@ -198,15 +198,13 @@
|
|||
if(u.role==="pupil"){
|
||||
const cls = CLASSES.find(c=>Number(c.id)===Number(u.classId));
|
||||
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (cls ? ` · ${cls.name}` : "")));
|
||||
}else if(u.role==="teacher"){
|
||||
}else{
|
||||
const names = (u.classIds||[])
|
||||
.map(cid=>CLASSES.find(c=>Number(c.id)===Number(cid))?.name)
|
||||
.filter(Boolean);
|
||||
row.appendChild(h("span","am-role", T(roleKey(u.role))
|
||||
+ (names.length ? ` · ${names.join(", ")}` : "")
|
||||
+ (u.pending ? ` · ${T("amPending")}` : "")));
|
||||
}else{
|
||||
row.appendChild(h("span","am-role", T(roleKey(u.role)) + (u.pending ? ` · ${T("amPending")}` : "")));
|
||||
}
|
||||
if(u.role!=="pupil" && can("users.role.change") && u.id!==currentUser.id){
|
||||
/* één functie per gebruiker: alleen de hoofdrol is te wijzigen (super);
|
||||
|
|
@ -220,9 +218,10 @@
|
|||
});
|
||||
tools.appendChild(field("amFieldRole", rs));
|
||||
}
|
||||
/* groepsleiding eenvoudig aan klassen koppelen: huidige klassen als
|
||||
chips met ✕, en een select om een klas toe te voegen */
|
||||
if(u.role==="teacher" && can("classes.manage") && CLASSES.length){
|
||||
/* elke staf-rol eenvoudig aan klassen koppelen (groepsleiding,
|
||||
schoolbeheerder én systeemmanager): huidige klassen als chips met ✕,
|
||||
en een select om een klas toe te voegen */
|
||||
if(u.role!=="pupil" && can("classes.manage") && CLASSES.length){
|
||||
const wrap = h("div","am-classes");
|
||||
(u.classIds||[]).forEach(cid=>{
|
||||
const c = CLASSES.find(x=>Number(x.id)===Number(cid));
|
||||
|
|
|
|||
|
|
@ -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.37-beta";
|
||||
const VERSION = "0.3.38-beta";
|
||||
(function(){
|
||||
const tag = document.getElementById("verTag");
|
||||
tag.textContent = "v"+VERSION;
|
||||
|
|
|
|||
13
src/api.js
13
src/api.js
|
|
@ -331,19 +331,22 @@ export default async function api(app) {
|
|||
return { ok: true };
|
||||
});
|
||||
// groepsleiding aan klas koppelen/loskoppelen
|
||||
// Elke staf-rol (groepsleiding, schoolbeheerder, systeemmanager) kan aan
|
||||
// een klas gekoppeld worden; alleen leerlingen niet (die horen al bij één
|
||||
// klas via users.class_id). Een systeemmanager is school-onafhankelijk en
|
||||
// mag dus aan klassen van elke school gekoppeld worden.
|
||||
app.post('/admin/classes/:id/teachers', async (req, reply) => {
|
||||
need(req, reply, PERMISSIONS['classes.manage']);
|
||||
const { userId, remove } = req.body ?? {};
|
||||
if (!userId) return fail(reply, 400, 'gebruiker verplicht');
|
||||
const c = (await pool.query('SELECT * FROM classes WHERE id = $1', [req.params.id])).rows[0];
|
||||
const u = (await pool.query(`SELECT u.*, EXISTS (
|
||||
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
|
||||
) AS extra_teacher FROM users u WHERE u.id = $1`, [userId])).rows[0];
|
||||
const u = (await pool.query('SELECT * FROM users WHERE id = $1', [userId])).rows[0];
|
||||
if (!c || !u) return fail(reply, 404, 'klas of gebruiker onbekend');
|
||||
if (!req.user.allRoles.includes('super') && Number(c.school_id) !== Number(req.user.school_id))
|
||||
return fail(reply, 403, 'geen rechten');
|
||||
if (Number(c.school_id) !== Number(u.school_id)) return fail(reply, 400, 'klas en groepsleiding moeten bij dezelfde school horen');
|
||||
if (u.role !== 'teacher' && !u.extra_teacher) return fail(reply, 400, 'gebruiker is geen groepsleiding');
|
||||
if (u.role === 'pupil') return fail(reply, 400, 'leerlingen horen al bij één klas');
|
||||
if (u.role !== 'super' && Number(c.school_id) !== Number(u.school_id))
|
||||
return fail(reply, 400, 'klas en gebruiker moeten bij dezelfde school horen');
|
||||
if (remove) await pool.query('DELETE FROM class_teachers WHERE class_id = $1 AND user_id = $2', [c.id, u.id]);
|
||||
else await pool.query('INSERT INTO class_teachers (class_id, user_id) VALUES ($1,$2) ON CONFLICT DO NOTHING', [c.id, u.id]);
|
||||
return { ok: true };
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import cookie from '@fastify/cookie';
|
|||
import rateLimit from '@fastify/rate-limit';
|
||||
import api from '../src/api.js';
|
||||
|
||||
function makeApp(user) {
|
||||
function makeApp(user, targetUser) {
|
||||
const calls = [];
|
||||
const pool = {
|
||||
async query(sql, params = []) {
|
||||
|
|
@ -13,6 +13,8 @@ function makeApp(user) {
|
|||
if (sql.includes('FROM sessions s JOIN users u')) return { rows: [user] };
|
||||
if (sql.includes('FROM user_roles')) return { rows: [] };
|
||||
if (sql.startsWith('UPDATE schools SET name')) return { rows: [{ id: params[1], name: params[0] }] };
|
||||
if (sql.startsWith('SELECT * FROM classes WHERE id')) return { rows: [{ id: 100, school_id: 2, name: 'Groep 4' }] };
|
||||
if (sql.startsWith('SELECT * FROM users WHERE id') && targetUser) return { rows: [targetUser] };
|
||||
return { rows: [] };
|
||||
},
|
||||
};
|
||||
|
|
@ -50,6 +52,33 @@ test('schoolbeheerder mag geen scholen hernoemen', async () => {
|
|||
await app.close();
|
||||
});
|
||||
|
||||
test('schoolbeheerder en systeemmanager kunnen aan een klas gekoppeld worden', async () => {
|
||||
// schoolbeheerder van dezelfde school
|
||||
const { app, calls } = await makeApp(superUser, { id: 3, role: 'admin', school_id: 2 });
|
||||
const res = await app.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies,
|
||||
payload: { userId: 3 } });
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.ok(calls.some((c) => c.sql.includes('INSERT INTO class_teachers')));
|
||||
await app.close();
|
||||
// systeemmanager (geen school) mag ook
|
||||
const { app: app2 } = await makeApp(superUser, { id: 1, role: 'super', school_id: null });
|
||||
const res2 = await app2.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies,
|
||||
payload: { userId: 1 } });
|
||||
assert.equal(res2.statusCode, 200, res2.body);
|
||||
await app2.close();
|
||||
// leerlingen niet, en staf van een andere school niet
|
||||
const { app: app3 } = await makeApp(superUser, { id: 9, role: 'pupil', school_id: 2 });
|
||||
const res3 = await app3.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies,
|
||||
payload: { userId: 9 } });
|
||||
assert.equal(res3.statusCode, 400);
|
||||
await app3.close();
|
||||
const { app: app4 } = await makeApp(superUser, { id: 4, role: 'admin', school_id: 3 });
|
||||
const res4 = await app4.inject({ method: 'POST', url: '/api/admin/classes/100/teachers', cookies,
|
||||
payload: { userId: 4 } });
|
||||
assert.equal(res4.statusCode, 400);
|
||||
await app4.close();
|
||||
});
|
||||
|
||||
test('hernoemen weigert een lege of te lange naam', async () => {
|
||||
const { app } = await makeApp(superUser);
|
||||
const res = await app.inject({ method: 'PATCH', url: '/api/admin/schools/5', cookies,
|
||||
|
|
|
|||
Loading…
Reference in a new issue