""" 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