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
5 changed files with 101 additions and 20 deletions
Showing only changes of commit 24180beec1 - Show all commits

View file

@ -1 +1 @@
0.3.27-beta
0.3.28-beta

View file

@ -0,0 +1,6 @@
-- Alleen-lezen-vlag voor gedeelde items: een vergrendeld school-item kan ook
-- door de eigenaar niet meer gewijzigd worden, alleen nog door de
-- schoolbeheerder of de systeemmanager (die de vlag ook zetten/weghalen).
-- Systeembrede items zijn impliciet al alleen-lezen voor iedereen behalve de
-- systeemmanager (beheerregel), de vlag is daar niet voor nodig.
ALTER TABLE shared_library ADD COLUMN IF NOT EXISTS readonly BOOLEAN NOT NULL DEFAULT false;

View file

@ -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.27-beta";
const VERSION = "0.3.28-beta";
(function(){
const tag = document.getElementById("verTag");
tag.textContent = "v"+VERSION;

View file

@ -743,9 +743,17 @@ export default async function api(app) {
need(req, reply);
if (req.user.role === 'pupil') { reply.code(403); throw new Error('geen rechten'); }
};
// Vergrendeld (readonly) school-item: ook de eigenaar kan er niet meer bij,
// alleen de schoolbeheerder en de systeemmanager nog.
const canManageShared = (req, item) =>
req.user.allRoles.includes('super')
|| Number(item.owner_id) === Number(req.user.id)
|| (item.school_id != null && req.user.allRoles.includes('admin')
&& Number(item.school_id) === Number(req.user.school_id))
|| (Number(item.owner_id) === Number(req.user.id) && !item.readonly);
// Wie mag de alleen-lezen-vlag zetten/weghalen: de systeemmanager altijd,
// de schoolbeheerder voor items van de eigen school (niet de leerkracht).
const canLockShared = (req, item) =>
req.user.allRoles.includes('super')
|| (item.school_id != null && req.user.allRoles.includes('admin')
&& Number(item.school_id) === Number(req.user.school_id));
const sharedItemVisible = (req, item) =>
@ -760,7 +768,9 @@ export default async function api(app) {
folder: it.folder || '',
scope: it.school_id == null ? 'global' : 'school',
ownerName: it.owner_name || null,
readonly: !!it.readonly,
canManage: canManageShared(req, it),
canLock: canLockShared(req, it),
});
app.get('/shared/:kind', async (req, reply) => {
@ -769,11 +779,11 @@ export default async function api(app) {
if (!SHARED_KINDS.includes(kind)) return fail(reply, 404, 'onbekende soort');
const rows = req.user.allRoles.includes('super')
? (await pool.query(
`SELECT s.id, s.school_id, s.owner_id, s.folder, s.name, u.display_name AS owner_name
`SELECT s.id, s.school_id, s.owner_id, s.folder, s.name, s.readonly, u.display_name AS owner_name
FROM shared_library s LEFT JOIN users u ON u.id = s.owner_id
WHERE s.kind = $1 ORDER BY s.folder, s.name`, [kind])).rows
: (await pool.query(
`SELECT s.id, s.school_id, s.owner_id, s.folder, s.name, u.display_name AS owner_name
`SELECT s.id, s.school_id, s.owner_id, s.folder, s.name, s.readonly, u.display_name AS owner_name
FROM shared_library s LEFT JOIN users u ON u.id = s.owner_id
WHERE s.kind = $1 AND (s.school_id IS NULL OR s.school_id = $2)
ORDER BY s.folder, s.name`, [kind, req.user.school_id])).rows;
@ -810,15 +820,19 @@ export default async function api(app) {
[schoolId, req.user.id, kind, folder || '', name.trim(), data]);
return { ok: true, id: Number(r.rows[0].id) };
});
// map-prefix-rewrite (map hernoemen/verplaatsen in de gedeelde boom); mag
// alleen als de gebruiker ALLE items onder het pad mag beheren
// map-prefix-rewrite (map hernoemen/verplaatsen: {from, to}) of de hele map
// op alleen-lezen zetten ({from, readonly}); rewrite mag alleen als de
// gebruiker ALLE items onder het pad mag beheren, vergrendelen alleen door
// wie de vlag op alle items mag zetten (schoolbeheerder/systeemmanager)
app.patch('/shared/:kind/folder', async (req, reply) => {
sharedStaff(req, reply);
const { kind } = req.params;
if (!SHARED_KINDS.includes(kind)) return fail(reply, 404, 'onbekende soort');
const { scope, from, to } = req.body ?? {};
const { scope, from, to, readonly } = req.body ?? {};
if (scope !== 'global' && scope !== 'school') return fail(reply, 400, 'ongeldige scope');
if (!validSharedFolder(from) || !from || !validSharedFolder(to) || !to) return fail(reply, 400, 'ongeldige map');
if (!validSharedFolder(from) || !from) return fail(reply, 400, 'ongeldige map');
if (to === undefined && typeof readonly !== 'boolean') return fail(reply, 400, 'niets te doen');
if (to !== undefined && (!validSharedFolder(to) || !to)) return fail(reply, 400, 'ongeldige map');
const rows = scope === 'global'
? (await pool.query(
`SELECT * FROM shared_library WHERE kind = $1 AND school_id IS NULL AND (folder = $2 OR folder LIKE $3)`,
@ -826,11 +840,17 @@ export default async function api(app) {
: (await pool.query(
`SELECT * FROM shared_library WHERE kind = $1 AND school_id = $2 AND (folder = $3 OR folder LIKE $4)`,
[kind, req.user.school_id, from, from + '/%'])).rows;
if (to !== undefined){
if (rows.some((it) => !canManageShared(req, it))) return fail(reply, 403, 'geen rechten op alle items in deze map');
await pool.query(
`UPDATE shared_library SET folder = $1 || substr(folder, $2)
WHERE id = ANY($3)`,
[to, from.length + 1, rows.map((it) => it.id)]);
}
if (typeof readonly === 'boolean'){
if (rows.some((it) => !canLockShared(req, it))) return fail(reply, 403, 'alleen de schoolbeheerder of systeemmanager kan dit vergrendelen');
await pool.query('UPDATE shared_library SET readonly = $1 WHERE id = ANY($2)', [readonly, rows.map((it) => it.id)]);
}
return { ok: true };
});
app.patch('/shared/:kind/:id', async (req, reply) => {
@ -839,12 +859,19 @@ export default async function api(app) {
if (!SHARED_KINDS.includes(kind)) return fail(reply, 404, 'onbekende soort');
const it = (await pool.query('SELECT * FROM shared_library WHERE id = $1 AND kind = $2', [req.params.id, kind])).rows[0];
if (!it || !sharedItemVisible(req, it)) return fail(reply, 404, 'item onbekend');
const { name, folder, readonly } = req.body ?? {};
if (name !== undefined || folder !== undefined){
if (!canManageShared(req, it)) return fail(reply, 403, 'geen rechten');
const { name, folder } = req.body ?? {};
if (name !== undefined && !validSharedName(name)) return fail(reply, 400, 'ongeldige naam');
if (folder !== undefined && !validSharedFolder(folder)) return fail(reply, 400, 'ongeldige map');
if (name !== undefined) await pool.query('UPDATE shared_library SET name = $1 WHERE id = $2', [name.trim(), it.id]);
if (folder !== undefined) await pool.query('UPDATE shared_library SET folder = $1 WHERE id = $2', [folder, it.id]);
}
if (readonly !== undefined){
if (typeof readonly !== 'boolean') return fail(reply, 400, 'ongeldige waarde');
if (!canLockShared(req, it)) return fail(reply, 403, 'alleen de schoolbeheerder of systeemmanager kan dit vergrendelen');
await pool.query('UPDATE shared_library SET readonly = $1 WHERE id = $2', [readonly, it.id]);
}
return { ok: true };
});
app.delete('/shared/:kind/:id', async (req, reply) => {

View file

@ -98,6 +98,54 @@ test('beheren mag alleen door eigenaar, schoolbeheerder of systeemmanager', asyn
await app.close();
});
test('een vergrendeld school-item is ook voor de eigenaar alleen-lezen', async () => {
const lockedOwn = { id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: '', name: 'X', data: {}, readonly: true };
const { app } = await makeApp(teacher, [lockedOwn]);
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/5', cookies,
payload: { name: 'Nieuwe naam' } });
assert.equal(res.statusCode, 403);
// kopiëren (GET met data) blijft gewoon werken
const get = await app.inject({ method: 'GET', url: '/api/shared/board/5', cookies });
assert.equal(get.statusCode, 200);
assert.equal(get.json().item.readonly, true);
assert.equal(get.json().item.canManage, false);
await app.close();
});
test('alleen schoolbeheerder/systeemmanager mag de alleen-lezen-vlag zetten', async () => {
const ownItem = { id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: '', name: 'X', data: {}, readonly: false };
// eigenaar-leerkracht mag de vlag NIET zetten
const { app } = await makeApp(teacher, [ownItem]);
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/5', cookies,
payload: { readonly: true } });
assert.equal(res.statusCode, 403);
await app.close();
// schoolbeheerder van dezelfde school wel
const admin = { id: 3, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 };
const { app: app2, calls } = await makeApp(admin, [ownItem]);
const res2 = await app2.inject({ method: 'PATCH', url: '/api/shared/board/5', cookies,
payload: { readonly: true } });
assert.equal(res2.statusCode, 200, res2.body);
const upd = calls.find((c) => c.sql.includes('SET readonly'));
assert.deepEqual(upd.params, [true, 5]);
await app2.close();
});
test('map vergrendelen zet de vlag op alle items onder het pad', async () => {
const admin = { id: 3, username: 'beheer', role: 'admin', school_id: 2, class_id: null, data: {}, data_rev: 0 };
const rows = [
{ id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: 'Taal', name: 'A', data: {}, readonly: false },
{ id: 6, school_id: 2, owner_id: 8, kind: 'board', folder: 'Taal/Sub', name: 'B', data: {}, readonly: false },
];
const { app, calls } = await makeApp(admin, rows);
const res = await app.inject({ method: 'PATCH', url: '/api/shared/board/folder', cookies,
payload: { scope: 'school', from: 'Taal', readonly: true } });
assert.equal(res.statusCode, 200, res.body);
const upd = calls.find((c) => c.sql.includes('SET readonly'));
assert.deepEqual(upd.params, [true, [5, 6]]);
await app.close();
});
test('map-rewrite weigert als niet alle items beheerd mogen worden', async () => {
const rows = [
{ id: 5, school_id: 2, owner_id: 7, kind: 'board', folder: 'Taal', name: 'A', data: {} },