"""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)