diff --git a/src/backend/app/drones/drone_crud.py b/src/backend/app/drones/drone_crud.py new file mode 100644 index 00000000..0bb50783 --- /dev/null +++ b/src/backend/app/drones/drone_crud.py @@ -0,0 +1,130 @@ +from app.drones import drone_schemas +from app.models.enums import HTTPStatus +from databases import Database +from loguru import logger as log +from fastapi import HTTPException +from asyncpg import UniqueViolationError +from typing import List +from app.drones.drone_schemas import DroneOut + + +async def read_all_drones(db: Database) -> List[DroneOut]: + """ + Retrieves all drone records from the database. + + Args: + db (Database): The database connection object. + + Returns: + List[DroneOut]: A list of all drone records. + """ + try: + select_query = """ + SELECT * FROM drones + """ + results = await db.fetch_all(select_query) + return results + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" + ) from e + + +async def delete_drone(db: Database, drone_id: int) -> bool: + """ + Deletes a drone record from the database, along with associated drone flights. + + Args: + db (Database): The database connection object. + drone_id (int): The ID of the drone to be deleted. + + Returns: + bool: True if the drone was successfully deleted, False otherwise. + """ + try: + delete_query = """ + WITH deleted_flights AS ( + DELETE FROM drone_flights + WHERE drone_id = :drone_id + RETURNING drone_id + ) + DELETE FROM drones + WHERE id = :drone_id + """ + await db.execute(delete_query, {"drone_id": drone_id}) + return True + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Deletion failed" + ) from e + + +async def get_drone(db: Database, drone_id: int): + """ + Retrieves a drone record from the database. + + Args: + db (Database): The database connection object. + drone_id (int): The ID of the drone to be retrieved. + + Returns: + dict: The drone record if found, otherwise None. + """ + try: + select_query = """ + SELECT * FROM drones + WHERE id = :id + """ + result = await db.fetch_one(select_query, {"id": drone_id}) + return result + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Retrieval failed" + ) from e + + +async def create_drone(db: Database, drone_info: drone_schemas.DroneIn): + """ + Creates a new drone record in the database. + + Args: + db (Database): The database connection object. + drone (drone_schemas.DroneIn): The schema object containing drone details. + + Returns: + The ID of the newly created drone record. + """ + try: + insert_query = """ + INSERT INTO drones ( + model, manufacturer, camera_model, sensor_width, sensor_height, + max_battery_health, focal_length, image_width, image_height, + max_altitude, max_speed, weight, created + ) VALUES ( + :model, :manufacturer, :camera_model, :sensor_width, :sensor_height, + :max_battery_health, :focal_length, :image_width, :image_height, + :max_altitude, :max_speed, :weight, CURRENT_TIMESTAMP + ) + RETURNING id + """ + result = await db.execute(insert_query, drone_info.__dict__) + return result + + except UniqueViolationError as e: + log.exception("Unique constraint violation: %s", e) + raise HTTPException( + status_code=HTTPStatus.CONFLICT, + detail="A drone with this model already exists", + ) + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Drone creation failed" + ) from e diff --git a/src/backend/app/drones/drone_routes.py b/src/backend/app/drones/drone_routes.py new file mode 100644 index 00000000..a3fc232e --- /dev/null +++ b/src/backend/app/drones/drone_routes.py @@ -0,0 +1,106 @@ +from app.users.user_deps import login_required +from app.users.user_schemas import AuthUser +from app.models.enums import HTTPStatus +from fastapi import APIRouter, Depends, HTTPException +from app.db.database import get_db +from app.config import settings +from app.drones import drone_schemas +from databases import Database +from app.drones import drone_crud +from typing import List + + +router = APIRouter( + prefix=f"{settings.API_PREFIX}/drones", + responses={404: {"description": "Not found"}}, +) + + +@router.get("/", tags=["Drones"], response_model=List[drone_schemas.DroneOut]) +async def read_drones( + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Retrieves all drone records from the database. + + Args: + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + List[drone_schemas.DroneOut]: A list of all drone records. + """ + drones = await drone_crud.read_all_drones(db) + return drones + + +@router.delete("/{drone_id}", tags=["Drones"]) +async def delete_drone( + drone_id: int, + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Deletes a drone record from the database. + + Args: + drone_id (int): The ID of the drone to be deleted. + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + dict: A success message if the drone was deleted. + """ + success = await drone_crud.delete_drone(db, drone_id) + if not success: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") + return {"message": "Drone deleted successfully"} + + +@router.post("/create_drone", tags=["Drones"]) +async def create_drone( + drone_info: drone_schemas.DroneIn, + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Creates a new drone record in the database. + + Args: + drone_info (drone_schemas.DroneIn): The schema object containing drone details. + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + dict: A dictionary containing a success message and the ID of the newly created drone. + """ + drone_id = await drone_crud.create_drone(db, drone_info) + if not drone_id: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail="Drone creation failed" + ) + return {"message": "Drone created successfully", "drone_id": drone_id} + + +@router.get("/{drone_id}", tags=["Drones"], response_model=drone_schemas.DroneOut) +async def read_drone( + drone_id: int, + db: Database = Depends(get_db), + user_data: AuthUser = Depends(login_required), +): + """ + Retrieves a drone record from the database. + + Args: + drone_id (int): The ID of the drone to be retrieved. + db (Database, optional): The database session object. + user_data (AuthUser, optional): The authenticated user data. + + Returns: + dict: The drone record if found. + """ + drone = await drone_crud.get_drone(db, drone_id) + if not drone: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Drone not found") + return drone diff --git a/src/backend/app/drones/drone_schemas.py b/src/backend/app/drones/drone_schemas.py new file mode 100644 index 00000000..f92cb136 --- /dev/null +++ b/src/backend/app/drones/drone_schemas.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel + + +class DroneIn(BaseModel): + model: str + manufacturer: str + camera_model: str + sensor_width: float + sensor_height: float + max_battery_health: float + focal_length: float + image_width: int + image_height: int + max_altitude: float + max_speed: float + weight: float + + +class DroneOut(BaseModel): + id: int + model: str diff --git a/src/backend/app/main.py b/src/backend/app/main.py index 64981266..643340a7 100644 --- a/src/backend/app/main.py +++ b/src/backend/app/main.py @@ -10,6 +10,7 @@ from app.config import settings from app.projects import project_routes +from app.drones import drone_routes from app.waypoints import waypoint_routes from app.users import oauth_routes from app.users import user_routes @@ -94,7 +95,7 @@ def get_application() -> FastAPI: allow_headers=["*"], expose_headers=["Content-Disposition"], ) - + _app.include_router(drone_routes.router) _app.include_router(project_routes.router) _app.include_router(waypoint_routes.router) _app.include_router(user_routes.router) diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py index c59e9e43..757873c5 100644 --- a/src/backend/app/projects/project_routes.py +++ b/src/backend/app/projects/project_routes.py @@ -50,7 +50,11 @@ async def delete_project_by_id( ), deleted_tasks AS ( DELETE FROM tasks WHERE project_id = :project_id - RETURNING project_id + RETURNING id + ), deleted_task_events AS ( + DELETE FROM task_events + WHERE project_id = :project_id + RETURNING id ) SELECT id FROM deleted_project """