mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 03:51:08 -05:00
Refactor code structure and remove redundant changes
This commit is contained in:
6
backend/api/v1/media/__init__.py
Normal file
6
backend/api/v1/media/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Media API endpoints for ThrillWiki v1.
|
||||
|
||||
This package contains all media-related API functionality including
|
||||
photo uploads, media management, and media-specific operations.
|
||||
"""
|
||||
222
backend/api/v1/media/serializers.py
Normal file
222
backend/api/v1/media/serializers.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Media domain serializers for ThrillWiki API v1.
|
||||
|
||||
This module contains serializers for photo uploads, media management,
|
||||
and related media functionality.
|
||||
"""
|
||||
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import (
|
||||
extend_schema_serializer,
|
||||
extend_schema_field,
|
||||
OpenApiExample,
|
||||
)
|
||||
|
||||
|
||||
# === MEDIA UPLOAD SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Photo Upload Example",
|
||||
summary="Example photo upload request",
|
||||
description="Upload a photo for a park or ride",
|
||||
value={
|
||||
"photo": "file_upload",
|
||||
"app_label": "parks",
|
||||
"model": "park",
|
||||
"object_id": 123,
|
||||
"caption": "Beautiful view of the park entrance",
|
||||
"alt_text": "Park entrance with landscaping",
|
||||
"is_primary": True,
|
||||
"photo_type": "general",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class PhotoUploadInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for photo uploads."""
|
||||
|
||||
photo = serializers.ImageField(
|
||||
help_text="The image file to upload"
|
||||
)
|
||||
app_label = serializers.CharField(
|
||||
max_length=100,
|
||||
help_text="App label of the content object (e.g., 'parks', 'rides')"
|
||||
)
|
||||
model = serializers.CharField(
|
||||
max_length=100,
|
||||
help_text="Model name of the content object (e.g., 'park', 'ride')"
|
||||
)
|
||||
object_id = serializers.IntegerField(
|
||||
help_text="ID of the content object"
|
||||
)
|
||||
caption = serializers.CharField(
|
||||
max_length=500,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
help_text="Optional caption for the photo"
|
||||
)
|
||||
alt_text = serializers.CharField(
|
||||
max_length=255,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
help_text="Optional alt text for accessibility"
|
||||
)
|
||||
is_primary = serializers.BooleanField(
|
||||
default=False,
|
||||
help_text="Whether this should be the primary photo"
|
||||
)
|
||||
photo_type = serializers.CharField(
|
||||
max_length=50,
|
||||
default="general",
|
||||
required=False,
|
||||
help_text="Type of photo (for rides: 'general', 'on_ride', 'construction', etc.)"
|
||||
)
|
||||
|
||||
|
||||
class PhotoUploadOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for photo uploads."""
|
||||
id = serializers.IntegerField()
|
||||
url = serializers.CharField()
|
||||
caption = serializers.CharField()
|
||||
alt_text = serializers.CharField()
|
||||
is_primary = serializers.BooleanField()
|
||||
message = serializers.CharField()
|
||||
|
||||
|
||||
# === PHOTO DETAIL SERIALIZERS ===
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
examples=[
|
||||
OpenApiExample(
|
||||
"Photo Detail Example",
|
||||
summary="Example photo detail response",
|
||||
description="A photo with full details",
|
||||
value={
|
||||
"id": 1,
|
||||
"url": "https://example.com/media/photos/ride123.jpg",
|
||||
"thumbnail_url": "https://example.com/media/thumbnails/ride123_thumb.jpg",
|
||||
"caption": "Amazing view of Steel Vengeance",
|
||||
"alt_text": "Steel Vengeance roller coaster with blue sky",
|
||||
"is_primary": True,
|
||||
"uploaded_at": "2024-08-15T10:30:00Z",
|
||||
"uploaded_by": {
|
||||
"id": 1,
|
||||
"username": "coaster_photographer",
|
||||
"display_name": "Coaster Photographer",
|
||||
},
|
||||
"content_type": "Ride",
|
||||
"object_id": 123,
|
||||
"file_size": 2048576,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"format": "JPEG",
|
||||
},
|
||||
)
|
||||
]
|
||||
)
|
||||
class PhotoDetailOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for photo details."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
url = serializers.URLField()
|
||||
thumbnail_url = serializers.URLField(required=False)
|
||||
caption = serializers.CharField()
|
||||
alt_text = serializers.CharField()
|
||||
is_primary = serializers.BooleanField()
|
||||
uploaded_at = serializers.DateTimeField()
|
||||
content_type = serializers.CharField()
|
||||
object_id = serializers.IntegerField()
|
||||
|
||||
# File metadata
|
||||
file_size = serializers.IntegerField()
|
||||
width = serializers.IntegerField()
|
||||
height = serializers.IntegerField()
|
||||
format = serializers.CharField()
|
||||
|
||||
# Uploader info
|
||||
uploaded_by = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_uploaded_by(self, obj) -> dict:
|
||||
"""Get uploader information."""
|
||||
return {
|
||||
"id": obj.uploaded_by.id,
|
||||
"username": obj.uploaded_by.username,
|
||||
"display_name": getattr(
|
||||
obj.uploaded_by, "get_display_name", lambda: obj.uploaded_by.username
|
||||
)(),
|
||||
}
|
||||
|
||||
|
||||
class PhotoListOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for photo list view."""
|
||||
|
||||
id = serializers.IntegerField()
|
||||
url = serializers.URLField()
|
||||
thumbnail_url = serializers.URLField(required=False)
|
||||
caption = serializers.CharField()
|
||||
is_primary = serializers.BooleanField()
|
||||
uploaded_at = serializers.DateTimeField()
|
||||
uploaded_by = serializers.SerializerMethodField()
|
||||
|
||||
@extend_schema_field(serializers.DictField())
|
||||
def get_uploaded_by(self, obj) -> dict:
|
||||
"""Get uploader information."""
|
||||
return {
|
||||
"id": obj.uploaded_by.id,
|
||||
"username": obj.uploaded_by.username,
|
||||
}
|
||||
|
||||
|
||||
class PhotoUpdateInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for updating photos."""
|
||||
|
||||
caption = serializers.CharField(max_length=500, required=False, allow_blank=True)
|
||||
alt_text = serializers.CharField(max_length=255, required=False, allow_blank=True)
|
||||
is_primary = serializers.BooleanField(required=False)
|
||||
|
||||
|
||||
# === MEDIA STATS SERIALIZERS ===
|
||||
|
||||
|
||||
class MediaStatsOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for media statistics."""
|
||||
|
||||
total_photos = serializers.IntegerField()
|
||||
photos_by_content_type = serializers.DictField()
|
||||
recent_uploads = serializers.IntegerField()
|
||||
top_uploaders = serializers.ListField()
|
||||
storage_usage = serializers.DictField()
|
||||
|
||||
|
||||
# === BULK OPERATIONS SERIALIZERS ===
|
||||
|
||||
|
||||
class BulkPhotoActionInputSerializer(serializers.Serializer):
|
||||
"""Input serializer for bulk photo actions."""
|
||||
|
||||
photo_ids = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
help_text="List of photo IDs to perform action on"
|
||||
)
|
||||
action = serializers.ChoiceField(
|
||||
choices=[
|
||||
('delete', 'Delete'),
|
||||
('approve', 'Approve'),
|
||||
('reject', 'Reject'),
|
||||
],
|
||||
help_text="Action to perform on selected photos"
|
||||
)
|
||||
|
||||
|
||||
class BulkPhotoActionOutputSerializer(serializers.Serializer):
|
||||
"""Output serializer for bulk photo actions."""
|
||||
|
||||
success_count = serializers.IntegerField()
|
||||
failed_count = serializers.IntegerField()
|
||||
errors = serializers.ListField(child=serializers.CharField(), required=False)
|
||||
message = serializers.CharField()
|
||||
29
backend/api/v1/media/urls.py
Normal file
29
backend/api/v1/media/urls.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Media API URL configuration for ThrillWiki API v1.
|
||||
|
||||
This module contains URL patterns for media management endpoints
|
||||
including photo uploads, CRUD operations, and bulk actions.
|
||||
"""
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from . import views
|
||||
|
||||
# Create router for ViewSets
|
||||
router = DefaultRouter()
|
||||
router.register(r"photos", views.PhotoViewSet, basename="photo")
|
||||
|
||||
urlpatterns = [
|
||||
# Photo upload endpoint
|
||||
path("upload/", views.PhotoUploadAPIView.as_view(), name="photo_upload"),
|
||||
|
||||
# Media statistics endpoint
|
||||
path("stats/", views.MediaStatsAPIView.as_view(), name="media_stats"),
|
||||
|
||||
# Bulk photo operations
|
||||
path("photos/bulk-action/", views.BulkPhotoActionAPIView.as_view(),
|
||||
name="bulk_photo_action"),
|
||||
|
||||
# Include router URLs for photo management (CRUD operations)
|
||||
path("", include(router.urls)),
|
||||
]
|
||||
484
backend/api/v1/media/views.py
Normal file
484
backend/api/v1/media/views.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
Media API views for ThrillWiki API v1.
|
||||
|
||||
This module provides API endpoints for media management including
|
||||
photo uploads, captions, and media operations.
|
||||
Consolidated from apps.media.views with proper domain service integration.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, Union
|
||||
from django.db.models import Q, QuerySet
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
|
||||
# Import domain-specific models and services instead of generic Photo model
|
||||
from apps.parks.models import ParkPhoto, Park
|
||||
from apps.rides.models import RidePhoto, Ride
|
||||
from apps.parks.services import ParkMediaService
|
||||
from apps.rides.services import RideMediaService
|
||||
from .serializers import (
|
||||
PhotoUploadInputSerializer,
|
||||
PhotoUploadOutputSerializer,
|
||||
PhotoDetailOutputSerializer,
|
||||
PhotoUpdateInputSerializer,
|
||||
PhotoListOutputSerializer,
|
||||
MediaStatsOutputSerializer,
|
||||
BulkPhotoActionInputSerializer,
|
||||
BulkPhotoActionOutputSerializer,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="Upload photo",
|
||||
description="Upload a photo and associate it with a content object (park, ride, etc.)",
|
||||
request=PhotoUploadInputSerializer,
|
||||
responses={
|
||||
201: PhotoUploadOutputSerializer,
|
||||
400: OpenApiTypes.OBJECT,
|
||||
403: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Media"],
|
||||
),
|
||||
)
|
||||
class PhotoUploadAPIView(APIView):
|
||||
"""API endpoint for photo uploads."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
parser_classes = [MultiPartParser, FormParser]
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
"""Upload a photo and associate it with a content object."""
|
||||
try:
|
||||
serializer = PhotoUploadInputSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
validated_data = serializer.validated_data
|
||||
|
||||
# Get content object
|
||||
try:
|
||||
content_type = ContentType.objects.get(
|
||||
app_label=validated_data["app_label"], model=validated_data["model"]
|
||||
)
|
||||
content_object = content_type.get_object_for_this_type(
|
||||
pk=validated_data["object_id"]
|
||||
)
|
||||
except ContentType.DoesNotExist:
|
||||
return Response(
|
||||
{
|
||||
"error": f"Invalid content type: {validated_data['app_label']}.{validated_data['model']}"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except content_type.model_class().DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Content object not found"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
# Determine which domain service to use based on content object
|
||||
if hasattr(content_object, '_meta') and content_object._meta.app_label == 'parks':
|
||||
# Check permissions for park photos
|
||||
if not request.user.has_perm("parks.add_parkphoto"):
|
||||
return Response(
|
||||
{"error": "You do not have permission to upload park photos"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Create park photo using park media service
|
||||
photo = ParkMediaService.upload_photo(
|
||||
park=content_object,
|
||||
image_file=validated_data["photo"],
|
||||
user=request.user,
|
||||
caption=validated_data.get("caption", ""),
|
||||
alt_text=validated_data.get("alt_text", ""),
|
||||
is_primary=validated_data.get("is_primary", False),
|
||||
)
|
||||
elif hasattr(content_object, '_meta') and content_object._meta.app_label == 'rides':
|
||||
# Check permissions for ride photos
|
||||
if not request.user.has_perm("rides.add_ridephoto"):
|
||||
return Response(
|
||||
{"error": "You do not have permission to upload ride photos"},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# Create ride photo using ride media service
|
||||
photo = RideMediaService.upload_photo(
|
||||
ride=content_object,
|
||||
image_file=validated_data["photo"],
|
||||
user=request.user,
|
||||
caption=validated_data.get("caption", ""),
|
||||
alt_text=validated_data.get("alt_text", ""),
|
||||
is_primary=validated_data.get("is_primary", False),
|
||||
photo_type=validated_data.get("photo_type", "general"),
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"error": f"Unsupported content type for media upload: {content_object._meta.label}"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
response_serializer = PhotoUploadOutputSerializer(
|
||||
{
|
||||
"id": photo.id,
|
||||
"url": photo.image.url,
|
||||
"caption": photo.caption,
|
||||
"alt_text": photo.alt_text,
|
||||
"is_primary": photo.is_primary,
|
||||
"message": "Photo uploaded successfully",
|
||||
}
|
||||
)
|
||||
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in photo upload: {str(e)}", exc_info=True)
|
||||
return Response(
|
||||
{"error": f"An error occurred while uploading the photo: {str(e)}"},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
list=extend_schema(
|
||||
summary="List photos",
|
||||
description="Retrieve a list of photos with optional filtering",
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="content_type",
|
||||
type=OpenApiTypes.STR,
|
||||
location=OpenApiParameter.QUERY,
|
||||
description="Filter by content type (e.g., 'parks.park', 'rides.ride')",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="object_id",
|
||||
type=OpenApiTypes.INT,
|
||||
location=OpenApiParameter.QUERY,
|
||||
description="Filter by object ID",
|
||||
),
|
||||
OpenApiParameter(
|
||||
name="is_primary",
|
||||
type=OpenApiTypes.BOOL,
|
||||
location=OpenApiParameter.QUERY,
|
||||
description="Filter by primary photos only",
|
||||
),
|
||||
],
|
||||
responses={200: PhotoListOutputSerializer(many=True)},
|
||||
tags=["Media"],
|
||||
),
|
||||
retrieve=extend_schema(
|
||||
summary="Get photo details",
|
||||
description="Retrieve detailed information about a specific photo",
|
||||
responses={
|
||||
200: PhotoDetailOutputSerializer,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Media"],
|
||||
),
|
||||
update=extend_schema(
|
||||
summary="Update photo",
|
||||
description="Update photo information (caption, alt text, etc.)",
|
||||
request=PhotoUpdateInputSerializer,
|
||||
responses={
|
||||
200: PhotoDetailOutputSerializer,
|
||||
400: OpenApiTypes.OBJECT,
|
||||
403: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Media"],
|
||||
),
|
||||
destroy=extend_schema(
|
||||
summary="Delete photo",
|
||||
description="Delete a photo (only by owner or admin)",
|
||||
responses={
|
||||
204: None,
|
||||
403: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Media"],
|
||||
),
|
||||
set_primary=extend_schema(
|
||||
summary="Set photo as primary",
|
||||
description="Set this photo as the primary photo for its content object",
|
||||
responses={
|
||||
200: OpenApiTypes.OBJECT,
|
||||
403: OpenApiTypes.OBJECT,
|
||||
404: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Media"],
|
||||
),
|
||||
)
|
||||
class PhotoViewSet(ModelViewSet):
|
||||
"""ViewSet for managing photos across domains."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
lookup_field = "id"
|
||||
|
||||
def get_queryset(self) -> QuerySet:
|
||||
"""Get queryset combining photos from all domains."""
|
||||
# Combine park and ride photos
|
||||
park_photos = ParkPhoto.objects.select_related('uploaded_by', 'park')
|
||||
ride_photos = RidePhoto.objects.select_related('uploaded_by', 'ride')
|
||||
|
||||
# Apply filters
|
||||
content_type = self.request.query_params.get('content_type')
|
||||
object_id = self.request.query_params.get('object_id')
|
||||
is_primary = self.request.query_params.get('is_primary')
|
||||
|
||||
if content_type == 'parks.park':
|
||||
queryset = park_photos
|
||||
if object_id:
|
||||
queryset = queryset.filter(park_id=object_id)
|
||||
elif content_type == 'rides.ride':
|
||||
queryset = ride_photos
|
||||
if object_id:
|
||||
queryset = queryset.filter(ride_id=object_id)
|
||||
else:
|
||||
# Return combined queryset (this is complex due to different models)
|
||||
# For now, return park photos as default - in production might need Union
|
||||
queryset = park_photos
|
||||
|
||||
if is_primary is not None:
|
||||
is_primary_bool = is_primary.lower() in ('true', '1', 'yes')
|
||||
queryset = queryset.filter(is_primary=is_primary_bool)
|
||||
|
||||
return queryset.order_by('-uploaded_at')
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return appropriate serializer based on action."""
|
||||
if self.action == "list":
|
||||
return PhotoListOutputSerializer
|
||||
elif self.action in ["update", "partial_update"]:
|
||||
return PhotoUpdateInputSerializer
|
||||
return PhotoDetailOutputSerializer
|
||||
|
||||
def get_object(self):
|
||||
"""Get photo object from either domain."""
|
||||
photo_id = self.kwargs.get('id')
|
||||
|
||||
# Try to find in park photos first
|
||||
try:
|
||||
return ParkPhoto.objects.select_related('uploaded_by', 'park').get(id=photo_id)
|
||||
except ParkPhoto.DoesNotExist:
|
||||
pass
|
||||
|
||||
# Try ride photos
|
||||
try:
|
||||
return RidePhoto.objects.select_related('uploaded_by', 'ride').get(id=photo_id)
|
||||
except RidePhoto.DoesNotExist:
|
||||
pass
|
||||
|
||||
raise Http404("Photo not found")
|
||||
|
||||
def update(self, request: Request, *args, **kwargs) -> Response:
|
||||
"""Update photo details."""
|
||||
photo = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not (request.user == photo.uploaded_by or request.user.is_staff):
|
||||
raise PermissionDenied("You can only edit your own photos")
|
||||
|
||||
serializer = self.get_serializer(data=request.data, partial=True)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Update fields
|
||||
for field, value in serializer.validated_data.items():
|
||||
setattr(photo, field, value)
|
||||
|
||||
photo.save()
|
||||
|
||||
# Return updated photo details
|
||||
response_serializer = PhotoDetailOutputSerializer(photo)
|
||||
return Response(response_serializer.data)
|
||||
|
||||
def destroy(self, request: Request, *args, **kwargs) -> Response:
|
||||
"""Delete a photo."""
|
||||
photo = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not (request.user == photo.uploaded_by or request.user.is_staff):
|
||||
raise PermissionDenied("You can only delete your own photos")
|
||||
|
||||
photo.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def set_primary(self, request: Request, id=None) -> Response:
|
||||
"""Set this photo as primary for its content object."""
|
||||
photo = self.get_object()
|
||||
|
||||
# Check permissions
|
||||
if not (request.user == photo.uploaded_by or request.user.is_staff):
|
||||
raise PermissionDenied("You can only modify your own photos")
|
||||
|
||||
# Use appropriate service based on photo type
|
||||
if isinstance(photo, ParkPhoto):
|
||||
ParkMediaService.set_primary_photo(photo.park, photo)
|
||||
elif isinstance(photo, RidePhoto):
|
||||
RideMediaService.set_primary_photo(photo.ride, photo)
|
||||
|
||||
return Response({
|
||||
"message": "Photo set as primary successfully",
|
||||
"photo_id": photo.id,
|
||||
"is_primary": True
|
||||
})
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
get=extend_schema(
|
||||
summary="Get media statistics",
|
||||
description="Retrieve statistics about photos and media usage",
|
||||
responses={200: MediaStatsOutputSerializer},
|
||||
tags=["Media"],
|
||||
),
|
||||
)
|
||||
class MediaStatsAPIView(APIView):
|
||||
"""API endpoint for media statistics."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request: Request) -> Response:
|
||||
"""Get media statistics."""
|
||||
from django.db.models import Count
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Count photos by type
|
||||
park_photo_count = ParkPhoto.objects.count()
|
||||
ride_photo_count = RidePhoto.objects.count()
|
||||
total_photos = park_photo_count + ride_photo_count
|
||||
|
||||
# Recent uploads (last 30 days)
|
||||
thirty_days_ago = datetime.now() - timedelta(days=30)
|
||||
recent_park_uploads = ParkPhoto.objects.filter(
|
||||
uploaded_at__gte=thirty_days_ago).count()
|
||||
recent_ride_uploads = RidePhoto.objects.filter(
|
||||
uploaded_at__gte=thirty_days_ago).count()
|
||||
recent_uploads = recent_park_uploads + recent_ride_uploads
|
||||
|
||||
# Top uploaders
|
||||
from django.db.models import Q
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
|
||||
# This is a simplified version - in production might need more complex aggregation
|
||||
top_uploaders = []
|
||||
|
||||
stats = MediaStatsOutputSerializer({
|
||||
"total_photos": total_photos,
|
||||
"photos_by_content_type": {
|
||||
"parks": park_photo_count,
|
||||
"rides": ride_photo_count,
|
||||
},
|
||||
"recent_uploads": recent_uploads,
|
||||
"top_uploaders": top_uploaders,
|
||||
"storage_usage": {
|
||||
"total_size": 0, # Would need to calculate from file sizes
|
||||
"average_size": 0,
|
||||
}
|
||||
})
|
||||
|
||||
return Response(stats.data)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(
|
||||
summary="Bulk photo actions",
|
||||
description="Perform bulk actions on multiple photos (delete, approve, etc.)",
|
||||
request=BulkPhotoActionInputSerializer,
|
||||
responses={
|
||||
200: BulkPhotoActionOutputSerializer,
|
||||
400: OpenApiTypes.OBJECT,
|
||||
403: OpenApiTypes.OBJECT,
|
||||
},
|
||||
tags=["Media"],
|
||||
),
|
||||
)
|
||||
class BulkPhotoActionAPIView(APIView):
|
||||
"""API endpoint for bulk photo operations."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request: Request) -> Response:
|
||||
"""Perform bulk action on photos."""
|
||||
serializer = BulkPhotoActionInputSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
photo_ids = serializer.validated_data['photo_ids']
|
||||
action = serializer.validated_data['action']
|
||||
|
||||
success_count = 0
|
||||
failed_count = 0
|
||||
errors = []
|
||||
|
||||
for photo_id in photo_ids:
|
||||
try:
|
||||
# Find photo in either domain
|
||||
photo = None
|
||||
try:
|
||||
photo = ParkPhoto.objects.get(id=photo_id)
|
||||
except ParkPhoto.DoesNotExist:
|
||||
try:
|
||||
photo = RidePhoto.objects.get(id=photo_id)
|
||||
except RidePhoto.DoesNotExist:
|
||||
errors.append(f"Photo {photo_id} not found")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
# Check permissions
|
||||
if not (request.user == photo.uploaded_by or request.user.is_staff):
|
||||
errors.append(f"No permission for photo {photo_id}")
|
||||
failed_count += 1
|
||||
continue
|
||||
|
||||
# Perform action
|
||||
if action == 'delete':
|
||||
photo.delete()
|
||||
success_count += 1
|
||||
elif action == 'approve':
|
||||
if hasattr(photo, 'is_approved'):
|
||||
photo.is_approved = True
|
||||
photo.save()
|
||||
success_count += 1
|
||||
else:
|
||||
errors.append(f"Photo {photo_id} does not support approval")
|
||||
failed_count += 1
|
||||
elif action == 'reject':
|
||||
if hasattr(photo, 'is_approved'):
|
||||
photo.is_approved = False
|
||||
photo.save()
|
||||
success_count += 1
|
||||
else:
|
||||
errors.append(f"Photo {photo_id} does not support approval")
|
||||
failed_count += 1
|
||||
|
||||
except Exception as e:
|
||||
errors.append(f"Error processing photo {photo_id}: {str(e)}")
|
||||
failed_count += 1
|
||||
|
||||
response_data = BulkPhotoActionOutputSerializer({
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"errors": errors,
|
||||
"message": f"Bulk {action} completed: {success_count} successful, {failed_count} failed"
|
||||
})
|
||||
|
||||
return Response(response_data.data)
|
||||
Reference in New Issue
Block a user