274 lines
12 KiB
Python
274 lines
12 KiB
Python
"""
|
|
📘 Handleidingen: stappenplannen voor het beheer (bv. Google Admin).
|
|
|
|
- Lezen: iedereen met een PIVOT-account (handleidingen met Zichtbaar = 'admins' enkel administrators).
|
|
Leerkrachten die enkel met Google inloggen (zonder PIVOT-account) zien ze niet.
|
|
- Aanmaken, bewerken, verwijderen, vorige versie terugzetten: enkel administrators.
|
|
|
|
Handleidingen staan in de centrale database: ze gelden voor alle scholen.
|
|
Elke opslag bewaart de vorige tekst in Handleidingen_versies.
|
|
"""
|
|
import re
|
|
import unicodedata
|
|
|
|
from flask import Blueprint, render_template, request, redirect, url_for, flash, session, abort
|
|
|
|
import rechten
|
|
from blueprints.auth import login_required
|
|
from database import central_query, central_insert
|
|
from rechten import admin_vereist, is_admin
|
|
from services.html_schoon import schoon
|
|
|
|
handleidingen_bp = Blueprint('handleidingen', __name__)
|
|
|
|
ZICHTBAAR = {'iedereen': 'Iedereen met een PIVOT-account', 'admins': 'Enkel administrators'}
|
|
MAX_VERSIES = 30
|
|
|
|
|
|
def mag_lezen():
|
|
return is_admin() or bool(session.get('gebruiker_id'))
|
|
|
|
|
|
def _zichtbaar_filter():
|
|
return ("Zichtbaar IN ('iedereen', 'admins')" if is_admin() else "Zichtbaar = 'iedereen'")
|
|
|
|
|
|
def lijst():
|
|
"""Zichtbare handleidingen voor de ingelogde gebruiker (ook gebruikt door het dashboard)."""
|
|
if not mag_lezen():
|
|
return []
|
|
try:
|
|
return central_query(f"SELECT Handleiding_ID, Slug, Titel, Categorie, Samenvatting, Zichtbaar, Gewijzigd_op, "
|
|
f"Gewijzigd_door FROM Handleidingen WHERE {_zichtbaar_filter()} "
|
|
f"ORDER BY Volgorde, Titel")
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def _maak_slug(titel, behalve=None):
|
|
s = unicodedata.normalize('NFKD', titel).encode('ascii', 'ignore').decode().lower()
|
|
s = re.sub(r'[^a-z0-9]+', '-', s).strip('-')[:80] or 'handleiding'
|
|
if s == 'nieuw': # botst met de route /handleidingen/nieuw
|
|
s = 'nieuw-1'
|
|
kandidaat, n = s, 2
|
|
while central_query("SELECT 1 FROM Handleidingen WHERE Slug = %s AND Handleiding_ID <> %s",
|
|
(kandidaat, behalve or 0)):
|
|
kandidaat, n = f"{s}-{n}", n + 1
|
|
return kandidaat
|
|
|
|
|
|
def _haal(slug):
|
|
rows = central_query(f"SELECT * FROM Handleidingen WHERE Slug = %s AND {_zichtbaar_filter()}", (slug,))
|
|
return rows[0] if rows else None
|
|
|
|
|
|
def _inhoudstafel(html):
|
|
"""[(id, tekst, niveau)] van de h2/h3-koppen; koppen zonder id krijgen er een."""
|
|
gezien, toc = set(), []
|
|
|
|
def vervang(m):
|
|
tag, attrs, tekst = m.group(1), m.group(2) or '', m.group(3)
|
|
platte = re.sub(r'<[^>]+>', '', tekst).strip()
|
|
idm = re.search(r'id="([^"]+)"', attrs)
|
|
hid = idm.group(1) if idm else (re.sub(r'[^a-z0-9]+', '-', platte.lower()).strip('-') or 'kop')
|
|
basis, n = hid, 2
|
|
while hid in gezien:
|
|
hid, n = f"{basis}-{n}", n + 1
|
|
gezien.add(hid)
|
|
toc.append((hid, platte, int(tag[1])))
|
|
attrs = re.sub(r'\s*id="[^"]*"', '', attrs)
|
|
return f'<{tag} id="{hid}"{attrs}>{tekst}</{tag}>'
|
|
|
|
html = re.sub(r'<(h[23])((?:\s[^>]*)?)>(.*?)</\1>', vervang, schoon(html or ''), flags=re.S)
|
|
return html, toc
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Lezen
|
|
# ------------------------------------------------------------------
|
|
@handleidingen_bp.route('/', methods=['GET'])
|
|
@login_required
|
|
def overzicht():
|
|
if not mag_lezen():
|
|
flash("Handleidingen zijn enkel beschikbaar voor PIVOT-gebruikers.", "error")
|
|
return redirect(url_for('main.dashboard'))
|
|
q = (request.args.get('q') or '').strip()
|
|
items = lijst()
|
|
if q:
|
|
treffers = {r['Handleiding_ID'] for r in central_query(
|
|
f"SELECT Handleiding_ID FROM Handleidingen WHERE {_zichtbaar_filter()} AND "
|
|
"(Titel LIKE %s OR Samenvatting LIKE %s OR Inhoud LIKE %s)", (f"%{q}%",) * 3)}
|
|
items = [h for h in items if h['Handleiding_ID'] in treffers]
|
|
per_cat = {}
|
|
for h in items:
|
|
per_cat.setdefault(h['Categorie'] or 'Algemeen', []).append(h)
|
|
return render_template('handleidingen.html', per_cat=per_cat, q=q, ZICHTBAAR=ZICHTBAAR)
|
|
|
|
|
|
@handleidingen_bp.route('/<slug>', methods=['GET'])
|
|
@login_required
|
|
def bekijk(slug):
|
|
if not mag_lezen():
|
|
return redirect(url_for('main.dashboard'))
|
|
h = _haal(slug)
|
|
if not h:
|
|
flash("Deze handleiding bestaat niet (meer).", "error")
|
|
return redirect(url_for('handleidingen.overzicht'))
|
|
inhoud, toc = _inhoudstafel(h['Inhoud'])
|
|
return render_template('handleiding.html', h=h, inhoud=inhoud, toc=toc, ZICHTBAAR=ZICHTBAAR)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Bewerken (enkel administrators)
|
|
# ------------------------------------------------------------------
|
|
def _formulier():
|
|
titel = (request.form.get('titel') or '').strip()[:200]
|
|
return {
|
|
'Titel': titel,
|
|
'Categorie': (request.form.get('categorie') or '').strip()[:100] or None,
|
|
'Samenvatting': (request.form.get('samenvatting') or '').strip()[:300] or None,
|
|
'Inhoud': schoon(request.form.get('inhoud') or ''),
|
|
'Zichtbaar': request.form.get('zichtbaar') if request.form.get('zichtbaar') in ZICHTBAAR else 'iedereen',
|
|
'Volgorde': request.form.get('volgorde', type=int) or 0,
|
|
}
|
|
|
|
|
|
def _te_groot(e):
|
|
t = str(e)
|
|
if 'max_allowed_packet' in t or 'too large' in t.lower() or '1153' in t:
|
|
return (": de handleiding is te groot voor de database (te veel of te grote afbeeldingen). Verdeel ze over "
|
|
"meerdere handleidingen, of verhoog max_allowed_packet in MySQL.")
|
|
return f": {t}"
|
|
|
|
|
|
def _categorieen():
|
|
return [r['Categorie'] for r in central_query(
|
|
"SELECT DISTINCT Categorie FROM Handleidingen WHERE Categorie IS NOT NULL AND Zichtbaar <> 'weg' ORDER BY Categorie")]
|
|
|
|
|
|
@handleidingen_bp.route('/nieuw', methods=['GET', 'POST'])
|
|
@login_required
|
|
@admin_vereist
|
|
def nieuw():
|
|
if request.method == 'POST':
|
|
f = _formulier()
|
|
if not f['Titel']:
|
|
flash("Geef de handleiding een titel.", "error")
|
|
return render_template('handleiding_bewerken.html', h=f, nieuw=True, categorieen=_categorieen(),
|
|
ZICHTBAAR=ZICHTBAAR, versies=[])
|
|
slug = _maak_slug(f['Titel'])
|
|
try:
|
|
central_insert("INSERT INTO Handleidingen (Slug, Titel, Categorie, Samenvatting, Inhoud, Zichtbaar, "
|
|
"Volgorde, Gewijzigd_op, Gewijzigd_door) VALUES (%s, %s, %s, %s, %s, %s, %s, NOW(), %s)",
|
|
(slug, f['Titel'], f['Categorie'], f['Samenvatting'], f['Inhoud'], f['Zichtbaar'],
|
|
f['Volgorde'], session.get('user')))
|
|
except Exception as e:
|
|
flash(f"Opslaan mislukt{_te_groot(e)}", "error")
|
|
return render_template('handleiding_bewerken.html', h=f, nieuw=True, categorieen=_categorieen(),
|
|
ZICHTBAAR=ZICHTBAAR, versies=[])
|
|
rechten.log(actie=f"Handleiding '{f['Titel']}' aangemaakt")
|
|
flash("Handleiding aangemaakt.", "success")
|
|
return redirect(url_for('handleidingen.bekijk', slug=slug))
|
|
leeg = {'Titel': '', 'Categorie': request.args.get('categorie') or '', 'Samenvatting': '', 'Inhoud': '',
|
|
'Zichtbaar': 'iedereen', 'Volgorde': 100}
|
|
return render_template('handleiding_bewerken.html', h=leeg, nieuw=True, categorieen=_categorieen(),
|
|
ZICHTBAAR=ZICHTBAAR, versies=[])
|
|
|
|
|
|
@handleidingen_bp.route('/<slug>/bewerken', methods=['GET', 'POST'])
|
|
@login_required
|
|
@admin_vereist
|
|
def bewerken(slug):
|
|
h = _haal(slug)
|
|
if not h:
|
|
abort(404)
|
|
if request.method == 'POST':
|
|
f = _formulier()
|
|
if not f['Titel']:
|
|
flash("De titel mag niet leeg zijn.", "error")
|
|
return redirect(url_for('handleidingen.bewerken', slug=slug))
|
|
if f['Titel'] != h['Titel'] or f['Inhoud'] != (h['Inhoud'] or ''):
|
|
central_query("INSERT INTO Handleidingen_versies (Handleiding_ID, Titel, Inhoud, Opgeslagen_op, Door) "
|
|
"VALUES (%s, %s, %s, %s, %s)",
|
|
(h['Handleiding_ID'], h['Titel'], h['Inhoud'], h['Gewijzigd_op'], h['Gewijzigd_door']),
|
|
commit=True)
|
|
_versies_opruimen(h['Handleiding_ID'])
|
|
try:
|
|
central_query("UPDATE Handleidingen SET Titel = %s, Categorie = %s, Samenvatting = %s, Inhoud = %s, "
|
|
"Zichtbaar = %s, Volgorde = %s, Gewijzigd_op = NOW(), Gewijzigd_door = %s "
|
|
"WHERE Handleiding_ID = %s",
|
|
(f['Titel'], f['Categorie'], f['Samenvatting'], f['Inhoud'], f['Zichtbaar'], f['Volgorde'],
|
|
session.get('user'), h['Handleiding_ID']), commit=True)
|
|
except Exception as e:
|
|
flash(f"Opslaan mislukt{_te_groot(e)}", "error")
|
|
return redirect(url_for('handleidingen.bewerken', slug=slug))
|
|
rechten.log(actie=f"Handleiding '{f['Titel']}' bewerkt")
|
|
flash("Handleiding opgeslagen.", "success")
|
|
if request.form.get('verder'):
|
|
return redirect(url_for('handleidingen.bewerken', slug=slug))
|
|
return redirect(url_for('handleidingen.bekijk', slug=slug))
|
|
versies = central_query("SELECT Versie_ID, Titel, Opgeslagen_op, Door FROM Handleidingen_versies "
|
|
"WHERE Handleiding_ID = %s ORDER BY Opgeslagen_op DESC, Versie_ID DESC",
|
|
(h['Handleiding_ID'],))
|
|
h['Inhoud'] = schoon(h['Inhoud'] or '')
|
|
return render_template('handleiding_bewerken.html', h=h, nieuw=False, categorieen=_categorieen(),
|
|
ZICHTBAAR=ZICHTBAAR, versies=versies)
|
|
|
|
|
|
def _versies_opruimen(hid):
|
|
oud = central_query("SELECT Versie_ID FROM Handleidingen_versies WHERE Handleiding_ID = %s "
|
|
"ORDER BY Opgeslagen_op DESC, Versie_ID DESC", (hid,))[MAX_VERSIES:]
|
|
for r in oud:
|
|
central_query("DELETE FROM Handleidingen_versies WHERE Versie_ID = %s", (r['Versie_ID'],), commit=True)
|
|
|
|
|
|
@handleidingen_bp.route('/<slug>/versie/<int:vid>', methods=['GET'])
|
|
@login_required
|
|
@admin_vereist
|
|
def versie(slug, vid):
|
|
h = _haal(slug)
|
|
rows = central_query("SELECT * FROM Handleidingen_versies WHERE Versie_ID = %s AND Handleiding_ID = %s",
|
|
(vid, h['Handleiding_ID'] if h else 0))
|
|
if not h or not rows:
|
|
abort(404)
|
|
v = rows[0]
|
|
inhoud, toc = _inhoudstafel(v['Inhoud'])
|
|
return render_template('handleiding.html', h=h, inhoud=inhoud, toc=toc, versie=v, ZICHTBAAR=ZICHTBAAR)
|
|
|
|
|
|
@handleidingen_bp.route('/<slug>/versie/<int:vid>/terugzetten', methods=['POST'])
|
|
@login_required
|
|
@admin_vereist
|
|
def terugzetten(slug, vid):
|
|
h = _haal(slug)
|
|
rows = central_query("SELECT * FROM Handleidingen_versies WHERE Versie_ID = %s AND Handleiding_ID = %s",
|
|
(vid, h['Handleiding_ID'] if h else 0))
|
|
if not h or not rows:
|
|
abort(404)
|
|
v = rows[0]
|
|
central_query("INSERT INTO Handleidingen_versies (Handleiding_ID, Titel, Inhoud, Opgeslagen_op, Door) "
|
|
"VALUES (%s, %s, %s, %s, %s)",
|
|
(h['Handleiding_ID'], h['Titel'], h['Inhoud'], h['Gewijzigd_op'], h['Gewijzigd_door']), commit=True)
|
|
central_query("UPDATE Handleidingen SET Titel = %s, Inhoud = %s, Gewijzigd_op = NOW(), Gewijzigd_door = %s "
|
|
"WHERE Handleiding_ID = %s", (v['Titel'], v['Inhoud'], session.get('user'), h['Handleiding_ID']),
|
|
commit=True)
|
|
_versies_opruimen(h['Handleiding_ID'])
|
|
rechten.log(actie=f"Handleiding '{v['Titel']}': versie van {v['Opgeslagen_op']:%d-%m-%Y %H:%M} teruggezet")
|
|
flash(f"Versie van {v['Opgeslagen_op']:%d-%m-%Y %H:%M} teruggezet. De vorige tekst staat bij de versies.", "success")
|
|
return redirect(url_for('handleidingen.bekijk', slug=slug))
|
|
|
|
|
|
@handleidingen_bp.route('/<slug>/verwijderen', methods=['POST'])
|
|
@login_required
|
|
@admin_vereist
|
|
def verwijderen(slug):
|
|
h = _haal(slug)
|
|
if not h:
|
|
abort(404)
|
|
# Verbergen i.p.v. wissen: zo komt een meegeleverde handleiding niet terug bij het opstarten
|
|
central_query("UPDATE Handleidingen SET Zichtbaar = 'weg', Gewijzigd_op = NOW(), Gewijzigd_door = %s "
|
|
"WHERE Handleiding_ID = %s", (session.get('user'), h['Handleiding_ID']), commit=True)
|
|
rechten.log(actie=f"Handleiding '{h['Titel']}' verwijderd")
|
|
flash(f"Handleiding '{h['Titel']}' verwijderd.", "success")
|
|
return redirect(url_for('handleidingen.overzicht'))
|