Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user