Initial commit

This commit is contained in:
Kristof committed 2026-10-14 12:29:29 +02:00
commit 536ff43ce4
158 files changed
+30355

No files matched your search

+273
View File
@@ -0,0 +1,273 @@
"""
Scholengroep: leerlingenaantallen van alle scholen in PIVOT.
Elke school wordt geteld met haar EIGEN Informat-koppeling (instellingsnummer,
client-ID en secret uit het scholenregister). Het resultaat wordt bewaard in de
centrale database, zodat elke school het overzicht van de scholengroep ziet.
- Huidig aantal: peildatum vandaag + 1 maand (kolommen in Scholen).
- Telling op peildatum: een gekozen datum (bv. 1 februari), bewaard in Leerlingenaantal_tellingen,
zodat je ze later opnieuw kan bekijken en exporteren.
"""
import csv
import io
import json
from datetime import date, datetime
from flask import Blueprint, render_template, request, flash, redirect, url_for, session, Response
from blueprints.auth import login_required
from database import central_query
from rechten import recht_vereist
from services.sync_service import SyncService
from tenancy import alle_scholen, school_op_code, in_school, herlaad, ONDERWIJSNIVEAUS
leerlingenaantal_bp = Blueprint('leerlingenaantal', __name__)
INFORMAT_VELDEN = ('INFORMAT_INST_NUMBER', 'INFORMAT_ID', 'INFORMAT_SECRET')
# Teksten en routes voor het gedeelde sjabloon leerlingenaantal.html (ook gebruikt door de personeelsaantallen)
WEERGAVE = dict(ep='leerlingenaantal', pagina='leerlingenaantal_page', titel='Leerlingenaantallen',
eenheid='Leerlingen', kort='lln.')
def _datum(waarde):
try:
return datetime.strptime((waarde or '').strip(), '%Y-%m-%d').date()
except ValueError:
return None
def _fmt(dt, leeg=''):
if not dt:
return leeg
return dt.strftime('%d-%m-%Y %H:%M') if hasattr(dt, 'strftime') else str(dt)
def _detail(tekst):
try:
return json.loads(tekst) if tekst else []
except ValueError:
return []
NIVEAUS = list(ONDERWIJSNIVEAUS) + [('', 'Onderwijsniveau niet ingesteld')]
NIVEAU_LABEL = dict(NIVEAUS)
def _niveau(school):
n = school.waarden.get('ONDERWIJSNIVEAU') or ''
return n if n in NIVEAU_LABEL else ''
def _som(rijen, veld):
return sum(r[veld] for r in rijen if isinstance(r[veld], int))
def _groepen(rows, veld):
"""Rijen per onderwijsniveau (basis, secundair, niet ingesteld) met subtotaal; lege groepen vallen weg."""
uit = []
for code, label in NIVEAUS:
rijen = [r for r in rows if r['Niveau'] == code]
if rijen:
uit.append({'code': code, 'label': label, 'rijen': rijen, 'subtotaal': _som(rijen, veld)})
return uit
def _huidig(query=''):
rows = []
for s in alle_scholen():
if query and query.lower() not in s.naam.lower():
continue
rows.append({
'Code': s.code,
'Naam_school': s.naam,
'Niveau': _niveau(s),
'Leerlingenaantal': s.leerlingenaantal if s.leerlingenaantal is not None else '–',
'Huidig': s.leerlingenaantal if s.leerlingenaantal is not None else '–',
'Laatste_synchronisatie': s.leerlingenaantal_sync,
'Informat': s.heeft(*INFORMAT_VELDEN),
'Campussen': s.leerlingenaantal_detail or [], # [{'naam', 'nummer', 'aantal'}] bij meerdere campussen
})
return rows
def _telling(peildatum, query=''):
"""Alle actieve scholen met hun aantal op de peildatum (None = niet geteld)."""
per = {r['School_ID']: r for r in central_query(
"SELECT School_ID, Aantal, Detail, Schooljaar, Opgevraagd_op, Door FROM Leerlingenaantal_tellingen "
"WHERE Peildatum = %s", (peildatum,))}
rows = []
for s in alle_scholen():
if query and query.lower() not in s.naam.lower():
continue
t = per.get(s.id) or {}
rows.append({'Code': s.code, 'Naam_school': s.naam, 'Niveau': _niveau(s), 'Aantal': t.get('Aantal'),
'Schooljaar': t.get('Schooljaar'), 'Opgevraagd_op': t.get('Opgevraagd_op'),
'Door': t.get('Door'), 'Informat': s.heeft(*INFORMAT_VELDEN),
'Campussen': _detail(t.get('Detail'))})
return rows
def _peildata():
return central_query("SELECT Peildatum, COUNT(*) AS Scholen, SUM(Aantal) AS Totaal, MAX(Opgevraagd_op) AS Laatst "
"FROM Leerlingenaantal_tellingen GROUP BY Peildatum ORDER BY Peildatum DESC")
@leerlingenaantal_bp.route("/leerlingenaantal", methods=["GET", "POST"])
@login_required
@recht_vereist('leerlingenaantal')
def leerlingenaantal_page():
query = (request.values.get("query") or "").strip()
herlaad(force=True)
results = _huidig(query)
totaal = _som(results, 'Leerlingenaantal')
peildatum = _datum(request.args.get('peildatum'))
telling = _telling(peildatum, query) if peildatum else None
telling_totaal = _som(telling, 'Aantal') if telling else 0
return render_template("leerlingenaantal.html", results=results, query=query, totaal=totaal,
groepen=_groepen(results, 'Leerlingenaantal'),
telling_groepen=_groepen(telling, 'Aantal') if telling else [],
peildatum=peildatum, telling=telling, telling_totaal=telling_totaal,
peildata=_peildata(), vandaag=date.today(), **WEERGAVE)
@leerlingenaantal_bp.route("/sync-aanvragen", methods=["POST"])
@login_required
@recht_vereist('leerlingenaantal')
def sync_aanvragen():
codes = [s.code for s in alle_scholen()] if request.form.get("alle") else [request.form.get("school_code")]
for code in codes:
school = school_op_code(code)
if not school:
continue
if not school.heeft(*INFORMAT_VELDEN):
flash(f"{school.naam}: Informat is nog niet ingesteld.", "error")
continue
with in_school(school):
success, message = SyncService().sync_student_count()
flash(f"{school.naam}: {message}", "success" if success else "error")
return redirect(url_for('leerlingenaantal.leerlingenaantal_page'))
@leerlingenaantal_bp.route("/leerlingenaantal/peildatum", methods=["POST"])
@login_required
@recht_vereist('leerlingenaantal')
def tel_peildatum():
"""Telt de leerlingen op de gekozen datum, voor alle scholen of één school."""
peildatum = _datum(request.form.get('peildatum'))
if not peildatum:
flash("Kies een geldige datum.", "error")
return redirect(url_for('leerlingenaantal.leerlingenaantal_page'))
code = request.form.get('school_code')
scholen = [school_op_code(code)] if code else alle_scholen()
door = session.get('user')
gelukt = 0
for school in scholen:
if not school:
continue
if not school.heeft(*INFORMAT_VELDEN):
if code:
flash(f"{school.naam}: Informat is nog niet ingesteld.", "error")
continue
with in_school(school):
ok, res = SyncService().tel_op_peildatum(peildatum, door)
if ok:
gelukt += 1
else:
flash(f"{school.naam}: {res}", "error")
if gelukt:
flash(f"Leerlingen geteld op {peildatum:%d-%m-%Y} voor {gelukt} school/scholen.", "success")
return redirect(url_for('leerlingenaantal.leerlingenaantal_page', peildatum=peildatum.isoformat()))
@leerlingenaantal_bp.route("/leerlingenaantal/peildatum/verwijderen", methods=["POST"])
@login_required
@recht_vereist('leerlingenaantal')
def verwijder_peildatum():
peildatum = _datum(request.form.get('peildatum'))
if peildatum:
central_query("DELETE FROM Leerlingenaantal_tellingen WHERE Peildatum = %s", (peildatum,), commit=True)
flash(f"Telling van {peildatum:%d-%m-%Y} verwijderd.", "success")
return redirect(url_for('leerlingenaantal.leerlingenaantal_page'))
# ------------------------------------------------------------------
# CSV
# ------------------------------------------------------------------
def _csv(naam, kop, rijen):
buf = io.StringIO()
w = csv.writer(buf, delimiter=';') # ; zodat Excel (Belgische instellingen) de kolommen meteen splitst
w.writerow(kop)
w.writerows(rijen)
return Response('' + buf.getvalue(), mimetype='text/csv',
headers={'Content-Disposition': f'attachment; filename={naam}'})
@leerlingenaantal_bp.route("/leerlingenaantal/export.csv", methods=["GET"])
@login_required
@recht_vereist('leerlingenaantal')
def export_csv():
"""?peildatum=YYYY-MM-DD → die telling; ?alle=1 → alle peildata naast elkaar; anders het huidige aantal."""
query = (request.args.get('query') or '').strip()
if request.args.get('alle'):
data = central_query("SELECT School_ID, Peildatum, Aantal, Detail FROM Leerlingenaantal_tellingen ORDER BY Peildatum")
datums = sorted({r['Peildatum'] for r in data})
per = {(r['School_ID'], r['Peildatum']): r['Aantal'] for r in data}
# campus-aantallen: {(school_id, campusnaam, datum): aantal}
campus, campusnamen = {}, {}
for r in data:
for d in _detail(r['Detail']):
campus[(r['School_ID'], d['naam'], r['Peildatum'])] = d['aantal']
campusnamen.setdefault(r['School_ID'], [])
if d['naam'] not in campusnamen[r['School_ID']]:
campusnamen[r['School_ID']].append(d['naam'])
scholen = [s for s in alle_scholen() if not query or query.lower() in s.naam.lower()]
rijen = []
for code, label in NIVEAUS:
groep = [s for s in scholen if _niveau(s) == code]
if not groep:
continue
for s in groep:
rijen.append([label, s.naam, ''] + [per.get((s.id, d), '') for d in datums])
for naam in campusnamen.get(s.id, []):
rijen.append([label, s.naam, naam] + [campus.get((s.id, naam, d), '') for d in datums])
if code:
rijen.append([f"Subtotaal {label}", '', ''] + [sum(per.get((s.id, d)) or 0 for s in groep) for d in datums])
rijen.append(['Totaal', '', ''] + [sum(per.get((s.id, d)) or 0 for s in scholen) for d in datums])
return _csv(f"leerlingenaantallen_alle_peildata_{date.today():%Y%m%d}.csv",
['Onderwijsniveau', 'School', 'Campus'] + [f"{d:%d-%m-%Y}" for d in datums], rijen)
peildatum = _datum(request.args.get('peildatum'))
if peildatum:
rows = _telling(peildatum, query)
rijen = []
for g in _groepen(rows, 'Aantal'):
for r in g['rijen']:
rijen.append([g['label'], r['Naam_school'], '', '' if r['Aantal'] is None else r['Aantal'],
r['Schooljaar'] or '', _fmt(r['Opgevraagd_op'], 'niet geteld'), r['Door'] or ''])
for c in r['Campussen']:
rijen.append([g['label'], r['Naam_school'], c['naam'], c['aantal'], r['Schooljaar'] or '', '', ''])
if g['code']:
rijen.append([f"Subtotaal {g['label']}", '', '', g['subtotaal'], '', '', ''])
rijen.append(['Totaal', '', '', _som(rows, 'Aantal'), '', '', ''])
return _csv(f"leerlingenaantallen_{peildatum:%Y%m%d}.csv",
['Onderwijsniveau', 'School', 'Campus', f"Leerlingen op {peildatum:%d-%m-%Y}", 'Schooljaar',
'Opgevraagd op', 'Door'], rijen)
rows = _huidig(query)
rijen = []
for g in _groepen(rows, 'Leerlingenaantal'):
for r in g['rijen']:
rijen.append([g['label'], r['Naam_school'], '',
r['Leerlingenaantal'] if isinstance(r['Leerlingenaantal'], int) else '',
_fmt(r['Laatste_synchronisatie'], 'nog nooit')])
for c in r['Campussen']:
rijen.append([g['label'], r['Naam_school'], c['naam'], c['aantal'], ''])
if g['code']:
rijen.append([f"Subtotaal {g['label']}", '', '', g['subtotaal'], ''])
rijen.append(['Totaal', '', '', _som(rows, 'Leerlingenaantal'), ''])
return _csv(f"leerlingenaantallen_{date.today():%Y%m%d}.csv",
['Onderwijsniveau', 'School', 'Campus', 'Leerlingen', 'Laatste synchronisatie'], rijen)