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' && ( +
+ +
+ )} {taskAssetsInformation?.state === 'IMAGE_PROCESSING_FAILED' && (
- )}
); diff --git a/src/frontend/src/components/IndividualProject/MapSection/Legend.tsx b/src/frontend/src/components/IndividualProject/MapSection/Legend.tsx index f3b3417e..cbc0fc19 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/Legend.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/Legend.tsx @@ -1,61 +1,68 @@ import area from '@Assets/images/area-icon.png'; import lock from '@Assets/images/lock.png'; +import { FlexColumn } from '@Components/common/Layouts'; import { useState } from 'react'; const Legend = () => { const [showLegendItems, setShowLegendItems] = useState(true); return (
-
-
Legend
- setShowLegendItems(!showLegendItems)} - > - {showLegendItems ? 'expand_more' : 'expand_less'} - -
- {showLegendItems && ( -
-
-
-
Finished Tasks
-
-
-
-
Image Processing
-
-
-
-
Image Processing Failed
-
-
-
-
Requested Tasks
-
-
-
- area-icon + +
+
Legend
+ setShowLegendItems(!showLegendItems)} + > + {showLegendItems ? 'expand_more' : 'expand_less'} + +
+ {showLegendItems && ( +
+
+
+

Finished Tasks

-
Locked Tasks
-
-
-
-
Remaining Task
-
-
-
- area-icon +
+
+

Image Uploaded

+
+
+
+

Image Processing Started

+
+
+
+

Image Processing Failed

+
+
+
+

Requested Tasks

+
+
+
+ area-icon +
+

Locked Tasks

+
+
+
+

Remaining Task

+
+
+
+ area-icon +
+

Project Area

+
+
+
+

Unflyable Areas

-
Project Area
-
-
-
-
Unflyable Areas
-
- )} + )} +
); }; diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx index 0bc71cf0..d10d9147 100644 --- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx +++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx @@ -151,10 +151,12 @@ const MapSection = ({ projectData }: { projectData: Record }) => { return 'This task is not flyable'; case 'IMAGE_UPLOADED': return `This task's Images has been uploaded ${properties.locked_user_name ? `by ${userDetails?.id === properties?.locked_user_id ? 'you' : properties?.locked_user_name}` : ''}`; - case 'IMAGE_PROCESSED': + case 'IMAGE_PROCESSING_STARTED': + return `This task is started ${properties.locked_user_name ? `by ${userDetails?.id === properties?.locked_user_id ? 'you' : properties?.locked_user_name}` : ''}`; + case 'IMAGE_PROCESSING_FINISHED': return `This task is completed ${properties.locked_user_name ? `by ${userDetails?.id === properties?.locked_user_id ? 'you' : properties?.locked_user_name}` : ''}`; case 'IMAGE_PROCESSING_FAILED': - return `This task's image processing is failed started ${properties.locked_user_name ? `by ${userDetails?.id === properties?.locked_user_id ? 'you' : properties?.locked_user_name}` : ''}`; + return `The image processing task started ${userDetails?.id === properties?.locked_user_id ? 'by you' : `by ${properties?.locked_user_name}`} has failed.`; default: return ''; } diff --git a/src/frontend/src/constants/index.ts b/src/frontend/src/constants/index.ts index 7a33e3a2..a03cb94b 100644 --- a/src/frontend/src/constants/index.ts +++ b/src/frontend/src/constants/index.ts @@ -94,7 +94,7 @@ export const rowsPerPageOptions = [ export const taskStatusObj = { request_logs: ['REQUEST_FOR_MAPPING'], - ongoing: ['LOCKED_FOR_MAPPING', 'IMAGE_UPLOADED', 'IMAGE_PROCESSING_FAILED'], - completed: ['IMAGE_PROCESSED'], + ongoing: ['LOCKED_FOR_MAPPING', 'IMAGE_UPLOADED', 'IMAGE_PROCESSING_STARTED', 'IMAGE_PROCESSING_FAILED'], + completed: ['IMAGE_PROCESSING_FINISHED'], unflyable: ['UNFLYABLE_TASK'], }; diff --git a/src/frontend/src/constants/projectDescription.ts b/src/frontend/src/constants/projectDescription.ts index cf99e1e6..67509617 100644 --- a/src/frontend/src/constants/projectDescription.ts +++ b/src/frontend/src/constants/projectDescription.ts @@ -25,7 +25,7 @@ export const getLayerOptionsByStatus = (status: string) => { 'fill-opacity': 0.5, }, }, - IMAGE_PROCESSED: { + IMAGE_PROCESSING_FINISHED: { type: 'fill', paint: { 'fill-color': '#ACD2C4', @@ -34,6 +34,14 @@ export const getLayerOptionsByStatus = (status: string) => { }, }, IMAGE_UPLOADED: { + type: 'fill', + paint: { + 'fill-color': '#9ec7ff', + 'fill-outline-color': '#484848', + 'fill-opacity': 0.5, + }, + }, + IMAGE_PROCESSING_STARTED: { type: 'fill', paint: { 'fill-color': '#9C77B2', @@ -86,7 +94,10 @@ export const showPrimaryButton = ( case 'IMAGE_UPLOADED': if (lockedUser === currentUser || author === currentUser) return true; return false; - case 'IMAGE_PROCESSED': + case 'IMAGE_PROCESSING_STARTED': + if (lockedUser === currentUser || author === currentUser) return true; + return false; + case 'IMAGE_PROCESSING_FINISHED': return true; case 'IMAGE_PROCESSING_FAILED': if (lockedUser === currentUser || author === currentUser) return true; diff --git a/src/frontend/src/utils/index.ts b/src/frontend/src/utils/index.ts index bf63d50d..cdcf5883 100644 --- a/src/frontend/src/utils/index.ts +++ b/src/frontend/src/utils/index.ts @@ -82,7 +82,7 @@ export const getFrontOverlap = (agl: number, forwardSpacing: number) => { // remove underscore and capitalize the word export const formatString = (value: string) => { if (!value) return ''; - if (value === 'IMAGE_PROCESSED') return 'Completed'; + if (value === 'IMAGE_PROCESSING_FINISHED') return 'Completed'; return value .replace(/_/g, ' ') .toLowerCase()