Initial commit
This commit is contained in:
commit
536ff43ce4
158 files changed
+30355
No files matched your search
@@ -0,0 +1,371 @@
|
||||
"""
|
||||
Personeel: afwezigheden van leerkrachten, nascholingen en uitstappen.
|
||||
|
||||
Rollen (config.ini › [personeel]):
|
||||
- leerkracht : iedereen in personeel_ous die met Google inlogt (PIVOT-groep 5).
|
||||
Ziet en beheert enkel zijn eigen meldingen/aanvragen.
|
||||
- directie : e-mailadressen in 'directie' → keurt nascholingen en uitstappen goed.
|
||||
- secretariaat : e-mailadressen in 'secretariaat' → dagoverzicht, vervangingen,
|
||||
melden namens een leerkracht.
|
||||
PIVOT-gebruikers met het recht 'personeel_secretariaat' / 'personeel_directie' in deze
|
||||
school krijgen die rol ook. Administrators hebben de secretariaatsrechten, en ook de
|
||||
directierechten zolang er nog geen directie is ingesteld.
|
||||
|
||||
Soorten:
|
||||
- afwezigheid : geen goedkeuring nodig, meteen zichtbaar voor het secretariaat
|
||||
- nascholing : goedkeuring door de directie
|
||||
- uitstap : goedkeuring door de directie (klassen + begeleiders zijn weg)
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import date, timedelta
|
||||
|
||||
from config import Config
|
||||
from database import execute_query, get_db_connection
|
||||
from tenancy import huidige_school, met_school
|
||||
|
||||
LEERKRACHT_GROEP = 5
|
||||
|
||||
SOORTEN = {
|
||||
'afwezigheid': {'label': 'Afwezigheid', 'icoon': '🤒', 'goedkeuring': False},
|
||||
'nascholing': {'label': 'Nascholing', 'icoon': '🎓', 'goedkeuring': True},
|
||||
'uitstap': {'label': 'Uitstap', 'icoon': '🚌', 'goedkeuring': True},
|
||||
}
|
||||
STATUS = {
|
||||
'gemeld': ('Gemeld', 'info'),
|
||||
'ingediend': ('Wacht op directie', 'warning'),
|
||||
'vraag': ('Vraag van directie', 'warning'),
|
||||
'goedgekeurd': ('Goedgekeurd', 'success'),
|
||||
'geweigerd': ('Geweigerd', 'danger'),
|
||||
'geannuleerd': ('Geannuleerd', ''),
|
||||
}
|
||||
DAGDELEN = {'dag': 'Hele dag', 'vm': 'Voormiddag', 'nm': 'Namiddag', 'uren': 'Bepaalde lesuren'}
|
||||
# Statussen die in het dagoverzicht meetellen als "is weg"
|
||||
ACTIEF = ('gemeld', 'goedgekeurd')
|
||||
|
||||
|
||||
def _lijst(waarde):
|
||||
return [x.strip().lower() for x in (waarde or '').replace(';', ',').split(',') if x.strip()]
|
||||
|
||||
|
||||
# ---------------- rollen ----------------
|
||||
def directie():
|
||||
return _lijst(Config.PERSONEEL_DIRECTIE)
|
||||
|
||||
|
||||
def secretariaat():
|
||||
return _lijst(Config.PERSONEEL_SECRETARIAAT)
|
||||
|
||||
|
||||
def rollen(sess):
|
||||
"""Rollen in de personeelsmodule: via de e-maillijsten van de school of via de PIVOT-rechten
|
||||
'personeel_directie' / 'personeel_secretariaat' (Gebruikers & rechten)."""
|
||||
from rechten import heeft_recht, is_admin
|
||||
email = (sess.get('user') or '').lower()
|
||||
admin = is_admin()
|
||||
r = {'leerkracht'}
|
||||
if email in directie() or (heeft_recht('personeel_directie') and (not admin or not directie())):
|
||||
r.add('directie')
|
||||
if email in secretariaat() or heeft_recht('personeel_secretariaat'):
|
||||
r.add('secretariaat')
|
||||
return r
|
||||
|
||||
|
||||
_ou_cache = {}
|
||||
|
||||
|
||||
def is_personeelslid(email):
|
||||
"""True als het Google-account in een van de personeels-OU's zit (cache 10 min)."""
|
||||
email = (email or '').lower()
|
||||
school = huidige_school()
|
||||
if email.rsplit('@', 1)[-1] not in school.domeinen:
|
||||
return False
|
||||
sleutel = (school.code, email)
|
||||
hit = _ou_cache.get(sleutel)
|
||||
if hit and time.time() - hit[0] < 600:
|
||||
return hit[1]
|
||||
ok = False
|
||||
try:
|
||||
from services.google_service import GoogleAdminService
|
||||
u = GoogleAdminService().get_user_info(email) or {}
|
||||
ou = u.get('orgUnitPath', '')
|
||||
ok = (not u.get('suspended')) and any(ou == p or ou.startswith(p.rstrip('/') + '/')
|
||||
for p in [x.strip() for x in Config.PERSONEEL_OUS.split(',') if x.strip()])
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Personeel: OU-controle mislukt voor {email}: {e}\n")
|
||||
_ou_cache[sleutel] = (time.time(), ok)
|
||||
return ok
|
||||
|
||||
|
||||
# ---------------- klassen uit Smartschool ----------------
|
||||
def klassen():
|
||||
"""Officiële klassen uit Smartschool (cache 6 uur, per school). Lege lijst als het niet lukt."""
|
||||
_klassen_cache = huidige_school().object('klassen_cache', lambda: {'t': 0, 'lijst': []})
|
||||
if _klassen_cache['lijst'] and time.time() - _klassen_cache['t'] < 6 * 3600:
|
||||
return _klassen_cache['lijst']
|
||||
try:
|
||||
from services.smartschool_api import SmartschoolService
|
||||
ss = SmartschoolService(platform=Config.SS_API_PLATFORM, webservice_pwd=Config.SS_API_PW)
|
||||
raw = ss.client.service.getClassListJson(Config.SS_API_PW)
|
||||
data = json.loads(raw) if isinstance(raw, str) else raw
|
||||
if isinstance(data, dict):
|
||||
data = list(data.values())
|
||||
namen = []
|
||||
for k in data or []:
|
||||
if not isinstance(k, dict):
|
||||
continue
|
||||
officieel = k.get('isKlas', k.get('isOfficial', True))
|
||||
if str(officieel).lower() in ('false', '0', 'none'):
|
||||
continue
|
||||
naam = (k.get('name') or k.get('code') or '').strip()
|
||||
if naam:
|
||||
namen.append(naam)
|
||||
lijst = sorted(set(namen), key=lambda n: (len(n) > 3, n))
|
||||
if lijst:
|
||||
_klassen_cache.update(t=time.time(), lijst=lijst)
|
||||
return lijst
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Personeel: klassenlijst uit Smartschool mislukt: {e}\n")
|
||||
return _klassen_cache['lijst']
|
||||
|
||||
|
||||
# ---------------- database ----------------
|
||||
def ensure_tables():
|
||||
execute_query("""
|
||||
CREATE TABLE IF NOT EXISTS personeel_aanvragen (
|
||||
ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Soort VARCHAR(20) NOT NULL,
|
||||
Status VARCHAR(20) NOT NULL,
|
||||
Email VARCHAR(255) NOT NULL,
|
||||
Naam VARCHAR(255) NULL,
|
||||
Ingediend_door VARCHAR(255) NULL,
|
||||
Titel VARCHAR(255) NOT NULL,
|
||||
Omschrijving TEXT NULL,
|
||||
Locatie VARCHAR(255) NULL,
|
||||
Van_datum DATE NOT NULL,
|
||||
Tot_datum DATE NOT NULL,
|
||||
Dagdeel VARCHAR(10) NOT NULL DEFAULT 'dag',
|
||||
Lesuren VARCHAR(50) NULL,
|
||||
Klassen TEXT NULL,
|
||||
Begeleiders TEXT NULL,
|
||||
Kost DECIMAL(10,2) NULL,
|
||||
Vervanging_ok TINYINT(1) NOT NULL DEFAULT 0,
|
||||
Vervanging_info VARCHAR(255) NULL,
|
||||
Aangemaakt DATETIME NOT NULL,
|
||||
Bijgewerkt DATETIME NOT NULL,
|
||||
INDEX idx_status (Status),
|
||||
INDEX idx_datum (Van_datum, Tot_datum),
|
||||
INDEX idx_email (Email)
|
||||
)""", commit=True)
|
||||
execute_query("""
|
||||
CREATE TABLE IF NOT EXISTS personeel_reacties (
|
||||
ID INT AUTO_INCREMENT PRIMARY KEY,
|
||||
Aanvraag_ID INT NOT NULL,
|
||||
Tijdstip DATETIME NOT NULL,
|
||||
Email VARCHAR(255) NULL,
|
||||
Naam VARCHAR(255) NULL,
|
||||
Actie VARCHAR(30) NOT NULL,
|
||||
Tekst TEXT NULL,
|
||||
INDEX idx_aanvraag (Aanvraag_ID)
|
||||
)""", commit=True)
|
||||
|
||||
|
||||
def _reactie(aanvraag_id, email, naam, actie, tekst=None):
|
||||
execute_query("INSERT INTO personeel_reacties (Aanvraag_ID, Tijdstip, Email, Naam, Actie, Tekst) "
|
||||
"VALUES (%s, NOW(), %s, %s, %s, %s)", (aanvraag_id, email, naam, actie, tekst), commit=True)
|
||||
|
||||
|
||||
def aanmaken(data, door_email, door_naam):
|
||||
soort = data['soort']
|
||||
status = 'ingediend' if SOORTEN[soort]['goedkeuring'] else 'gemeld'
|
||||
conn = get_db_connection()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO personeel_aanvragen (Soort, Status, Email, Naam, Ingediend_door, Titel, Omschrijving, Locatie,
|
||||
Van_datum, Tot_datum, Dagdeel, Lesuren, Klassen, Begeleiders, Kost, Aangemaakt, Bijgewerkt)
|
||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,NOW(),NOW())""",
|
||||
(soort, status, data['email'], data['naam'], door_email if door_email != data['email'] else None,
|
||||
data['titel'], data.get('omschrijving'), data.get('locatie'), data['van'], data['tot'],
|
||||
data['dagdeel'], data.get('lesuren'), ', '.join(data.get('klassen') or []),
|
||||
data.get('begeleiders'), data.get('kost')))
|
||||
conn.commit()
|
||||
aid = cur.lastrowid
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
tekst = None if door_email == data['email'] else f"Gemeld door {door_naam or door_email}"
|
||||
_reactie(aid, door_email, door_naam, 'gemeld' if status == 'gemeld' else 'ingediend', tekst)
|
||||
return aid
|
||||
|
||||
|
||||
def get(aid):
|
||||
rows = execute_query("SELECT * FROM personeel_aanvragen WHERE ID = %s", (aid,))
|
||||
return rows[0] if rows else None
|
||||
|
||||
|
||||
def reacties(aid):
|
||||
return execute_query(
|
||||
"SELECT Tijdstip, Email, Naam, Actie, Tekst FROM personeel_reacties WHERE Aanvraag_ID = %s ORDER BY Tijdstip, ID",
|
||||
(aid,))
|
||||
|
||||
|
||||
def wijzig_status(aid, nieuwe_status, door_email, door_naam, actie, tekst=None):
|
||||
execute_query("UPDATE personeel_aanvragen SET Status = %s, Bijgewerkt = NOW() WHERE ID = %s",
|
||||
(nieuwe_status, aid), commit=True)
|
||||
_reactie(aid, door_email, door_naam, actie, tekst)
|
||||
|
||||
|
||||
def commentaar(aid, door_email, door_naam, tekst):
|
||||
execute_query("UPDATE personeel_aanvragen SET Bijgewerkt = NOW() WHERE ID = %s", (aid,), commit=True)
|
||||
_reactie(aid, door_email, door_naam, 'commentaar', tekst)
|
||||
|
||||
|
||||
def vervanging(aid, ok, info, door_email, door_naam):
|
||||
execute_query("UPDATE personeel_aanvragen SET Vervanging_ok = %s, Vervanging_info = %s, Bijgewerkt = NOW() "
|
||||
"WHERE ID = %s", (1 if ok else 0, (info or '')[:255] or None, aid), commit=True)
|
||||
_reactie(aid, door_email, door_naam, 'vervanging',
|
||||
('Vervanging geregeld' if ok else 'Vervanging nog niet geregeld') + (f": {info}" if info else ''))
|
||||
|
||||
|
||||
def mijn(email):
|
||||
return execute_query("SELECT * FROM personeel_aanvragen WHERE Email = %s ORDER BY Van_datum DESC, ID DESC LIMIT 200",
|
||||
(email,))
|
||||
|
||||
|
||||
def te_beoordelen():
|
||||
return execute_query("SELECT * FROM personeel_aanvragen WHERE Status IN ('ingediend','vraag') "
|
||||
"ORDER BY Status = 'vraag', Van_datum, ID")
|
||||
|
||||
|
||||
def recent_beslist(limit=20):
|
||||
return execute_query("SELECT * FROM personeel_aanvragen WHERE Status IN ('goedgekeurd','geweigerd') "
|
||||
"AND Soort <> 'afwezigheid' ORDER BY Bijgewerkt DESC LIMIT %s", (int(limit),))
|
||||
|
||||
|
||||
def periode(van, tot, statussen=ACTIEF):
|
||||
ph = ','.join(['%s'] * len(statussen))
|
||||
return execute_query(
|
||||
f"SELECT * FROM personeel_aanvragen WHERE Status IN ({ph}) AND Van_datum <= %s AND Tot_datum >= %s "
|
||||
"ORDER BY Van_datum, Naam", tuple(statussen) + (tot, van))
|
||||
|
||||
|
||||
def aantal_te_beoordelen():
|
||||
try:
|
||||
return execute_query("SELECT COUNT(*) AS n FROM personeel_aanvragen WHERE Status = 'ingediend'")[0]['n']
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------- dagoverzicht ----------------
|
||||
def dagoverzicht(van, tot):
|
||||
"""Per dag: wie is weg en welke klassen zijn weg (enkel gemeld/goedgekeurd), plus wat nog wacht."""
|
||||
items = periode(van, tot)
|
||||
wachtend = periode(van, tot, ('ingediend', 'vraag'))
|
||||
dagen = []
|
||||
d = van
|
||||
while d <= tot:
|
||||
if d.weekday() < 5:
|
||||
vandaag = [i for i in items if i['Van_datum'] <= d <= i['Tot_datum']]
|
||||
personen, klassen_weg = [], []
|
||||
for i in vandaag:
|
||||
personen.append({'naam': i['Naam'] or i['Email'], 'email': i['Email'], 'item': i, 'rol': None})
|
||||
if i['Soort'] == 'uitstap':
|
||||
for b in [x.strip() for x in (i['Begeleiders'] or '').split(',') if x.strip()]:
|
||||
personen.append({'naam': b, 'email': None, 'item': i, 'rol': 'begeleider'})
|
||||
for k in [x.strip() for x in (i['Klassen'] or '').split(',') if x.strip()]:
|
||||
klassen_weg.append({'klas': k, 'item': i})
|
||||
dagen.append({
|
||||
'datum': d, 'personen': personen,
|
||||
'klassen': sorted(klassen_weg, key=lambda k: k['klas']),
|
||||
'wachtend': [w for w in wachtend if w['Van_datum'] <= d <= w['Tot_datum']],
|
||||
'open_vervanging': sum(1 for i in vandaag if i['Klassen'] and i['Soort'] != 'uitstap' and not i['Vervanging_ok']),
|
||||
})
|
||||
d += timedelta(days=1)
|
||||
return dagen
|
||||
|
||||
|
||||
# ---------------- e-mail ----------------
|
||||
def _mail_async(ontvangers, onderwerp, html):
|
||||
ontvangers = sorted({o for o in ontvangers if o and '@' in o})
|
||||
if not Config.PERSONEEL_MAILS or not ontvangers:
|
||||
return
|
||||
|
||||
def verstuur():
|
||||
try:
|
||||
from services.google_service import GoogleAdminService
|
||||
svc = GoogleAdminService()
|
||||
for o in ontvangers:
|
||||
ok, msg = svc.send_custom_mail(o, onderwerp, html)
|
||||
if not ok:
|
||||
sys.stderr.write(f"Personeel: mail naar {o} mislukt: {msg}\n")
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"Personeel: mail mislukt: {e}\n")
|
||||
|
||||
threading.Thread(target=met_school(verstuur), daemon=True).start()
|
||||
|
||||
|
||||
def _mailtekst(item, intro, link, extra=None):
|
||||
soort = SOORTEN[item['Soort']]
|
||||
periode_txt = item['Van_datum'].strftime('%d/%m/%Y')
|
||||
if item['Tot_datum'] != item['Van_datum']:
|
||||
periode_txt += ' – ' + item['Tot_datum'].strftime('%d/%m/%Y')
|
||||
dagdeel = DAGDELEN.get(item['Dagdeel'], '')
|
||||
if item['Dagdeel'] == 'uren' and item['Lesuren']:
|
||||
dagdeel += f" ({item['Lesuren']})"
|
||||
rijen = [('Wie', item['Naam'] or item['Email']), ('Wat', f"{soort['icoon']} {soort['label']}: {item['Titel']}"),
|
||||
('Wanneer', f"{periode_txt}, {dagdeel}")]
|
||||
if item['Klassen']:
|
||||
rijen.append(('Klassen', item['Klassen']))
|
||||
if item['Begeleiders']:
|
||||
rijen.append(('Begeleiders', item['Begeleiders']))
|
||||
if item['Kost']:
|
||||
rijen.append(('Kost', f"€ {item['Kost']}"))
|
||||
tabel = ''.join(f"<tr><td style='padding:4px 12px 4px 0;color:#64748b'>{k}</td><td style='padding:4px 0'><b>{v}</b></td></tr>"
|
||||
for k, v in rijen)
|
||||
extra_html = f"<p style='background:#f1f5f9;padding:10px 14px;border-radius:6px'>{extra}</p>" if extra else ''
|
||||
return (f"<div style='font-family:Segoe UI,Arial,sans-serif;color:#1e293b'>"
|
||||
f"<p>{intro}</p><table>{tabel}</table>{extra_html}"
|
||||
f"<p><a href='{link}' style='background:#1e6fa5;color:#fff;padding:10px 16px;border-radius:6px;text-decoration:none'>"
|
||||
f"Openen in PIVOT</a></p></div>")
|
||||
|
||||
|
||||
def secretariaat_adressen():
|
||||
return _lijst(Config.PERSONEEL_SECRETARIAAT_MAIL) or secretariaat()
|
||||
|
||||
|
||||
def mail_nieuw(item, link):
|
||||
soort = SOORTEN[item['Soort']]
|
||||
if soort['goedkeuring']:
|
||||
_mail_async(directie(), f"Nieuwe aanvraag {soort['label'].lower()}: {item['Naam']} – {item['Titel']}",
|
||||
_mailtekst(item, f"Er is een nieuwe aanvraag voor een {soort['label'].lower()} die je goedkeuring vraagt.", link))
|
||||
else:
|
||||
_mail_async(secretariaat_adressen(), f"Afwezig: {item['Naam']} ({item['Van_datum']:%d/%m})",
|
||||
_mailtekst(item, "Er is een afwezigheid gemeld.", link, item['Omschrijving']))
|
||||
|
||||
|
||||
def mail_beslissing(item, beslissing, tekst, link):
|
||||
soort = SOORTEN[item['Soort']]
|
||||
woorden = {'goedgekeurd': 'is goedgekeurd ✅', 'geweigerd': 'is geweigerd ❌', 'vraag': 'heeft een vraag van de directie ❓'}
|
||||
_mail_async([item['Email']], f"Je aanvraag {soort['label'].lower()} {woorden[beslissing]}",
|
||||
_mailtekst(item, f"Je aanvraag <b>{item['Titel']}</b> {woorden[beslissing]}.", link,
|
||||
f"Commentaar: {tekst}" if tekst else None))
|
||||
if beslissing == 'goedgekeurd':
|
||||
_mail_async(secretariaat_adressen(), f"Goedgekeurd: {soort['label'].lower()} {item['Naam']} ({item['Van_datum']:%d/%m})",
|
||||
_mailtekst(item, f"Een {soort['label'].lower()} is goedgekeurd en staat nu in het dagoverzicht.", link))
|
||||
|
||||
|
||||
def mail_antwoord(item, tekst, link):
|
||||
_mail_async(directie(), f"Antwoord van {item['Naam']}: {item['Titel']}",
|
||||
_mailtekst(item, "De leerkracht heeft geantwoord op je vraag; de aanvraag wacht opnieuw op je beslissing.",
|
||||
link, tekst))
|
||||
|
||||
|
||||
def mail_geannuleerd(item, link):
|
||||
ontvangers = secretariaat_adressen() if item['Status'] in ('gemeld', 'goedgekeurd') else []
|
||||
if SOORTEN[item['Soort']]['goedkeuring']:
|
||||
ontvangers = ontvangers + directie()
|
||||
_mail_async(ontvangers, f"Geannuleerd: {SOORTEN[item['Soort']]['label'].lower()} {item['Naam']} ({item['Van_datum']:%d/%m})",
|
||||
_mailtekst(item, "Deze melding/aanvraag is geannuleerd.", link))
|
||||
Reference in new issue
Block a user