194 lines
7.6 KiB
Python
194 lines
7.6 KiB
Python
"""
|
||
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
|