All checks were successful
Build & Push / Build & Push image (push) Successful in 1m1s
192 lines
6.0 KiB
Python
192 lines
6.0 KiB
Python
"""
|
|
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
|
|
import threading
|
|
|
|
# In-memory cache: { vak_id: {'data': {...}, 'mtime': float} }
|
|
# Geïnvalideerd bij upload/delete — voorkomt schijf-I/O bij elke request
|
|
_cache: dict = {}
|
|
_cache_lock = threading.Lock()
|
|
_INDEX_KEY = "__index__"
|
|
|
|
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 _cache_get(key):
|
|
path = os.path.join(DOELEN_DIR, 'index.json' if key == _INDEX_KEY else f'{key}.json')
|
|
if not os.path.exists(path):
|
|
return None
|
|
try:
|
|
mtime = os.path.getmtime(path)
|
|
except OSError:
|
|
return None
|
|
with _cache_lock:
|
|
entry = _cache.get(key)
|
|
if entry and entry['mtime'] == mtime:
|
|
return entry['data']
|
|
return None
|
|
|
|
|
|
def _cache_set(key, data):
|
|
path = os.path.join(DOELEN_DIR, 'index.json' if key == _INDEX_KEY else f'{key}.json')
|
|
try:
|
|
mtime = os.path.getmtime(path)
|
|
except OSError:
|
|
return
|
|
with _cache_lock:
|
|
_cache[key] = {'data': data, 'mtime': mtime}
|
|
|
|
|
|
def cache_invalidate(vak_id=None):
|
|
"""Verwijder één of alle entries uit de cache (aanroepen na upload/delete)."""
|
|
with _cache_lock:
|
|
if vak_id:
|
|
_cache.pop(vak_id, None)
|
|
_cache.pop(_INDEX_KEY, None)
|
|
else:
|
|
_cache.clear()
|
|
|
|
|
|
def load_index():
|
|
cached = _cache_get(_INDEX_KEY)
|
|
if cached:
|
|
return cached
|
|
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'])
|
|
_cache_set(_INDEX_KEY, data)
|
|
return data
|
|
|
|
|
|
def load_vak(vak_id):
|
|
if not is_valid_vak_id(vak_id):
|
|
return None
|
|
cached = _cache_get(vak_id)
|
|
if cached:
|
|
return cached
|
|
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)
|
|
_cache_set(vak_id, data)
|
|
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)
|
|
cache_invalidate(vak_id)
|
|
rebuild_index()
|
|
|
|
|
|
def delete_vak(vak_id):
|
|
path = get_doelen_path(vak_id)
|
|
if not os.path.exists(path):
|
|
return False
|
|
os.remove(path)
|
|
cache_invalidate(vak_id)
|
|
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', '?'),
|
|
'bronDatum': data.get('bronDatum'), # 'gewijzigd' uit Excel metadata
|
|
'bronBestand': data.get('bronBestand'), # originele bestandsnaam
|
|
})
|
|
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)
|