Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -0,0 +1,619 @@
|
||||
"""
|
||||
GO! Pro: personeelsleden van de actieve school uit Informat (Personeel WEB API, versie 2).
|
||||
|
||||
- GET /employees → naam, voornaam, isActive, comnrs (telefoonnummers), ...
|
||||
- GET /employees/assignments → opdrachten met ambt (functie), begindatum en einddatum
|
||||
Scope: api_informat_sas_personeel.personeel.{instellingsnummer} (zelfde client-ID/secret als de leerlingen-API,
|
||||
maar de client moet ook deze scope hebben).
|
||||
|
||||
Per personeelslid komt er één rij. Functie en rol kies je in PIVOT uit dezelfde lijst (standaard Leerkracht,
|
||||
een directeur Directeur) en worden per personeelslid bewaard in de schooldatabase (tabel gopro_keuzes).
|
||||
"""
|
||||
import re
|
||||
import unicodedata
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
import requests
|
||||
|
||||
from config import Config
|
||||
from database import execute_query
|
||||
from services.sync_service import informat_get
|
||||
|
||||
ROLLEN = [
|
||||
'Algemeen Directeur', 'Coordinerend Directeur', 'Communicatie', 'Financiën', 'Infrastructuur', 'HR', 'ICT',
|
||||
'Preventie en veiligheid', 'MVD', 'Administratief', 'RVB-lid', 'RVB', 'Voorzitter RVB', 'Beleidsondersteuner',
|
||||
'Directeur Coordinator', 'Directeur', 'Internationalisering', 'Leerkracht', 'GO Studio Schoolbeheerder',
|
||||
'Medewerker SGR', 'Adjunct-directeur', 'Administratief school', 'Beleidsondersteuner school',
|
||||
]
|
||||
STANDAARD_ROL = 'Leerkracht'
|
||||
|
||||
|
||||
|
||||
def standaard_rol(ambten):
|
||||
"""Functie en rol zolang er in PIVOT niets gekozen is: een directeur blijft Directeur (adjunct-directeur blijft
|
||||
Adjunct-directeur), alle anderen Leerkracht. `ambten` = de ambten uit Informat (lopende opdrachten + hoofdambt)."""
|
||||
tekst = ' '.join(a.lower() for a in ambten if a)
|
||||
if 'adjunct' in tekst and 'directeur' in tekst:
|
||||
return 'Adjunct-directeur'
|
||||
if 'directeur' in tekst:
|
||||
return 'Directeur'
|
||||
return STANDAARD_ROL
|
||||
|
||||
|
||||
# Kolommen van het GO! Pro-importbestand (zelfde namen en volgorde)
|
||||
EXPORT_KOLOMMEN = ['Naam', 'Achternaam', 'AanmeldEmailadres', 'UPN', 'CommunicatiefEmailadres',
|
||||
'OnderwijsinstellingNr', 'Functie', 'rol', 'StartDatum', 'EindDatum', 'ProfessioneelTelefoonNr']
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Hulpfuncties
|
||||
# ---------------------------------------------------------------------
|
||||
def naamdeel(tekst):
|
||||
"""'Van den Broeck' → 'vandenbroeck', 'Zoë' → 'zoe': kleine letters, geen accenten, spaties of leestekens
|
||||
(een koppelteken blijft staan)."""
|
||||
t = unicodedata.normalize('NFKD', str(tekst or '')).encode('ascii', 'ignore').decode()
|
||||
return re.sub(r'[^a-z0-9-]', '', t.lower()).strip('-')
|
||||
|
||||
|
||||
def upn_van(voornaam, achternaam):
|
||||
v, a = naamdeel(voornaam), naamdeel(achternaam)
|
||||
return f"{v}.{a}" if v and a else (v or a)
|
||||
|
||||
|
||||
def _datum(waarde):
|
||||
"""'2026-09-01T00:00:00' → date; leeg of ongeldig → None."""
|
||||
if not waarde:
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(str(waarde)[:10], '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _lijst(data, *sleutels):
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
for s in sleutels:
|
||||
if isinstance(data, dict) and isinstance(data.get(s), list):
|
||||
return data[s]
|
||||
return []
|
||||
|
||||
|
||||
def _telefoon(personeelslid):
|
||||
"""Enkel een professioneel nummer (type 'Werk'): privénummers gaan niet naar GO! Pro."""
|
||||
for c in personeelslid.get('comnrs') or []:
|
||||
if str(c.get('type') or '').strip().lower() == 'werk' and c.get('nr'):
|
||||
return str(c['nr']).strip()
|
||||
return ''
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Keuzes voor functie en rol (schooldatabase)
|
||||
# ---------------------------------------------------------------------
|
||||
VELDEN = ('functie', 'rol', 'email', 'upn')
|
||||
ADRES_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
||||
|
||||
|
||||
def ensure_tables():
|
||||
execute_query("""
|
||||
CREATE TABLE IF NOT EXISTS gopro_keuzes (
|
||||
Person_ID VARCHAR(64) NOT NULL PRIMARY KEY,
|
||||
Functie VARCHAR(100) NULL,
|
||||
Rol VARCHAR(100) NULL,
|
||||
Email VARCHAR(255) NULL,
|
||||
UPN VARCHAR(255) NULL,
|
||||
Bijgewerkt DATETIME NOT NULL,
|
||||
Door VARCHAR(255) NULL
|
||||
)""", commit=True)
|
||||
# Bestaande tabel van vóór de bewerkbare e-mail/UPN
|
||||
kolommen = {r['Field'].lower() for r in execute_query("SHOW COLUMNS FROM gopro_keuzes") or []}
|
||||
for kolom in ('Email', 'UPN'):
|
||||
if kolom.lower() not in kolommen:
|
||||
execute_query(f"ALTER TABLE gopro_keuzes ADD COLUMN {kolom} VARCHAR(255) NULL AFTER Rol", commit=True)
|
||||
|
||||
|
||||
def keuzes():
|
||||
"""{person_id: {'functie': ..., 'rol': ...}} met wat in PIVOT gekozen is (None = niets gekozen).
|
||||
Rollen uit de vroegere tabel gopro_rollen (indien die bestaat) tellen mee als er nog geen nieuwe keuze is."""
|
||||
ensure_tables()
|
||||
uit = {r['Person_ID']: {'functie': r['Functie'], 'rol': r['Rol'], 'email': r['Email'], 'upn': r['UPN']}
|
||||
for r in execute_query("SELECT Person_ID, Functie, Rol, Email, UPN FROM gopro_keuzes") or []}
|
||||
if execute_query("SHOW TABLES LIKE 'gopro_rollen'"):
|
||||
for r in execute_query("SELECT Person_ID, Rol FROM gopro_rollen ORDER BY Bijgewerkt DESC") or []:
|
||||
k = uit.setdefault(r['Person_ID'], {'functie': None, 'rol': None, 'email': None, 'upn': None})
|
||||
k['rol'] = k['rol'] or r['Rol']
|
||||
return uit
|
||||
|
||||
|
||||
def keuze_opslaan(person_id, veld, waarde, door=None):
|
||||
"""Bewaart één keuze. Voor e-mail en UPN: leeg = terug naar het standaardadres (voornaam.achternaam@domein)."""
|
||||
if veld not in VELDEN:
|
||||
raise ValueError(f"Onbekend veld '{veld}'")
|
||||
if not person_id:
|
||||
raise ValueError("Geen personeelslid opgegeven")
|
||||
if veld in ('email', 'upn'):
|
||||
waarde = (waarde or '').strip().lower() or None
|
||||
if waarde and not ADRES_RE.match(waarde):
|
||||
raise ValueError(f"'{waarde}' is geen geldig e-mailadres.")
|
||||
elif waarde not in ROLLEN:
|
||||
raise ValueError(f"Onbekende waarde '{waarde}'")
|
||||
ensure_tables()
|
||||
kolom = {'functie': 'Functie', 'rol': 'Rol', 'email': 'Email', 'upn': 'UPN'}[veld]
|
||||
execute_query(
|
||||
f"INSERT INTO gopro_keuzes (Person_ID, {kolom}, Bijgewerkt, Door) VALUES (%s, %s, NOW(), %s) "
|
||||
f"ON DUPLICATE KEY UPDATE {kolom} = VALUES({kolom}), Bijgewerkt = NOW(), Door = VALUES(Door)",
|
||||
(person_id, waarde, door), commit=True)
|
||||
|
||||
|
||||
def pas_keuzes_toe(rij, gekozen):
|
||||
"""Zet functie en rol van een rij: wat in PIVOT gekozen is, anders de standaard uit Informat."""
|
||||
k = gekozen.get(rij['person_id']) or {}
|
||||
rij['functie'] = k.get('functie') or rij['standaard']
|
||||
rij['rol'] = k.get('rol') or rij['standaard']
|
||||
rij['email'] = k.get('email') or rij['standaard_email']
|
||||
rij['upn'] = k.get('upn') or rij['standaard_upn']
|
||||
rij['email_aangepast'] = bool(k.get('email'))
|
||||
rij['upn_aangepast'] = bool(k.get('upn'))
|
||||
return rij
|
||||
|
||||
|
||||
def markeer_dubbel(rijen):
|
||||
"""Rijen waarvan het e-mailadres ook bij een ander personeelslid staat (bv. zelfde naam) krijgen 'dubbel'."""
|
||||
per_mail = {}
|
||||
for r in rijen:
|
||||
per_mail.setdefault(r['email'], set()).add(r['person_id'])
|
||||
for r in rijen:
|
||||
r['dubbel'] = bool(r['email']) and len(per_mail[r['email']]) > 1
|
||||
return rijen
|
||||
|
||||
|
||||
def google_kleur(rij, google):
|
||||
"""'account' als e-mail of UPN een account is in Google, 'alias' als één ervan een alias is, anders 'geen'.
|
||||
Zelfde regel als de kleuren op de pagina."""
|
||||
soorten = [google.get((rij.get(v) or '').strip().lower()) for v in ('email', 'upn')]
|
||||
return 'account' if 'primair' in soorten else 'alias' if 'alias' in soorten else 'geen'
|
||||
|
||||
|
||||
def google_adressen(vernieuw=False):
|
||||
"""{adres: 'primair' | 'alias'} voor alle gebruikers in Google Workspace van de huidige school, 10 minuten
|
||||
bewaard. Gebruikt de bestaande Directory-koppeling (scope admin.directory.user)."""
|
||||
import time
|
||||
from tenancy import huidige_school
|
||||
school = huidige_school()
|
||||
bewaard = school.cache.get('gopro_google')
|
||||
if bewaard and not vernieuw and time.time() - bewaard[0] < 600:
|
||||
return bewaard[1]
|
||||
from services.google_service import GoogleAdminService
|
||||
users = GoogleAdminService().service.users()
|
||||
adressen, token = {}, None
|
||||
while True:
|
||||
res = users.list(customer='my_customer', maxResults=500, pageToken=token,
|
||||
fields='nextPageToken,users(primaryEmail,aliases)').execute()
|
||||
for u in res.get('users', []):
|
||||
for a in u.get('aliases') or []:
|
||||
adressen.setdefault(str(a).lower(), 'alias')
|
||||
if u.get('primaryEmail'):
|
||||
adressen[str(u['primaryEmail']).lower()] = 'primair'
|
||||
token = res.get('nextPageToken')
|
||||
if not token:
|
||||
break
|
||||
school.cache['gopro_google'] = (time.time(), adressen)
|
||||
return adressen
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Informat
|
||||
# ---------------------------------------------------------------------
|
||||
class GoProService:
|
||||
def __init__(self):
|
||||
self.api = str(Config.INFORMAT_PERSONEEL_API or '').rstrip('/')
|
||||
self.scope_basis = re.sub(r'\.\d+$', '', str(Config.INFORMAT_PERSONEEL_SCOPE or '').strip())
|
||||
self.nummers = [n for n, _ in (Config.INFORMAT_CAMPUSSEN or [])] or [Config.INFORMAT_INST_NUMBER]
|
||||
|
||||
def _token(self, nummer):
|
||||
try:
|
||||
r = requests.post(Config.INFORMAT_AUTH_URL, timeout=15, data={
|
||||
'grant_type': 'client_credentials', 'client_id': Config.INFORMAT_ID,
|
||||
'client_secret': Config.INFORMAT_SECRET, 'scope': f"{self.scope_basis}.{nummer}"})
|
||||
except requests.RequestException as e:
|
||||
raise RuntimeError(f"Informat niet bereikbaar: {e}")
|
||||
try:
|
||||
antwoord = r.json()
|
||||
except ValueError:
|
||||
antwoord = {}
|
||||
if r.status_code == 200 and antwoord.get('access_token'):
|
||||
return antwoord['access_token']
|
||||
# Het echte antwoord van de Informat-aanmeldserver tonen (bv. invalid_scope of invalid_client)
|
||||
reden = ' – '.join(str(antwoord[k]) for k in ('error', 'error_description') if antwoord.get(k)) \
|
||||
or r.text[:200] or f"HTTP {r.status_code}"
|
||||
uitleg = {
|
||||
'invalid_scope': f"de client heeft de scope {self.scope_basis}.{nummer} (nog) niet.",
|
||||
'invalid_client': "de client-ID of het client-secret van deze school klopt niet.",
|
||||
'unauthorized_client': "deze client mag zich niet op deze manier aanmelden.",
|
||||
}.get(antwoord.get('error'), "")
|
||||
raise RuntimeError(
|
||||
f"Aanmelden bij de Informat Personeel-API mislukt voor instellingsnummer {nummer} "
|
||||
f"(scope {self.scope_basis}.{nummer}). Informat antwoordt: {reden}" + (f". Dat betekent: {uitleg}" if uitleg else ""))
|
||||
|
||||
def _get(self, pad, nummer, token, schooljaar):
|
||||
headers = {'Authorization': f'BEARER {token}', 'InstituteNo': nummer, 'Api-Version': '2',
|
||||
'Accept': 'application/json',
|
||||
'Timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z')}
|
||||
r = informat_get(f"{self.api}{pad}?schoolYear={schooljaar}", headers)
|
||||
if r.status_code in (401, 403):
|
||||
raise RuntimeError(f"Informat weigert de toegang tot {pad} (instellingsnummer {nummer}). Heeft de client "
|
||||
f"de scope {self.scope_basis}.{nummer}?")
|
||||
if r.status_code != 200:
|
||||
raise RuntimeError(f"Informat Personeel-API fout {r.status_code} op {pad} "
|
||||
f"(instellingsnummer {nummer}): {r.text[:300]}")
|
||||
return r.json()
|
||||
|
||||
def personeel(self, domein, instellingsnummer_officieel='', schooljaar=None, vandaag=None):
|
||||
"""Lijst van rijen (dicts) voor het scherm en de export, gesorteerd op achternaam en voornaam."""
|
||||
schooljaar = schooljaar or Config.SCHOOLYEAR
|
||||
vandaag = vandaag or date.today()
|
||||
domein = (domein or '').lstrip('@')
|
||||
gekozen = keuzes()
|
||||
rijen, gezien = [], set()
|
||||
|
||||
for nummer in self.nummers:
|
||||
token = self._token(nummer)
|
||||
leden = _lijst(self._get('/employees', nummer, token, schooljaar), 'employees', 'items', 'data')
|
||||
opdrachten = _lijst(self._get('/employees/assignments', nummer, token, schooljaar),
|
||||
'assignments', 'items', 'data')
|
||||
|
||||
# Lopende opdrachten per personeelslid (alle ambten samen): vroegste begin, laatste einde
|
||||
per = {}
|
||||
for o in opdrachten:
|
||||
begin, einde = _datum(o.get('begindatum')), _datum(o.get('einddatum'))
|
||||
if begin and begin > vandaag or einde and einde < vandaag:
|
||||
continue
|
||||
f = per.setdefault(str(o.get('personId')), {'begin': begin, 'einde': einde, 'open': einde is None,
|
||||
'ambten': set()})
|
||||
f['ambten'].add((o.get('ambt') or '').strip())
|
||||
if begin and (not f['begin'] or begin < f['begin']):
|
||||
f['begin'] = begin
|
||||
if einde is None:
|
||||
f['open'] = True
|
||||
elif not f['open'] and (not f['einde'] or einde > f['einde']):
|
||||
f['einde'] = einde
|
||||
|
||||
for p in leden:
|
||||
pid = str(p.get('personId') or p.get('pPersoon') or '')
|
||||
if not pid or p.get('isActive') is False or p.get('isOverleden'):
|
||||
continue
|
||||
voornaam, achternaam = (p.get('voornaam') or '').strip(), (p.get('naam') or '').strip()
|
||||
# Eén rij per persoon, ook als die in meer dan één instellingsnummer voorkomt
|
||||
# (daar kan Informat een ander personId geven: dan herkennen we de persoon aan naam + geboortedatum)
|
||||
geboren = str(p.get('geboortedatum') or '')[:10]
|
||||
persoon = (naamdeel(voornaam), naamdeel(achternaam), geboren) if geboren else ('pid', pid)
|
||||
f = per.get(pid) or {'begin': None, 'einde': None, 'open': True, 'ambten': set()}
|
||||
ambten = f['ambten'] | {((p.get('hoofdAmbt') or {}).get('naam') or '').strip()}
|
||||
if pid in gezien or persoon in gezien:
|
||||
bestaand = next(r for r in rijen if r['person_id'] == pid or r['_persoon'] == persoon)
|
||||
if f['begin'] and (not bestaand['start'] or f['begin'] < bestaand['start']):
|
||||
bestaand['start'] = f['begin']
|
||||
if f['open']:
|
||||
bestaand['einde'] = None
|
||||
elif bestaand['einde'] and f['einde'] and f['einde'] > bestaand['einde']:
|
||||
bestaand['einde'] = f['einde']
|
||||
bestaand['telefoon'] = bestaand['telefoon'] or _telefoon(p)
|
||||
nieuw = standaard_rol(ambten) # bv. directeur in het tweede instellingsnummer
|
||||
if nieuw != STANDAARD_ROL and bestaand['standaard'] == STANDAARD_ROL:
|
||||
bestaand['standaard'] = nieuw
|
||||
pas_keuzes_toe(bestaand, gekozen)
|
||||
continue
|
||||
gezien.update((pid, persoon))
|
||||
upn = upn_van(voornaam, achternaam)
|
||||
rijen.append({
|
||||
'id': pid,
|
||||
'person_id': pid,
|
||||
'_persoon': persoon,
|
||||
'voornaam': voornaam,
|
||||
'achternaam': achternaam,
|
||||
'standaard_email': f"{upn}@{domein}" if upn and domein else '',
|
||||
'standaard_upn': f"{upn}@{domein}" if upn and domein else upn, # UPN = het volledige e-mailadres
|
||||
'instellingsnummer': instellingsnummer_officieel or nummer,
|
||||
'standaard': standaard_rol(ambten),
|
||||
'start': f['begin'],
|
||||
'einde': None if f['open'] else f['einde'],
|
||||
'telefoon': _telefoon(p),
|
||||
})
|
||||
pas_keuzes_toe(rijen[-1], gekozen)
|
||||
|
||||
markeer_dubbel(rijen)
|
||||
rijen.sort(key=lambda r: (naamdeel(r['achternaam']), naamdeel(r['voornaam'])))
|
||||
return rijen
|
||||
|
||||
def tel(self, peildatum):
|
||||
"""Aantal personeelsleden met een lopende opdracht op `peildatum` (één keer per persoon, ook als die in
|
||||
meerdere instellingsnummers werkt). Geeft (totaal, detail) met detail = [{'naam', 'nummer', 'aantal'}]
|
||||
per instellingsnummer als de school er meer dan één heeft, anders None."""
|
||||
from tenancy import _automatisch_schooljaar
|
||||
schooljaar = _automatisch_schooljaar(peildatum)
|
||||
namen = dict(Config.INFORMAT_CAMPUSSEN or [])
|
||||
alle, detail = set(), []
|
||||
for nummer in self.nummers:
|
||||
token = self._token(nummer)
|
||||
leden = _lijst(self._get('/employees', nummer, token, schooljaar), 'employees', 'items', 'data')
|
||||
opdrachten = _lijst(self._get('/employees/assignments', nummer, token, schooljaar),
|
||||
'assignments', 'items', 'data')
|
||||
# personId → persoon (naam + geboortedatum), om dezelfde persoon in een ander nummer te herkennen
|
||||
persoon = {}
|
||||
for p in leden:
|
||||
pid = str(p.get('personId') or p.get('pPersoon') or '')
|
||||
geboren = str(p.get('geboortedatum') or '')[:10]
|
||||
persoon[pid] = ((naamdeel(p.get('voornaam')), naamdeel(p.get('naam')), geboren) if geboren
|
||||
else (nummer, pid))
|
||||
hier = set()
|
||||
for o in opdrachten:
|
||||
begin, einde = _datum(o.get('begindatum')), _datum(o.get('einddatum'))
|
||||
if (begin and begin > peildatum) or (einde and einde < peildatum):
|
||||
continue
|
||||
pid = str(o.get('personId') or '')
|
||||
if pid:
|
||||
hier.add(persoon.get(pid, (nummer, pid)))
|
||||
alle |= hier
|
||||
detail.append({'naam': namen.get(nummer) or nummer, 'nummer': nummer, 'aantal': len(hier)})
|
||||
return len(alle), (detail if len(detail) > 1 else None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Verzameltabel (centrale database): personeel van alle scholen samen, voor één grote GO! Pro-import
|
||||
# ---------------------------------------------------------------------
|
||||
def schooljaar_periode(vandaag=None):
|
||||
"""(1 september, 31 augustus) van het lopende schooljaar: in de verzameltabel heeft iedereen deze periode."""
|
||||
vandaag = vandaag or date.today()
|
||||
jaar = vandaag.year if vandaag.month >= 9 else vandaag.year - 1
|
||||
return date(jaar, 9, 1), date(jaar + 1, 8, 31)
|
||||
|
||||
|
||||
_periode_gezet = {'dag': None}
|
||||
|
||||
|
||||
def verzamel_ensure():
|
||||
from database import central_query
|
||||
central_query("""
|
||||
CREATE TABLE IF NOT EXISTS GoPro_verzameltabel (
|
||||
ID INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
School_code VARCHAR(32) NOT NULL,
|
||||
School_naam VARCHAR(255) NULL,
|
||||
Person_ID VARCHAR(64) NOT NULL,
|
||||
Voornaam VARCHAR(100) NULL,
|
||||
Achternaam VARCHAR(100) NULL,
|
||||
Email VARCHAR(255) NULL,
|
||||
UPN VARCHAR(255) NULL,
|
||||
Instellingsnummer VARCHAR(20) NULL,
|
||||
Functie VARCHAR(100) NULL,
|
||||
Rol VARCHAR(100) NULL,
|
||||
Startdatum DATE NULL,
|
||||
Einddatum DATE NULL,
|
||||
Telefoon VARCHAR(50) NULL,
|
||||
Toegevoegd DATETIME NOT NULL,
|
||||
Door VARCHAR(255) NULL,
|
||||
Soort VARCHAR(10) NOT NULL DEFAULT 'groen',
|
||||
Verzonden DATETIME NULL,
|
||||
Verzonden_door VARCHAR(255) NULL,
|
||||
UNIQUE KEY uq_gopro_verz (School_code, Person_ID),
|
||||
KEY idx_gopro_verz_email (Email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""", commit=True)
|
||||
# Soort: 'groen' = gaat mee in de GO! Pro-CSV, 'genegeerd' = rode lijst (nooit meer tonen, niet exporteren),
|
||||
# 'verzonden' = al naar GO! Pro doorgestuurd (niet meer in de verzameltabel, nooit opnieuw toevoegen)
|
||||
kolommen = {r['Field'].lower() for r in central_query("SHOW COLUMNS FROM GoPro_verzameltabel") or []}
|
||||
if 'soort' not in kolommen:
|
||||
central_query("ALTER TABLE GoPro_verzameltabel ADD COLUMN Soort VARCHAR(10) NOT NULL DEFAULT 'groen'", commit=True)
|
||||
if 'verzonden' not in kolommen:
|
||||
central_query("ALTER TABLE GoPro_verzameltabel ADD COLUMN Verzonden DATETIME NULL, "
|
||||
"ADD COLUMN Verzonden_door VARCHAR(255) NULL", commit=True)
|
||||
# Iedereen in de verzameltabel: periode = het lopende schooljaar (1 keer per dag nagekeken)
|
||||
if _periode_gezet['dag'] != date.today():
|
||||
start, einde = schooljaar_periode()
|
||||
central_query("UPDATE GoPro_verzameltabel SET Startdatum = %s, Einddatum = %s WHERE Soort = 'groen' "
|
||||
"AND (Startdatum IS NULL OR Einddatum IS NULL OR Startdatum <> %s OR Einddatum <> %s)",
|
||||
(start, einde, start, einde), commit=True)
|
||||
_periode_gezet['dag'] = date.today()
|
||||
|
||||
|
||||
def verzameld_sleutels():
|
||||
"""({(school_code, person_id): soort}, {e-mailadres: soort}) van alles in de verzameltabel (groen én genegeerd)."""
|
||||
from database import central_query
|
||||
verzamel_ensure()
|
||||
rijen = central_query("SELECT School_code, Person_ID, Email, Soort FROM GoPro_verzameltabel") or []
|
||||
return ({(r['School_code'], str(r['Person_ID'])): r['Soort'] for r in rijen},
|
||||
{(r['Email'] or '').strip().lower(): r['Soort'] for r in rijen if r['Email']})
|
||||
|
||||
|
||||
def is_verzameld(rij, school_code, sleutels):
|
||||
"""In welke lijst staat dit personeelslid al ('groen' / 'genegeerd'), of None.
|
||||
Herkend aan school + personeelslid, of aan het e-mailadres."""
|
||||
paren, mails = sleutels
|
||||
return paren.get((school_code, str(rij['person_id']))) or mails.get((rij.get('email') or '').strip().lower())
|
||||
|
||||
|
||||
def verzamel_toevoegen(rijen, school, door=None, soort='groen'):
|
||||
"""Voegt de rijen toe aan de verzameltabel (soort 'groen') of aan de rode lijst ('genegeerd').
|
||||
Geeft (toegevoegd, overgeslagen)."""
|
||||
from database import central_query
|
||||
sleutels = verzameld_sleutels()
|
||||
periode = schooljaar_periode() # iedereen: 1 september – 31 augustus van het lopende schooljaar
|
||||
toegevoegd = overgeslagen = 0
|
||||
for r in rijen:
|
||||
if is_verzameld(r, school.code, sleutels):
|
||||
overgeslagen += 1
|
||||
continue
|
||||
central_query(
|
||||
"INSERT INTO GoPro_verzameltabel (School_code, School_naam, Person_ID, Voornaam, Achternaam, Email, UPN, "
|
||||
"Instellingsnummer, Functie, Rol, Startdatum, Einddatum, Telefoon, Toegevoegd, Door, Soort) "
|
||||
"VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, NOW(), %s, %s)",
|
||||
(school.code, school.naam, str(r['person_id']), r['voornaam'], r['achternaam'], r['email'], r['upn'],
|
||||
r['instellingsnummer'], r['functie'], r['rol'], periode[0], periode[1], r['telefoon'], door, soort),
|
||||
commit=True)
|
||||
sleutels[0][(school.code, str(r['person_id']))] = soort
|
||||
if r.get('email'):
|
||||
sleutels[1][r['email'].strip().lower()] = soort
|
||||
toegevoegd += 1
|
||||
return toegevoegd, overgeslagen
|
||||
|
||||
|
||||
def verzameld_alle(school_code=None, soort='groen'):
|
||||
from database import central_query
|
||||
verzamel_ensure()
|
||||
if school_code:
|
||||
return central_query("SELECT * FROM GoPro_verzameltabel WHERE School_code = %s AND Soort = %s "
|
||||
"ORDER BY Achternaam, Voornaam", (school_code, soort)) or []
|
||||
return central_query("SELECT * FROM GoPro_verzameltabel WHERE Soort = %s "
|
||||
"ORDER BY School_naam, Achternaam, Voornaam", (soort,)) or []
|
||||
|
||||
|
||||
def markeer_verzonden(door=None, school_code=None):
|
||||
"""Alle personeelsleden in de verzameltabel (of enkel die van één school) als verzonden markeren.
|
||||
Ze verdwijnen uit de verzameltabel en worden nooit opnieuw toegevoegd. Geeft het aantal."""
|
||||
from database import central_query
|
||||
verzamel_ensure()
|
||||
n = len(verzameld_alle(school_code))
|
||||
if school_code:
|
||||
central_query("UPDATE GoPro_verzameltabel SET Soort = 'verzonden', Verzonden = NOW(), Verzonden_door = %s "
|
||||
"WHERE Soort = 'groen' AND School_code = %s", (door, school_code), commit=True)
|
||||
else:
|
||||
central_query("UPDATE GoPro_verzameltabel SET Soort = 'verzonden', Verzonden = NOW(), Verzonden_door = %s "
|
||||
"WHERE Soort = 'groen'", (door,), commit=True)
|
||||
return n
|
||||
|
||||
|
||||
def aantal_verzonden(school_code):
|
||||
from database import central_query
|
||||
verzamel_ensure()
|
||||
r = central_query("SELECT COUNT(*) AS n FROM GoPro_verzameltabel WHERE School_code = %s AND Soort = 'verzonden'",
|
||||
(school_code,)) or [{}]
|
||||
return int(r[0].get('n') or 0)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Nachtelijke verzameling: elke nacht van alle scholen de groene personeelsleden naar de verzameltabel
|
||||
# ---------------------------------------------------------------------
|
||||
NACHT_SLEUTEL = 'gopro_nachtelijk' # '1' = aan
|
||||
NACHT_DAG_SLEUTEL = 'gopro_nachtelijk_dag' # laatste datum waarop het liep (zodat het één keer per nacht loopt)
|
||||
NACHT_RESULTAAT_SLEUTEL = 'gopro_nachtelijk_resultaat'
|
||||
NACHT_UUR_VAN, NACHT_UUR_TOT = 2, 6 # tussen 02:00 en 06:00
|
||||
NACHT_DOOR = 'Automatisch (nacht)'
|
||||
_nacht = {'bezig': False}
|
||||
|
||||
|
||||
def _instelling_ensure():
|
||||
from services import ipblokkade
|
||||
ipblokkade.ensure_table() # maakt PIVOT_instellingen aan als die nog niet bestaat
|
||||
|
||||
|
||||
def nacht_instelling():
|
||||
"""{'aan': bool, 'door', 'bijgewerkt', 'resultaat', 'resultaat_op'}"""
|
||||
from database import central_query
|
||||
_instelling_ensure()
|
||||
rijen = {r['Sleutel']: r for r in central_query(
|
||||
"SELECT Sleutel, Waarde, Bijgewerkt, Door FROM PIVOT_instellingen WHERE Sleutel IN (%s, %s)",
|
||||
(NACHT_SLEUTEL, NACHT_RESULTAAT_SLEUTEL)) or []}
|
||||
aan, res = rijen.get(NACHT_SLEUTEL) or {}, rijen.get(NACHT_RESULTAAT_SLEUTEL) or {}
|
||||
return {'aan': aan.get('Waarde') == '1', 'door': aan.get('Door'), 'bijgewerkt': aan.get('Bijgewerkt'),
|
||||
'resultaat': res.get('Waarde'), 'resultaat_op': res.get('Bijgewerkt'), 'bezig': _nacht['bezig']}
|
||||
|
||||
|
||||
def zet_nacht(aan, door=None):
|
||||
from database import central_query
|
||||
_instelling_ensure()
|
||||
central_query("INSERT INTO PIVOT_instellingen (Sleutel, Waarde, Bijgewerkt, Door) VALUES (%s, %s, NOW(), %s) "
|
||||
"ON DUPLICATE KEY UPDATE Waarde = VALUES(Waarde), Bijgewerkt = NOW(), Door = VALUES(Door)",
|
||||
(NACHT_SLEUTEL, '1' if aan else '0', door), commit=True)
|
||||
|
||||
|
||||
def verzamel_groene_van_school(school, door=NACHT_DOOR):
|
||||
"""Haalt het personeel van `school` op (Informat), kijkt na wie een account heeft in Google en zet de groene
|
||||
die nog niet in de verzameltabel of op de rode lijst staan in de verzameltabel. Geeft het aantal toegevoegd."""
|
||||
from tenancy import in_school
|
||||
with in_school(school):
|
||||
rijen = GoProService().personeel(school.domein, Config.INSTELLINGSNUMMER)
|
||||
school.cache.pop('gopro', None) # de GO! Pro-pagina haalt daarna vers op
|
||||
google = google_adressen(vernieuw=True)
|
||||
sleutels = verzameld_sleutels()
|
||||
groen = [r for r in rijen if not is_verzameld(r, school.code, sleutels) and google_kleur(r, google) == 'account']
|
||||
toegevoegd, _ = verzamel_toevoegen(groen, school, door)
|
||||
return toegevoegd
|
||||
|
||||
|
||||
def verzamel_alle_scholen(door=NACHT_DOOR):
|
||||
"""Voor elke actieve school met Informat en een e-maildomein. Geeft een korte samenvatting."""
|
||||
from database import central_query
|
||||
from tenancy import alle_scholen
|
||||
from rechten import log
|
||||
_nacht['bezig'] = True
|
||||
try:
|
||||
totaal, scholen, fouten, details = 0, 0, [], []
|
||||
for school in alle_scholen():
|
||||
if not school.heeft('INFORMAT_INST_NUMBER', 'INFORMAT_ID', 'INFORMAT_SECRET') or not school.domein:
|
||||
continue
|
||||
try:
|
||||
n = verzamel_groene_van_school(school, door)
|
||||
totaal += n
|
||||
scholen += 1
|
||||
details.append(f"{school.naam}: {n}")
|
||||
except Exception as e:
|
||||
fouten.append(f"{school.naam}: {str(e)[:120]}")
|
||||
import sys
|
||||
sys.stderr.write(f"[{school.code}] GO! Pro nachtelijk verzamelen: {e}\n")
|
||||
samenvatting = f"{totaal} toegevoegd uit {scholen} school/scholen"
|
||||
if fouten:
|
||||
samenvatting += f"; {len(fouten)} mislukt ({' | '.join(fouten)})"
|
||||
try:
|
||||
log(f"GO! Pro automatisch verzamelen: {samenvatting}. " + '; '.join(details), url='(nachtelijke taak)')
|
||||
except Exception:
|
||||
pass
|
||||
_instelling_ensure()
|
||||
central_query("INSERT INTO PIVOT_instellingen (Sleutel, Waarde, Bijgewerkt, Door) VALUES (%s, %s, NOW(), %s) "
|
||||
"ON DUPLICATE KEY UPDATE Waarde = VALUES(Waarde), Bijgewerkt = NOW(), Door = VALUES(Door)",
|
||||
(NACHT_RESULTAAT_SLEUTEL, samenvatting[:255], door), commit=True)
|
||||
return samenvatting
|
||||
finally:
|
||||
_nacht['bezig'] = False
|
||||
|
||||
|
||||
def _nacht_claim(vandaag):
|
||||
"""True als dit proces de run van vandaag mag doen (ook als PIVOT in meerdere processen draait)."""
|
||||
from database import central_query
|
||||
central_query("INSERT IGNORE INTO PIVOT_instellingen (Sleutel, Waarde, Bijgewerkt) VALUES (%s, '', NOW())",
|
||||
(NACHT_DAG_SLEUTEL,), commit=True)
|
||||
return central_query("UPDATE PIVOT_instellingen SET Waarde = %s, Bijgewerkt = NOW() "
|
||||
"WHERE Sleutel = %s AND (Waarde IS NULL OR Waarde <> %s)",
|
||||
(vandaag, NACHT_DAG_SLEUTEL, vandaag), commit=True) == 1
|
||||
|
||||
|
||||
_nacht_thread = None
|
||||
|
||||
|
||||
def start_scheduler():
|
||||
"""Achtergrondtaak: elk kwartier kijken of het nacht is en de schakelaar aan staat."""
|
||||
import threading, time, sys
|
||||
global _nacht_thread
|
||||
if _nacht_thread:
|
||||
return
|
||||
|
||||
def loop():
|
||||
time.sleep(60)
|
||||
while True:
|
||||
try:
|
||||
nu = datetime.now()
|
||||
if NACHT_UUR_VAN <= nu.hour < NACHT_UUR_TOT and nacht_instelling()['aan'] \
|
||||
and _nacht_claim(nu.strftime('%Y-%m-%d')):
|
||||
verzamel_alle_scholen()
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"GO! Pro nachtelijke taak: {e}\n")
|
||||
time.sleep(15 * 60)
|
||||
|
||||
_nacht_thread = threading.Thread(target=loop, daemon=True)
|
||||
_nacht_thread.start()
|
||||
|
||||
|
||||
def verzamel_alle_scholen_op_achtergrond(door):
|
||||
import threading
|
||||
if _nacht['bezig']:
|
||||
return False
|
||||
_nacht['bezig'] = True
|
||||
threading.Thread(target=lambda: verzamel_alle_scholen(door), daemon=True).start()
|
||||
return True
|
||||
Reference in new issue
Block a user