221 lines
8.2 KiB
Python
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 |