nexus-5/core/graphql/mutations/session_image.py
2026-01-26 11:09:40 -05:00

221 lines
8.2 KiB
Python

from typing import Optional, cast
import io
import strawberry
from strawberry import Info
from strawberry.file_uploads import Upload
from strawberry.relay import GlobalID
from channels.db import database_sync_to_async
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from core.models.session import ServiceSession, ProjectSession
from core.models.session_image import ServiceSessionImage, ProjectSessionImage
from core.graphql.types.session_image import (
ServiceSessionImageType,
ProjectSessionImageType,
)
from core.graphql.inputs.session_image import (
ServiceSessionImageUpdateInput,
ProjectSessionImageUpdateInput,
)
from core.graphql.utils import update_object, delete_object, _decode_global_id
from core.services.events import (
publish_session_image_uploaded,
publish_session_image_updated,
publish_session_image_deleted,
publish_session_media_internal_flagged,
)
def _verify_image_bytes(data: bytes) -> None:
"""
Verify the uploaded bytes are a valid image payload using Pillow.
Uses a safe import for UnidentifiedImageError for broader compatibility.
"""
from PIL import Image as PilImage
try:
from PIL import UnidentifiedImageError as _UIE # type: ignore
except (ImportError, AttributeError):
_UIE = None
invalid_img_exc = (_UIE, OSError, ValueError) if _UIE else (OSError, ValueError)
try:
PilImage.open(io.BytesIO(data)).verify()
except invalid_img_exc:
raise ValidationError("Uploaded file is not a valid image.")
@strawberry.type
class Mutation:
@strawberry.mutation(description="Upload an image to a ServiceSession")
async def upload_service_session_image(
self,
info: Info,
session_id: GlobalID,
file: Upload,
title: Optional[str] = None,
notes: Optional[str] = None,
internal: bool = True,
) -> ServiceSessionImageType:
req_profile = getattr(info.context.request, "profile", None)
if not req_profile:
raise ValidationError("Authentication required.")
if not file or not getattr(file, "filename", None):
raise ValidationError("No file provided.")
filename: str = file.filename
content_type: str = getattr(file, "content_type", "") or ""
data = await file.read()
if not data:
raise ValidationError("Empty file upload.")
_verify_image_bytes(data)
sess_pk = _decode_global_id(session_id)
def _create_img_sync() -> ServiceSessionImage:
sess = ServiceSession.objects.get(pk=sess_pk)
img = ServiceSessionImage(
title=title or "",
notes=notes or "",
service_session=sess,
uploaded_by_team_profile=req_profile,
content_type=content_type,
internal=internal,
)
img.image.save(filename, ContentFile(data), save=True)
return img
instance: ServiceSessionImage = await database_sync_to_async(_create_img_sync)()
# Publish events
profile = getattr(info.context.request, 'profile', None)
await publish_session_image_uploaded(
image_id=str(instance.id),
session_id=str(instance.service_session_id),
is_internal=internal,
triggered_by=profile
)
return cast(ServiceSessionImageType, instance)
@strawberry.mutation(description="Upload an image to a ProjectSession")
async def upload_project_session_image(
self,
info: Info,
session_id: GlobalID,
file: Upload,
title: Optional[str] = None,
notes: Optional[str] = None,
internal: bool = True,
) -> ProjectSessionImageType:
req_profile = getattr(info.context.request, "profile", None)
if not req_profile:
raise ValidationError("Authentication required.")
if not file or not getattr(file, "filename", None):
raise ValidationError("No file provided.")
filename: str = file.filename
content_type: str = getattr(file, "content_type", "") or ""
data = await file.read()
if not data:
raise ValidationError("Empty file upload.")
_verify_image_bytes(data)
sess_pk = _decode_global_id(session_id)
def _create_img_sync() -> ProjectSessionImage:
sess = ProjectSession.objects.get(pk=sess_pk)
img = ProjectSessionImage(
title=title or "",
notes=notes or "",
project_session=sess,
uploaded_by_team_profile=req_profile,
content_type=content_type,
internal=internal,
)
img.image.save(filename, ContentFile(data), save=True)
return img
instance: ProjectSessionImage = await database_sync_to_async(_create_img_sync)()
# Publish events
profile = getattr(info.context.request, 'profile', None)
await publish_session_image_uploaded(
image_id=str(instance.id),
session_id=str(instance.project_session_id),
is_internal=internal,
triggered_by=profile
)
return cast(ProjectSessionImageType, instance)
@strawberry.mutation(description="Update an existing ServiceSession image (e.g., title)")
async def update_service_session_image(
self, info: Info, input: ServiceSessionImageUpdateInput
) -> ServiceSessionImageType:
payload = {"id": input.id, "title": input.title, "notes": input.notes, "internal": input.internal}
instance = await update_object(payload, ServiceSessionImage)
# Publish events
profile = getattr(info.context.request, 'profile', None)
await publish_session_image_updated(
image_id=str(instance.id),
session_id=str(instance.service_session_id),
triggered_by=profile
)
return cast(ServiceSessionImageType, instance)
@strawberry.mutation(description="Update an existing ProjectSession image (e.g., title)")
async def update_project_session_image(
self, info: Info, input: ProjectSessionImageUpdateInput
) -> ProjectSessionImageType:
payload = {"id": input.id, "title": input.title, "notes": input.notes, "internal": input.internal}
instance = await update_object(payload, ProjectSessionImage)
# Publish events
profile = getattr(info.context.request, 'profile', None)
await publish_session_image_updated(
image_id=str(instance.id),
session_id=str(instance.project_session_id),
triggered_by=profile
)
return cast(ProjectSessionImageType, instance)
@strawberry.mutation(description="Delete a ServiceSession image")
async def delete_service_session_image(self, info: Info, id: strawberry.ID) -> strawberry.ID:
# Delete the instance (delete_object returns the instance before deletion)
instance = await delete_object(id, ServiceSessionImage)
if not instance:
raise ValueError(f"ServiceSessionImage with ID {id} does not exist")
# Publish events
profile = getattr(info.context.request, 'profile', None)
await publish_session_image_deleted(
image_id=str(instance.id),
session_id=str(instance.service_session_id),
triggered_by=profile
)
return id
@strawberry.mutation(description="Delete a ProjectSession image")
async def delete_project_session_image(self, info: Info, id: strawberry.ID) -> strawberry.ID:
# Delete the instance (delete_object returns the instance before deletion)
instance = await delete_object(id, ProjectSessionImage)
if not instance:
raise ValueError(f"ProjectSessionImage with ID {id} does not exist")
# Publish events
profile = getattr(info.context.request, 'profile', None)
await publish_session_image_deleted(
image_id=str(instance.id),
session_id=str(instance.project_session_id),
triggered_by=profile
)
return id