Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -0,0 +1,273 @@
|
||||
"""
|
||||
📘 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'))
|
||||
Reference in new issue
Block a user