From 9e07ff588cae52edf93bec391f66f3b19a173a2b Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 11:28:43 +0545 Subject: [PATCH 1/7] feat: added download tasl boundaries --- src/backend/app/projects/project_routes.py | 57 +++++++++++++++++++++- src/backend/app/tasks/task_schemas.py | 48 ++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index a2da7722..33232d46 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -1,8 +1,19 @@ import os -from typing import Annotated +from typing import Annotated, Optional +from uuid import UUID import geojson from datetime import timedelta -from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form +from fastapi import ( + APIRouter, + HTTPException, + Depends, + Path, + Query, + UploadFile, + File, + Form, + Response, +) from geojson_pydantic import FeatureCollection from loguru import logger as log from psycopg import Connection @@ -16,6 +27,7 @@ from app.config import settings from app.users.user_deps import login_required from app.users.user_schemas import AuthUser +from app.tasks import task_schemas router = APIRouter( prefix=f"{settings.API_PREFIX}/projects", @@ -23,6 +35,47 @@ ) +@router.get("/{project_id}/download_tasks", tags=["Projects"]) +async def download_task_boundaries( + project_id: Annotated[ + UUID, + Path( + description="The project ID in UUID format.", + ), + ], + db: Annotated[Connection, Depends(database.get_db)], + user_data: Annotated[AuthUser, Depends(login_required)], + task_id: Optional[UUID] = Query( + default=None, + description="The task ID in UUID format. If not provided, all tasks will be downloaded.", + ), +): + """Downloads the boundary of the tasks for a project as a GeoJSON file. + + Args: + project_id (UUID): The ID of the project in UUID format. + db (Connection): The database connection, provided automatically. + user_data (AuthUser): The authenticated user data, checks if the user has permission. + + Returns: + Response: The HTTP response object containing the downloaded file. + """ + out = await task_schemas.TaskState.get_task_geometry(db, project_id, task_id) + + if out is None: + return Response( + status_code=404, content={"message": "Task or project geometry not found."} + ) + + filename = f"task_{task_id}.geojson" if task_id else "project_outline.geojson" + + headers = { + "Content-Disposition": f"attachment; filename={filename}", + "Content-Type": "application/geo+json", + } + return Response(content=out, headers=headers) + + @router.delete("/{project_id}", tags=["Projects"]) async def delete_project_by_id( project: Annotated[ diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 13edf3ff..d823cadb 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -19,6 +19,54 @@ class TaskState(BaseModel): state: str project_id: uuid.UUID + @staticmethod + async def get_task_geometry( + db: Connection, project_id: uuid.UUID, task_id: Optional[uuid.UUID] = None + ) -> str: + """Fetches the geometry of a single task or all tasks in a project. + + Args: + db (Connection): The database connection. + project_id (UUID): The ID of the project. + task_id (UUID, optional): The ID of a specific task. Defaults to None. + + Returns: + str: The GeoJSON representation of the task or project geometry. + """ + async with db.cursor(row_factory=dict_row) as cur: + if task_id: + row = await cur.execute( + """ + SELECT ST_AsGeoJSON(outline) AS geojson + FROM tasks + WHERE project_id = %(project_id)s AND id = %(task_id)s + """, + {"project_id": project_id, "task_id": task_id}, + ) + return row["geojson"] + else: + await cur.execute( + """ + SELECT ST_AsGeoJSON(outline) AS geojson + FROM tasks + WHERE project_id = %(project_id)s + """, + {"project_id": project_id}, + ) + + # Fetch the result + rows = await cur.fetchall() + if rows: + ## Create a FeatureCollection with empty properties for each feature + features = [ + f'{{"type": "Feature", "geometry": {row["geojson"]}, "properties": {{}}}}' + for row in rows + ] + feature_collection = f'{{"type": "FeatureCollection", "features": [{",".join(features)}]}}' + return feature_collection + else: + return None + @staticmethod async def get_all_tasks(db: Connection, project_id: uuid.UUID): async with db.cursor(row_factory=dict_row) as cur: From d4127599c26d5d4f654fd5798da4d3ff9910c35e Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 11:41:19 +0545 Subject: [PATCH 2/7] refractor: changes TaskState schemas to Task schema --- src/backend/app/projects/project_routes.py | 2 +- src/backend/app/tasks/task_routes.py | 2 +- src/backend/app/tasks/task_schemas.py | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 33232d46..1040d836 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -60,7 +60,7 @@ async def download_task_boundaries( Returns: Response: The HTTP response object containing the downloaded file. """ - out = await task_schemas.TaskState.get_task_geometry(db, project_id, task_id) + out = await task_schemas.Task.get_task_geometry(db, project_id, task_id) if out is None: return Response( diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 9687f2f9..68c5518d 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -158,7 +158,7 @@ async def task_states( db: Annotated[Connection, Depends(database.get_db)], project_id: uuid.UUID ): """Get all tasks states for a project.""" - return await task_schemas.TaskState.all(db, project_id) + return await task_schemas.Task.all(db, project_id) @router.post("/event/{project_id}/{task_id}") diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index d823cadb..75f22b89 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -14,10 +14,11 @@ class NewEvent(BaseModel): comment: Optional[str] = None -class TaskState(BaseModel): - task_id: uuid.UUID - state: str - project_id: uuid.UUID +class Task(BaseModel): + task_id: Optional[uuid.UUID] = None + state: Optional[str] = None + project_id: Optional[uuid.UUID] = None + outline: Optional[str] = None @staticmethod async def get_task_geometry( @@ -33,7 +34,7 @@ async def get_task_geometry( Returns: str: The GeoJSON representation of the task or project geometry. """ - async with db.cursor(row_factory=dict_row) as cur: + async with db.cursor(row_factory=class_row(Task)) as cur: if task_id: row = await cur.execute( """ @@ -47,7 +48,7 @@ async def get_task_geometry( else: await cur.execute( """ - SELECT ST_AsGeoJSON(outline) AS geojson + SELECT ST_AsGeoJSON(outline) AS outline FROM tasks WHERE project_id = %(project_id)s """, @@ -59,7 +60,7 @@ async def get_task_geometry( if rows: ## Create a FeatureCollection with empty properties for each feature features = [ - f'{{"type": "Feature", "geometry": {row["geojson"]}, "properties": {{}}}}' + f'{{"type": "Feature", "geometry": {row.outline}, "properties": {{}}}}' for row in rows ] feature_collection = f'{{"type": "FeatureCollection", "features": [{",".join(features)}]}}' @@ -84,7 +85,7 @@ async def get_all_tasks(db: Connection, project_id: uuid.UUID): @staticmethod async def all(db: Connection, project_id: uuid.UUID): - async with db.cursor(row_factory=class_row(TaskState)) as cur: + async with db.cursor(row_factory=class_row(Task)) as cur: await cur.execute( """SELECT DISTINCT ON (task_id) project_id, task_id, state FROM task_events @@ -96,7 +97,7 @@ async def all(db: Connection, project_id: uuid.UUID): existing_tasks = await cur.fetchall() # Get all task_ids from the tasks table - task_ids = await TaskState.get_all_tasks(db, project_id) + task_ids = await Task.get_all_tasks(db, project_id) # Create a set of existing task_ids for quick lookup existing_task_ids = {task.task_id for task in existing_tasks} From 234fbb474c24324879d084823670a46069f2e6de Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 11:51:35 +0545 Subject: [PATCH 3/7] fix: issues reslove on single task area download --- src/backend/app/tasks/task_schemas.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 75f22b89..5aac2382 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -36,15 +36,16 @@ async def get_task_geometry( """ async with db.cursor(row_factory=class_row(Task)) as cur: if task_id: - row = await cur.execute( + await cur.execute( """ - SELECT ST_AsGeoJSON(outline) AS geojson + SELECT ST_AsGeoJSON(outline) AS outline FROM tasks WHERE project_id = %(project_id)s AND id = %(task_id)s """, {"project_id": project_id, "task_id": task_id}, ) - return row["geojson"] + row = await cur.fetchone() + return row.outline else: await cur.execute( """ From 6e01f77b8d66ebc688a7d98f940cc5cfb7907dc0 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 11:55:07 +0545 Subject: [PATCH 4/7] feat: added exception handling on get task geometry --- src/backend/app/tasks/task_schemas.py | 75 +++++++++++++++------------ 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 5aac2382..50c3e98e 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -34,40 +34,49 @@ async def get_task_geometry( Returns: str: The GeoJSON representation of the task or project geometry. """ - async with db.cursor(row_factory=class_row(Task)) as cur: - if task_id: - await cur.execute( - """ - SELECT ST_AsGeoJSON(outline) AS outline - FROM tasks - WHERE project_id = %(project_id)s AND id = %(task_id)s - """, - {"project_id": project_id, "task_id": task_id}, - ) - row = await cur.fetchone() - return row.outline - else: - await cur.execute( - """ - SELECT ST_AsGeoJSON(outline) AS outline - FROM tasks - WHERE project_id = %(project_id)s - """, - {"project_id": project_id}, - ) - - # Fetch the result - rows = await cur.fetchall() - if rows: - ## Create a FeatureCollection with empty properties for each feature - features = [ - f'{{"type": "Feature", "geometry": {row.outline}, "properties": {{}}}}' - for row in rows - ] - feature_collection = f'{{"type": "FeatureCollection", "features": [{",".join(features)}]}}' - return feature_collection + try: + async with db.cursor(row_factory=class_row(Task)) as cur: + if task_id: + await cur.execute( + """ + SELECT ST_AsGeoJSON(outline) AS outline + FROM tasks + WHERE project_id = %(project_id)s AND id = %(task_id)s + """, + {"project_id": project_id, "task_id": task_id}, + ) + row = await cur.fetchone() + if row: + return row.outline + else: + raise HTTPException(status_code=404, detail="Task not found.") else: - return None + await cur.execute( + """ + SELECT ST_AsGeoJSON(outline) AS outline + FROM tasks + WHERE project_id = %(project_id)s + """, + {"project_id": project_id}, + ) + + # Fetch the result + rows = await cur.fetchall() + if rows: + # Create a FeatureCollection with empty properties for each feature + features = [ + f'{{"type": "Feature", "geometry": {row.outline}, "properties": {{}}}}' + for row in rows + ] + feature_collection = f'{{"type": "FeatureCollection", "features": [{",".join(features)}]}}' + return feature_collection + else: + raise HTTPException( + status_code=404, detail="No tasks found for this project." + ) + except Exception as e: + log.error(f"Error fetching task geometry: {e}") + raise HTTPException(status_code=500, detail="Internal server error.") @staticmethod async def get_all_tasks(db: Connection, project_id: uuid.UUID): From 707ca6cb7efefc4fb85c515bcfe48abd12ecd69c Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 12:19:10 +0545 Subject: [PATCH 5/7] feat: api download if split_area is false --- src/backend/app/projects/project_routes.py | 137 +++++++++++++++++++-- src/backend/app/tasks/task_schemas.py | 31 +++-- 2 files changed, 146 insertions(+), 22 deletions(-) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 1040d836..761feaee 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -35,8 +35,8 @@ ) -@router.get("/{project_id}/download_tasks", tags=["Projects"]) -async def download_task_boundaries( +@router.get("/{project_id}/download-boundaries", tags=["Projects"]) +async def download_boundaries( project_id: Annotated[ UUID, Path( @@ -49,31 +49,142 @@ async def download_task_boundaries( default=None, description="The task ID in UUID format. If not provided, all tasks will be downloaded.", ), + split_area: bool = Query( + default=False, + description="Whether to split the area or not. Set to True to download task boundaries, otherwise AOI will be downloaded.", + ), ): - """Downloads the boundary of the tasks for a project as a GeoJSON file. + """Downloads the AOI or task boundaries for a project as a GeoJSON file. Args: project_id (UUID): The ID of the project in UUID format. db (Connection): The database connection, provided automatically. user_data (AuthUser): The authenticated user data, checks if the user has permission. + task_id (Optional[UUID]): The task ID in UUID format. If not provided and split_area is True, all tasks will be downloaded. + split_area (bool): Whether to split the area or not. Set to True to download task boundaries, otherwise AOI will be downloaded. Returns: Response: The HTTP response object containing the downloaded file. """ - out = await task_schemas.Task.get_task_geometry(db, project_id, task_id) + try: + out = await task_schemas.Task.get_task_geometry( + db, project_id, task_id, split_area + ) - if out is None: - return Response( - status_code=404, content={"message": "Task or project geometry not found."} + if out is None: + raise HTTPException(status_code=404, detail="Geometry not found.") + + filename = ( + (f"task_{task_id}.geojson" if task_id else "project_outline.geojson") + if split_area + else "project_aoi.geojson" ) - filename = f"task_{task_id}.geojson" if task_id else "project_outline.geojson" + headers = { + "Content-Disposition": f"attachment; filename={filename}", + "Content-Type": "application/geo+json", + } + return Response(content=out, headers=headers) + + except HTTPException as e: + log.error(f"Error during boundaries download: {e.detail}") + raise e - headers = { - "Content-Disposition": f"attachment; filename={filename}", - "Content-Type": "application/geo+json", - } - return Response(content=out, headers=headers) + except Exception as e: + log.error(f"Unexpected error during boundaries download: {e}") + raise HTTPException(status_code=500, detail="Internal server error.") + + +# @router.get("/{project_id}/download-aoi", tags=["Projects"]) +# async def download_project_aoi( +# project_id: Annotated[ +# UUID, +# Path( +# description="The project ID in UUID format.", +# ), +# ], +# db: Annotated[Connection, Depends(database.get_db)], +# user_data: Annotated[AuthUser, Depends(login_required)], +# ): +# """Downloads the Area of Interest (AOI) of a project as a GeoJSON file. + +# Args: +# project_id (UUID): The ID of the project in UUID format. +# db (Connection): The database connection, provided automatically. +# user_data (AuthUser): The authenticated user data, checks if the user has permission. + +# Returns: +# Response: The HTTP response object containing the downloaded AOI file. +# """ +# try: +# # Assuming there is a function `get_project_aoi_geometry` to get the AOI geometry +# out = await task_schemas.Task.get_task_geometry( +# db, project_id, split_area=False +# ) + +# if out is None: +# raise HTTPException( +# status_code=404, detail="Project AOI geometry not found." +# ) + +# filename = "project_aoi.geojson" + +# headers = { +# "Content-Disposition": f"attachment; filename={filename}", +# "Content-Type": "application/geo+json", +# } +# return Response(content=out, headers=headers) + +# except HTTPException as e: +# log.error(f"Error during AOI download: {e.detail}") +# raise e + +# except Exception as e: +# log.error(f"Unexpected error during AOI download: {e}") +# raise HTTPException(status_code=500, detail="Internal server error.") + + +# @router.get("/{project_id}/download-tasks", tags=["Projects"]) +# async def download_task_boundaries( +# project_id: Annotated[ +# UUID, +# Path( +# description="The project ID in UUID format.", +# ), +# ], +# db: Annotated[Connection, Depends(database.get_db)], +# user_data: Annotated[AuthUser, Depends(login_required)], +# task_id: Optional[UUID] = Query( +# default=None, +# description="The task ID in UUID format. If not provided, all tasks will be downloaded.", +# ), +# ): +# """Downloads the boundary of the tasks for a project as a GeoJSON file. + +# Args: +# project_id (UUID): The ID of the project in UUID format. +# db (Connection): The database connection, provided automatically. +# user_data (AuthUser): The authenticated user data, checks if the user has permission. + +# Returns: +# Response: The HTTP response object containing the downloaded file. +# """ +# out = await task_schemas.Task.get_task_geometry( +# db, project_id, task_id, split_area=True +# ) + +# if out is None: +# raise HTTPException( +# status_code=404, detail="Task or project geometry not found." +# ) + +# filename = f"task_{task_id}.geojson" if task_id else "project_outline.geojson" + +# headers = { +# "Content-Disposition": f"attachment; filename={filename}", +# "Content-Type": "application/geo+json", +# } +# return Response(content=out, headers=headers) @router.delete("/{project_id}", tags=["Projects"]) diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py index 50c3e98e..12cba359 100644 --- a/src/backend/app/tasks/task_schemas.py +++ b/src/backend/app/tasks/task_schemas.py @@ -22,7 +22,10 @@ class Task(BaseModel): @staticmethod async def get_task_geometry( - db: Connection, project_id: uuid.UUID, task_id: Optional[uuid.UUID] = None + db: Connection, + project_id: uuid.UUID, + task_id: Optional[uuid.UUID] = None, + split_area: Optional[bool] = False, ) -> str: """Fetches the geometry of a single task or all tasks in a project. @@ -51,14 +54,24 @@ async def get_task_geometry( else: raise HTTPException(status_code=404, detail="Task not found.") else: - await cur.execute( - """ - SELECT ST_AsGeoJSON(outline) AS outline - FROM tasks - WHERE project_id = %(project_id)s - """, - {"project_id": project_id}, - ) + if split_area: + await cur.execute( + """ + SELECT ST_AsGeoJSON(outline) AS outline + FROM tasks + WHERE project_id = %(project_id)s + """, + {"project_id": project_id}, + ) + else: + await cur.execute( + """ + SELECT ST_AsGeoJSON(ST_Union(outline)) AS outline + FROM tasks + WHERE project_id = %(project_id)s + """, + {"project_id": project_id}, + ) # Fetch the result rows = await cur.fetchall() From e6932870343c5311a47008c13c35988e158f20c4 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 12:21:18 +0545 Subject: [PATCH 6/7] fix: remove commented code --- src/backend/app/projects/project_routes.py | 92 ---------------------- 1 file changed, 92 deletions(-) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 761feaee..151b872c 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -95,98 +95,6 @@ async def download_boundaries( raise HTTPException(status_code=500, detail="Internal server error.") -# @router.get("/{project_id}/download-aoi", tags=["Projects"]) -# async def download_project_aoi( -# project_id: Annotated[ -# UUID, -# Path( -# description="The project ID in UUID format.", -# ), -# ], -# db: Annotated[Connection, Depends(database.get_db)], -# user_data: Annotated[AuthUser, Depends(login_required)], -# ): -# """Downloads the Area of Interest (AOI) of a project as a GeoJSON file. - -# Args: -# project_id (UUID): The ID of the project in UUID format. -# db (Connection): The database connection, provided automatically. -# user_data (AuthUser): The authenticated user data, checks if the user has permission. - -# Returns: -# Response: The HTTP response object containing the downloaded AOI file. -# """ -# try: -# # Assuming there is a function `get_project_aoi_geometry` to get the AOI geometry -# out = await task_schemas.Task.get_task_geometry( -# db, project_id, split_area=False -# ) - -# if out is None: -# raise HTTPException( -# status_code=404, detail="Project AOI geometry not found." -# ) - -# filename = "project_aoi.geojson" - -# headers = { -# "Content-Disposition": f"attachment; filename={filename}", -# "Content-Type": "application/geo+json", -# } -# return Response(content=out, headers=headers) - -# except HTTPException as e: -# log.error(f"Error during AOI download: {e.detail}") -# raise e - -# except Exception as e: -# log.error(f"Unexpected error during AOI download: {e}") -# raise HTTPException(status_code=500, detail="Internal server error.") - - -# @router.get("/{project_id}/download-tasks", tags=["Projects"]) -# async def download_task_boundaries( -# project_id: Annotated[ -# UUID, -# Path( -# description="The project ID in UUID format.", -# ), -# ], -# db: Annotated[Connection, Depends(database.get_db)], -# user_data: Annotated[AuthUser, Depends(login_required)], -# task_id: Optional[UUID] = Query( -# default=None, -# description="The task ID in UUID format. If not provided, all tasks will be downloaded.", -# ), -# ): -# """Downloads the boundary of the tasks for a project as a GeoJSON file. - -# Args: -# project_id (UUID): The ID of the project in UUID format. -# db (Connection): The database connection, provided automatically. -# user_data (AuthUser): The authenticated user data, checks if the user has permission. - -# Returns: -# Response: The HTTP response object containing the downloaded file. -# """ -# out = await task_schemas.Task.get_task_geometry( -# db, project_id, task_id, split_area=True -# ) - -# if out is None: -# raise HTTPException( -# status_code=404, detail="Task or project geometry not found." -# ) - -# filename = f"task_{task_id}.geojson" if task_id else "project_outline.geojson" - -# headers = { -# "Content-Disposition": f"attachment; filename={filename}", -# "Content-Type": "application/geo+json", -# } -# return Response(content=out, headers=headers) - - @router.delete("/{project_id}", tags=["Projects"]) async def delete_project_by_id( project: Annotated[ From 8bccf67326e70611038a4c0be22acdfd617895e3 Mon Sep 17 00:00:00 2001 From: Pradip-p Date: Mon, 2 Sep 2024 14:38:49 +0545 Subject: [PATCH 7/7] feat: Automatically approve task locking when requested by project owner --- src/backend/app/tasks/task_routes.py | 43 ++++++++++++++++------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/backend/app/tasks/task_routes.py b/src/backend/app/tasks/task_routes.py index 68c5518d..95d96d67 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -177,27 +177,31 @@ async def new_event( project = project.model_dump() match detail.event: case EventType.REQUESTS: - if project["requires_approval_from_manager_for_locking"] is False: - data = await task_logic.request_mapping( - db, - project_id, - task_id, - user_id, - "Request accepted automatically", - State.UNLOCKED_TO_MAP, - State.LOCKED_FOR_MAPPING, + # Determine the appropriate state and message + is_author = project["author_id"] == user_id + requires_approval = project["requires_approval_from_manager_for_locking"] + + if is_author or not requires_approval: + state_after = State.LOCKED_FOR_MAPPING + message = "Request accepted automatically" + ( + " as the author" if is_author else "" ) else: - data = await task_logic.request_mapping( - db, - project_id, - task_id, - user_id, - "Request for mapping", - State.UNLOCKED_TO_MAP, - State.REQUEST_FOR_MAPPING, - ) - # email notification + state_after = State.REQUEST_FOR_MAPPING + message = "Request for mapping" + + # Perform the mapping request + data = await task_logic.request_mapping( + db, + project_id, + task_id, + user_id, + message, + State.UNLOCKED_TO_MAP, + state_after, + ) + # Send email notification if approval is required + if state_after == State.REQUEST_FOR_MAPPING: author = await user_schemas.DbUser.get_user_by_id( db, project["author_id"] ) @@ -217,6 +221,7 @@ async def new_event( "Request for mapping", html_content, ) + return data case EventType.MAP: