"""Logo-beheer met kwaliteitscheck. GET /api/huisstijl/ -> huidig logo (alle ingelogde gebruikers). PUT /api/huisstijl/ -> upload logo (alleen beheerders); voert een kwaliteitscheck uit en geeft waarschuwingen terug. DELETE /api/huisstijl/ -> logo verwijderen (alleen beheerders). """ from rest_framework.decorators import api_view, parser_classes, permission_classes from rest_framework.parsers import FormParser, JSONParser, MultiPartParser from rest_framework.response import Response from core.audit import audit_log from core.permissions import CoreDataPermission, module_permission_class from .models import Huisstijl from .serializers import HuisstijlSerializer TOEGESTAAN = {"png", "jpg", "jpeg", "gif", "webp"} MAX_BYTES = 3 * 1024 * 1024 # 3 MB MAX_PIXELS = 4000 * 4000 HuisstijlModuleEnabled = module_permission_class("huisstijl") def _hex(rgb): return "#%02x%02x%02x" % (rgb[0], rgb[1], rgb[2]) def analyseer_logo(f): """Beoordeel een geüpload logo. Geeft {ok, fouten, waarschuwingen, info}. 'ok' is False bij harde problemen (verkeerd bestandstype, onleesbaar). Anders wordt het bestand opgeslagen, maar mogelijk met waarschuwingen (bv. een niet-transparante / gevulde achtergrond, of lage resolutie). """ naam = (f.name or "").lower() ext = naam.rsplit(".", 1)[-1] if "." in naam else "" fouten, waarsch = [], [] if ext not in TOEGESTAAN: fouten.append(f"Bestandstype '.{ext}' wordt niet ondersteund. Gebruik PNG, JPG, GIF of WebP.") return {"ok": False, "fouten": fouten, "waarschuwingen": waarsch, "info": {}} if getattr(f, "size", 0) and f.size > MAX_BYTES: fouten.append(f"Het bestand is te groot ({f.size // 1024} KB); maximaal 3 MB.") return {"ok": False, "fouten": fouten, "waarschuwingen": waarsch, "info": {}} try: from PIL import Image except Exception: # Zonder Pillow slaan we toch op, maar zonder beeldcontrole. return {"ok": True, "fouten": [], "waarschuwingen": waarsch + ["Beeldcontrole niet beschikbaar."], "info": {"formaat": ext.upper()}} try: f.seek(0) img = Image.open(f) img.load() except Exception: return {"ok": False, "fouten": ["Kan de afbeelding niet lezen of het bestand is beschadigd."], "waarschuwingen": [], "info": {}} breedte, hoogte = img.size if breedte * hoogte > MAX_PIXELS: fouten.append("Afbeelding is te groot in pixels; maximaal 4000 x 4000.") return {"ok": False, "fouten": fouten, "waarschuwingen": waarsch, "info": {}} transparant = False achtergrond = None heeft_alpha = img.mode in ("RGBA", "LA") or (img.mode == "P" and "transparency" in img.info) if heeft_alpha: rgba = img.convert("RGBA") hoeken = [rgba.getpixel((0, 0)), rgba.getpixel((breedte - 1, 0)), rgba.getpixel((0, hoogte - 1)), rgba.getpixel((breedte - 1, hoogte - 1))] if all(p[3] < 16 for p in hoeken): transparant = True else: achtergrond = hoeken[0][:3] waarsch.append("De hoeken zijn niet doorzichtig; het logo lijkt een gevulde achtergrond te hebben. " "Op een gekleurde balk valt dat lelijk op — gebruik bij voorkeur een transparante PNG of SVG.") else: rgb = img.convert("RGB") hoeken = [rgb.getpixel((0, 0)), rgb.getpixel((breedte - 1, 0)), rgb.getpixel((0, hoogte - 1)), rgb.getpixel((breedte - 1, hoogte - 1))] achtergrond = hoeken[0] eensgezind = len(set(hoeken)) == 1 if eensgezind and min(achtergrond) > 245: waarsch.append("Effen witte achtergrond zonder transparantie. Op een gekleurd vlak valt het witte blok op; " "een transparante PNG of SVG is netter.") elif eensgezind: waarsch.append(f"Effen achtergrondkleur gedetecteerd ({_hex(achtergrond)}). Gebruik bij voorkeur een transparante PNG of SVG.") else: waarsch.append("Geen transparantie (bv. JPG). Voor een nette weergave op gekleurde vlakken is een transparante PNG of SVG beter.") if breedte < 80 or hoogte < 80: waarsch.append(f"Lage resolutie ({breedte}×{hoogte}px); dit kan korrelig printen.") info = { "formaat": img.format or ext.upper(), "breedte": breedte, "hoogte": hoogte, "transparant": transparant, "achtergrond": _hex(achtergrond) if achtergrond else None, } return {"ok": True, "fouten": [], "waarschuwingen": waarsch, "info": info} @api_view(["GET", "PUT", "POST", "DELETE"]) @parser_classes([MultiPartParser, FormParser, JSONParser]) @permission_classes([CoreDataPermission, HuisstijlModuleEnabled]) def huisstijl(request): obj = Huisstijl.actueel() schrijven = request.method in ("PUT", "POST", "DELETE") if schrijven and not (request.user and request.user.is_authenticated and request.user.is_staff): return Response({"detail": "Alleen beheerders (is_staff) kunnen het logo wijzigen. Vraag een beheerder om het logo in te stellen."}, status=403) if request.method == "DELETE": if obj.logo: obj.logo.delete(save=True) audit_log(request, "delete", "Huisstijl.logo") return Response(HuisstijlSerializer(obj, context={"request": request}).data) if request.method in ("PUT", "POST"): analyse = None # Thema-tekstvelden (kleuren/naam) — uit JSON of form-data. for veld in ("schoolnaam", "primaire_kleur", "accent_kleur"): if veld in request.data: waarde = (request.data.get(veld) or "").strip() if veld.endswith("_kleur") and waarde and not _is_kleur(waarde): return Response({"detail": f"Ongeldige kleur voor {veld}: gebruik een hex-code zoals #2563eb."}, status=400) setattr(obj, veld, waarde) # Optioneel een nieuw logo. f = request.FILES.get("logo") if f: try: analyse = analyseer_logo(f) except Exception as e: # noqa: BLE001 — geef een leesbare reden i.p.v. 500. return Response({"detail": f"Kon de afbeelding niet verwerken: {e}"}, status=400) if not analyse["ok"]: return Response({"detail": " ".join(analyse["fouten"]), "kwaliteit": analyse}, status=400) f.seek(0) if obj.logo: obj.logo.delete(save=False) obj.logo = f try: obj.save() except Exception as e: # noqa: BLE001 return Response({"detail": f"Opslaan mislukte: {e}"}, status=400) audit_log(request, "update", "Huisstijl:1", {"logo": bool(f)}) data = HuisstijlSerializer(obj, context={"request": request}).data if analyse is not None: data["kwaliteit"] = analyse return Response(data) return Response(HuisstijlSerializer(obj, context={"request": request}).data) def _is_kleur(s): import re return bool(re.fullmatch(r"#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})", s or ""))