first commit
This commit is contained in:
0
backend/services/__init__.py
Normal file
0
backend/services/__init__.py
Normal file
69
backend/services/audit.py
Normal file
69
backend/services/audit.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Audit logging service.
|
||||
Gebruik: from services.audit import audit_log
|
||||
audit_log('user.create', 'user', target_id=str(user.id), detail={'email': email})
|
||||
"""
|
||||
import json
|
||||
from datetime import datetime
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
|
||||
|
||||
def audit_log(action: str, category: str, *,
|
||||
target_type: str = None, target_id: str = None,
|
||||
detail: dict = None, school_id: int = None,
|
||||
user_id: int = None):
|
||||
"""
|
||||
Schrijf een audit entry naar de database.
|
||||
|
||||
action: korte actienaam, bv. 'user.create', 'school.delete', 'login.success'
|
||||
category: auth | user | school | class | assessment | doelen | system
|
||||
target_type: wat er veranderd is, bv. 'user', 'school', 'class'
|
||||
target_id: identifier van het object (als string)
|
||||
detail: dict met extra context, wordt als JSON opgeslagen
|
||||
school_id: override school_id (standaard current_user.school_id)
|
||||
user_id: override user_id (standaard current_user.id)
|
||||
"""
|
||||
from models import AuditLog
|
||||
|
||||
try:
|
||||
uid = user_id
|
||||
sid = school_id
|
||||
|
||||
if uid is None:
|
||||
try:
|
||||
uid = current_user.id if current_user.is_authenticated else None
|
||||
except Exception:
|
||||
uid = None
|
||||
|
||||
if sid is None:
|
||||
try:
|
||||
sid = current_user.school_id if current_user.is_authenticated else None
|
||||
except Exception:
|
||||
sid = None
|
||||
|
||||
ip = None
|
||||
try:
|
||||
ip = request.remote_addr
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
entry = AuditLog(
|
||||
timestamp = datetime.utcnow(),
|
||||
user_id = uid,
|
||||
school_id = sid,
|
||||
action = action,
|
||||
category = category,
|
||||
target_type = target_type,
|
||||
target_id = str(target_id) if target_id is not None else None,
|
||||
detail = json.dumps(detail, ensure_ascii=False) if detail else None,
|
||||
ip_address = ip,
|
||||
)
|
||||
db.session.add(entry)
|
||||
# Geen commit hier — de aanroeper commit zelf (of we flushen mee)
|
||||
db.session.flush()
|
||||
except Exception as e:
|
||||
# Audit failures mogen de hoofdflow nooit blokkeren
|
||||
import logging
|
||||
logging.getLogger(__name__).warning(f"Audit log failed: {e}")
|
||||
137
backend/services/doelen.py
Normal file
137
backend/services/doelen.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Doelen service - centrale logica voor vak ID's, namen, validatie en upload.
|
||||
Eén bron van waarheid. Geen andere plek in de app definieert vaknamen.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
DOELEN_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'doelen')
|
||||
|
||||
VAK_NAMEN = {
|
||||
'doelenset-bao-aardrijkskunde': 'Aardrijkskunde',
|
||||
'doelenset-bao-burgerschap': 'Burgerschap',
|
||||
'doelenset-bao-frans': 'Frans',
|
||||
'doelenset-bao-geschiedenis': 'Geschiedenis',
|
||||
'doelenset-bao-ict': 'ICT',
|
||||
'doelenset-bao-leren-leren': 'Leren leren',
|
||||
'doelenset-bao-lichamelijke-opvoeding': 'Lichamelijke opvoeding',
|
||||
'doelenset-bao-muzische-vorming': 'Muzische vorming',
|
||||
'doelenset-bao-nederlands': 'Nederlands',
|
||||
'doelenset-bao-sociale-vaardigheden': 'Sociale vaardigheden',
|
||||
'doelenset-bao-wetenschap-techniek': 'Wetenschap en techniek',
|
||||
'doelenset-bao-wiskunde': 'Wiskunde',
|
||||
}
|
||||
|
||||
|
||||
def vak_naam(vak_id):
|
||||
if vak_id in VAK_NAMEN:
|
||||
return VAK_NAMEN[vak_id]
|
||||
cleaned = re.sub(r'^doelenset-bao-', '', vak_id)
|
||||
return cleaned.replace('-', ' ').title()
|
||||
|
||||
|
||||
def is_valid_vak_id(vak_id):
|
||||
return bool(re.match(r'^[a-z0-9][a-z0-9\-]{2,78}[a-z0-9]$', vak_id))
|
||||
|
||||
|
||||
def get_doelen_path(vak_id):
|
||||
return os.path.join(DOELEN_DIR, f'{vak_id}.json')
|
||||
|
||||
|
||||
def list_installed_vakken():
|
||||
if not os.path.exists(DOELEN_DIR):
|
||||
return []
|
||||
return sorted([
|
||||
f[:-5] for f in os.listdir(DOELEN_DIR)
|
||||
if f.endswith('.json') and f != 'index.json' and is_valid_vak_id(f[:-5])
|
||||
])
|
||||
|
||||
|
||||
def load_index():
|
||||
path = os.path.join(DOELEN_DIR, 'index.json')
|
||||
if not os.path.exists(path):
|
||||
rebuild_index()
|
||||
with open(path, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for vak in data.get('vakken', []):
|
||||
vak['naam'] = vak_naam(vak['id'])
|
||||
data['vakken'].sort(key=lambda v: v['naam'])
|
||||
return data
|
||||
|
||||
|
||||
def load_vak(vak_id):
|
||||
if not is_valid_vak_id(vak_id):
|
||||
return None
|
||||
path = get_doelen_path(vak_id)
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
with open(path, encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
data['vakNaam'] = vak_naam(vak_id)
|
||||
return data
|
||||
|
||||
|
||||
def validate_vak_json(data):
|
||||
errors = []
|
||||
for key in ['vak', 'versie', 'rijen']:
|
||||
if key not in data:
|
||||
errors.append(f'Verplicht veld ontbreekt: "{key}"')
|
||||
if errors:
|
||||
return errors
|
||||
if not isinstance(data['rijen'], list) or len(data['rijen']) == 0:
|
||||
errors.append('"rijen" moet een niet-lege lijst zijn')
|
||||
return errors
|
||||
doelzinnen = [r for r in data['rijen'] if r.get('type') == 'doelzin']
|
||||
if not doelzinnen:
|
||||
errors.append('Geen doelzinnen gevonden (type="doelzin") — verkeerd bestand?')
|
||||
else:
|
||||
zonder_nr = [r for r in doelzinnen if not r.get('goNr')]
|
||||
if zonder_nr:
|
||||
errors.append(f'{len(zonder_nr)} doelzin(nen) missen een GO! nummer (goNr)')
|
||||
vak_id = data.get('vak', '')
|
||||
if vak_id and not is_valid_vak_id(vak_id):
|
||||
errors.append(f'Ongeldig vak ID: "{vak_id}"')
|
||||
return errors
|
||||
|
||||
|
||||
def save_vak(vak_id, data):
|
||||
os.makedirs(DOELEN_DIR, exist_ok=True)
|
||||
data['vakNaam'] = vak_naam(vak_id)
|
||||
path = get_doelen_path(vak_id)
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
rebuild_index()
|
||||
|
||||
|
||||
def delete_vak(vak_id):
|
||||
path = get_doelen_path(vak_id)
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
os.remove(path)
|
||||
rebuild_index()
|
||||
return True
|
||||
|
||||
|
||||
def rebuild_index():
|
||||
os.makedirs(DOELEN_DIR, exist_ok=True)
|
||||
vakken = []
|
||||
for vak_id in list_installed_vakken():
|
||||
try:
|
||||
data = load_vak(vak_id)
|
||||
if not data:
|
||||
continue
|
||||
doelzinnen = [r for r in data.get('rijen', []) if r.get('type') == 'doelzin']
|
||||
vakken.append({
|
||||
'id': vak_id,
|
||||
'naam': vak_naam(vak_id),
|
||||
'aantalDoelzinnen': len(doelzinnen),
|
||||
'versie': data.get('versie', '?'),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
vakken.sort(key=lambda v: v['naam'])
|
||||
index = {'versie': '2025-01', 'vakken': vakken}
|
||||
with open(os.path.join(DOELEN_DIR, 'index.json'), 'w', encoding='utf-8') as f:
|
||||
json.dump(index, f, ensure_ascii=False, indent=2)
|
||||
Reference in New Issue
Block a user