roosterwijs/backend/modules/huisstijl/views.py

147 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.response import Response
from .models import Huisstijl
from .serializers import HuisstijlSerializer
TOEGESTAAN = {"png", "jpg", "jpeg", "gif", "webp", "svg"}
MAX_BYTES = 3 * 1024 * 1024 # 3 MB
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, SVG, JPG, GIF of WebP.")
return {"ok": False, "fouten": fouten, "waarschuwingen": waarsch, "info": {}}
if getattr(f, "size", 0) and f.size > MAX_BYTES:
waarsch.append(f"Het bestand is groot ({f.size // 1024} KB); overweeg een kleiner logo.")
if ext == "svg":
return {"ok": True, "fouten": [], "waarschuwingen": waarsch,
"info": {"formaat": "SVG", "transparant": True, "breedte": None, "hoogte": None, "achtergrond": None}}
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
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])
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)
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)
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 ""))