Files
leerdoelen_tracker/backend/routes/api.py
2026-02-28 00:02:02 +01:00

317 lines
11 KiB
Python

from datetime import datetime
from flask import Blueprint, jsonify, request
from flask_login import login_required, current_user
from models import User, SchoolYear, Assessment, School, Class, AuditLog
from app import db
from services.doelen import load_index, load_vak, is_valid_vak_id
from services.audit import audit_log
from functools import wraps
api_bp = Blueprint('api', __name__)
def director_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not current_user.is_director:
return jsonify({'error': 'Geen toegang'}), 403
return f(*args, **kwargs)
return decorated
def get_active_year(school_id=None):
"""Geeft het globaal actief schooljaar terug (school_id wordt genegeerd)."""
return SchoolYear.query.filter_by(school_id=None, is_active=True).first()
# ── Doelen (statische JSON bestanden) ─────────────────────────────────────────
@api_bp.route('/doelen/index')
@login_required
def doelen_index():
data = load_index()
if not data['vakken']:
return jsonify({
'error': 'Geen doelen gevonden. Upload eerst de JSON bestanden via het beheerderspaneel.'
}), 404
return jsonify(data)
@api_bp.route('/doelen/<vak_id>')
@login_required
def doelen_vak(vak_id):
if not is_valid_vak_id(vak_id):
return jsonify({'error': 'Ongeldig vak ID'}), 400
data = load_vak(vak_id)
if not data:
return jsonify({'error': f'Vak "{vak_id}" niet gevonden'}), 404
return jsonify(data)
# ── Beoordelingen ─────────────────────────────────────────────────────────────
@api_bp.route('/assessments', methods=['GET'])
@login_required
def get_assessments():
if not current_user.school_id:
return jsonify({'assessments': []})
school_year = get_active_year(current_user.school_id)
if not school_year:
return jsonify({'assessments': []})
year_id = request.args.get('year_id', school_year.id)
vak_id = request.args.get('vak_id')
query = Assessment.query.filter_by(user_id=current_user.id, school_year_id=year_id)
if vak_id:
query = query.filter_by(vak_id=vak_id)
return jsonify({'assessments': [a.to_dict() for a in query.all()]})
@api_bp.route('/assessments', methods=['POST'])
@login_required
def save_assessment():
data = request.get_json() or {}
vak_id = (data.get('vak_id') or '').strip()
goal_id = (data.get('goal_id') or '').strip()
status = (data.get('status') or '').strip()
if not vak_id or not goal_id:
return jsonify({'error': 'vak_id en goal_id zijn verplicht'}), 400
if status not in ('groen', 'oranje', 'roze', ''):
return jsonify({'error': 'Ongeldige status — gebruik groen, oranje, roze of leeg'}), 400
if not current_user.school_id:
return jsonify({'error': 'Account is nog niet gekoppeld aan een school'}), 400
school_year = get_active_year(current_user.school_id)
if not school_year:
return jsonify({'error': 'Geen actief schooljaar gevonden'}), 400
assessment = Assessment.query.filter_by(
user_id=current_user.id,
school_year_id=school_year.id,
vak_id=vak_id,
goal_id=goal_id,
).first()
if status == '':
if assessment:
db.session.delete(assessment)
db.session.commit()
return jsonify({'deleted': True})
if assessment:
assessment.status = status
assessment.updated_at = datetime.utcnow()
else:
assessment = Assessment(
user_id=current_user.id,
school_id=current_user.school_id,
school_year_id=school_year.id,
vak_id=vak_id,
goal_id=goal_id,
status=status,
)
db.session.add(assessment)
db.session.commit()
# Auditlog enkel bij statuswijziging (niet bij elke klik)
audit_log('assessment.save', 'assessment',
target_type='goal', target_id=f'{vak_id}:{goal_id}',
detail={'status': status})
return jsonify({'assessment': assessment.to_dict()})
# ── Directeur schooloverzicht ──────────────────────────────────────────────────
@api_bp.route('/school/overview')
@login_required
@director_required
def school_overview():
if not current_user.school_id:
return jsonify({'error': 'Geen school gekoppeld'}), 400
school_year = get_active_year(current_user.school_id)
if not school_year:
return jsonify({'error': 'Geen actief schooljaar'}), 400
# year_id param: directeur/admin kan wisselen, leerkracht zit vast aan actief jaar
year_id_param = request.args.get('year_id')
if year_id_param and current_user.is_director:
year_id = int(year_id_param)
selected_year = SchoolYear.query.filter_by(
id=year_id, school_id=current_user.school_id
).first() or school_year
else:
selected_year = school_year
year_id = school_year.id
vak_id = request.args.get('vak_id')
teachers = User.query.filter_by(
school_id=current_user.school_id, role='teacher', is_active=True
).all()
query = Assessment.query.filter_by(
school_id=current_user.school_id, school_year_id=year_id
)
if vak_id:
query = query.filter_by(vak_id=vak_id)
by_teacher = {t.id: {} for t in teachers}
for a in query.all():
by_teacher.setdefault(a.user_id, {})
by_teacher[a.user_id].setdefault(a.vak_id, {})
by_teacher[a.user_id][a.vak_id][a.goal_id] = a.status
return jsonify({
'school_year': selected_year.to_dict(),
'teachers': [t.to_dict() for t in teachers],
'assessments_by_teacher': by_teacher,
})
# ── Gebruikersbeheer (school_ict / directeur) ──────────────────────────────────
@api_bp.route('/users', methods=['GET'])
@login_required
@director_required
def list_users():
users = User.query.filter_by(
school_id=current_user.school_id, is_active=True
).order_by(User.last_name, User.first_name).all()
return jsonify({'users': [u.to_dict() for u in users]})
@api_bp.route('/users', methods=['POST'])
@login_required
@director_required
def create_user():
data = request.get_json() or {}
email = data.get('email', '').strip().lower()
if not email:
return jsonify({'error': 'E-mailadres is verplicht'}), 400
if User.query.filter_by(email=email).first():
return jsonify({'error': 'E-mailadres is al in gebruik'}), 409
user = User(
email=email,
first_name=data.get('first_name', '').strip(),
last_name=data.get('last_name', '').strip(),
role='teacher',
school_id=current_user.school_id,
)
db.session.add(user)
db.session.commit()
return jsonify({'user': user.to_dict()}), 201
@api_bp.route('/users/<int:user_id>', methods=['DELETE'])
@login_required
@director_required
def delete_user(user_id):
user = User.query.filter_by(
id=user_id, school_id=current_user.school_id
).first_or_404()
user.is_active = False
db.session.commit()
return jsonify({'deleted': True})
# ── Schooljaren (directeur/admin leesbaar) ────────────────────────────────────
@api_bp.route('/school/years')
@login_required
@director_required
def get_school_years():
"""Geeft alle globale schooljaren terug (voor jaarselectie in directeur dashboard)."""
years = SchoolYear.query.filter_by(school_id=None) .order_by(SchoolYear.label.desc()).all()
return jsonify({'years': [y.to_dict() for y in years]})
# ── Huidig ingelogde gebruiker ────────────────────────────────────────────────
@api_bp.route('/me')
@login_required
def me():
school_year = get_active_year(current_user.school_id) if current_user.school_id else None
return jsonify({
'user': current_user.to_dict(),
'school_year': school_year.to_dict() if school_year else None,
})
# ── Klassen voor leerkracht (zelf instellen) ──────────────────────────────────
@api_bp.route('/my/classes', methods=['GET'])
@login_required
def my_classes():
"""Geeft alle beschikbare klassen en eigen klassen terug."""
if not current_user.school_id:
return jsonify({'all_classes': [], 'my_classes': []})
all_cls = Class.query.filter_by(school_id=current_user.school_id) .order_by(Class.name).all()
return jsonify({
'all_classes': [{'id': c.id, 'name': c.name} for c in all_cls],
'my_classes': [{'id': c.id, 'name': c.name} for c in current_user.classes],
})
@api_bp.route('/my/classes', methods=['PUT'])
@login_required
def set_my_classes():
"""Leerkracht stelt eigen klassen in."""
data = request.get_json() or {}
class_ids = data.get('class_ids', [])
classes = Class.query.filter(
Class.id.in_(class_ids),
Class.school_id == current_user.school_id
).all()
current_user.classes = classes
audit_log('class.user_assignment', 'class', target_type='user',
target_id=str(current_user.id),
detail={'class_ids': class_ids, 'class_names': [c.name for c in classes]})
db.session.commit()
return jsonify({'my_classes': [{'id': c.id, 'name': c.name} for c in current_user.classes]})
# ── Auditlog ──────────────────────────────────────────────────────────────────
@api_bp.route('/audit-log')
@login_required
def get_audit_log():
if not current_user.is_school_ict:
return jsonify({'error': 'Geen toegang'}), 403
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 50))
category = request.args.get('category')
search = request.args.get('search', '').strip()
query = AuditLog.query
# School ICT ziet enkel eigen school
if not current_user.is_scholengroep_ict:
query = query.filter(AuditLog.school_id == current_user.school_id)
if category:
query = query.filter(AuditLog.category == category)
if search:
query = query.filter(
db.or_(
AuditLog.action.ilike(f'%{search}%'),
AuditLog.detail.ilike(f'%{search}%'),
)
)
total = query.count()
entries = query.order_by(AuditLog.timestamp.desc()) .offset((page - 1) * per_page).limit(per_page).all()
return jsonify({
'total': total,
'page': page,
'pages': (total + per_page - 1) // per_page,
'entries': [e.to_dict() for e in entries],
})