Implement basic aircraft management
This commit is contained in:
parent
b7610e9b6f
commit
04e8c8ca8c
@ -6,7 +6,7 @@ from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from database.utils import create_admin_user
|
||||
from routes import users, flights, auth
|
||||
from routes import users, flights, auth, aircraft
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
@ -31,4 +31,5 @@ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
||||
# Add subroutes
|
||||
app.include_router(users.router, tags=["Users"], prefix="/users")
|
||||
app.include_router(flights.router, tags=["Flights"], prefix="/flights")
|
||||
app.include_router(aircraft.router, tags=["Aircraft"], prefix="/aircraft")
|
||||
app.include_router(auth.router, tags=["Auth"], prefix="/auth")
|
||||
|
87
api/database/aircraft.py
Normal file
87
api/database/aircraft.py
Normal file
@ -0,0 +1,87 @@
|
||||
from bson import ObjectId
|
||||
from fastapi import HTTPException
|
||||
|
||||
from database.db import aircraft_collection
|
||||
from database.utils import aircraft_display_helper, aircraft_add_helper
|
||||
from schemas.aircraft import AircraftDisplaySchema, AircraftCreateSchema
|
||||
|
||||
|
||||
async def retrieve_aircraft(user: str = "") -> list[AircraftDisplaySchema]:
|
||||
"""
|
||||
Retrieve a list of aircraft, optionally filtered by user
|
||||
|
||||
:param user: User to filter aircraft by
|
||||
:return: List of aircraft
|
||||
"""
|
||||
aircraft = []
|
||||
if user == "":
|
||||
async for doc in aircraft_collection.find():
|
||||
aircraft.append(AircraftDisplaySchema(**aircraft_display_helper(doc)))
|
||||
else:
|
||||
async for doc in aircraft_collection.find({"user": ObjectId(user)}):
|
||||
aircraft.append(AircraftDisplaySchema(**aircraft_display_helper(doc)))
|
||||
|
||||
return aircraft
|
||||
|
||||
|
||||
async def retrieve_aircraft_by_id(id: str) -> AircraftDisplaySchema:
|
||||
"""
|
||||
Retrieve details about the requested aircraft
|
||||
|
||||
:param id: ID of desired aircraft
|
||||
:return: Aircraft details
|
||||
"""
|
||||
aircraft = await aircraft_collection.find_one({"_id": ObjectId(id)})
|
||||
|
||||
if aircraft is None:
|
||||
raise HTTPException(404, "Aircraft not found")
|
||||
|
||||
return AircraftDisplaySchema(**aircraft_display_helper(aircraft))
|
||||
|
||||
|
||||
async def insert_aircraft(body: AircraftCreateSchema, id: str) -> ObjectId:
|
||||
"""
|
||||
Insert a new aircraft into the database
|
||||
|
||||
:param body: Aircraft data
|
||||
:param id: ID of creating user
|
||||
:return: ID of inserted aircraft
|
||||
"""
|
||||
aircraft = await aircraft_collection.insert_one(aircraft_add_helper(body.model_dump(), id))
|
||||
return aircraft.inserted_id
|
||||
|
||||
|
||||
async def update_aircraft(body: AircraftCreateSchema, id: str) -> AircraftDisplaySchema:
|
||||
"""
|
||||
Update given aircraft in the database
|
||||
|
||||
:param body: Updated aircraft data
|
||||
:param id: ID of aircraft to update
|
||||
:return: ID of updated aircraft
|
||||
"""
|
||||
aircraft = await aircraft_collection.find_one({"_id": ObjectId(id)})
|
||||
|
||||
if aircraft is None:
|
||||
raise HTTPException(404, "Aircraft not found")
|
||||
|
||||
updated_aircraft = await aircraft_collection.update_one({"_id": ObjectId(id)}, {"$set": body.model_dump()})
|
||||
if updated_aircraft is None:
|
||||
raise HTTPException(500, "Failed to update flight")
|
||||
|
||||
return id
|
||||
|
||||
|
||||
async def delete_aircraft(id: str) -> AircraftDisplaySchema:
|
||||
"""
|
||||
Delete the given aircraft from the database
|
||||
|
||||
:param id: ID of aircraft to delete
|
||||
:return: Deleted aircraft information
|
||||
"""
|
||||
aircraft = await aircraft_collection.find_one({"_id": ObjectId(id)})
|
||||
|
||||
if aircraft is None:
|
||||
raise HTTPException(404, "Aircraft not found")
|
||||
|
||||
await aircraft_collection.delete_one({"_id": ObjectId(id)})
|
||||
return AircraftDisplaySchema(**aircraft_display_helper(aircraft))
|
@ -24,4 +24,5 @@ except Exception as e:
|
||||
# Get db collections
|
||||
user_collection = db_client["user"]
|
||||
flight_collection = db_client["flight"]
|
||||
aircraft_collection = db_client["aircraft"]
|
||||
token_collection = db_client["token_blacklist"]
|
||||
|
@ -88,8 +88,7 @@ async def retrieve_flight(id: str) -> FlightDisplaySchema:
|
||||
:param id: ID of flight to retrieve
|
||||
:return: Flight information
|
||||
"""
|
||||
oid = ObjectId(id)
|
||||
flight = await flight_collection.find_one({"_id": oid})
|
||||
flight = await flight_collection.find_one({"_id": ObjectId(id)})
|
||||
|
||||
if flight is None:
|
||||
raise HTTPException(404, "Flight not found")
|
||||
|
@ -3,6 +3,7 @@ import logging
|
||||
from bson import ObjectId
|
||||
|
||||
from app.config import get_settings
|
||||
from schemas.aircraft import AircraftCategory, AircraftClass
|
||||
from .db import user_collection
|
||||
from routes.utils import get_hashed_password
|
||||
from schemas.user import AuthLevel, UserCreateSchema
|
||||
@ -13,6 +14,7 @@ logger = logging.getLogger("api")
|
||||
def user_helper(user) -> dict:
|
||||
"""
|
||||
Convert given db response into a format usable by UserDisplaySchema
|
||||
|
||||
:param user: Database response
|
||||
:return: Usable dict
|
||||
"""
|
||||
@ -26,6 +28,7 @@ def user_helper(user) -> dict:
|
||||
def system_user_helper(user) -> dict:
|
||||
"""
|
||||
Convert given db response to a format usable by UserSystemSchema
|
||||
|
||||
:param user: Database response
|
||||
:return: Usable dict
|
||||
"""
|
||||
@ -40,6 +43,7 @@ def system_user_helper(user) -> dict:
|
||||
def create_user_helper(user) -> dict:
|
||||
"""
|
||||
Convert given db response to a format usable by UserCreateSchema
|
||||
|
||||
:param user: Database response
|
||||
:return: Usable dict
|
||||
"""
|
||||
@ -53,6 +57,7 @@ def create_user_helper(user) -> dict:
|
||||
def flight_display_helper(flight: dict) -> dict:
|
||||
"""
|
||||
Convert given db response to a format usable by FlightDisplaySchema
|
||||
|
||||
:param flight: Database response
|
||||
:return: Usable dict
|
||||
"""
|
||||
@ -65,6 +70,7 @@ def flight_display_helper(flight: dict) -> dict:
|
||||
def flight_add_helper(flight: dict, user: str) -> dict:
|
||||
"""
|
||||
Convert given flight schema and user string to a format that can be inserted into the db
|
||||
|
||||
:param flight: Flight request body
|
||||
:param user: User that created flight
|
||||
:return: Combined dict that can be inserted into db
|
||||
@ -73,6 +79,36 @@ def flight_add_helper(flight: dict, user: str) -> dict:
|
||||
return flight
|
||||
|
||||
|
||||
def aircraft_add_helper(aircraft: dict, user: str) -> dict:
|
||||
"""
|
||||
Convert given aircraft dict to a format that can be inserted into the db
|
||||
|
||||
:param aircraft: Aircraft request body
|
||||
:param user: User that created aircraft
|
||||
:return: Combined dict that can be inserted into db
|
||||
"""
|
||||
aircraft["user"] = ObjectId(user)
|
||||
aircraft["aircraft_category"] = aircraft["aircraft_category"].name
|
||||
aircraft["aircraft_class"] = aircraft["aircraft_class"].name
|
||||
return aircraft
|
||||
|
||||
|
||||
def aircraft_display_helper(aircraft: dict) -> dict:
|
||||
"""
|
||||
Convert given db response into a format usable by AircraftDisplaySchema
|
||||
|
||||
:param aircraft:
|
||||
:return: USable dict
|
||||
"""
|
||||
aircraft["id"] = str(aircraft["_id"])
|
||||
aircraft["user"] = str(aircraft["user"])
|
||||
if aircraft["aircraft_category"] is not AircraftCategory:
|
||||
aircraft["aircraft_category"] = AircraftCategory.__members__.get(aircraft["aircraft_category"])
|
||||
if aircraft["aircraft_class"] is not AircraftClass:
|
||||
aircraft["aircraft_class"] = AircraftClass.__members__.get(aircraft["aircraft_class"])
|
||||
return aircraft
|
||||
|
||||
|
||||
# UTILS #
|
||||
|
||||
async def create_admin_user():
|
||||
|
117
api/routes/aircraft.py
Normal file
117
api/routes/aircraft.py
Normal file
@ -0,0 +1,117 @@
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from app.deps import get_current_user, admin_required
|
||||
from database import aircraft as db
|
||||
from schemas.aircraft import AircraftDisplaySchema, AircraftCreateSchema
|
||||
from schemas.user import UserDisplaySchema, AuthLevel
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
logger = logging.getLogger("aircraft")
|
||||
|
||||
|
||||
@router.get('/', summary="Get aircraft created by the currently logged-in user", status_code=200)
|
||||
async def get_aircraft(user: UserDisplaySchema = Depends(get_current_user)) -> list[AircraftDisplaySchema]:
|
||||
"""
|
||||
Get a list of aircraft created by the currently logged-in user
|
||||
|
||||
:param user: Current user
|
||||
:return: List of aircraft
|
||||
"""
|
||||
aircraft = await db.retrieve_aircraft(user.id)
|
||||
return aircraft
|
||||
|
||||
|
||||
@router.get('/all', summary="Get all aircraft created by all users", status_code=200,
|
||||
dependencies=[Depends(admin_required)], response_model=list[AircraftDisplaySchema])
|
||||
async def get_all_aircraft() -> list[AircraftDisplaySchema]:
|
||||
"""
|
||||
Get a list of all aircraft created by any user
|
||||
|
||||
:return: List of aircraft
|
||||
"""
|
||||
aircraft = await db.retrieve_aircraft()
|
||||
return aircraft
|
||||
|
||||
|
||||
@router.get('/{aircraft_id}', summary="Get details of a given aircraft", response_model=AircraftDisplaySchema,
|
||||
status_code=200)
|
||||
async def get_aircraft_by_id(aircraft_id: str,
|
||||
user: UserDisplaySchema = Depends(get_current_user)) -> AircraftDisplaySchema:
|
||||
"""
|
||||
Get all details of a given aircraft
|
||||
|
||||
:param aircraft_id: ID of requested aircraft
|
||||
:param user: Currently logged-in user
|
||||
:return: Aircraft details
|
||||
"""
|
||||
aircraft = await db.retrieve_aircraft_by_id(aircraft_id)
|
||||
if str(aircraft.user) != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||
logger.info("Attempted access to unauthorized aircraft by %s", user.username)
|
||||
raise HTTPException(403, "Unauthorized access")
|
||||
|
||||
return aircraft
|
||||
|
||||
|
||||
@router.post('/', summary="Add an aircraft", status_code=200)
|
||||
async def add_aircraft(aircraft_body: AircraftCreateSchema,
|
||||
user: UserDisplaySchema = Depends(get_current_user)) -> dict:
|
||||
"""
|
||||
Add an aircraft to the database
|
||||
|
||||
:param aircraft_body: Information associated with new aircraft
|
||||
:param user: Currently logged-in user
|
||||
:return: Error message if request invalid, else ID of newly created aircraft
|
||||
"""
|
||||
|
||||
aircraft = await db.insert_aircraft(aircraft_body, user.id)
|
||||
|
||||
return {"id": str(aircraft)}
|
||||
|
||||
|
||||
@router.put('/{aircraft_id}', summary="Update the given aircraft with new information", status_code=200)
|
||||
async def update_aircraft(aircraft_id: str, aircraft_body: AircraftCreateSchema,
|
||||
user: UserDisplaySchema = Depends(get_current_user)) -> dict:
|
||||
"""
|
||||
Update the given aircraft with new information
|
||||
|
||||
:param aircraft_id: ID of aircraft to update
|
||||
:param aircraft_body: New aircraft information to update with
|
||||
:param user: Currently logged-in user
|
||||
:return: Updated aircraft
|
||||
"""
|
||||
aircraft = await get_aircraft_by_id(aircraft_id, user)
|
||||
if aircraft is None:
|
||||
raise HTTPException(404, "Aircraft not found")
|
||||
|
||||
if str(aircraft.user) != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||
logger.info("Attempted access to unauthorized aircraft by %s", user.username)
|
||||
raise HTTPException(403, "Unauthorized access")
|
||||
|
||||
updated_aircraft_id = await db.update_aircraft(aircraft_body, aircraft_id)
|
||||
|
||||
return {"id": str(updated_aircraft_id)}
|
||||
|
||||
|
||||
@router.delete('/{aircraft_id}', summary="Delete the given aircraft", status_code=200,
|
||||
response_model=AircraftDisplaySchema)
|
||||
async def delete_aircraft(aircraft_id: str,
|
||||
user: UserDisplaySchema = Depends(get_current_user)) -> AircraftDisplaySchema:
|
||||
"""
|
||||
Delete the given aircraft
|
||||
|
||||
:param aircraft_id: ID of aircraft to delete
|
||||
:param user: Currently logged-in user
|
||||
:return: 200
|
||||
"""
|
||||
aircraft = await get_aircraft_by_id(aircraft_id, user)
|
||||
|
||||
if str(aircraft.user) != user.id and AuthLevel(user.level) != AuthLevel.ADMIN:
|
||||
logger.info("Attempted access to unauthorized aircraft by %s", user.username)
|
||||
raise HTTPException(403, "Unauthorized access")
|
||||
|
||||
deleted = await db.delete_aircraft(aircraft_id)
|
||||
|
||||
return deleted
|
@ -1,14 +1,12 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Union, List
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
|
||||
from app.deps import get_current_user, admin_required
|
||||
from database import flights as db
|
||||
from schemas.flight import FlightConciseSchema, FlightDisplaySchema, FlightCreateSchema, FlightByDateSchema
|
||||
|
||||
from schemas.flight import FlightConciseSchema, FlightDisplaySchema, FlightCreateSchema, FlightByDateSchema
|
||||
from schemas.user import UserDisplaySchema, AuthLevel
|
||||
|
||||
router = APIRouter()
|
||||
@ -27,7 +25,6 @@ async def get_flights(user: UserDisplaySchema = Depends(get_current_user), sort:
|
||||
:param order: Order of sorting (asc/desc)
|
||||
:return: List of flights
|
||||
"""
|
||||
# l = get_flight_list(filters=[[{"field": "user", "operator": "eq", "value": user.id}]])
|
||||
flights = await db.retrieve_flights(user.id, sort, order)
|
||||
return flights
|
||||
|
||||
|
101
api/schemas/aircraft.py
Normal file
101
api/schemas/aircraft.py
Normal file
@ -0,0 +1,101 @@
|
||||
from enum import Enum
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, field_validator, Field
|
||||
from pydantic_core.core_schema import ValidationInfo
|
||||
|
||||
from schemas.flight import PyObjectId
|
||||
|
||||
|
||||
class AircraftCategory(Enum):
|
||||
airplane = "Airplane"
|
||||
rotorcraft = "Rotorcraft"
|
||||
powered_lift = "Powered Lift"
|
||||
glider = "Glider"
|
||||
lighter_than_air = "Lighter-Than-Air"
|
||||
ppg = "Powered Parachute"
|
||||
weight_shift = "Weight-Shift Control"
|
||||
|
||||
|
||||
class AircraftClass(Enum):
|
||||
# Airplane
|
||||
sel = "Single-Engine Land"
|
||||
ses = "Single-Engine Sea"
|
||||
mel = "Multi-Engine Land"
|
||||
mes = "Multi-Engine Sea"
|
||||
|
||||
# Rotorcraft
|
||||
helicopter = "Helicopter"
|
||||
gyroplane = "Gyroplane"
|
||||
|
||||
# Powered Lift
|
||||
powered_lift = "Powered Lift"
|
||||
|
||||
# Glider
|
||||
glider = "Glider"
|
||||
|
||||
# Lighther-than-air
|
||||
airship = "Airship"
|
||||
balloon = "Balloon"
|
||||
|
||||
# Powered Parachute
|
||||
ppl = "Powered Parachute Land"
|
||||
pps = "Powered Parachute Sea"
|
||||
|
||||
# Weight-Shift
|
||||
wsl = "Weight-Shift Control Land"
|
||||
wss = "Weight-Shift Control Sea"
|
||||
|
||||
|
||||
PositiveFloat = Annotated[float, Field(default=0., ge=0)]
|
||||
|
||||
|
||||
class AircraftCreateSchema(BaseModel):
|
||||
tail_no: str
|
||||
make: str
|
||||
model: str
|
||||
aircraft_category: AircraftCategory
|
||||
aircraft_class: AircraftClass
|
||||
|
||||
hobbs: PositiveFloat
|
||||
tach: PositiveFloat
|
||||
|
||||
@field_validator('aircraft_class')
|
||||
def validate_class(cls, v: str, info: ValidationInfo, **kwargs):
|
||||
"""
|
||||
Dependent field validator for aircraft class. Ensures class corresponds to the correct category
|
||||
|
||||
:param v: Value of aircraft_class
|
||||
:param values: Other values in schema
|
||||
:param kwargs:
|
||||
:return: v
|
||||
"""
|
||||
if 'aircraft_category' in info.data.keys():
|
||||
category = info.data['aircraft_category']
|
||||
if category == AircraftCategory.airplane and v not in [AircraftClass.sel, AircraftClass.mel,
|
||||
AircraftClass.ses, AircraftClass.mes]:
|
||||
raise ValueError("Class must be SEL, MEL, SES, or MES for Airplane category")
|
||||
elif category == AircraftCategory.rotorcraft and v not in [AircraftClass.helicopter,
|
||||
AircraftClass.gyroplane]:
|
||||
raise ValueError("Class must be Helicopter or Gyroplane for Rotorcraft category")
|
||||
elif category == AircraftCategory.powered_lift and not v == AircraftClass.powered_lift:
|
||||
raise ValueError("Class must be Powered Lift for Powered Lift category")
|
||||
elif category == AircraftCategory.glider and not v == AircraftClass.glider:
|
||||
raise ValueError("Class must be Glider for Glider category")
|
||||
elif category == AircraftCategory.lighter_than_air and v not in [
|
||||
AircraftClass.airship, AircraftClass.balloon]:
|
||||
raise ValueError("Class must be Airship or Balloon for Lighter-Than-Air category")
|
||||
elif category == AircraftCategory.ppg and v not in [AircraftClass.ppl,
|
||||
AircraftClass.pps]:
|
||||
raise ValueError("Class must be Powered Parachute Land or "
|
||||
"Powered Parachute Sea for Powered Parachute category")
|
||||
elif category == AircraftCategory.weight_shift and v not in [AircraftClass.wsl,
|
||||
AircraftClass.wss]:
|
||||
raise ValueError("Class must be Weight-Shift Control Land or Weight-Shift "
|
||||
"Control Sea for Weight-Shift Control category")
|
||||
return v
|
||||
|
||||
|
||||
class AircraftDisplaySchema(AircraftCreateSchema):
|
||||
user: PyObjectId
|
||||
id: PyObjectId
|
Loading…
x
Reference in New Issue
Block a user