From acacd4f2069126c8bd9f32f310793d5df5b27b49 Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 9 Sep 2024 12:07:06 +0545 Subject: [PATCH 01/25] feat(project-dashboard): show project id insted of slug on project cards --- src/frontend/src/components/Projects/ProjectCard/index.tsx | 5 ++--- src/frontend/src/views/Projects/index.tsx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/frontend/src/components/Projects/ProjectCard/index.tsx b/src/frontend/src/components/Projects/ProjectCard/index.tsx index 57019075..11a6e677 100644 --- a/src/frontend/src/components/Projects/ProjectCard/index.tsx +++ b/src/frontend/src/components/Projects/ProjectCard/index.tsx @@ -4,7 +4,6 @@ interface IProjectCardProps { id: number; title: string; description: string; - slug: string; imageUrl: string | null; } @@ -12,7 +11,7 @@ export default function ProjectCard({ id, title, description, - slug, + imageUrl, }: IProjectCardProps) { const navigate = useNavigate(); @@ -40,7 +39,7 @@ export default function ProjectCard({ )}

- {slug} + #{id}

{title} diff --git a/src/frontend/src/views/Projects/index.tsx b/src/frontend/src/views/Projects/index.tsx index 0431554e..8786db87 100644 --- a/src/frontend/src/views/Projects/index.tsx +++ b/src/frontend/src/views/Projects/index.tsx @@ -46,7 +46,6 @@ const Projects = () => { imageUrl={project?.image_url} title={project.name} description={project.description} - slug={project?.slug} /> ), ) From fabdbf62a6e288468273286ce3d3fafe627785d2 Mon Sep 17 00:00:00 2001 From: Sujit Date: Mon, 9 Sep 2024 12:09:00 +0545 Subject: [PATCH 02/25] fix(project-dashboard): map crash issue on 0 project --- .../components/Projects/MapSection/index.tsx | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/frontend/src/components/Projects/MapSection/index.tsx b/src/frontend/src/components/Projects/MapSection/index.tsx index 559d84cd..8ac709dd 100644 --- a/src/frontend/src/components/Projects/MapSection/index.tsx +++ b/src/frontend/src/components/Projects/MapSection/index.tsx @@ -26,37 +26,38 @@ const ProjectsMapSection = () => { }, disableRotation: true, }); - const { data: projectsList, isLoading } = useGetProjectsListQuery({ - select: (data: any) => { - // find all polygons centroid and set to geojson save to single geojson - const combinedGeojson = data?.data?.reduce( - (acc: Record, current: Record) => { - return { - ...acc, - features: [ - ...acc.features, - { - ...centroid(current.outline), - properties: { - id: current?.id, - name: current?.name, - slug: current?.slug, + const { data: projectsList, isLoading }: Record = + useGetProjectsListQuery({ + select: (data: any) => { + // find all polygons centroid and set to geojson save to single geojson + const combinedGeojson = data?.data?.reduce( + (acc: Record, current: Record) => { + return { + ...acc, + features: [ + ...acc.features, + { + ...centroid(current.outline), + properties: { + id: current?.id, + name: current?.name, + slug: current?.slug, + }, }, - }, - ], - }; - }, - { - type: 'FeatureCollection', - features: [], - }, - ); - return combinedGeojson; - }, - }); + ], + }; + }, + { + type: 'FeatureCollection', + features: [], + }, + ); + return combinedGeojson; + }, + }); useEffect(() => { - if (!projectsList) return; + if (!projectsList || !projectsList?.features?.length) return; const bbox = getBbox(projectsList as FeatureCollection); map?.fitBounds(bbox as LngLatBoundsLike, { padding: 100 }); }, [projectsList, map]); From 416ef00733222e65335e659258c03ce236e2e27e Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 10 Sep 2024 10:44:13 +0545 Subject: [PATCH 03/25] feat: add reset task if locked --- src/backend/app/models/enums.py | 2 ++ src/backend/app/tasks/task_logic.py | 32 ++++++++++++++++++++++++++++ src/backend/app/tasks/task_routes.py | 24 +++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 2a3d3ddd..6d404d3f 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -161,6 +161,7 @@ class EventType(str, Enum): - ``split`` -- Set the state *unlocked done* then generate additional subdivided task areas. - ``assign`` -- For a requester user to assign a task to another user. Set the state *locked for mapping* passing in the required user id. - ``comment`` -- Keep the state the same, but simply add a comment. + - ``reset`` -- Reset a task state by unlocking it if it's locked. Note that ``task_id`` must be specified in the endpoint too. """ @@ -175,3 +176,4 @@ class EventType(str, Enum): SPLIT = "split" ASSIGN = "assign" COMMENT = "comment" + RESET = "reset" diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index 68ec2210..f8bdb05d 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -128,3 +128,35 @@ async def request_mapping( ) result = await cur.fetchone() return result + + +async def get_task_state( + db: Connection, project_id: uuid.UUID, task_id: uuid.UUID +) -> dict: + """ + Retrieve the latest state of a task by querying the task_events table. + + Args: + db (Connection): The database connection. + project_id (uuid.UUID): The project ID. + task_id (uuid.UUID): The task ID. + + Returns: + dict: A dictionary containing the task's state and associated metadata. + """ + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + SELECT state, user_id, created_at, comment + FROM task_events + WHERE project_id = %(project_id)s AND task_id = %(task_id)s + ORDER BY created_at DESC + LIMIT 1; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + }, + ) + result = await cur.fetchone() + return result diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index c7323d46..7d67576a 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -368,4 +368,28 @@ async def new_event( State.UNFLYABLE_TASK, ) + case EventType.RESET: + current_task_state = await task_logic.get_task_state( + db, project_id, task_id + ) + + if ( + current_task_state["state"] == State.LOCKED_FOR_MAPPING.name + and user_id == current_task_state["user_id"] + ): + # Task is locked by the user, so reset it (unlock) + return await task_logic.update_task_state( + db, + project_id, + task_id, + user_id, + "Task reset by user", + State.LOCKED_FOR_MAPPING, + State.UNLOCKED_TO_MAP, + ) + raise HTTPException( + status_code=400, + detail="Task is not locked by the user or cannot be reset.", + ) + return True From de97d79182d4face56fd52bdb48b50f0accc3300 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 10 Sep 2024 10:46:30 +0545 Subject: [PATCH 04/25] feat: add exception handling in get task state --- src/backend/app/tasks/task_logic.py | 62 ++++++++++++++++------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index f8bdb05d..ca962312 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -131,32 +131,40 @@ async def request_mapping( async def get_task_state( - db: Connection, project_id: uuid.UUID, task_id: uuid.UUID -) -> dict: - """ - Retrieve the latest state of a task by querying the task_events table. + db: Connection, project_id: uuid.UUID, task_id: uuid.UUID + ) -> dict: + """ + Retrieve the latest state of a task by querying the task_events table. + + Args: + db (Connection): The database connection. + project_id (uuid.UUID): The project ID. + task_id (uuid.UUID): The task ID. + + Returns: + dict: A dictionary containing the task's state and associated metadata. + """ + try: + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + SELECT state, user_id, created_at, comment + FROM task_events + WHERE project_id = %(project_id)s AND task_id = %(task_id)s + ORDER BY created_at DESC + LIMIT 1; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + }, + ) + result = await cur.fetchone() + return result + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"An error occurred while retrieving the task state: {str(e)}" + ) - Args: - db (Connection): The database connection. - project_id (uuid.UUID): The project ID. - task_id (uuid.UUID): The task ID. - Returns: - dict: A dictionary containing the task's state and associated metadata. - """ - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - SELECT state, user_id, created_at, comment - FROM task_events - WHERE project_id = %(project_id)s AND task_id = %(task_id)s - ORDER BY created_at DESC - LIMIT 1; - """, - { - "project_id": str(project_id), - "task_id": str(task_id), - }, - ) - result = await cur.fetchone() - return result From a864954da414c76bec8bb73f0febec1aefc706f3 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 10 Sep 2024 10:59:06 +0545 Subject: [PATCH 05/25] feat: add proper execption handling in reset task --- src/backend/app/tasks/task_logic.py | 68 ++++++++++++++-------------- src/backend/app/tasks/task_routes.py | 41 ++++++++++------- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index ca962312..c44c11b0 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -131,40 +131,38 @@ async def request_mapping( async def get_task_state( - db: Connection, project_id: uuid.UUID, task_id: uuid.UUID - ) -> dict: - """ - Retrieve the latest state of a task by querying the task_events table. - - Args: - db (Connection): The database connection. - project_id (uuid.UUID): The project ID. - task_id (uuid.UUID): The task ID. - - Returns: - dict: A dictionary containing the task's state and associated metadata. - """ - try: - async with db.cursor(row_factory=dict_row) as cur: - await cur.execute( - """ - SELECT state, user_id, created_at, comment - FROM task_events - WHERE project_id = %(project_id)s AND task_id = %(task_id)s - ORDER BY created_at DESC - LIMIT 1; - """, - { - "project_id": str(project_id), - "task_id": str(task_id), - }, - ) - result = await cur.fetchone() - return result - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"An error occurred while retrieving the task state: {str(e)}" - ) + db: Connection, project_id: uuid.UUID, task_id: uuid.UUID +) -> dict: + """ + Retrieve the latest state of a task by querying the task_events table. + Args: + db (Connection): The database connection. + project_id (uuid.UUID): The project ID. + task_id (uuid.UUID): The task ID. + Returns: + dict: A dictionary containing the task's state and associated metadata. + """ + try: + async with db.cursor(row_factory=dict_row) as cur: + await cur.execute( + """ + SELECT state, user_id, created_at, comment + FROM task_events + WHERE project_id = %(project_id)s AND task_id = %(task_id)s + ORDER BY created_at DESC + LIMIT 1; + """, + { + "project_id": str(project_id), + "task_id": str(task_id), + }, + ) + result = await cur.fetchone() + return result + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"An error occurred while retrieving the task state: {str(e)}", + ) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 7d67576a..47e3cab7 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -369,27 +369,36 @@ async def new_event( ) case EventType.RESET: + # Fetch the task state current_task_state = await task_logic.get_task_state( db, project_id, task_id ) - if ( - current_task_state["state"] == State.LOCKED_FOR_MAPPING.name - and user_id == current_task_state["user_id"] - ): - # Task is locked by the user, so reset it (unlock) - return await task_logic.update_task_state( - db, - project_id, - task_id, - user_id, - "Task reset by user", - State.LOCKED_FOR_MAPPING, - State.UNLOCKED_TO_MAP, + # Extract state and user from the result + state = current_task_state.get("state") + locked_user_id = current_task_state.get("user_id") + + # Determine error conditions in a single pass + if state != State.LOCKED_FOR_MAPPING.name: + raise HTTPException( + status_code=400, + detail="Task state does not match expected state for reset operation.", ) - raise HTTPException( - status_code=400, - detail="Task is not locked by the user or cannot be reset.", + if user_id != locked_user_id: + raise HTTPException( + status_code=403, + detail="You cannot unlock this task as it is locked by another user.", + ) + + # Proceed with resetting the task + return await task_logic.update_task_state( + db, + project_id, + task_id, + user_id, + f"Task has been reset by user {user_data.name}.", + State.LOCKED_FOR_MAPPING, + State.UNLOCKED_TO_MAP, ) return True From fc7c826fd3dd0293032928fbae971040246c3879 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Tue, 10 Sep 2024 11:24:39 +0545 Subject: [PATCH 06/25] fix: changes reset to unlock key --- src/backend/app/models/enums.py | 4 ++-- src/backend/app/tasks/task_routes.py | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 6d404d3f..2282f5ca 100644 --- a/src/backend/app/models/enums.py +++ b/src/backend/app/models/enums.py @@ -161,7 +161,7 @@ class EventType(str, Enum): - ``split`` -- Set the state *unlocked done* then generate additional subdivided task areas. - ``assign`` -- For a requester user to assign a task to another user. Set the state *locked for mapping* passing in the required user id. - ``comment`` -- Keep the state the same, but simply add a comment. - - ``reset`` -- Reset a task state by unlocking it if it's locked. + - ``unlock`` -- Unlock a task state by unlocking it if it's locked. Note that ``task_id`` must be specified in the endpoint too. """ @@ -176,4 +176,4 @@ class EventType(str, Enum): SPLIT = "split" ASSIGN = "assign" COMMENT = "comment" - RESET = "reset" + UNLOCK = "unlock" diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 47e3cab7..890ad7fa 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -368,21 +368,20 @@ async def new_event( State.UNFLYABLE_TASK, ) - case EventType.RESET: + case EventType.UNLOCK: # Fetch the task state current_task_state = await task_logic.get_task_state( db, project_id, task_id ) - # Extract state and user from the result state = current_task_state.get("state") locked_user_id = current_task_state.get("user_id") - # Determine error conditions in a single pass + # Determine error conditions if state != State.LOCKED_FOR_MAPPING.name: raise HTTPException( status_code=400, - detail="Task state does not match expected state for reset operation.", + detail="Task state does not match expected state for unlock operation.", ) if user_id != locked_user_id: raise HTTPException( @@ -390,13 +389,13 @@ async def new_event( detail="You cannot unlock this task as it is locked by another user.", ) - # Proceed with resetting the task + # Proceed with unlocking the task return await task_logic.update_task_state( db, project_id, task_id, user_id, - f"Task has been reset by user {user_data.name}.", + f"Task has been unlock by user {user_data.name}.", State.LOCKED_FOR_MAPPING, State.UNLOCKED_TO_MAP, ) From d2bd55ae914e96b4540f58a124913fba9ba6a035 Mon Sep 17 00:00:00 2001 From: Sujit Date: Tue, 10 Sep 2024 13:19:25 +0545 Subject: [PATCH 07/25] feat(project-creation): add task dimension limitation to 50-700 --- .../FormContents/GenerateTask/index.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx index 6f1a015c..36cf3f57 100644 --- a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/index.tsx @@ -1,4 +1,6 @@ import { useMutation } from '@tanstack/react-query'; +import { useState } from 'react'; +import ErrorMessage from '@Components/common/ErrorMessage'; import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; import { FormControl, Label, Input } from '@Components/common/FormUI'; import { Button } from '@Components/RadixComponents/Button'; @@ -11,6 +13,7 @@ import MapSection from './MapSection'; export default function GenerateTask({ formProps }: { formProps: any }) { const dispatch = useTypedDispatch(); + const [error, setError] = useState(''); const { register, watch } = formProps; const dimension = watch('task_split_dimension'); @@ -53,11 +56,15 @@ export default function GenerateTask({ formProps }: { formProps: any }) { type="number" className="naxatw-mt-1" value={dimension} + min={50} + max={700} {...register('task_split_dimension', { required: 'Required', valueAsNumber: true, })} + onFocus={() => setError('')} /> + {error && }