Inital stab at local flights

This commit is contained in:
2025-12-12 06:14:36 -05:00
parent 56e4ab6e3e
commit 0aeed2268a
8 changed files with 1217 additions and 84 deletions

View File

@@ -1,10 +1,11 @@
from fastapi import APIRouter
from app.api.endpoints import auth, pprs, public, aircraft, airport
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights
api_router = APIRouter()
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
api_router.include_router(pprs.router, prefix="/pprs", tags=["pprs"])
api_router.include_router(local_flights.router, prefix="/local-flights", tags=["local_flights"])
api_router.include_router(public.router, prefix="/public", tags=["public"])
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])

View File

@@ -0,0 +1,195 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session
from datetime import date
from app.api.deps import get_db, get_current_read_user, get_current_operator_user
from app.crud.crud_local_flight import local_flight as crud_local_flight
from app.schemas.local_flight import LocalFlight, LocalFlightCreate, LocalFlightUpdate, LocalFlightStatus, LocalFlightType, LocalFlightStatusUpdate
from app.models.ppr import User
from app.core.utils import get_client_ip
router = APIRouter()
@router.get("/", response_model=List[LocalFlight])
async def get_local_flights(
request: Request,
skip: int = 0,
limit: int = 100,
status: Optional[LocalFlightStatus] = None,
flight_type: Optional[LocalFlightType] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get local flight records with optional filtering"""
flights = crud_local_flight.get_multi(
db, skip=skip, limit=limit, status=status,
flight_type=flight_type, date_from=date_from, date_to=date_to
)
return flights
@router.post("/", response_model=LocalFlight)
async def create_local_flight(
request: Request,
flight_in: LocalFlightCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Create a new local flight record (book out)"""
flight = crud_local_flight.create(db, obj_in=flight_in, created_by=current_user.username)
# Send real-time update via WebSocket
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_booked_out",
"data": {
"id": flight.id,
"registration": flight.registration,
"flight_type": flight.flight_type.value,
"status": flight.status.value
}
})
return flight
@router.get("/{flight_id}", response_model=LocalFlight)
async def get_local_flight(
flight_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get a specific local flight record"""
flight = crud_local_flight.get(db, flight_id=flight_id)
if not flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
return flight
@router.put("/{flight_id}", response_model=LocalFlight)
async def update_local_flight(
request: Request,
flight_id: int,
flight_in: LocalFlightUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update a local flight record"""
db_flight = crud_local_flight.get(db, flight_id=flight_id)
if not db_flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
flight = crud_local_flight.update(db, db_obj=db_flight, obj_in=flight_in)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_updated",
"data": {
"id": flight.id,
"registration": flight.registration,
"status": flight.status.value
}
})
return flight
@router.patch("/{flight_id}/status", response_model=LocalFlight)
async def update_local_flight_status(
request: Request,
flight_id: int,
status_update: LocalFlightStatusUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update local flight status (LANDED, CANCELLED, etc.)"""
flight = crud_local_flight.update_status(
db,
flight_id=flight_id,
status=status_update.status,
timestamp=status_update.timestamp
)
if not flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_status_update",
"data": {
"id": flight.id,
"registration": flight.registration,
"status": flight.status.value,
"landed_dt": flight.landed_dt.isoformat() if flight.landed_dt else None
}
})
return flight
@router.delete("/{flight_id}", response_model=LocalFlight)
async def cancel_local_flight(
request: Request,
flight_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Cancel a local flight record"""
flight = crud_local_flight.cancel(db, flight_id=flight_id)
if not flight:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Local flight record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "local_flight_cancelled",
"data": {
"id": flight.id,
"registration": flight.registration
}
})
return flight
@router.get("/active/current", response_model=List[LocalFlight])
async def get_active_flights(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get currently active (booked out) flights"""
return crud_local_flight.get_active_flights(db)
@router.get("/today/departures", response_model=List[LocalFlight])
async def get_today_departures(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get today's departures (booked out or departed)"""
return crud_local_flight.get_departures_today(db)
@router.get("/today/booked-out", response_model=List[LocalFlight])
async def get_today_booked_out(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get all flights booked out today"""
return crud_local_flight.get_booked_out_today(db)

View File

@@ -0,0 +1,136 @@
from typing import List, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_, func, desc
from datetime import date, datetime
from app.models.local_flight import LocalFlight, LocalFlightStatus, LocalFlightType
from app.schemas.local_flight import LocalFlightCreate, LocalFlightUpdate, LocalFlightStatusUpdate
class CRUDLocalFlight:
def get(self, db: Session, flight_id: int) -> Optional[LocalFlight]:
return db.query(LocalFlight).filter(LocalFlight.id == flight_id).first()
def get_multi(
self,
db: Session,
skip: int = 0,
limit: int = 100,
status: Optional[LocalFlightStatus] = None,
flight_type: Optional[LocalFlightType] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None
) -> List[LocalFlight]:
query = db.query(LocalFlight)
if status:
query = query.filter(LocalFlight.status == status)
if flight_type:
query = query.filter(LocalFlight.flight_type == flight_type)
if date_from:
query = query.filter(func.date(LocalFlight.booked_out_dt) >= date_from)
if date_to:
query = query.filter(func.date(LocalFlight.booked_out_dt) <= date_to)
return query.order_by(desc(LocalFlight.booked_out_dt)).offset(skip).limit(limit).all()
def get_active_flights(self, db: Session) -> List[LocalFlight]:
"""Get currently active (booked out or departed) flights"""
return db.query(LocalFlight).filter(
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.DEPARTED
)
).order_by(desc(LocalFlight.booked_out_dt)).all()
def get_departures_today(self, db: Session) -> List[LocalFlight]:
"""Get today's departures (booked out or departed)"""
today = date.today()
return db.query(LocalFlight).filter(
and_(
func.date(LocalFlight.booked_out_dt) == today,
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.DEPARTED
)
)
).order_by(LocalFlight.booked_out_dt).all()
def get_booked_out_today(self, db: Session) -> List[LocalFlight]:
"""Get all flights booked out today"""
today = date.today()
return db.query(LocalFlight).filter(
and_(
func.date(LocalFlight.booked_out_dt) == today,
or_(
LocalFlight.status == LocalFlightStatus.BOOKED_OUT,
LocalFlight.status == LocalFlightStatus.LANDED
)
)
).order_by(LocalFlight.booked_out_dt).all()
def create(self, db: Session, obj_in: LocalFlightCreate, created_by: str) -> LocalFlight:
db_obj = LocalFlight(
**obj_in.dict(),
created_by=created_by,
status=LocalFlightStatus.BOOKED_OUT
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, db_obj: LocalFlight, obj_in: LocalFlightUpdate) -> LocalFlight:
update_data = obj_in.dict(exclude_unset=True)
for field, value in update_data.items():
if value is not None:
setattr(db_obj, field, value)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update_status(
self,
db: Session,
flight_id: int,
status: LocalFlightStatus,
timestamp: Optional[datetime] = None
) -> Optional[LocalFlight]:
db_obj = self.get(db, flight_id)
if not db_obj:
return None
# Ensure status is a LocalFlightStatus enum
if isinstance(status, str):
status = LocalFlightStatus(status)
db_obj.status = status
# Set timestamps based on status
current_time = timestamp if timestamp is not None else datetime.utcnow()
if status == LocalFlightStatus.DEPARTED:
db_obj.departure_dt = current_time
elif status == LocalFlightStatus.LANDED:
db_obj.landed_dt = current_time
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def cancel(self, db: Session, flight_id: int) -> Optional[LocalFlight]:
db_obj = self.get(db, flight_id)
if db_obj:
db_obj.status = LocalFlightStatus.CANCELLED
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
local_flight = CRUDLocalFlight()

View File

@@ -8,6 +8,10 @@ import redis.asyncio as redis
from app.core.config import settings
from app.api.api import api_router
# Import models to ensure they're registered with SQLAlchemy
from app.models.ppr import PPRRecord, User, Journal, Airport, Aircraft
from app.models.local_flight import LocalFlight
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

View File

@@ -0,0 +1,35 @@
from sqlalchemy import Column, Integer, String, DateTime, Text, Enum as SQLEnum, BigInteger
from sqlalchemy.sql import func
from enum import Enum
from app.db.session import Base
class LocalFlightType(str, Enum):
LOCAL = "LOCAL"
CIRCUITS = "CIRCUITS"
DEPARTURE = "DEPARTURE"
class LocalFlightStatus(str, Enum):
BOOKED_OUT = "BOOKED_OUT"
DEPARTED = "DEPARTED"
LANDED = "LANDED"
CANCELLED = "CANCELLED"
class LocalFlight(Base):
__tablename__ = "local_flights"
id = Column(BigInteger, primary_key=True, autoincrement=True)
registration = Column(String(16), nullable=False, index=True)
type = Column(String(32), nullable=False) # Aircraft type
callsign = Column(String(16), nullable=True)
pob = Column(Integer, nullable=False) # Persons on board
flight_type = Column(SQLEnum(LocalFlightType), nullable=False, index=True)
status = Column(SQLEnum(LocalFlightStatus), nullable=False, default=LocalFlightStatus.BOOKED_OUT, index=True)
notes = Column(Text, nullable=True)
booked_out_dt = Column(DateTime, nullable=False, server_default=func.current_timestamp(), index=True)
departure_dt = Column(DateTime, nullable=True) # Actual takeoff time
landed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())

View File

@@ -0,0 +1,81 @@
from pydantic import BaseModel, validator
from datetime import datetime
from typing import Optional
from enum import Enum
class LocalFlightType(str, Enum):
LOCAL = "LOCAL"
CIRCUITS = "CIRCUITS"
DEPARTURE = "DEPARTURE"
class LocalFlightStatus(str, Enum):
BOOKED_OUT = "BOOKED_OUT"
DEPARTED = "DEPARTED"
LANDED = "LANDED"
CANCELLED = "CANCELLED"
class LocalFlightBase(BaseModel):
registration: str
type: str # Aircraft type
callsign: Optional[str] = None
pob: int
flight_type: LocalFlightType
notes: Optional[str] = None
@validator('registration')
def validate_registration(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Aircraft registration is required')
return v.strip().upper()
@validator('type')
def validate_type(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Aircraft type is required')
return v.strip()
@validator('pob')
def validate_pob(cls, v):
if v is not None and v < 1:
raise ValueError('Persons on board must be at least 1')
return v
class LocalFlightCreate(LocalFlightBase):
pass
class LocalFlightUpdate(BaseModel):
registration: Optional[str] = None
type: Optional[str] = None
callsign: Optional[str] = None
pob: Optional[int] = None
flight_type: Optional[LocalFlightType] = None
status: Optional[LocalFlightStatus] = None
departure_dt: Optional[datetime] = None
notes: Optional[str] = None
class LocalFlightStatusUpdate(BaseModel):
status: LocalFlightStatus
timestamp: Optional[datetime] = None
class LocalFlightInDBBase(LocalFlightBase):
id: int
status: LocalFlightStatus
booked_out_dt: datetime
departure_dt: Optional[datetime] = None
landed_dt: Optional[datetime] = None
created_by: Optional[str] = None
updated_at: datetime
class Config:
from_attributes = True
class LocalFlight(LocalFlightInDBBase):
pass