PIVOT 1.1.0
This commit is contained in:
commit
8a53234eb6
161 files changed
+31109
No files matched your search
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Smartschool › SGR groepen: de subgroepen van één hoofdgroep (standaard 'groepen GO-Antwerpen') met hun huidige leden.
|
||||
|
||||
Werking (Smartschool-webservices V3):
|
||||
1. getAllGroupsAndClasses → de volledige boom van groepen en klassen (XML, meestal base64-gecodeerd)
|
||||
2. de hoofdgroep zoeken op naam of code, en alle groepen eronder verzamelen (ook dieper geneste)
|
||||
3. getAllAccountsExtended(code, '0') → de rechtstreekse leden van elke (sub)groep
|
||||
Het resultaat wordt 10 minuten per school bewaard (knop 'Opnieuw ophalen' om te vernieuwen).
|
||||
"""
|
||||
import base64
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from config import Config
|
||||
|
||||
STANDAARD_GROEP = 'groepen GO-Antwerpen'
|
||||
CACHE_SECONDEN = 10 * 60
|
||||
ROLLEN = {'1': 'Leerling', '2': 'Leerkracht', '3': 'Directie', '4': 'Andere', '5': 'Andere', '6': 'Andere'}
|
||||
|
||||
|
||||
class SgrFout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _foutcode(raw):
|
||||
return isinstance(raw, (int, float)) or (isinstance(raw, str) and raw.strip().lstrip('-').isdigit())
|
||||
|
||||
|
||||
def _tekst(raw):
|
||||
"""Antwoord van Smartschool → tekst (base64 wordt gedecodeerd als dat XML/JSON oplevert)."""
|
||||
if isinstance(raw, bytes):
|
||||
raw = raw.decode('utf-8', 'replace')
|
||||
t = str(raw or '').strip()
|
||||
if t and t[0] not in '<{[':
|
||||
try:
|
||||
d = base64.b64decode(t, validate=False).decode('utf-8', 'replace').strip()
|
||||
if d[:1] in ('<', '{', '['):
|
||||
return d
|
||||
except Exception:
|
||||
pass
|
||||
return t
|
||||
|
||||
|
||||
def _kind(el, *namen):
|
||||
for n in namen:
|
||||
for c in el:
|
||||
if c.tag.lower() == n:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def _veld(el, *namen):
|
||||
c = _kind(el, *namen)
|
||||
return (c.text or '').strip() if c is not None and c.text else ''
|
||||
|
||||
|
||||
def _groepen_xml(root):
|
||||
"""XML-boom → [{'naam','code','omschrijving','type','kinderen':[...]}]."""
|
||||
def groep(el):
|
||||
details = _kind(el, 'details')
|
||||
d = details if details is not None else el
|
||||
kids_el = _kind(el, 'children')
|
||||
kinderen = [groep(c) for c in (kids_el if kids_el is not None else el) if c.tag.lower() == 'group']
|
||||
return {'naam': _veld(d, 'name', 'naam'), 'code': _veld(d, 'code'), 'omschrijving': _veld(d, 'desc', 'description'),
|
||||
'type': _veld(d, 'type'), 'kinderen': kinderen}
|
||||
if root.tag.lower() == 'group':
|
||||
return [groep(root)]
|
||||
return [groep(c) for c in root if c.tag.lower() == 'group']
|
||||
|
||||
|
||||
def _groepen_json(data):
|
||||
def groep(g):
|
||||
kids = g.get('children') or g.get('groups') or []
|
||||
if isinstance(kids, dict):
|
||||
kids = kids.get('group') or list(kids.values())
|
||||
if isinstance(kids, dict):
|
||||
kids = [kids]
|
||||
d = g.get('details') or g
|
||||
return {'naam': str(d.get('name') or ''), 'code': str(d.get('code') or ''), 'omschrijving': str(d.get('desc') or ''),
|
||||
'type': str(d.get('type') or ''), 'kinderen': [groep(k) for k in kids if isinstance(k, dict)]}
|
||||
if isinstance(data, dict):
|
||||
data = data.get('groups') or data.get('group') or [data]
|
||||
if isinstance(data, dict):
|
||||
data = [data]
|
||||
return [groep(g) for g in data if isinstance(g, dict)]
|
||||
|
||||
|
||||
def boom(ss):
|
||||
raw = ss.client.service.getAllGroupsAndClasses(Config.SS_API_PW)
|
||||
if _foutcode(raw):
|
||||
raise SgrFout(f"Smartschool weigert de groepenlijst (foutcode {raw}). Heeft het webservice-wachtwoord het recht "
|
||||
"'getAllGroupsAndClasses'?")
|
||||
t = _tekst(raw)
|
||||
try:
|
||||
return _groepen_xml(ET.fromstring(t)) if t.startswith('<') else _groepen_json(json.loads(t))
|
||||
except Exception as e:
|
||||
raise SgrFout(f"Kon de groepenlijst van Smartschool niet lezen: {e}")
|
||||
|
||||
|
||||
def zoek(groepen, naam):
|
||||
"""Eerste groep (diepte-eerst) waarvan naam of code overeenkomt (hoofdletterongevoelig)."""
|
||||
n = (naam or '').strip().lower()
|
||||
for g in groepen:
|
||||
if g['naam'].lower() == n or g['code'].lower() == n:
|
||||
return g
|
||||
hit = zoek(g['kinderen'], naam)
|
||||
if hit:
|
||||
return hit
|
||||
return None
|
||||
|
||||
|
||||
def plat(groep, pad=()):
|
||||
"""Alle groepen onder `groep` (niet de groep zelf), met hun pad en diepte, in boomvolgorde."""
|
||||
uit = []
|
||||
for k in sorted(groep['kinderen'], key=lambda g: g['naam'].lower()):
|
||||
uit.append({**k, 'pad': list(pad), 'diepte': len(pad)})
|
||||
uit += plat(k, pad + (k['naam'],))
|
||||
return uit
|
||||
|
||||
|
||||
def _leden(ss, code, recursief='0'):
|
||||
raw = ss.client.service.getAllAccountsExtended(Config.SS_API_PW, code, recursief)
|
||||
if _foutcode(raw):
|
||||
raise SgrFout(f"foutcode {raw}")
|
||||
t = _tekst(raw)
|
||||
if not t:
|
||||
return []
|
||||
data = json.loads(t)
|
||||
if isinstance(data, dict):
|
||||
data = next((data[k] for k in ('accounts', 'users', 'items') if isinstance(data.get(k), list)), list(data.values()))
|
||||
uit = []
|
||||
for a in data or []:
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
rol = str(a.get('basisrol') or a.get('basisRol') or '').strip()
|
||||
uit.append({'voornaam': str(a.get('voornaam') or '').strip(), 'naam': str(a.get('naam') or '').strip(),
|
||||
'gebruikersnaam': str(a.get('gebruikersnaam') or '').strip(),
|
||||
'rol': ROLLEN.get(rol, rol), 'email': str(a.get('emailadres') or a.get('email') or '').strip(),
|
||||
'status': str(a.get('status') or '').strip()})
|
||||
return sorted(uit, key=lambda x: (x['naam'].lower(), x['voornaam'].lower()))
|
||||
|
||||
|
||||
def overzicht(hoofdgroep=None, vernieuw=False):
|
||||
"""(hoofdgroep-dict, [subgroepen met 'leden' of 'fout'], opgehaald-tijdstip)."""
|
||||
from tenancy import huidige_school, in_school
|
||||
from services.smartschool_api import SmartschoolService
|
||||
school = huidige_school()
|
||||
if not school.heeft('SS_API_PLATFORM', 'SS_API_PW'):
|
||||
raise SgrFout("Smartschool is nog niet ingesteld voor deze school (🔌 Koppelingen › Smartschool).")
|
||||
hoofdgroep = (hoofdgroep or school.get('SS_SGR_GROEP') or STANDAARD_GROEP).strip()
|
||||
sleutel = ('sgr_groepen', hoofdgroep.lower())
|
||||
bewaard = school.cache.get(sleutel)
|
||||
if bewaard and not vernieuw and time.time() - bewaard[0] < CACHE_SECONDEN:
|
||||
return bewaard[1], bewaard[2], bewaard[0]
|
||||
|
||||
try:
|
||||
ss = SmartschoolService(Config.SS_API_PLATFORM, Config.SS_API_PW)
|
||||
except Exception as e:
|
||||
raise SgrFout(f"Smartschool is niet bereikbaar: {e}")
|
||||
hoofd = zoek(boom(ss), hoofdgroep)
|
||||
if not hoofd:
|
||||
raise SgrFout(f"De groep '{hoofdgroep}' bestaat niet in Smartschool ({Config.SS_API_PLATFORM}).")
|
||||
groepen = plat(hoofd)
|
||||
|
||||
lokaal = threading.local()
|
||||
|
||||
def haal(g):
|
||||
with in_school(school):
|
||||
if not hasattr(lokaal, 'ss'):
|
||||
lokaal.ss = SmartschoolService(Config.SS_API_PLATFORM, Config.SS_API_PW)
|
||||
try:
|
||||
if not g['code']:
|
||||
return {**g, 'leden': [], 'fout': 'geen code'}
|
||||
leden = _leden(lokaal.ss, g['code'])
|
||||
if not leden and not g['kinderen']:
|
||||
# Groep zonder subgroepen: rechtstreeks = recursief; sommige platformen geven enkel zo de leden
|
||||
leden = _leden(lokaal.ss, g['code'], '1')
|
||||
return {**g, 'leden': leden, 'fout': None}
|
||||
except Exception as e:
|
||||
return {**g, 'leden': [], 'fout': str(e)[:200]}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
resultaat = list(pool.map(haal, groepen))
|
||||
for g in resultaat:
|
||||
g.pop('kinderen', None)
|
||||
g['aantal_sub'] = sum(1 for x in resultaat if x['pad'][:len(g['pad']) + 1] == g['pad'] + [g['naam']])
|
||||
info = {'naam': hoofd['naam'], 'code': hoofd['code'], 'omschrijving': hoofd['omschrijving']}
|
||||
nu = time.time()
|
||||
school.cache[sleutel] = (nu, info, resultaat)
|
||||
return info, resultaat, nu
|
||||
Reference in new issue
Block a user