Refactor code structure and remove redundant changes

This commit is contained in:
pacnpal
2025-08-26 13:19:04 -04:00
parent bf7e0c0f40
commit 831be6a2ee
151 changed files with 16260 additions and 9137 deletions

View File

@@ -0,0 +1,6 @@
"""
Media API module for ThrillWiki API v1.
This module provides API endpoints for media management including
photo uploads, captions, and media operations.
"""

View File

@@ -0,0 +1,113 @@
"""
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 SERIALIZERS ===
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()
@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,
},
)
]
)
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)

View File

@@ -0,0 +1,19 @@
"""
Media API URL configuration.
Centralized from apps.media.urls
"""
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"),
# Include router URLs for photo management
path("", include(router.urls)),
]

View File

@@ -0,0 +1,233 @@
"""
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
"""
import json
import logging
from typing import Any, Dict
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
from apps.rides.models import RidePhoto
from apps.parks.services import ParkMediaService
from apps.rides.services import RideMediaService
from apps.core.services.media_service import MediaService
from .serializers import (
PhotoUploadInputSerializer,
PhotoUploadOutputSerializer,
PhotoDetailOutputSerializer,
PhotoUpdateInputSerializer,
PhotoListOutputSerializer,
)
from ..parks.serializers import ParkPhotoSerializer
from ..rides.serializers import RidePhotoSerializer
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')",
),
OpenApiParameter(
name="object_id",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
description="Filter by object ID",
),
],
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."""
permission_classes = [IsAuthenticated]
lookup_field = "id"
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