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

+200
View File
@@ -0,0 +1,200 @@
"""
Co-accounts van alle leerlingen uit Smartschool, met klas en laatste aanmelding.
Werking (achtergrondtaak, per school):
1. getClassListJson → de officiële klassen
2. getAllAccountsExtended → de leerlingen per klas (met hun co-accountvelden, als Smartschool die meegeeft)
3. getUserDetailsByUsername → enkel voor leerlingen waarvan de co-accountvelden ontbraken
Het resultaat komt in de tabel smartschool_coaccounts (telkens volledig vervangen).
Wordt opnieuw opgehaald als de gegevens ouder zijn dan VERVERS_UREN, of met de knop op de pagina.
"""
import json
import sys
import threading
from datetime import datetime
from config import Config
from database import execute_query
from tenancy import school_singleton, met_school
VERVERS_UREN = 24
MAX_CO = 6
def _json(raw):
if raw is None or raw == '':
return None
if isinstance(raw, (int, float)) or (isinstance(raw, str) and raw.strip().lstrip('-').isdigit()):
raise RuntimeError(f"Smartschool foutcode {raw}")
return json.loads(raw) if isinstance(raw, str) else raw
def _lijst(data):
if isinstance(data, dict):
for k in ('accounts', 'users', 'items'):
if isinstance(data.get(k), list):
return data[k]
return list(data.values())
return data or []
def _login(waarde):
"""'2026-09-12 08:01:22' → datetime; leeg, '0000-00-00…' of ongeldig → None."""
t = str(waarde or '').strip()
if not t or t.startswith('0000') or t.lower() in ('none', 'nooit', 'never'):
return None
for fmt in ('%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M', '%Y-%m-%d', '%d-%m-%Y %H:%M:%S', '%d/%m/%Y %H:%M'):
try:
return datetime.strptime(t[:19], fmt)
except ValueError:
continue
return None
def co_accounts_uit(data):
"""Co-accounts (1-6) uit de gegevens van één leerling. Lege lijst als er geen co-accountvelden zijn."""
uit = []
for i in range(1, MAX_CO + 1):
voornaam = str(data.get(f"voornaam_coaccount{i}") or '').strip()
naam = str(data.get(f"naam_coaccount{i}") or '').strip()
if not (voornaam or naam):
continue
uit.append({'nr': i, 'voornaam': voornaam, 'naam': naam,
'type': str(data.get(f"type_coaccount{i}") or '').strip(),
'email': str(data.get(f"email_coaccount{i}") or '').strip(),
'login': _login(data.get(f"last_successful_login_coaccount{i}"))})
return uit
def heeft_co_velden(data):
return any(k.startswith('voornaam_coaccount') for k in data)
class CoAccountsService:
def __new__(cls):
def init(inst):
inst._lock = threading.Lock()
inst.status = {'bezig': False, 'voortgang': 0, 'totaal': 0, 'fout': None, 'resultaat': None}
return school_singleton(cls, init)
@staticmethod
def ensure_tables():
execute_query("""
CREATE TABLE IF NOT EXISTS smartschool_coaccounts (
Internnummer VARCHAR(32) NOT NULL,
Nr TINYINT NOT NULL,
Gebruikersnaam VARCHAR(100) NULL,
Leerling_naam VARCHAR(100) NULL,
Leerling_voornaam VARCHAR(100) NULL,
Klas VARCHAR(50) NULL,
Voornaam VARCHAR(100) NULL,
Naam VARCHAR(100) NULL,
Type VARCHAR(50) NULL,
Email VARCHAR(255) NULL,
Laatste_login DATETIME NULL,
Opgehaald DATETIME NOT NULL,
PRIMARY KEY (Internnummer, Nr),
KEY idx_coacc_klas (Klas)
)""", commit=True)
# ---------------- opvragen ----------------
def laatst_opgehaald(self):
rows = execute_query("SELECT MAX(Opgehaald) AS m FROM smartschool_coaccounts")
return rows[0]['m'] if rows else None
def alle(self):
return execute_query(
"SELECT * FROM smartschool_coaccounts ORDER BY Klas, Leerling_naam, Leerling_voornaam, Nr") or []
def verouderd(self):
laatst = self.laatst_opgehaald()
return not laatst or (datetime.now() - laatst).total_seconds() > VERVERS_UREN * 3600
# ---------------- ophalen uit Smartschool ----------------
def _leerlingen(self, ss):
"""[(account-dict, klasnaam)] voor alle leerlingen in een officiële klas (elke leerling één keer)."""
klassen = []
for k in _lijst(_json(ss.client.service.getClassListJson(Config.SS_API_PW))):
if not isinstance(k, dict):
continue
if str(k.get('isKlas', k.get('isOfficial', True))).lower() in ('false', '0', 'none'):
continue
code = str(k.get('code') or '').strip()
if code:
klassen.append((code, str(k.get('name') or code).strip()))
klassen.sort(key=lambda x: x[1])
self.status.update(totaal=len(klassen), voortgang=0)
gezien, uit = set(), []
for i, (code, naam) in enumerate(klassen, 1):
try:
leden = _lijst(_json(ss.client.service.getAllAccountsExtended(Config.SS_API_PW, code, '0')))
except Exception as e:
sys.stderr.write(f"Co-accounts: klas {naam}: {e}\n")
leden = []
for a in leden:
if not isinstance(a, dict):
continue
rol = str(a.get('basisrol') or a.get('basisRol') or '').lower()
if rol and rol not in ('1', 'leerling', 'student'):
continue
sleutel = str(a.get('internnummer') or a.get('gebruikersnaam') or '').strip()
if sleutel and sleutel not in gezien:
gezien.add(sleutel)
uit.append((a, naam))
self.status['voortgang'] = i
return uit
def sync(self):
if not self._lock.acquire(blocking=False):
return False, "Er loopt al een ophaling."
try:
from services.smartschool_api import SmartschoolService
self.status.update(bezig=True, voortgang=0, totaal=0, fout=None)
self.ensure_tables()
ss = SmartschoolService(Config.SS_API_PLATFORM, Config.SS_API_PW)
leerlingen = self._leerlingen(ss)
# Leerlingen zonder co-accountvelden in de klaslijst: details apart opvragen
zonder = [i for i, (a, _) in enumerate(leerlingen) if not heeft_co_velden(a)]
self.status.update(totaal=self.status['totaal'] + len(zonder))
for n, i in enumerate(zonder, 1):
a, klas = leerlingen[i]
try:
d = _json(ss.client.service.getUserDetailsByUsername(Config.SS_API_PW, a.get('gebruikersnaam')))
if isinstance(d, dict):
leerlingen[i] = ({**a, **d}, klas)
except Exception as e:
sys.stderr.write(f"Co-accounts: {a.get('gebruikersnaam')}: {e}\n")
self.status['voortgang'] += 1
nu = datetime.now()
rijen = []
for a, klas in leerlingen:
intern = str(a.get('internnummer') or a.get('gebruikersnaam') or '').strip()
for c in co_accounts_uit(a):
rijen.append((intern, c['nr'], a.get('gebruikersnaam'), a.get('naam'), a.get('voornaam'), klas,
c['voornaam'], c['naam'], c['type'], c['email'], c['login'], nu))
if not leerlingen:
raise RuntimeError("Smartschool gaf geen leerlingen terug. Heeft het webservice-wachtwoord de "
"rechten voor gebruikers en klassen?")
execute_query("DELETE FROM smartschool_coaccounts", commit=True)
for r in rijen:
execute_query("INSERT INTO smartschool_coaccounts (Internnummer, Nr, Gebruikersnaam, Leerling_naam, "
"Leerling_voornaam, Klas, Voornaam, Naam, Type, Email, Laatste_login, Opgehaald) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", r, commit=True)
nooit = sum(1 for r in rijen if not r[10])
msg = (f"{len(leerlingen)} leerlingen en {len(rijen)} co-accounts opgehaald; "
f"{nooit} co-account(s) nog nooit aangemeld.")
self.status['resultaat'] = msg
return True, msg
except Exception as e:
self.status['fout'] = str(e)
return False, f"Ophalen mislukt: {e}"
finally:
self.status['bezig'] = False
self._lock.release()
def sync_op_achtergrond(self):
if self.status['bezig']:
return False
self.status.update(bezig=True, voortgang=0, totaal=0)
threading.Thread(target=met_school(self.sync), daemon=True).start()
return True