""" 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