PIVOT 1.1.0

This commit is contained in:
marijn committed 2026-10-14 13:34:37 +02:00
commit 8a53234eb6
161 files changed
+31109

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
"""
Overzicht van alle lopende e-mailmachtigingen (Gmail-delegaties) van de school.
Google heeft geen lijst "alle delegaties van het domein": je kan ze enkel per mailbox opvragen
(users.settings.delegates.list, in naam van de eigenaar). Daarom loopt er een achtergrondtaak die
alle actieve (niet-geschorste) gebruikers afgaat, met een paar parallelle verzoeken.
Het resultaat komt in de tabel gmail_delegaties en wordt:
- volledig vernieuwd met de knop op de pagina, of vanzelf als het ouder is dan VERVERS_UREN;
- meteen bijgewerkt als in PIVOT een machtiging toegevoegd of ingetrokken wordt, of als je een mailbox opent.
"""
import sys
import threading
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime
from database import execute_query
from tenancy import school_singleton, met_school, huidige_school, in_school
VERVERS_UREN = 24
PARALLEL = 8
class DelegatieOverzicht:
def __new__(cls):
def init(inst):
inst._lock = threading.Lock()
inst.status = {'bezig': False, 'voortgang': 0, 'totaal': 0, 'fout': None, 'resultaat': None}
return school_singleton(cls, init)
@staticmethod
def ensure_table():
execute_query("""
CREATE TABLE IF NOT EXISTS gmail_delegaties (
Mailbox VARCHAR(255) NOT NULL,
Mailbox_naam VARCHAR(255) NULL,
Gedelegeerde VARCHAR(255) NOT NULL,
Gedelegeerde_naam VARCHAR(255) NULL,
Status VARCHAR(20) NULL,
Opgehaald DATETIME NOT NULL,
PRIMARY KEY (Mailbox, Gedelegeerde),
KEY idx_gmail_deleg_gedelegeerde (Gedelegeerde)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci""", commit=True)
execute_query("CREATE TABLE IF NOT EXISTS gmail_delegaties_scan (ID TINYINT NOT NULL PRIMARY KEY, "
"Opgehaald DATETIME NULL, Mailboxen INT NULL, Fouten INT NULL) ENGINE=InnoDB", commit=True)
# ---------------- opvragen ----------------
def alle(self):
return execute_query("SELECT * FROM gmail_delegaties ORDER BY Mailbox, Gedelegeerde") or []
def laatst_opgehaald(self):
rij = execute_query("SELECT Opgehaald, Mailboxen, Fouten FROM gmail_delegaties_scan WHERE ID = 1")
return rij[0] if rij else None
def verouderd(self):
laatst = self.laatst_opgehaald()
return not laatst or not laatst['Opgehaald'] or \
(datetime.now() - laatst['Opgehaald']).total_seconds() > VERVERS_UREN * 3600
# ---------------- één mailbox bijwerken (na toevoegen/intrekken/openen in PIVOT) ----------------
def bewaar_mailbox(self, mailbox, naam, delegates, namen=None):
"""Vervangt de rijen van één mailbox door de opgegeven gedelegeerden [{'email','status'}]."""
try:
self.ensure_table()
# Namen die we al kennen (uit de volledige ophaling) behouden
bekend = {}
for r in execute_query("SELECT Mailbox, Mailbox_naam, Gedelegeerde, Gedelegeerde_naam FROM gmail_delegaties") or []:
if r['Mailbox_naam']:
bekend[r['Mailbox'].lower()] = r['Mailbox_naam']
if r['Gedelegeerde_naam']:
bekend[r['Gedelegeerde'].lower()] = r['Gedelegeerde_naam']
namen = {**bekend, **(namen or {})}
execute_query("DELETE FROM gmail_delegaties WHERE Mailbox = %s", (mailbox,), commit=True)
for d in delegates:
execute_query("INSERT INTO gmail_delegaties (Mailbox, Mailbox_naam, Gedelegeerde, Gedelegeerde_naam, "
"Status, Opgehaald) VALUES (%s, %s, %s, %s, %s, NOW())",
(mailbox, naam, d['email'], namen.get(d['email'].lower()) or d.get('naam'),
d.get('status')), commit=True)
except Exception as e:
sys.stderr.write(f"Delegaties: mailbox {mailbox} niet bewaard: {e}\n")
# ---------------- alles ophalen uit Google ----------------
def _gebruikers(self, directory):
uit, token = [], None
while True:
resp = directory.users().list(customer='my_customer', maxResults=500, pageToken=token,
fields='nextPageToken,users(primaryEmail,name/fullName,suspended)').execute()
for u in resp.get('users', []):
uit.append({'email': u['primaryEmail'].lower(), 'naam': (u.get('name') or {}).get('fullName', ''),
'geschorst': bool(u.get('suspended'))})
token = resp.get('nextPageToken')
if not token:
return uit
def sync(self):
if not self._lock.acquire(blocking=False):
return False, "Er loopt al een ophaling."
try:
from services.gmail_delegation_service import GmailDelegationService, DelegationError
self.status.update(bezig=True, voortgang=0, totaal=0, fout=None, resultaat=None)
self.ensure_table()
svc = GmailDelegationService()
gebruikers = self._gebruikers(svc._directory_api())
namen = {u['email']: u['naam'] for u in gebruikers}
actief = [u for u in gebruikers if not u['geschorst']]
self.status['totaal'] = len(actief)
school = huidige_school()
fouten, rechtenfout, gelukt = [], [], []
def een(u):
# Krijgt Google telkens dezelfde rechtenfout, dan niet alle gebruikers blijven proberen
if len(rechtenfout) >= 3 and not gelukt:
self.status['voortgang'] += 1
return u, None
with in_school(school):
try:
delegates = svc.list_delegates(u['email'])
gelukt.append(1)
return u, delegates
except DelegationError as e:
tekst = str(e)
if 'serviceaccount mist een Gmail-recht' in tekst or 'Gmail API staat nog niet aan' in tekst:
rechtenfout.append(tekst)
fouten.append(u['email'])
return u, None
except Exception:
fouten.append(u['email'])
return u, None
finally:
self.status['voortgang'] += 1
resultaten = []
with ThreadPoolExecutor(max_workers=PARALLEL) as pool:
for u, delegates in pool.map(een, actief):
if delegates:
resultaten.append((u, delegates))
if rechtenfout and not gelukt:
raise RuntimeError(rechtenfout[0])
nu = datetime.now()
execute_query("DELETE FROM gmail_delegaties", commit=True)
aantal = 0
for u, delegates in resultaten:
for d in delegates:
execute_query("INSERT INTO gmail_delegaties (Mailbox, Mailbox_naam, Gedelegeerde, Gedelegeerde_naam, "
"Status, Opgehaald) VALUES (%s, %s, %s, %s, %s, %s)",
(u['email'], u['naam'], d['email'], namen.get(d['email'].lower()), d['status'], nu),
commit=True)
aantal += 1
execute_query("INSERT INTO gmail_delegaties_scan (ID, Opgehaald, Mailboxen, Fouten) VALUES (1, %s, %s, %s) "
"ON DUPLICATE KEY UPDATE Opgehaald = VALUES(Opgehaald), Mailboxen = VALUES(Mailboxen), "
"Fouten = VALUES(Fouten)", (nu, len(actief), len(fouten)), commit=True)
msg = f"{len(actief)} mailboxen nagekeken: {aantal} machtiging(en) bij {len(resultaten)} mailbox(en)."
if fouten:
msg += f" {len(fouten)} mailbox(en) konden niet bevraagd worden (bv. zonder Gmail-licentie)."
self.status['resultaat'] = msg
return True, msg
except Exception as e:
self.status['fout'] = str(e)
return False, f"Ophalen mislukt: {e}"
finally:
self.status['bezig'] = False
self._lock.release()
def sync_op_achtergrond(self):
if self.status['bezig']:
return False
self.status.update(bezig=True, voortgang=0, totaal=0, fout=None)
threading.Thread(target=met_school(self.sync), daemon=True).start()
return True