294 lines
11 KiB
Python
294 lines
11 KiB
Python
from typing import cast
|
|
import strawberry
|
|
from strawberry.types import Info
|
|
from channels.db import database_sync_to_async
|
|
from core.graphql.pubsub import pubsub
|
|
from core.graphql.inputs.scope import (
|
|
ScopeInput, ScopeUpdateInput,
|
|
AreaInput, AreaUpdateInput,
|
|
TaskInput, TaskUpdateInput,
|
|
TaskCompletionInput, TaskCompletionUpdateInput,
|
|
)
|
|
from core.graphql.types.scope import (
|
|
ScopeType,
|
|
AreaType,
|
|
TaskType,
|
|
TaskCompletionType,
|
|
)
|
|
from core.models.scope import Scope, Area, Task, TaskCompletion
|
|
from core.models.session import ServiceSession
|
|
from core.graphql.utils import create_object, update_object, delete_object, _decode_global_id
|
|
from core.services.events import (
|
|
publish_scope_created, publish_scope_updated, publish_scope_deleted,
|
|
publish_area_created, publish_area_updated, publish_area_deleted,
|
|
publish_task_created, publish_task_updated, publish_task_deleted,
|
|
publish_task_completion_recorded,
|
|
)
|
|
|
|
|
|
@strawberry.type
|
|
class Mutation:
|
|
@strawberry.mutation(description="Create a new scope")
|
|
async def create_scope(self, input: ScopeInput, info: Info) -> ScopeType:
|
|
instance = await create_object(input, Scope)
|
|
await pubsub.publish("scope_created", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_scope_created(
|
|
scope_id=str(instance.id),
|
|
account_id=str(instance.account_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(ScopeType, instance)
|
|
|
|
@strawberry.mutation(description="Update an existing scope")
|
|
async def update_scope(self, input: ScopeUpdateInput, info: Info) -> ScopeType:
|
|
instance = await update_object(input, Scope)
|
|
await pubsub.publish("scope_updated", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_scope_updated(
|
|
scope_id=str(instance.id),
|
|
account_id=str(instance.account_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(ScopeType, instance)
|
|
|
|
@strawberry.mutation(description="Delete an existing scope")
|
|
async def delete_scope(self, id: strawberry.ID, info: Info) -> strawberry.ID:
|
|
def _delete_scope_sync(scope_id):
|
|
"""
|
|
Smart delete: soft-delete if sessions reference this scope, hard-delete otherwise.
|
|
Returns (account_id, action) where action is 'deleted' or 'deactivated'.
|
|
"""
|
|
pk = _decode_global_id(scope_id)
|
|
try:
|
|
scope = Scope.objects.get(pk=pk)
|
|
except Scope.DoesNotExist:
|
|
return None, None
|
|
|
|
account_id = scope.account_id
|
|
|
|
# Check if any service sessions reference this scope
|
|
has_sessions = ServiceSession.objects.filter(scope_id=pk).exists()
|
|
|
|
if has_sessions:
|
|
# Soft delete - deactivate the scope to preserve historical data
|
|
scope.is_active = False
|
|
scope.save(update_fields=['is_active'])
|
|
else:
|
|
# Hard delete - no sessions reference this scope
|
|
scope.delete()
|
|
|
|
return account_id, 'deactivated' if has_sessions else 'deleted'
|
|
|
|
account_id, action = await database_sync_to_async(_delete_scope_sync)(id)
|
|
|
|
if account_id is None:
|
|
raise ValueError(f"Scope with ID {id} does not exist")
|
|
|
|
await pubsub.publish("scope_deleted", id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_scope_deleted(
|
|
scope_id=str(id),
|
|
account_id=str(account_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return id
|
|
|
|
@strawberry.mutation(description="Create a new area")
|
|
async def create_area(self, input: AreaInput, info: Info) -> AreaType:
|
|
instance = await create_object(input, Area)
|
|
await pubsub.publish("area_created", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_area_created(
|
|
area_id=str(instance.id),
|
|
scope_id=str(instance.scope_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(AreaType, instance)
|
|
|
|
@strawberry.mutation(description="Update an existing area")
|
|
async def update_area(self, input: AreaUpdateInput, info: Info) -> AreaType:
|
|
instance = await update_object(input, Area)
|
|
await pubsub.publish("area_updated", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_area_updated(
|
|
area_id=str(instance.id),
|
|
scope_id=str(instance.scope_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(AreaType, instance)
|
|
|
|
@strawberry.mutation(description="Delete an existing area")
|
|
async def delete_area(self, id: strawberry.ID, info: Info) -> strawberry.ID:
|
|
def _delete_area_sync(area_id):
|
|
"""
|
|
Delete an area if no task completions reference its tasks.
|
|
Returns scope_id on success, raises ValueError if completions exist.
|
|
"""
|
|
pk = _decode_global_id(area_id)
|
|
try:
|
|
area = Area.objects.get(pk=pk)
|
|
except Area.DoesNotExist:
|
|
return None
|
|
|
|
# Check if any task completions reference tasks in this area
|
|
has_completions = TaskCompletion.objects.filter(task__area_id=pk).exists()
|
|
|
|
if has_completions:
|
|
raise ValueError(
|
|
"Cannot delete area: it contains tasks with recorded completions. "
|
|
"Deactivate the scope instead to preserve historical data."
|
|
)
|
|
|
|
scope_id = area.scope_id
|
|
area.delete()
|
|
return scope_id
|
|
|
|
scope_id = await database_sync_to_async(_delete_area_sync)(id)
|
|
|
|
if scope_id is None:
|
|
raise ValueError(f"Area with ID {id} does not exist")
|
|
|
|
await pubsub.publish("area_deleted", id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_area_deleted(
|
|
area_id=str(id),
|
|
scope_id=str(scope_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return id
|
|
|
|
@strawberry.mutation(description="Create a new task")
|
|
async def create_task(self, input: TaskInput, info: Info) -> TaskType:
|
|
instance = await create_object(input, Task)
|
|
await pubsub.publish("task_created", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_task_created(
|
|
task_id=str(instance.id),
|
|
area_id=str(instance.area_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(TaskType, instance)
|
|
|
|
@strawberry.mutation(description="Update an existing task")
|
|
async def update_task(self, input: TaskUpdateInput, info: Info) -> TaskType:
|
|
instance = await update_object(input, Task)
|
|
await pubsub.publish("task_updated", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_task_updated(
|
|
task_id=str(instance.id),
|
|
area_id=str(instance.area_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(TaskType, instance)
|
|
|
|
@strawberry.mutation(description="Delete an existing task")
|
|
async def delete_task(self, id: strawberry.ID, info: Info) -> strawberry.ID:
|
|
def _delete_task_sync(task_id):
|
|
"""
|
|
Delete a task if no task completions reference it.
|
|
Returns area_id on success, raises ValueError if completions exist.
|
|
"""
|
|
pk = _decode_global_id(task_id)
|
|
try:
|
|
task = Task.objects.get(pk=pk)
|
|
except Task.DoesNotExist:
|
|
return None
|
|
|
|
# Check if any task completions reference this task
|
|
has_completions = TaskCompletion.objects.filter(task_id=pk).exists()
|
|
|
|
if has_completions:
|
|
raise ValueError(
|
|
"Cannot delete task: it has recorded completions. "
|
|
"Deactivate the scope instead to preserve historical data."
|
|
)
|
|
|
|
area_id = task.area_id
|
|
task.delete()
|
|
return area_id
|
|
|
|
area_id = await database_sync_to_async(_delete_task_sync)(id)
|
|
|
|
if area_id is None:
|
|
raise ValueError(f"Task with ID {id} does not exist")
|
|
|
|
await pubsub.publish("task_deleted", id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_task_deleted(
|
|
task_id=str(id),
|
|
area_id=str(area_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return id
|
|
|
|
@strawberry.mutation(description="Create a new task completion")
|
|
async def create_task_completion(self, input: TaskCompletionInput, info: Info) -> TaskCompletionType:
|
|
instance = await create_object(input, TaskCompletion)
|
|
await pubsub.publish("task_completion_created", instance.id)
|
|
|
|
# Publish event
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_task_completion_recorded(
|
|
completion_id=str(instance.id),
|
|
task_id=str(instance.task_id),
|
|
service_id=str(instance.service_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(TaskCompletionType, instance)
|
|
|
|
@strawberry.mutation(description="Update an existing task completion")
|
|
async def update_task_completion(self, input: TaskCompletionUpdateInput, info: Info) -> TaskCompletionType:
|
|
instance = await update_object(input, TaskCompletion)
|
|
await pubsub.publish("task_completion_updated", instance.id)
|
|
|
|
# Publish event (reuse the same event for updates)
|
|
profile = getattr(info.context.request, 'profile', None)
|
|
await publish_task_completion_recorded(
|
|
completion_id=str(instance.id),
|
|
task_id=str(instance.task_id),
|
|
service_id=str(instance.service_id),
|
|
triggered_by=profile
|
|
)
|
|
|
|
return cast(TaskCompletionType, instance)
|
|
|
|
@strawberry.mutation(description="Delete an existing task completion")
|
|
async def delete_task_completion(self, id: strawberry.ID, info: Info) -> strawberry.ID:
|
|
instance = await delete_object(id, TaskCompletion)
|
|
if not instance:
|
|
raise ValueError(f"TaskCompletion with ID {id} does not exist")
|
|
await pubsub.publish("task_completion_deleted", id)
|
|
|
|
# Note: No event publication for deletion as there's no corresponding delete event
|
|
# in the events.py file for task completions
|
|
|
|
return id |