Files
Pivot/services/gopro_service.py
2026-10-14 13:34:37 +02:00

642 lines
31 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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}
_tabel_klaar = {'ok': False} # tabel/kolommen/index maar één keer per proces nakijken
def verzamel_ensure():
from database import central_query
if _tabel_klaar['ok']:
_periode_bijwerken()
return
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),
KEY idx_gopro_verz_soort (Soort, School_naam)
) 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)
indexen = {r['Key_name'] for r in central_query("SHOW INDEX FROM GoPro_verzameltabel") or []}
if 'idx_gopro_verz_soort' not in indexen:
central_query("ALTER TABLE GoPro_verzameltabel ADD KEY idx_gopro_verz_soort (Soort, School_naam)", commit=True)
_tabel_klaar['ok'] = True
_periode_bijwerken()
def _periode_bijwerken():
from database import central_query
# 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 aantallen():
"""{'groen': n, 'genegeerd': n, 'verzonden': n} in één query (in plaats van alle rijen op te halen)."""
from database import central_query
verzamel_ensure()
return {r['Soort']: int(r['n']) for r in
central_query("SELECT Soort, COUNT(*) AS n FROM GoPro_verzameltabel GROUP BY 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