# PIVOT 1.1.0 (meerdere scholen) – overdracht Dit document vat samen hoe PIVOT in elkaar zit, welke afspraken er in de code gelden en wat er recent bijkwam. Het is bedoeld voor wie verder werkt aan PIVOT (zelf, of samen met een AI-assistent: geef dan dit bestand en de zip van het project mee). Lees ook **MEERDERE_SCHOLEN.md** (installatie, rollen) en **DATABASE.md** (tabellen). > ⚠️ `config.ini` en de map `geheimen/` bevatten echte wachtwoorden en sleutels. Deel ze niet en zet ze nooit in > een chat, ticket of e-mail. Geef een collega een kopie van het project **zonder** die bestanden en laat hem/haar > een eigen `config.ini` invullen. --- ## 1. Architectuur in het kort - **Flask-app** (`app.py`) met ±40 blueprints (`blueprints/`) en de logica in `services/`; HTML in `templates/` (Jinja, `base.html` + `static/global.css`). - **Multi-tenant** (`tenancy.py`): één centrale database (`Scholen`, `Gebruikers`, rechten, logs, ...) en één database per school (toestellen, leerlingen, ...). - `huidige_school()` = de school van het verzoek; `in_school(school)` / `met_school(fn)` voor achtergrondthreads. - Instellingen per school = `Veld(...)` in `tenancy.VELDEN` (verschijnen vanzelf in 🔌 Koppelingen); lezen met `school.get('SLEUTEL')` of `Config.SLEUTEL` (metaclass leest de huidige school). - `school.cache` = per-school geheugen (wordt leeggemaakt bij wijzigingen aan de school). - `school_singleton(cls, init)` = één object per school (gebruikt voor achtergrondtaken met `status`-dict). - `google_credentials(scopes, subject)` = serviceaccount van de school, gedelegeerd naar `subject`. - **Database** (`database.py`): `execute_query` (schooldatabase), `central_query` (centraal); `commit=True` geeft het aantal rijen terug. Pools per database (`config.ini › [database] › pool_size`, standaard 3). - **Rechten** (`rechten.py`): `ONDERDELEN` = lijst (code, naam, categorie); routes met `@recht_vereist('code')`, templates met `{% if mag('code') %}`. Een nieuw onderdeel verschijnt vanzelf in 🔐 Gebruikers & rechten. `log(...)` schrijft in het logboek. - **Achtergrondtaken**: threads gestart in `app.py` (enkel in het echte proces, niet in de reloader): afwezigheden, 2FA, Skore, GO! Pro (nachtelijk verzamelen), Gmail-handtekeningen (nachtelijk). Taken die maar één keer per dag mogen lopen, "claimen" de dag met een `UPDATE ... WHERE` (werkt ook met meerdere processen). - **Opstarten**: `tenancy.ensure_schema()` (centrale tabellen) en `services/tabelnamen.py` (zie §5). ## 2. Afspraken in de code - Alles in het **Nederlands** (namen, meldingen, commentaar). - Tabellen die een module zelf nodig heeft: `CREATE TABLE IF NOT EXISTS` in een `ensure_tables()`/`*_ensure()` in de service, én dezelfde DDL in `sql/school.sql` of `sql/centraal.sql` + `sql/installatie_pivot_26-27.sql`, én een regel in `DATABASE.md`. Kolommen die later bijkomen: `SHOW COLUMNS` + `ALTER TABLE` in de ensure-functie. - Tabelnamen met hoofdletters moeten exact kloppen (Linux!); zie §5. - **Templates**: - `{% block main_class %}wide{% endblock %}` voor brede tabellen; tabellen met zoeken/sorteren/paginering in de browser (helpers `perPagina(sleutel, standaard)` en `perPaginaKiezer(...)` uit `base.html`). - In HTML-attributen (`onsubmit="..."`, `onchange`, `data-...`) altijd `|tojson|forceescape`, anders breekt het attribuut op de aanhalingstekens (bevestigingsvragen werkten daardoor niet meer). - Elementen die met het `hidden`-attribuut verborgen worden: zet `[hidden] { display: none !important; }` in de pagina-CSS (knopstijlen overschrijven het anders). - **CSV-exports**: `;` als scheidingsteken en UTF-8 met BOM (Excel). **Excel**: openpyxl. - **Google-fouten** vertalen naar begrijpelijke meldingen (`vriendelijk(e)` in de services) mét de scope die ontbreekt. ## 3. Nieuw onderdeel toevoegen (checklist) 1. `services/_service.py` (logica, tabellen) en `blueprints/_bp.py` (routes). 2. Blueprint registreren in `app.py`. 3. Recht in `rechten.ONDERDELEN` (code, naam, categorie). 4. Link in het juiste blok van `templates/dashboard.html` (`{% if mag('code') %}`; voeg de code ook toe aan de `mag(...)`-voorwaarde van het blok). 5. Nieuwe Google-scope? Toevoegen aan de lijst + tabel in `services/handleidingen_standaard.py` (de handleiding in de database wordt **niet** automatisch bijgewerkt: zelf aanpassen in 📘 Handleidingen). 6. SQL-bestanden en `DATABASE.md` bijwerken. ## 4. Overzicht van recente onderdelen | Onderdeel | Bestanden | Opmerkingen | |---|---|---| | IP-blokkades / whitelist | `ipblokkade_bp.py`, `services/ipblokkade.py` | gewone gebruikers met recht `ip_whitelist` zien/beheren enkel hun eigen school | | Aanmelden | `blueprints/auth.py` | geen schoolkeuze meer: laatste school → domein → cookie → laagste instellingsnummer; domeinen mogen gedeeld worden door meerdere scholen | | GO! Pro + verzameltabel | `gopro_bp.py`, `gopro_service.py`, `gopro*.html` | verzameltabel (centraal), rode lijst, verzonden, nachtelijk verzamelen; periode = lopend schooljaar (1/9–31/8) | | Leerlingfacturatie | `facturatie_bp.py`, `facturatie_service.py` | Informat Invoice API (GraphQL); scope per toegangsgroep (`_`), wordt zelf gezocht | | Internaat | `internaat_bp.py`, `internaat_service.py`, `aanwezigheid_service.py` | Internaat WEB API; aanwezigheden per week; Smartschool-check "voornaam achternaam" | | Gmail | `gmail_delegatie_bp.py`, `gmail_labels_bp.py`, `handtekeningen_bp.py`, `contacten_bp.py`, `verzendlijsten_bp.py` | delegaties (+ overzicht), mails verplaatsen, handtekeningen (functie/telefoon uit Google, nachtelijk), contactlijsten in mailboxen + zichtbaarheid adresboek | | Google | `google_licenties_bp.py`, `gedeelde_drives_bp.py` | licenties (customerId: domein of klant-ID), gedeelde drives: leden/rechten, "waar is iemand lid van", gebruiker vervangen (zelfde rol) | | Smartschool | `sgr_groepen_bp.py`, `sgr_groepen_service.py` | subgroepen + leden van een hoofdgroep (standaard "groepen GO-Antwerpen") | | Basisonderwijs | `basisonderwijs_bp.py`, `bingel_service.py` | blok enkel bij Onderwijsniveau = basis; Bingel-Excel (klas uit Informat-inschrijvingen, klasnummer alfabetisch) | | Toestellen | `devices.py`, `inventaris.html`, `toestellen.html` | eigendom: alle leerlingen + filter met/zonder toestel, historie per leerling en per toestel, knoppen uitdelen/inleveren; Alle toestellen: blokkeren/deblokkeren in Google (kolom `Toestellen.Geblokkeerd`) | | Dashboard | `dashboard.html` | blokken Gmail, Internaat, Basisonderwijs, Leerlingfacturatie, Links (website + Smartschool) | Benodigde Google-scopes (domeinbrede delegatie) staan in 📘 Handleidingen; recent bijgekomen: `gmail.modify`, `gmail.settings.basic`, `contacts`, `drive`. De bijbehorende API's (Gmail, People, Drive, Enterprise License Manager) moeten aanstaan in het Cloud-project van het serviceaccount. ## 5. Omgeving en installatie-aandachtspunten - **Windows → Linux**: MySQL op Windows bewaart tabelnamen in kleine letters; op Linux (`lower_case_table_names = 0`) herstelt `services/tabelnamen.py` dat bij het opstarten en bij 🩺 Testen (enkel voor de tabellen die PIVOT kent; zijn beide versies gevuld, dan enkel een melding in het logboek). - **"1040 Too many connections"**: (aantal scholen + 1) × `pool_size` per PIVOT-proces moet onder `max_connections` van MySQL blijven. Oplossing: `SET PERSIST max_connections = 500;` of `pool_size` verlagen. - **Productie**: liefst nginx (HTTPS, Let's Encrypt) + gunicorn/waitress i.p.v. de Flask-ontwikkelserver, en de poort van PIVOT enkel intern openzetten. Het logo van Gmail-handtekeningen (`/handtekening-logo/...`) moet wel publiek bereikbaar zijn (de IP-blokkering slaat die route over). ## 6. Nog niet getest tegen de echte omgeving / openstaand Onderstaande onderdelen werden enkel getest met nagebootste antwoorden; bij het eerste echte gebruik goed nakijken: - **Bingel**: het veld met de klas in de Informat-inschrijvingen (`inschrKlassen`); de pagina toont de veldnamen die Informat teruggeeft als iets leeg blijft. - **SGR groepen**: formaat van `getAllGroupsAndClasses` (XML/base64) en de leden per groep. - **Contactlijsten** (People API), **gedeelde drives** (Drive API), **handtekeningen** (Gmail sendAs). - **Licenties**: terugval op de klant-ID als het schooldomein geen primair domein is. - **Leerlingfacturatie**: automatisch zoeken van de scope (identityserver). - Informat-fout "SqlDateTime overflow" bij één school = foute datum in Informat zelf (niet in PIVOT). ## 7. Verder werken - Elke wijziging werd telkens als volledige zip van `Pivot_1.1.0` aangeleverd (zonder `__pycache__`). - Bij een nieuwe vraag aan een AI-assistent: geef dit bestand, `DATABASE.md`, `MEERDERE_SCHOLEN.md` en de betrokken bestanden mee (of de hele zip zonder `config.ini`/`geheimen/`), en beschrijf wat je wil met een schermafbeelding erbij.