201 lines
8.8 KiB
Python
201 lines
8.8 KiB
Python
"""
|
|
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
|