diff --git a/src/backend/app/migrations/versions/e23c05f21542_change_task_events_state.py b/src/backend/app/migrations/versions/e23c05f21542_change_task_events_state.py new file mode 100644 index 00000000..dfa74c3b --- /dev/null +++ b/src/backend/app/migrations/versions/e23c05f21542_change_task_events_state.py @@ -0,0 +1,130 @@ +"""change task events state + +Revision ID: e23c05f21542 +Revises: 8ae4e43a7011 +Create Date: 2024-12-06 08:00:16.223517 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "e23c05f21542" +down_revision: Union[str, None] = "8ae4e43a7011" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +old_state_enum = sa.Enum( + "REQUEST_FOR_MAPPING", + "UNLOCKED_TO_MAP", + "LOCKED_FOR_MAPPING", + "UNLOCKED_TO_VALIDATE", + "LOCKED_FOR_VALIDATION", + "UNLOCKED_DONE", + "UNFLYABLE_TASK", + "IMAGE_UPLOADED", + "IMAGE_PROCESSED", + "IMAGE_PROCESSING_FAILED", + name="state", +) + +new_state_enum = sa.Enum( + "REQUEST_FOR_MAPPING", + "UNLOCKED_TO_MAP", + "LOCKED_FOR_MAPPING", + "UNLOCKED_TO_VALIDATE", + "LOCKED_FOR_VALIDATION", + "UNLOCKED_DONE", + "UNFLYABLE_TASK", + "IMAGE_UPLOADED", + "IMAGE_PROCESSING_FAILED", + "IMAGE_PROCESSING_STARTED", + "IMAGE_PROCESSING_FINISHED", + name="state_new", +) + + +def upgrade(): + # Step 1: Create the new enum type + new_state_enum.create(op.get_bind()) + + # Step 2: Add a new column with the new enum type + op.add_column("task_events", sa.Column("new_state", new_state_enum, nullable=True)) + + # Step 3: Populate the new state column with the transformed data + op.execute( + """ + UPDATE task_events + SET new_state = + CASE + WHEN state = 'IMAGE_PROCESSED' THEN 'IMAGE_PROCESSING_FINISHED' + ELSE state::text + END::state_new + """ + ) + + # Step 4: Drop the old state column + op.drop_column("task_events", "state") + + # Step 5: Rename the new_state column to state + op.alter_column("task_events", "new_state", new_column_name="state") + + # Step 6: Drop the old enum type + op.execute("DROP TYPE state;") + + # Step 7: Rename the new enum type to state + op.execute("ALTER TYPE state_new RENAME TO state;") + + ## then add the image processing started state to all the image uploaded file + op.execute(""" + WITH added_image_processing_started AS ( + SELECT gen_random_uuid() AS event_id, + task_id, + project_id, + user_id, + created_at + INTERVAL '10 seconds' AS created_at, + comment, + created_at + INTERVAL '10 seconds' AS updated_at, + 'IMAGE_PROCESSING_STARTED'::state AS state + FROM task_events WHERE state = 'IMAGE_UPLOADED' + ) + INSERT INTO task_events (event_id, task_id, project_id, user_id, created_at, comment, updated_at, state) + SELECT event_id, task_id, project_id, user_id, created_at, comment, updated_at, state + FROM added_image_processing_started; + """) + + +def downgrade(): + op.execute("DELETE from task_events WHERE state = 'IMAGE_PROCESSING_STARTED';") + # Step 1: Rename the new enum type back to the old name + op.execute("ALTER TYPE state RENAME TO state_new;") + + # Step 2: Create the old enum type again (assuming you have the definition of the old enum type) + # You would need to define the old state enum type here, e.g.: + old_state_enum.create(op.get_bind()) + + # Step 3: Add the old state column with the old enum type + op.add_column("task_events", sa.Column("state_old", old_state_enum, nullable=True)) + + # Step 4: Populate the old state column with the transformed data + op.execute( + """ + UPDATE task_events + SET state_old = + CASE + WHEN state = 'IMAGE_PROCESSING_FINISHED' THEN 'IMAGE_PROCESSED' + ELSE state::text + END::state + """ + ) + + # Step 5: Drop the new_state column + op.drop_column("task_events", "state") + op.alter_column("task_events", "state_old", new_column_name="state") + + # Step 6: Drop the new enum type + op.execute("DROP TYPE state_new;") diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index d15f5b94..7a021c15 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -155,8 +155,9 @@ class State(int, Enum): UNLOCKED_DONE = 4 UNFLYABLE_TASK = 5 IMAGE_UPLOADED = 6 - IMAGE_PROCESSED = 7 - IMAGE_PROCESSING_FAILED = 8 + IMAGE_PROCESSING_FAILED = 7 + IMAGE_PROCESSING_STARTED = 8 + IMAGE_PROCESSING_FINISHED = 9 class EventType(str, Enum): @@ -178,6 +179,7 @@ class EventType(str, Enum): - ``comment`` -- Keep the state the same, but simply add a comment. - ``unlock`` -- Unlock a task state by unlocking it if it's locked. - ``image_upload`` -- Set the state to *image uploaded* when the task image is uploaded. + - ``image_processing_start`` -- Set the state to *image processing started* when the image processing is started by user. Note that ``task_id`` must be specified in the endpoint too. """ @@ -194,3 +196,4 @@ class EventType(str, Enum): COMMENT = "comment" UNLOCK = "unlock" IMAGE_UPLOAD = "image_upload" + IMAGE_PROCESSING_START = "image_processing_start" diff --git a/src/backend/app/projects/image_processing.py b/src/backend/app/projects/image_processing.py index c7c5b9fe..d86600e0 100644 --- a/src/backend/app/projects/image_processing.py +++ b/src/backend/app/projects/image_processing.py @@ -190,7 +190,7 @@ def process_images_from_s3(self, bucket_name, name=None, options=[], webhook=Non self.user_id, "Task completed.", State.IMAGE_UPLOADED, - State.IMAGE_PROCESSED, + State.IMAGE_PROCESSING_FINISHED, timestamp(), ) return task @@ -315,11 +315,11 @@ async def download_and_upload_assets_from_odm_to_s3( user_id=user_id, comment=comment, initial_state=current_state, - final_state=State.IMAGE_PROCESSED, + final_state=State.IMAGE_PROCESSING_FINISHED, updated_at=timestamp(), ) log.info( - f"Task {dtm_task_id} state updated to IMAGE_PROCESSED in the database." + f"Task {dtm_task_id} state updated to IMAGE_PROCESSING_FINISHED in the database." ) except Exception as e: diff --git a/src/backend/app/projects/project_logic.py b/src/backend/app/projects/project_logic.py index 5d5db371..d08f06e2 100644 --- a/src/backend/app/projects/project_logic.py +++ b/src/backend/app/projects/project_logic.py @@ -34,7 +34,7 @@ async def get_centroids(db: Connection): ST_AsGeoJSON(p.centroid)::jsonb AS centroid, COUNT(t.id) AS total_task_count, COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'REQUEST_FOR_MAPPING', 'IMAGE_UPLOADED', 'UNFLYABLE_TASK') THEN 1 END) AS ongoing_task_count, - COUNT(CASE WHEN te.state = 'IMAGE_PROCESSED' THEN 1 END) AS completed_task_count + COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) AS completed_task_count FROM projects p LEFT JOIN diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index e6799b32..574c2c5b 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -525,7 +525,7 @@ async def odm_webhook( current_state = await task_logic.get_task_state(db, dtm_project_id, dtm_task_id) current_state_value = State[current_state.get("state")] match current_state_value: - case State.IMAGE_UPLOADED: + case State.IMAGE_PROCESSING_STARTED: log.info( f"Task ID: {task_id}, Status: already IMAGE_UPLOADED - no update needed." ) @@ -537,7 +537,7 @@ async def odm_webhook( dtm_project_id, dtm_task_id, dtm_user_id, - State.IMAGE_UPLOADED, + State.IMAGE_PROCESSING_STARTED, "Task completed.", ) @@ -553,7 +553,7 @@ async def odm_webhook( dtm_project_id, dtm_task_id, dtm_user_id, - State.IMAGE_UPLOADED, + State.IMAGE_PROCESSING_FAILED, "Task completed.", ) @@ -572,7 +572,7 @@ async def odm_webhook( dtm_task_id, dtm_user_id, "Image processing failed.", - State.IMAGE_UPLOADED, + State.IMAGE_PROCESSING_STARTED, State.IMAGE_PROCESSING_FAILED, timestamp(), ) diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index e9bc01d5..02297c2e 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -369,7 +369,7 @@ async def all( COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'REQUEST_FOR_MAPPING', 'IMAGE_UPLOADED', 'UNFLYABLE_TASK') THEN 1 END) AS ongoing_task_count, -- Count based on the latest state of tasks - COUNT(CASE WHEN te.state = 'IMAGE_PROCESSED' THEN 1 END) AS completed_task_count + COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) AS completed_task_count FROM projects p LEFT JOIN tasks t ON t.project_id = p.id diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index b08833ab..c9e09199 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -29,8 +29,8 @@ async def get_task_stats(db: Connection, user_data: AuthUser): raw_sql = """ SELECT COUNT(CASE WHEN te.state = 'REQUEST_FOR_MAPPING' THEN 1 END) AS request_logs, - COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'IMAGE_UPLOADED', 'IMAGE_PROCESSING_FAILED') THEN 1 END) AS ongoing_tasks, - COUNT(CASE WHEN te.state = 'IMAGE_PROCESSED' THEN 1 END) AS completed_tasks, + COUNT(CASE WHEN te.state IN ('LOCKED_FOR_MAPPING', 'IMAGE_UPLOADED', 'IMAGE_PROCESSING_STARTED','IMAGE_PROCESSING_FAILED') THEN 1 END) AS ongoing_tasks, + COUNT(CASE WHEN te.state = 'IMAGE_PROCESSING_FINISHED' THEN 1 END) AS completed_tasks, COUNT(CASE WHEN te.state = 'UNFLYABLE_TASK' THEN 1 END) AS unflyable_tasks FROM ( @@ -611,4 +611,40 @@ async def handle_event( detail.updated_at, ) + case EventType.IMAGE_PROCESSING_START: + current_task_state = await get_task_state(db, project_id, task_id) + if not current_task_state: + raise HTTPException( + status_code=400, detail="Task is not ready for image upload." + ) + state = current_task_state.get("state") + locked_user_id = current_task_state.get("user_id") + + # Determine error conditions: Current State must be IMAGE_UPLOADED or IMAGE_PROCESSING_FAILED. + if state not in ( + State.IMAGE_UPLOADED.name, + State.IMAGE_PROCESSING_FAILED.name, + ): + raise HTTPException( + status_code=400, + detail="Task state does not match expected state for image processing to start.", + ) + + if user_id != locked_user_id: + raise HTTPException( + status_code=403, + detail="You cannot upload an image for this task as it is locked by another user.", + ) + + return await update_task_state( + db, + project_id, + task_id, + user_id, + f"Task image processing started by user {user_data.name}.", + State.IMAGE_UPLOADED, + State.IMAGE_PROCESSING_STARTED, + detail.updated_at, + ) + return True diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx index 2d8b9fe7..23673ee9 100644 --- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx +++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx @@ -72,7 +72,7 @@ const DescriptionBox = () => { dispatch(resetFilesExifData()); }, [dispatch]); - const { mutate: reStartImageryProcess, isLoading: imageProcessingStarting } = + const { mutate: startImageryProcess, isLoading: imageProcessingStarting } = useMutation({ mutationFn: () => postProcessImagery(projectId as string, taskId as string), @@ -80,9 +80,12 @@ const DescriptionBox = () => { updateStatus({ projectId, taskId, - data: { event: 'image_upload', updated_at: new Date().toISOString() }, + data: { + event: 'image_processing_start', + updated_at: new Date().toISOString(), + }, }); - toast.success('Image processing re-started'); + toast.success('Image processing started'); }, }); @@ -184,28 +187,6 @@ const DescriptionBox = () => { const handleDownloadResult = () => { if (!taskAssetsInformation?.assets_url) return; - // fetch(`${taskAssetsInformation?.assets_url}`, { method: 'GET' }) - // .then(response => { - // if (!response.ok) { - // throw new Error(`Network response was ${response.statusText}`); - // } - // return response.blob(); - // }) - // .then(blob => { - // const url = window.URL.createObjectURL(blob); - // const link = document.createElement('a'); - // link.href = url; - // link.download = 'assets.zip'; - // document.body.appendChild(link); - // link.click(); - // link.remove(); - // window.URL.revokeObjectURL(url); - // }) - // .catch(error => - // toast.error(`There wan an error while downloading file - // ${error}`), - // ); - try { const link = document.createElement('a'); link.href = taskAssetsInformation?.assets_url; @@ -282,6 +263,20 @@ const DescriptionBox = () => { )} + {taskAssetsInformation?.state === 'IMAGE_UPLOADED' && ( +
Finished Tasks
Image Uploaded
+Image Processing Started
+Image Processing Failed
+Requested Tasks
+Locked Tasks
+Remaining Task
+Project Area
+Unflyable Areas