""" 📘 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}' html = re.sub(r'<(h[23])((?:\s[^>]*)?)>(.*?)', 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('/', 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('//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('//versie/', 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('//versie//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('//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'))