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

+292
View File
@@ -0,0 +1,292 @@
"""
Beheer van PIVOT vanaf de opdrachtregel.
python beheer.py init
Maakt de tabellen van de centrale database aan (Scholen, Gebruikers, rechten, Logs, Bugs).
python beheer.py nieuwe-schooldatabase DATABASE
Maakt een lege schooldatabase met de standaardstructuur (sql/school.sql).
python beheer.py gebruiker NAAM [--wachtwoord W] [--email E] [--admin]
Maakt een PIVOT-gebruiker aan, of zet een nieuw wachtwoord / maakt hem administrator.
python beheer.py rechten NAAM SCHOOLCODE [onderdeel ...|alles|geen]
Toont of zet de rechten van een gebruiker in een school.
python beheer.py importeer-config OUDE_CONFIG.ini [--google-sleutel sleutel.json] [--code kamxm] [--db Pivot_26-27]
Zet een school uit een config.ini van PIVOT 1.0.x in het scholenregister
(alle wachtwoorden en koppelingen worden versleuteld overgenomen).
python beheer.py lijst
Toont alle scholen.
python beheer.py kopieer-school BRONCODE NIEUWE_DATABASE
Maakt een nieuwe database als volledige kopie (tabellen + gegevens) van de database van BRONCODE.
Registreer ze daarna als school via PIVOT (Nieuwe school › Database aanmaken? › Nee, bestaat al).
python beheer.py nieuwe-sleutel
Toont een nieuwe versleutelingssleutel (voor PIVOT_SLEUTEL).
"""
import argparse
import configparser
import json
import os
import sys
from cryptography.fernet import Fernet
# Oude config.ini → velden in het scholenregister
MAPPING = {
('schoolgegevens', 'admin_email'): 'ADMIN_EMAIL',
('schooljaar', 'huidig_schooljaar'): 'SCHOOLYEAR',
('google', 'ou_nieuwe_lln'): 'STUDENT_OU',
('google', 'google_sso_clientid'): 'GOOGLE_SSO_CLIENTID',
('google', 'google_sso_clientsecret'): 'GOOGLE_SSO_CLIENTSECRET',
('google', 'google_smtp_server'): 'GOOGLE_SMTP_SERVER',
('google', 'google_smtp_port'): 'GOOGLE_SMTP_PORT',
('google', 'google_sender_adres'): 'GOOGLE_SMTP_SENDER_adres',
('google', 'google_email_passsword'): 'GOOGLE_SMTP_EMAIL_PASSWORD',
('google', 'google_api_scopes'): 'GOOGLE_API_SCOPES_RAW',
('google', 'tweestaps_uitzondering_groep'): 'TWEESTAPS_UITZONDERING_GROEP',
('google', 'tweestaps_ous'): 'TWEESTAPS_OUS',
('google', 'tweestaps_groepen'): 'TWEESTAPS_GROEPEN_RAW',
('informat', 'client_id'): 'INFORMAT_ID',
('informat', 'client_secret'): 'INFORMAT_SECRET',
('informat', 'intitute_number'): 'INFORMAT_INST_NUMBER',
('informat', 'autho_url'): 'INFORMAT_AUTH_URL',
('informat', 'api_base'): 'INFORMAT_API_BASE',
('informat', 'scope'): 'INFORMAT_SCOPE',
('smartschool', 'ss_client_id'): 'SS_CLIENT_ID',
('smartschool', 'ss_client_secret'): 'SS_CLIENT_SECRET',
('smartschool', 'ss_api_pw'): 'SS_API_PW',
('smartschool', 'ss_api_platform'): 'SS_API_PLATFORM',
('smartschool', 'afwezigheid_codes'): 'AFWEZIGHEID_CODES',
('smartschool', 'afwezigheid_sync_uur'): 'AFWEZIGHEID_SYNC_UUR',
('CO2_sensor', 'co2_netwerk'): 'CO2_NETWORK',
('CO2_sensor', 'co2_beginadres'): 'CO2_START_ADDR',
('CO2_sensor', 'co2_eindadres'): 'CO2_END_ADDR',
('CO2_sensor', 'co2_waardedrempel'): 'CO2_THRESHOLD',
('CO2_sensor', 'co2_scan_interval'): 'CO2_INTERVAL',
('personeel', 'personeel_ous'): 'PERSONEEL_OUS',
('personeel', 'directie'): 'PERSONEEL_DIRECTIE',
('personeel', 'secretariaat'): 'PERSONEEL_SECRETARIAAT',
('personeel', 'secretariaat_mail'): 'PERSONEEL_SECRETARIAAT_MAIL',
('personeel', 'mails_versturen'): 'PERSONEEL_MAILS',
('m4s', 'm4s_base_data_url'): 'M4S_BASE_URL',
('m4s', 'm4s_auth_url'): 'M4S_AUTH_URL',
('m4s', 'm4s_clientid'): 'M4S_CLIENT_ID',
('m4s', 'm4s_clientsecret'): 'M4S_CLIENT_SECRET',
('m4s', 'm4s_institutionguid'): 'M4S_INSTITUTIONGUID',
('m4s', 'm4s_repair_location_type'): 'M4S_REPAIR_LOCATION_TYPE',
('m4s', 'm4s_repair_status'): 'M4S_REPAIR_STATUS',
('m4s', 'm4s_truth_statement'): 'M4S_TRUTH_STATEMENT',
('m4s', 'm4s_repair_at_institution'): 'M4S_REPAIR_AT_INTITUTION',
('m4s', 'm4s_contact_first_name'): 'M4S_CONTACT_FIRST_NAME',
('m4s', 'm4s_contact_last_name'): 'M4S_CONTACT_LAST_NAME',
('m4s', 'm4s_contact_email'): 'M4S_CONTACT_EMAIL',
('m4s', 'm4s_contact_phone'): 'M4S_CONTACT_PHONE',
('m4s', 'm4s_repair_street'): 'M4S_REPAIR_STREET',
('m4s', 'm4s_repair_streetnumber'): 'M4S_REPAIR_STREETNUMBER',
('m4s', 'm4s_repair_zipcode'): 'M4S_REPAIR_ZIPCODE',
('m4s', 'm4s_repair_city'): 'M4S_REPAIIR_CITY',
('m4s', 'm4s_repair_countrycode'): 'M4S_REPAIR_COUNTRYCODE',
('m4s', 'm4s_language_code'): 'M4S_LANGUAGE_CODE',
('m4s', 'm4s_billing_first_name'): 'M4S_BILLING_FIRST_NAME',
('m4s', 'm4s_billing_last_name'): 'M4S_BILLING_LAST_NAME',
('m4s', 'm4s_billing_email'): 'M4S_BILLING_EMAIL',
('m4s', 'm4s_billing_phone'): 'M4S_BILLING_PHONE',
}
def lees_oude_config(pad):
cp = configparser.ConfigParser()
if not cp.read(pad, encoding='utf-8'):
sys.exit(f"Kan {pad} niet lezen.")
waarden = {}
for (sectie, key), veld in MAPPING.items():
if cp.has_option(sectie, key):
v = cp.get(sectie, key).strip()
if v and v != 'None':
waarden[veld] = v
# Smartschool-platform zonder https:// en pad
if 'SS_API_PLATFORM' in waarden:
waarden['SS_API_PLATFORM'] = waarden['SS_API_PLATFORM'].replace('https://', '').replace('http://', '').split('/')[0]
return cp, waarden
def cmd_importeer(args):
import tenancy
from config import Config
cp, waarden = lees_oude_config(args.config)
code = (args.code or cp.get('schoolgegevens', 'school_afkorting', fallback='')).strip().lower()
naam = cp.get('schoolgegevens', 'schoolnaam', fallback=code).strip()
domein = cp.get('schoolgegevens', 'schooldomein', fallback='').strip().lower()
db_naam = args.db or cp.get('database', 'database_name', fallback='').strip()
if not code or not domein or not db_naam:
sys.exit("Code, schooldomein of databasenaam ontbreekt (gebruik --code / --db).")
# Eigen databaseserver/gebruiker enkel overnemen als die verschilt van de nieuwe config.ini
for key, veld, standaard in [('host', 'DB_HOST', Config.DB_HOST), ('user', 'DB_USER', Config.DB_USER),
('password', 'DB_PASSWORD', Config.DB_PASSWORD)]:
v = cp.get('database', key, fallback='').strip()
if v and v != standaard:
waarden[veld] = v
# Google-serviceaccount
sleutel = None
kandidaten = [args.google_sleutel] if args.google_sleutel else [
cp.get('google', 'location_linux', fallback=''), cp.get('google', 'location', fallback='')]
for pad in kandidaten:
if pad and os.path.exists(pad):
with open(pad, encoding='utf-8') as f:
sleutel = json.load(f)
print(f"Google-sleutel gelezen uit {pad}")
break
if not sleutel:
print("⚠ Geen Google-sleutel gevonden; upload hem later via /scholen (of gebruik --google-sleutel).")
tenancy.ensure_schema()
bestaand = tenancy.school_op_code(code, ook_inactief=True)
if bestaand and not args.overschrijven:
sys.exit(f"School '{code}' bestaat al. Gebruik --overschrijven om bij te werken.")
tenancy.opslaan(code, naam, domein, '', db_naam, True, waarden, sleutel,
gebruiker='beheer.py', nieuw=not bestaand)
print(f"✓ School {naam} ({code}) opgeslagen: domein {domein}, database {db_naam}, "
f"{len(waarden)} instellingen{', Google-sleutel' if sleutel else ''}.")
def cmd_lijst(args):
import tenancy
tenancy.ensure_schema()
scholen = tenancy.alle_scholen(ook_inactief=True)
if not scholen:
print("Nog geen scholen.")
for s in scholen:
print(f"{s.code:<12} {s.naam:<40} {','.join(s.domeinen):<30} db={s.db_naam:<20} "
f"{'actief' if s.actief else 'INACTIEF'}{' google✓' if s.google_sleutel else ''}")
def cmd_kopieer(args):
import tenancy
from blueprints.scholen_bp import kopieer_school_db
bron = tenancy.school_op_code(args.bron, ook_inactief=True)
if not bron:
sys.exit(f"Onbekende school {args.bron}")
ok, msg = kopieer_school_db(bron, args.database, {})
print(msg)
sys.exit(0 if ok else 1)
def cmd_nieuwe_schooldatabase(args):
from blueprints.scholen_bp import maak_schooldatabase
ok, msg = maak_schooldatabase(args.database)
print(msg)
sys.exit(0 if ok else 1)
def cmd_gebruiker(args):
import getpass
import tenancy
import rechten
from database import central_query
from werkzeug.security import generate_password_hash
tenancy.ensure_schema()
pw = args.wachtwoord
bestaand = rechten.zoek_gebruiker(args.naam)
if pw is None and not bestaand:
pw = getpass.getpass("Wachtwoord: ")
groep = rechten.ADMIN if args.admin else None
if bestaand:
gid = bestaand['Gebruiker_ID']
if pw:
central_query("UPDATE Gebruikers SET Passwoordhash = %s WHERE Gebruiker_ID = %s",
(generate_password_hash(pw), gid), commit=True)
if args.email:
central_query("UPDATE Gebruikers SET Emailadres = %s WHERE Gebruiker_ID = %s",
(args.email.lower(), gid), commit=True)
if groep:
central_query("UPDATE Gebruikers SET Veiligheidsgroep = %s, Actief = 1 WHERE Gebruiker_ID = %s",
(groep, gid), commit=True)
print(f"✓ Gebruiker {bestaand['Gebruikersnaam']} bijgewerkt.")
else:
central_query("INSERT INTO Gebruikers (Gebruikersnaam, Veiligheidsgroep, Passwoordhash, Emailadres) "
"VALUES (%s, %s, %s, %s)",
(args.naam, groep or rechten.GEBRUIKER, generate_password_hash(pw) if pw else None,
args.email.lower() if args.email else None), commit=True)
print(f"✓ Gebruiker {args.naam} aangemaakt ({'Administrator' if groep else 'Gebruiker'}).")
def cmd_rechten(args):
import tenancy
import rechten
g = rechten.zoek_gebruiker(args.naam)
school = tenancy.school_op_code(args.school, ook_inactief=True)
if not g or not school:
sys.exit("Onbekende gebruiker of school.")
if args.onderdelen:
if args.onderdelen == ['alles']:
codes = rechten.CODES
elif args.onderdelen == ['geen']:
codes = []
else:
onbekend = [c for c in args.onderdelen if c not in rechten.CODES]
if onbekend:
sys.exit(f"Onbekende onderdelen: {', '.join(onbekend)}\nMogelijk: {', '.join(rechten.CODES)}")
codes = args.onderdelen
rechten.zet_rechten(g['Gebruiker_ID'], school.id, codes, door='beheer.py')
huidig = rechten.rechten_van(g['Gebruiker_ID'], school.id)
print(f"{g['Gebruikersnaam']} in {school.naam}:")
for code, naam, cat in rechten.ONDERDELEN:
print(f" [{'x' if code in huidig else ' '}] {code:<26} {naam}")
def main():
p = argparse.ArgumentParser(description="PIVOT beheer")
sub = p.add_subparsers(dest='cmd', required=True)
sub.add_parser('init')
i = sub.add_parser('importeer-config')
i.add_argument('config')
i.add_argument('--code')
i.add_argument('--db')
i.add_argument('--google-sleutel')
i.add_argument('--overschrijven', action='store_true')
sub.add_parser('lijst')
k = sub.add_parser('kopieer-school', aliases=['kopieer-schema'])
k.add_argument('bron')
k.add_argument('database')
sub.add_parser('nieuwe-sleutel')
n = sub.add_parser('nieuwe-schooldatabase')
n.add_argument('database')
gb = sub.add_parser('gebruiker')
gb.add_argument('naam')
gb.add_argument('--wachtwoord')
gb.add_argument('--email')
gb.add_argument('--admin', action='store_true')
r = sub.add_parser('rechten')
r.add_argument('naam')
r.add_argument('school')
r.add_argument('onderdelen', nargs='*')
args = p.parse_args()
if args.cmd == 'nieuwe-sleutel':
print(Fernet.generate_key().decode())
elif args.cmd == 'init':
import tenancy
tenancy.ensure_schema()
print("✓ Centrale tabellen aangemaakt (of bestonden al).")
elif args.cmd == 'importeer-config':
cmd_importeer(args)
elif args.cmd == 'lijst':
cmd_lijst(args)
elif args.cmd in ('kopieer-school', 'kopieer-schema'):
cmd_kopieer(args)
elif args.cmd == 'nieuwe-schooldatabase':
cmd_nieuwe_schooldatabase(args)
elif args.cmd == 'gebruiker':
cmd_gebruiker(args)
elif args.cmd == 'rechten':
cmd_rechten(args)
if __name__ == '__main__':
main()