diff --git a/src/backend/app/models/enums.py b/src/backend/app/models/enums.py index 2a3d3ddd..2282f5ca 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. + - ``unlock`` -- Unlock 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" + UNLOCK = "unlock" diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index 6ec05966..acf282b5 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -262,12 +262,25 @@ async def generate_presigned_url( async def read_projects( db: Annotated[Connection, Depends(database.get_db)], user_data: Annotated[AuthUser, Depends(login_required)], + filter_by_owner: Optional[bool] = Query( + False, description="Filter projects by authenticated user (creator)" + ), skip: int = 0, limit: int = 100, ): "Get all projects with task count." + try: - return await project_schemas.DbProject.all(db, skip, limit) + user_id = user_data.id if filter_by_owner else None + projects = await project_schemas.DbProject.all( + db, user_id=user_id, skip=skip, limit=limit + ) + if not projects: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="No projects found." + ) + + return projects except KeyError as e: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) from e diff --git a/src/backend/app/projects/project_schemas.py b/src/backend/app/projects/project_schemas.py index 80a0ee69..a292278f 100644 --- a/src/backend/app/projects/project_schemas.py +++ b/src/backend/app/projects/project_schemas.py @@ -278,20 +278,27 @@ async def one(db: Connection, project_id: uuid.UUID): async def all( db: Connection, + user_id: Optional[str] = None, skip: int = 0, limit: int = 100, ): - """Get all projects.""" + """ + Get all projects. Optionally filter by the project creator (user). + """ async with db.cursor(row_factory=dict_row) as cur: await cur.execute( """ - SELECT id, slug, name, description, per_task_instructions, ST_AsGeoJSON(outline)::jsonb AS outline, requires_approval_from_manager_for_locking - FROM projects - ORDER BY created_at DESC - OFFSET %(skip)s - LIMIT %(limit)s - """, - {"skip": skip, "limit": limit}, + SELECT + id, slug, name, description, per_task_instructions, + ST_AsGeoJSON(outline)::jsonb AS outline, + requires_approval_from_manager_for_locking + FROM projects + WHERE (author_id = COALESCE(%(user_id)s, author_id)) + ORDER BY created_at DESC + OFFSET %(skip)s + LIMIT %(limit)s + """, + {"skip": skip, "limit": limit, "user_id": user_id}, ) db_projects = await cur.fetchall() return db_projects diff --git a/src/backend/app/tasks/task_logic.py b/src/backend/app/tasks/task_logic.py index 68ec2210..c44c11b0 100644 --- a/src/backend/app/tasks/task_logic.py +++ b/src/backend/app/tasks/task_logic.py @@ -128,3 +128,41 @@ 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. + """ + 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 c7323d46..890ad7fa 100644 --- a/src/backend/app/tasks/task_routes.py +++ b/src/backend/app/tasks/task_routes.py @@ -368,4 +368,36 @@ async def new_event( State.UNFLYABLE_TASK, ) + case EventType.UNLOCK: + # Fetch the task state + current_task_state = await task_logic.get_task_state( + db, project_id, task_id + ) + + state = current_task_state.get("state") + locked_user_id = current_task_state.get("user_id") + + # Determine error conditions + if state != State.LOCKED_FOR_MAPPING.name: + raise HTTPException( + status_code=400, + detail="Task state does not match expected state for unlock operation.", + ) + 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 unlocking the task + return await task_logic.update_task_state( + db, + project_id, + task_id, + user_id, + f"Task has been unlock by user {user_data.name}.", + State.LOCKED_FOR_MAPPING, + State.UNLOCKED_TO_MAP, + ) + return True