140 lines
5.7 KiB
Python
140 lines
5.7 KiB
Python
"""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)
|