AccroCBio/backend/education/editor_views.py
Louis Mazin 4fd1c14d1d test
2026-10-15 16:45:02 +02:00

140 lines
5.7 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.

"""Routes d’administration du contenu métier protégées par session Django."""
import re
import uuid
from pathlib import Path
from django.conf import settings
from django.http import FileResponse, Http404
from rest_framework import permissions, serializers, status
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Announcement, ContactMessage, Resource, Testimonial
KEY_PATTERN = re.compile(r'^[a-z][a-z0-9._-]{1,99}$')
IMAGE_EXTENSIONS = {'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp'}
class EditorImageUploadView(APIView):
permission_classes = [permissions.IsAdminUser]
parser_classes = [MultiPartParser, FormParser]
def post(self, request):
file = request.FILES.get('image')
if not file or file.size > 5 * 1024 * 1024:
return Response({'detail': 'Image absente ou supérieure à 5 Mo.'}, status=400)
content_type = file.content_type or ''
if content_type not in IMAGE_EXTENSIONS:
return Response({'detail': 'Formats acceptés : PNG, JPEG, WebP.'}, status=400)
header = file.read(16)
file.seek(0)
valid = {
'image/png': header.startswith(b'\x89PNG\r\n\x1a\n'),
'image/jpeg': header.startswith(b'\xff\xd8\xff'),
'image/webp': header.startswith(b'RIFF') and header[8:12] == b'WEBP',
}
if not valid[content_type]:
return Response({'detail': 'Le contenu du fichier ne correspond pas à son format.'}, status=400)
filename = f'{uuid.uuid4().hex}.{IMAGE_EXTENSIONS[content_type]}'
directory = Path(settings.MEDIA_ROOT) / 'editor'
directory.mkdir(parents=True, exist_ok=True)
with (directory / filename).open('wb') as target:
for chunk in file.chunks():
target.write(chunk)
return Response({'url': f'/api/education/editor-images/{filename}'}, status=201)
def serve_editor_image(request, filename):
# Noms générés aléatoirement uniquement ; aucune traversée de dossier possible.
if not re.fullmatch(r'[a-f0-9]{32}\.(png|jpg|webp)', filename):
raise Http404()
image_path = Path(settings.MEDIA_ROOT) / 'editor' / filename
if not image_path.is_file():
raise Http404()
ext = image_path.suffix.lower()
content_type = {'.png': 'image/png', '.jpg': 'image/jpeg', '.webp': 'image/webp'}[ext]
return FileResponse(image_path.open('rb'), content_type=content_type)
RESOURCE_FIELDS = ('title', 'description', 'category', 'kind', 'url', 'published', 'position')
REVIEW_FIELDS = ('author', 'description', 'quote', 'published', 'position')
ANNOUNCEMENT_FIELDS = ('title', 'body', 'cta_label', 'active', 'starts_at', 'ends_at')
class EditorResourceSerializer(serializers.ModelSerializer):
class Meta:
model = Resource
fields = ('id',) + RESOURCE_FIELDS
class EditorReviewSerializer(serializers.ModelSerializer):
class Meta:
model = Testimonial
fields = ('id',) + REVIEW_FIELDS
class EditorAnnouncementSerializer(serializers.ModelSerializer):
class Meta:
model = Announcement
fields = ('id',) + ANNOUNCEMENT_FIELDS
class EditorDataView(APIView):
permission_classes = [permissions.IsAdminUser]
MODELS = {
'resources': (Resource, EditorResourceSerializer),
'reviews': (Testimonial, EditorReviewSerializer),
'announcements': (Announcement, EditorAnnouncementSerializer),
}
def get(self, request, category):
if category not in (*self.MODELS.keys(), 'messages'):
raise Http404()
if category == 'messages':
return Response(list(ContactMessage.objects.values('id', 'name', 'email', 'phone', 'level', 'message', 'read', 'created_at').order_by('-created_at')[:300]))
model, serializer = self.MODELS[category]
return Response(serializer(model.objects.all(), many=True).data)
def post(self, request, category):
if category not in (*self.MODELS.keys(), 'messages'):
raise Http404()
if category == 'messages':
return Response({'detail': 'Action non autorisée.'}, status=405)
_, serializer_class = self.MODELS[category]
serializer = serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
return Response(serializer_class(instance).data, status=201)
class EditorDataDetailView(APIView):
permission_classes = [permissions.IsAdminUser]
MODELS = EditorDataView.MODELS
def _get(self, category, pk):
if category not in (*self.MODELS.keys(), 'messages'):
raise Http404()
from django.shortcuts import get_object_or_404
if category == 'messages':
return get_object_or_404(ContactMessage, pk=pk)
return get_object_or_404(self.MODELS[category][0], pk=pk)
def patch(self, request, category, pk):
instance = self._get(category, pk)
if category == 'messages':
if not isinstance(request.data.get('read'), bool):
return Response({'detail': 'Champ read invalide.'}, status=400)
instance.read = request.data['read']
instance.save(update_fields=['read'])
return Response({'id': instance.id, 'read': instance.read})
serializer = self.MODELS[category][1](instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
def delete(self, request, category, pk):
instance = self._get(category, pk)
instance.delete()
return Response(status=204)