550 lines
27 KiB
Python
550 lines
27 KiB
Python
import json
|
||
import requests
|
||
from datetime import datetime, timezone # Essentieel voor de timestamp
|
||
from dateutil.relativedelta import relativedelta
|
||
from config import Config
|
||
from database import execute_query
|
||
|
||
# Informat geeft de volledige leerlingenlijst soms pas na een minuut of langer terug.
|
||
# (verbinden, lezen) in seconden
|
||
INFORMAT_TIMEOUT = (15, 300)
|
||
ONEROSTER_TIMEOUT = (15, 120)
|
||
|
||
|
||
def informat_get(url, headers, pogingen=2):
|
||
"""GET naar Informat met ruime wachttijd; bij een time-out nog één keer opnieuw."""
|
||
for poging in range(1, pogingen + 1):
|
||
try:
|
||
return requests.get(url, headers=headers, timeout=INFORMAT_TIMEOUT)
|
||
except requests.exceptions.Timeout:
|
||
if poging == pogingen:
|
||
raise RuntimeError(
|
||
f"Informat antwoordde niet binnen {INFORMAT_TIMEOUT[1] // 60} minuten "
|
||
f"({pogingen} pogingen). Probeer het later opnieuw; Informat is soms traag of in onderhoud.")
|
||
print(f"[Informat] time-out, nieuwe poging ({poging + 1}/{pogingen})...")
|
||
|
||
|
||
def zorg_vertrokken_kolom():
|
||
"""Kolom Leerlingen.Vertrokken_op toevoegen in een bestaande schooldatabase (één keer per school)."""
|
||
try:
|
||
from tenancy import huidige_school
|
||
school = huidige_school()
|
||
except Exception:
|
||
school = None
|
||
if school is not None and school.cache.get('lln_vertrokken_kolom'):
|
||
return
|
||
kolommen = {r['Field'].lower() for r in execute_query("SHOW COLUMNS FROM Leerlingen") or []}
|
||
if 'vertrokken_op' not in kolommen:
|
||
execute_query("ALTER TABLE Leerlingen ADD COLUMN Vertrokken_op DATE NULL AFTER Schooljaar_ID", commit=True)
|
||
if school is not None:
|
||
school.cache['lln_vertrokken_kolom'] = True
|
||
|
||
|
||
def veld_waarden(obj, pad):
|
||
"""Alle waarden op een pad met punten, ook door lijsten heen: veld_waarden(st, 'inschrijvingen.vestiging')."""
|
||
huidig = [obj]
|
||
for deel in pad.split('.'):
|
||
volgende = []
|
||
for o in huidig:
|
||
items = o if isinstance(o, list) else [o]
|
||
for it in items:
|
||
if isinstance(it, dict) and deel in it:
|
||
volgende.append(it[deel])
|
||
huidig = volgende
|
||
uit = []
|
||
for h in huidig:
|
||
uit.extend(h if isinstance(h, list) else [h])
|
||
return uit
|
||
|
||
|
||
class SyncService:
|
||
def __init__(self):
|
||
self.conf = Config()
|
||
# Informat-gegevens van de huidige school (beheerd via /scholen).
|
||
# Een school kan meerdere Informat-instellingsnummers hebben (één per campus).
|
||
self.campussen = Config.INFORMAT_CAMPUSSEN or []
|
||
self.institute_no = Config.INFORMAT_INST_NUMBER
|
||
self.auth_url = Config.INFORMAT_AUTH_URL
|
||
self.api_base = Config.INFORMAT_API_BASE
|
||
# De scope MOET het instellingsnummer bevatten
|
||
self.scope = Config.INFORMAT_FULL_SCOPE
|
||
|
||
def get_informat_token(self, nummer=None):
|
||
"""Haalt een token op met de scope van het (eerste of opgegeven) instellingsnummer."""
|
||
from tenancy import informat_scope
|
||
scope = informat_scope(Config.INFORMAT_SCOPE, nummer) if nummer else self.scope
|
||
payload = {
|
||
'grant_type': 'client_credentials',
|
||
'client_id': self.conf.INFORMAT_ID,
|
||
'client_secret': self.conf.INFORMAT_SECRET,
|
||
'scope': scope
|
||
}
|
||
try:
|
||
response = requests.post(self.auth_url, data=payload, timeout=10)
|
||
if response.status_code == 200:
|
||
return response.json().get('access_token')
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
def tel_leerlingen(self, refdate, schooljaar, nummer=None):
|
||
"""Aantal leerlingen in Informat op `refdate` (YYYY-MM-DD) voor `schooljaar` (bv. 2026-27), voor één
|
||
instellingsnummer (standaard het eerste). Geeft (True, aantal) of (False, foutmelding)."""
|
||
nummer = nummer or self.institute_no
|
||
token = self.get_informat_token(nummer)
|
||
if not token:
|
||
return False, f"Authenticatie bij Informat mislukt (instellingsnummer {nummer})."
|
||
url = f"{self.api_base}/students/?schoolYear={schooljaar}&refdate={refdate}"
|
||
headers = {
|
||
'Authorization': f'BEARER {token}',
|
||
'InstituteNo': nummer,
|
||
'Timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z'), # verplicht bij Informat
|
||
'Api-Version': '1',
|
||
'Accept': 'application/json'
|
||
}
|
||
try:
|
||
response = informat_get(url, headers)
|
||
except Exception as e:
|
||
return False, f"Fout tijdens tellen: {e}"
|
||
if response.status_code != 200:
|
||
return False, f"API fout {response.status_code} (instellingsnummer {nummer}): {response.text[:300]}"
|
||
data = response.json()
|
||
lijst = data if isinstance(data, list) else data.get('students', [])
|
||
self._laatste_lijst = lijst # voor tellen per campusveld en de veldentest
|
||
return True, len(lijst)
|
||
|
||
def informat_lijst(self, refdate, schooljaar, nummer=None):
|
||
"""De volledige leerlingenlijst van Informat (zelfde oproep als het tellen). (True, lijst) of (False, fout)."""
|
||
ok, res = self.tel_leerlingen(refdate, schooljaar, nummer)
|
||
return (True, self._laatste_lijst) if ok else (False, res)
|
||
|
||
def tel_locatie(self, refdate, schooljaar, locatie_id):
|
||
"""Aantal leerlingen op één schoollocatie (Discimus-nummer) via de Informat Student API
|
||
(GET /smartschool-users). Geeft (True, aantal) of (False, foutmelding).
|
||
Let op: deze API telt enkel leerlingen met een actieve inschrijving (gerealiseerd of parallel, niet GON)
|
||
op een locatie die aan het Smartschool-platform gekoppeld is."""
|
||
platform = (Config.SS_API_PLATFORM or '').strip()
|
||
if not platform:
|
||
return False, ("Voor het tellen per campus moet het Smartschool-platform ingevuld zijn "
|
||
"(🔌 Koppelingen › Smartschool).")
|
||
token = self.get_informat_token(self.institute_no)
|
||
if not token:
|
||
return False, f"Authenticatie bij Informat mislukt (instellingsnummer {self.institute_no})."
|
||
url = f"{str(Config.INFORMAT_STUDENT_API).rstrip('/')}/smartschool-users"
|
||
headers = {'Authorization': f'Bearer {token}', 'instituteNo': self.institute_no, 'Accept': 'application/json'}
|
||
# Informat vergelijkt het platformadres letterlijk met wat er in Informat staat. Dat kan met of zonder
|
||
# https:// of een / op het einde opgeslagen zijn: probeer de varianten tot er één gekend is (en onthoud die).
|
||
varianten = list(dict.fromkeys([v for v in (getattr(self, '_ss_platform_url', None), f"https://{platform}",
|
||
platform, f"https://{platform}/", f"http://{platform}") if v]))
|
||
response, geprobeerd = None, []
|
||
for platform_url in varianten:
|
||
params = {'smartschoolPlatformUrl': platform_url, 'schoolYear': schooljaar,
|
||
'locationIdDiscimus': int(locatie_id) if str(locatie_id).isdigit() else locatie_id,
|
||
'referenceDate': refdate}
|
||
try:
|
||
for poging in (1, 2):
|
||
try:
|
||
response = requests.get(url, params=params, headers=headers, timeout=INFORMAT_TIMEOUT)
|
||
break
|
||
except requests.exceptions.Timeout:
|
||
if poging == 2:
|
||
raise RuntimeError("Informat antwoordde niet op tijd.")
|
||
except Exception as e:
|
||
return False, f"Fout tijdens tellen: {e}"
|
||
geprobeerd.append(platform_url)
|
||
if response.status_code == 409 and 'smartschoolplatform' in response.text.lower():
|
||
continue # platform onder dit adres niet gekend: volgende variant
|
||
self._ss_platform_url = platform_url
|
||
break
|
||
if response.status_code != 200:
|
||
detail = ''
|
||
try:
|
||
detail = response.json().get('detail') or ''
|
||
except ValueError:
|
||
detail = response.text[:200]
|
||
if response.status_code == 409 and 'smartschoolplatform' in response.text.lower():
|
||
return False, (f"Informat kent het Smartschool-platform niet voor instellingsnummer {self.institute_no} "
|
||
f"en schooljaar {schooljaar} (geprobeerd: {', '.join(geprobeerd)}). Is in Informat "
|
||
"voor dit instellingsnummer en dit schooljaar een Smartschool-koppeling actief?")
|
||
return False, f"Student API fout {response.status_code} (campus {locatie_id}): {detail}"
|
||
data = response.json()
|
||
lijst = data if isinstance(data, list) else data.get('items', [])
|
||
# Eén leerling kan meer dan één inschrijving hebben: uniek op persoon
|
||
return True, len({u['ppersoon'] if u.get('ppersoon') is not None else (u.get('persoonId') or id(u))
|
||
for u in lijst})
|
||
|
||
def tel_per_campus(self, refdate, schooljaar):
|
||
"""Geeft (True, totaal, detail) of (False, fout, None). detail = [{'naam', 'nummer', 'aantal'}] als de
|
||
school meerdere campussen heeft, anders None.
|
||
- Campussen (Discimus-nummers) ingesteld → per locatie geteld via de Student API.
|
||
- Anders: per instellingsnummer (meestal één) via de leerlingen-API zoals voorheen."""
|
||
locaties = Config.INFORMAT_LOCATIES or []
|
||
veld = (Config.INFORMAT_CAMPUS_VELD or '').strip()
|
||
if veld:
|
||
return self._tel_op_veld(refdate, schooljaar, veld, dict(locaties))
|
||
if locaties:
|
||
telling = [(loc, naam, lambda loc=loc: self.tel_locatie(refdate, schooljaar, loc)) for loc, naam in locaties]
|
||
else:
|
||
campussen = self.campussen or [(self.institute_no, '')]
|
||
telling = [(nr, naam, lambda nr=nr: self.tel_leerlingen(refdate, schooljaar, nr)) for nr, naam in campussen]
|
||
detail, totaal = [], 0
|
||
for nummer, naam, tel in telling:
|
||
ok, res = tel()
|
||
if not ok:
|
||
return False, (f"{naam}: {res}" if len(telling) > 1 else res), None
|
||
detail.append({'naam': naam or nummer, 'nummer': nummer, 'aantal': res})
|
||
totaal += res
|
||
return True, totaal, (detail if len(detail) > 1 else None)
|
||
|
||
def _tel_op_veld(self, refdate, schooljaar, veld, namen):
|
||
"""Haalt de leerlingenlijst één keer op en telt per waarde van `veld` (pad met punten, bv.
|
||
'inschrijvingen.vestiging'). `namen` vertaalt een waarde naar een campusnaam (uit 'Campussen')."""
|
||
lijst, gezien = [], set()
|
||
for nummer, naam in (self.campussen or [(self.institute_no, '')]): # alle instellingsnummers van de school
|
||
ok, deel = self.informat_lijst(refdate, schooljaar, nummer)
|
||
if not ok:
|
||
return False, (f"{naam}: {deel}" if len(self.campussen) > 1 else deel), None
|
||
for st in deel:
|
||
sleutel = str(st.get('pPersoon') or st.get('id') or id(st))
|
||
if sleutel not in gezien:
|
||
gezien.add(sleutel)
|
||
lijst.append(st)
|
||
per = {}
|
||
for st in lijst:
|
||
for w in set(str(v) for v in veld_waarden(st, veld) if v not in (None, '')) or {'?'}:
|
||
per[w] = per.get(w, 0) + 1
|
||
volgorde = [w for w in namen if w in per] + sorted(w for w in per if w not in namen)
|
||
detail = [{'naam': namen.get(w) or (f"Onbekend ({veld} leeg)" if w == '?' else f"Campus {w}"),
|
||
'nummer': '' if w == '?' else w, 'aantal': per[w]} for w in volgorde]
|
||
return True, len(lijst), (detail if len(detail) > 1 else None)
|
||
|
||
def sync_student_count(self, school_naam=None):
|
||
"""Huidig aantal (peildatum vandaag + 1 maand, zodat net ingeschreven leerlingen meetellen)."""
|
||
refdate = (datetime.now() + relativedelta(months=1)).strftime('%Y-%m-%d')
|
||
ok, res, detail = self.tel_per_campus(refdate, Config.SCHOOLYEAR)
|
||
if not ok:
|
||
return False, res
|
||
# Bewaren bij de school in de centrale database (overzicht scholengroep)
|
||
from database import central_query
|
||
central_query("UPDATE Scholen SET Leerlingenaantal = %s, Leerlingenaantal_detail = %s, "
|
||
"Leerlingenaantal_sync = NOW() WHERE Code = %s",
|
||
(res, json.dumps(detail) if detail else None, Config.SCHOOLNAME_SHORT), commit=True)
|
||
if detail:
|
||
return True, f"Synchronisatie voltooid: {res} leerlingen (" + ", ".join(
|
||
f"{d['naam']}: {d['aantal']}" for d in detail) + ")."
|
||
return True, f"Synchronisatie voltooid: {res} leerlingen."
|
||
|
||
def tel_op_peildatum(self, peildatum, door=None):
|
||
"""Telt de leerlingen op een gekozen datum en bewaart het resultaat in Leerlingenaantal_tellingen."""
|
||
from database import central_query
|
||
from tenancy import huidige_school, _automatisch_schooljaar
|
||
schooljaar = _automatisch_schooljaar(peildatum)
|
||
ok, res, detail = self.tel_per_campus(peildatum.strftime('%Y-%m-%d'), schooljaar)
|
||
if not ok:
|
||
return False, res
|
||
central_query(
|
||
"INSERT INTO Leerlingenaantal_tellingen (School_ID, Peildatum, Schooljaar, Aantal, Detail, Opgevraagd_op, Door) "
|
||
"VALUES (%s, %s, %s, %s, %s, NOW(), %s) "
|
||
"ON DUPLICATE KEY UPDATE Schooljaar = VALUES(Schooljaar), Aantal = VALUES(Aantal), Detail = VALUES(Detail), "
|
||
"Opgevraagd_op = NOW(), Door = VALUES(Door)",
|
||
(huidige_school().id, peildatum, schooljaar, res, json.dumps(detail) if detail else None, door),
|
||
commit=True)
|
||
return True, res
|
||
|
||
#Haal ss token op
|
||
def get_smartschool_token(self):
|
||
"""Haalt een OAuth2 token op bij Smartschool."""
|
||
resp_ss = requests.post(
|
||
self.conf.SS_TOKEN_URL,
|
||
data={"grant_type": "client_credentials"},
|
||
auth=(self.conf.SS_CLIENT_ID, self.conf.SS_CLIENT_SECRET)
|
||
)
|
||
return resp_ss.json().get("access_token")
|
||
|
||
#Vergelijkt leerlingenlijst uit informat met namen uit DB en vult eventuele ontbrekende leerlingen aan met gegevens van Informat (Internnummer) en Smartschool (Gebruikersnaam)
|
||
def sync_leerlingen_informat(self):
|
||
"""
|
||
Synchroniseert leerlingen door Informat data te vergelijken met de DB
|
||
en ontbrekende data aan te vullen via Smartschool.
|
||
"""
|
||
try:
|
||
# 1. Haal Informat data op
|
||
inf_token = self.get_informat_token()
|
||
|
||
#Ik zet als parameter voor het ophalen huidige datum +1 maand om leerlingen die net ingeschreven zijn en pas na een vakantie starten mee te kunnen rekenen
|
||
future_date = datetime.now() + relativedelta(months=1)
|
||
refdate = future_date.strftime('%Y-%m-%d')
|
||
|
||
|
||
inf_url = f"{self.api_base}/students/?schoolYear={self.conf.INFORMAT_SCHOOLYEAR}&refdate={refdate}"
|
||
inf_headers = {
|
||
'Authorization': f'BEARER {inf_token}',
|
||
'InstituteNo': self.institute_no,
|
||
'Api-Version': '1',
|
||
'Accept': 'application/json'
|
||
}
|
||
inf_data = requests.get(inf_url, headers=inf_headers).json()
|
||
|
||
if not inf_data or 'students' not in inf_data:
|
||
return False, "Geen data van Informat ontvangen"
|
||
|
||
# 2. Haal Bestaande Database data op
|
||
# We gebruiken execute_query om alle bestaande Internnummers op te halen
|
||
db_rows = execute_query("SELECT Internnummer FROM Leerlingen")
|
||
db_numbers = {str(row['Internnummer']) for row in db_rows} if db_rows else set()
|
||
|
||
# 3. Zoek ontbrekende ID's (Informat - Database)
|
||
json_numbers = {str(s['pPersoon']) for s in inf_data['students'] if 'pPersoon' in s}
|
||
missing_ids = json_numbers - db_numbers
|
||
|
||
if not missing_ids:
|
||
return True, "Iedereen is al gesynchroniseerd."
|
||
|
||
# 4. Haal Smartschool data op voor de match
|
||
ss_token = self.get_smartschool_token()
|
||
ss_res = requests.get(
|
||
f"{self.conf.SS_BASE_URL}/students",
|
||
headers={"Authorization": f"Bearer {ss_token}"}
|
||
)
|
||
ss_students = ss_res.json().get("users", [])
|
||
|
||
added_count = 0
|
||
for m_id in missing_ids:
|
||
found_username = None
|
||
for ss_student in ss_students:
|
||
ss_internal = str(ss_student.get("metadata", {}).get("smsc.internalNumber", ""))
|
||
|
||
if ss_internal.strip() == m_id.strip():
|
||
found_username = ss_student.get("username")
|
||
break
|
||
|
||
if found_username:
|
||
# 5. DATABASE INVOER
|
||
execute_query(
|
||
"INSERT INTO Leerlingen (Internnummer, Gebruikersnaam, ss_nummer) VALUES (%s, %s, %s)",
|
||
(m_id, found_username, "Niet ingesteld"),
|
||
commit=True
|
||
)
|
||
added_count += 1
|
||
|
||
return True, f"Succes! {added_count} leerlingen toegevoegd."
|
||
|
||
except Exception as e:
|
||
return False, f"Fout tijdens synchronisatie: {str(e)}"
|
||
|
||
|
||
|
||
|
||
|
||
#---------------------Nieuwe aanpak------------------------
|
||
import requests
|
||
from datetime import datetime, timezone
|
||
from config import Config
|
||
from database import execute_query
|
||
|
||
class InformatSmartschoolSync:
|
||
|
||
|
||
def __init__(self):
|
||
self.conf = Config()
|
||
# Informat-gegevens van de huidige school (beheerd via /scholen)
|
||
self.institute_no = Config.INFORMAT_INST_NUMBER
|
||
self.auth_url = Config.INFORMAT_AUTH_URL
|
||
self.api_base = Config.INFORMAT_API_BASE
|
||
# De scope MOET het instellingsnummer bevatten
|
||
self.scope = Config.INFORMAT_FULL_SCOPE
|
||
self.campussen = Config.INFORMAT_CAMPUSSEN or [(self.institute_no, '')]
|
||
|
||
def get_informat_token(self, nummer=None):
|
||
"""Haalt token op met de scope van het (eerste of opgegeven) instellingsnummer."""
|
||
from tenancy import informat_scope
|
||
payload = {
|
||
'grant_type': 'client_credentials',
|
||
'client_id': self.conf.INFORMAT_ID,
|
||
'client_secret': self.conf.INFORMAT_SECRET,
|
||
'scope': informat_scope(Config.INFORMAT_SCOPE, nummer) if nummer else self.scope
|
||
}
|
||
try:
|
||
response = requests.post(self.auth_url, data=payload, timeout=10)
|
||
if response.status_code == 200:
|
||
return response.json().get('access_token')
|
||
except Exception as e:
|
||
print(f"Fout bij ophalen Informat token: {e}")
|
||
return None
|
||
|
||
def get_smartschool_oneroster_students(self):
|
||
"""Haalt actieve leerlingen op uit de Smartschool OneRoster API (v1p1). Bij een fout: {}."""
|
||
mapping, fout = self.smartschool_koppeling()
|
||
if fout:
|
||
print(f"Fout bij ophalen Smartschool OneRoster data: {fout}")
|
||
return mapping
|
||
|
||
def smartschool_koppeling(self):
|
||
"""{internnummer: {'username', 'ss_nummer'}} uit Smartschool OneRoster. Geeft (mapping, None) of ({}, fout)."""
|
||
if not (self.conf.SS_CLIENT_ID and self.conf.SS_CLIENT_SECRET):
|
||
return {}, "de OneRoster-koppeling van Smartschool is niet ingesteld"
|
||
try:
|
||
token_url = self.conf.SS_TOKEN_URL.strip()
|
||
if not token_url.startswith(("http://", "https://")):
|
||
token_url = f"https://{token_url}"
|
||
|
||
resp_ss = requests.post(
|
||
token_url,
|
||
data={"grant_type": "client_credentials"},
|
||
auth=(self.conf.SS_CLIENT_ID, self.conf.SS_CLIENT_SECRET),
|
||
timeout=10
|
||
)
|
||
resp_ss.raise_for_status()
|
||
ss_token = resp_ss.json().get("access_token")
|
||
|
||
if not ss_token:
|
||
return {}, "geen access_token ontvangen van Smartschool"
|
||
|
||
headers = {
|
||
"Authorization": f"Bearer {ss_token}",
|
||
"Accept": "application/json"
|
||
}
|
||
|
||
base_url = self.conf.SS_BASE_URL.strip()
|
||
if not base_url.startswith(("http://", "https://")):
|
||
base_url = f"https://{base_url}"
|
||
|
||
base_url = base_url.rstrip('/')
|
||
endpoint_url = f"{base_url}/students"
|
||
|
||
response = requests.get(endpoint_url, headers=headers, timeout=ONEROSTER_TIMEOUT)
|
||
response.raise_for_status()
|
||
|
||
json_data = response.json()
|
||
all_students = json_data.get("users", [])
|
||
|
||
ss_mapping = {}
|
||
for student in all_students:
|
||
metadata = student.get("metadata", {})
|
||
internal_number = metadata.get("smsc.internalNumber")
|
||
username = student.get("username")
|
||
sourced_id = student.get("sourcedId")
|
||
|
||
if internal_number:
|
||
ss_mapping[str(internal_number).strip()] = {
|
||
"username": username,
|
||
"ss_nummer": sourced_id if sourced_id else "Niet ingesteld"
|
||
}
|
||
if not ss_mapping:
|
||
return {}, "Smartschool gaf geen enkele leerling met een internnummer terug"
|
||
return ss_mapping, None
|
||
|
||
except Exception as e:
|
||
return {}, str(e)
|
||
|
||
def _informat_leerlingen(self, nummer, school_year, refdate):
|
||
"""Leerlingenlijst van één instellingsnummer. Geeft (lijst, None) of (None, fout)."""
|
||
inf_token = self.get_informat_token(nummer)
|
||
if not inf_token:
|
||
return None, f"Informat authenticatie mislukt (instellingsnummer {nummer})."
|
||
base_url_inf = self.api_base.strip().rstrip('/')
|
||
url = f"{base_url_inf}/students/?schoolYear={school_year}&refdate={refdate}"
|
||
headers = {
|
||
'Authorization': f'BEARER {inf_token}',
|
||
'InstituteNo': nummer,
|
||
'Timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z'),
|
||
'Api-Version': '1',
|
||
'Accept': 'application/json'
|
||
}
|
||
response = informat_get(url, headers)
|
||
if response.status_code != 200:
|
||
return None, f"Informat API fout {response.status_code} (instellingsnummer {nummer}): {response.text}"
|
||
json_data = response.json()
|
||
return (json_data if isinstance(json_data, list) else json_data.get('students', json_data.get('items', []))), None
|
||
|
||
def sync_actieve_leerlingen(self):
|
||
"""
|
||
Haalt actieve leerlingen op bij Informat (alle campussen / instellingsnummers van de school),
|
||
koppelt Smartschool OneRoster info en update de database 'Leerlingen'.
|
||
"""
|
||
school_year = Config.SCHOOLYEAR
|
||
# +1 maand: ook leerlingen die net ingeschreven zijn en pas na een vakantie starten
|
||
refdate = (datetime.now() + relativedelta(months=1)).strftime('%Y-%m-%d')
|
||
|
||
students_list, gezien = [], set()
|
||
try:
|
||
for nummer, naam in self.campussen:
|
||
lijst, fout = self._informat_leerlingen(nummer, school_year, refdate)
|
||
if fout:
|
||
return False, (f"{naam}: {fout}" if len(self.campussen) > 1 else fout)
|
||
for st in lijst:
|
||
sleutel = str(st.get('pPersoon') or st.get('internNummer') or st.get('id', '')).strip()
|
||
if sleutel and sleutel not in gezien:
|
||
gezien.add(sleutel)
|
||
students_list.append(st)
|
||
except Exception as e:
|
||
return False, f"Fout opgetreden tijdens de synchronisatie: {str(e)}"
|
||
|
||
if not students_list:
|
||
# Nooit iedereen als vertrokken markeren omdat Informat (tijdelijk) een lege lijst gaf
|
||
return False, "Informat gaf geen enkele actieve leerling terug; er is niets aangepast."
|
||
|
||
# Smartschool (gebruikersnaam en Smartschool-nummer). Lukt dat niet, dan blijven die velden zoals ze waren.
|
||
ss_leerlingen, ss_fout = self.smartschool_koppeling()
|
||
|
||
try:
|
||
print(f"[DEBUG] Aantal actieve leerlingen gevonden in Informat: {len(students_list)}")
|
||
zorg_vertrokken_kolom()
|
||
|
||
db_rows = execute_query("SELECT Internnummer FROM Leerlingen")
|
||
bestaande_internnummers = {str(row['Internnummer']).strip() for row in db_rows} if db_rows else set()
|
||
|
||
toegevoegd_teller = bijgewerkt_teller = 0
|
||
schooljaar_id = Config.SCHOOLYEAR[:4]
|
||
actief = set()
|
||
|
||
for student in students_list:
|
||
internnummer = str(student.get('pPersoon') or student.get('internNummer') or student.get('id', '')).strip()
|
||
voornaam = (student.get('voornaam' if 'voornaam' in student else 'firstName') or '').strip()
|
||
naam = (student.get('naam' if 'naam' in student else 'lastName') or '').strip()
|
||
if not internnummer or internnummer == "None":
|
||
continue
|
||
actief.add(internnummer)
|
||
ss_info = ss_leerlingen.get(internnummer)
|
||
standaard_gebruikersnaam = f"{voornaam}.{naam}".replace(" ", "").lower()
|
||
|
||
if internnummer in bestaande_internnummers:
|
||
if ss_info:
|
||
execute_query("""
|
||
UPDATE Leerlingen SET Gebruikersnaam = %s, Voornaam = %s, Naam = %s, SS_nummer = %s,
|
||
Schooljaar_ID = %s, Vertrokken_op = NULL
|
||
WHERE Internnummer = %s""",
|
||
(ss_info.get("username") or standaard_gebruikersnaam, voornaam, naam,
|
||
ss_info.get("ss_nummer") or "Niet ingesteld", schooljaar_id, internnummer), commit=True)
|
||
else:
|
||
# Niet (of niet bereikbaar) in Smartschool: gebruikersnaam en Smartschool-nummer niet overschrijven
|
||
execute_query("""
|
||
UPDATE Leerlingen SET Voornaam = %s, Naam = %s, Schooljaar_ID = %s, Vertrokken_op = NULL
|
||
WHERE Internnummer = %s""", (voornaam, naam, schooljaar_id, internnummer), commit=True)
|
||
bijgewerkt_teller += 1
|
||
else:
|
||
ss_info = ss_info or {}
|
||
execute_query("""
|
||
INSERT INTO Leerlingen (Internnummer, Gebruikersnaam, Voornaam, Naam, SS_nummer, Schooljaar_ID)
|
||
VALUES (%s, %s, %s, %s, %s, %s)""",
|
||
(internnummer, ss_info.get("username") or standaard_gebruikersnaam, voornaam, naam,
|
||
ss_info.get("ss_nummer") or "Niet ingesteld", schooljaar_id), commit=True)
|
||
bestaande_internnummers.add(internnummer)
|
||
toegevoegd_teller += 1
|
||
|
||
# Leerlingen die niet meer actief zijn in Informat: als vertrokken markeren (niet verwijderen)
|
||
nog_actief = execute_query("SELECT Internnummer FROM Leerlingen WHERE Vertrokken_op IS NULL") or []
|
||
weg = [r['Internnummer'] for r in nog_actief if str(r['Internnummer']).strip() not in actief]
|
||
for k in range(0, len(weg), 500):
|
||
deel = weg[k:k + 500]
|
||
execute_query(f"UPDATE Leerlingen SET Vertrokken_op = CURDATE() WHERE Internnummer IN "
|
||
f"({', '.join(['%s'] * len(deel))})", tuple(deel), commit=True)
|
||
|
||
melding = f"{toegevoegd_teller} nieuw, {bijgewerkt_teller} bijgewerkt"
|
||
if weg:
|
||
melding += f", {len(weg)} als vertrokken gemarkeerd"
|
||
return True, {'melding': melding + '.', 'smartschool_fout': ss_fout}
|
||
|
||
except Exception as e:
|
||
return False, f"Fout opgetreden tijdens de synchronisatie: {str(e)}"
|