PIVOT 1.1.0
This commit is contained in:
commit
8a53234eb6
161 files changed
+31109
No files matched your search
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user