Getting there

This commit is contained in:
2025-12-12 11:18:28 -05:00
parent f7467690e4
commit dbb285fa20
15 changed files with 1080 additions and 36 deletions

View File

@@ -20,13 +20,13 @@ depends_on = None
def upgrade() -> None:
"""
Create local_flights table for tracking aircraft that book out locally.
Create local_flights, departures, and arrivals tables.
"""
op.create_table('local_flights',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('registration', sa.String(length=16), nullable=False),
sa.Column('type', sa.String(length=32), nullable=False),
sa.Column('type', sa.String(length=32), nullable=True),
sa.Column('callsign', sa.String(length=16), nullable=True),
sa.Column('pob', sa.Integer(), nullable=False),
sa.Column('flight_type', sa.Enum('LOCAL', 'CIRCUITS', 'DEPARTURE', name='localflighttype'), nullable=False),
@@ -43,16 +43,70 @@ def upgrade() -> None:
mysql_collate='utf8mb4_unicode_ci'
)
# Create indexes for frequently queried columns
# Create indexes for local_flights
op.create_index('idx_registration', 'local_flights', ['registration'])
op.create_index('idx_flight_type', 'local_flights', ['flight_type'])
op.create_index('idx_status', 'local_flights', ['status'])
op.create_index('idx_booked_out_dt', 'local_flights', ['booked_out_dt'])
op.create_index('idx_created_by', 'local_flights', ['created_by'])
# Create departures table for non-PPR departures to other airports
op.create_table('departures',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('registration', sa.String(length=16), nullable=False),
sa.Column('type', sa.String(length=32), nullable=True),
sa.Column('callsign', sa.String(length=16), nullable=True),
sa.Column('pob', sa.Integer(), nullable=False),
sa.Column('out_to', sa.String(length=64), nullable=False),
sa.Column('status', sa.Enum('BOOKED_OUT', 'DEPARTED', 'CANCELLED', name='departuresstatus'), nullable=False, server_default='BOOKED_OUT'),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('booked_out_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('departure_dt', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.String(length=16), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci'
)
op.create_index('idx_dep_registration', 'departures', ['registration'])
op.create_index('idx_dep_out_to', 'departures', ['out_to'])
op.create_index('idx_dep_status', 'departures', ['status'])
op.create_index('idx_dep_booked_out_dt', 'departures', ['booked_out_dt'])
op.create_index('idx_dep_created_by', 'departures', ['created_by'])
# Create arrivals table for non-PPR arrivals from elsewhere
op.create_table('arrivals',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('registration', sa.String(length=16), nullable=False),
sa.Column('type', sa.String(length=32), nullable=True),
sa.Column('callsign', sa.String(length=16), nullable=True),
sa.Column('pob', sa.Integer(), nullable=False),
sa.Column('in_from', sa.String(length=64), nullable=False),
sa.Column('status', sa.Enum('BOOKED_IN', 'LANDED', 'CANCELLED', name='arrivalsstatus'), nullable=False, server_default='BOOKED_IN'),
sa.Column('notes', sa.Text(), nullable=True),
sa.Column('booked_in_dt', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('landed_dt', sa.DateTime(), nullable=True),
sa.Column('created_by', sa.String(length=16), nullable=True),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id'),
mysql_engine='InnoDB',
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci'
)
op.create_index('idx_arr_registration', 'arrivals', ['registration'])
op.create_index('idx_arr_in_from', 'arrivals', ['in_from'])
op.create_index('idx_arr_status', 'arrivals', ['status'])
op.create_index('idx_arr_booked_in_dt', 'arrivals', ['booked_in_dt'])
op.create_index('idx_arr_created_by', 'arrivals', ['created_by'])
def downgrade() -> None:
"""
Drop the local_flights table.
Drop the local_flights, departures, and arrivals tables.
"""
op.drop_table('arrivals')
op.drop_table('departures')
op.drop_table('local_flights')

View File

@@ -1,11 +1,13 @@
from fastapi import APIRouter
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals
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(departures.router, prefix="/departures", tags=["departures"])
api_router.include_router(arrivals.router, prefix="/arrivals", tags=["arrivals"])
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,167 @@
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_arrival import arrival as crud_arrival
from app.schemas.arrival import Arrival, ArrivalCreate, ArrivalUpdate, ArrivalStatus, ArrivalStatusUpdate
from app.models.ppr import User
from app.core.utils import get_client_ip
router = APIRouter()
@router.get("/", response_model=List[Arrival])
async def get_arrivals(
request: Request,
skip: int = 0,
limit: int = 100,
status: Optional[ArrivalStatus] = 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 arrival records with optional filtering"""
arrivals = crud_arrival.get_multi(
db, skip=skip, limit=limit, status=status,
date_from=date_from, date_to=date_to
)
return arrivals
@router.post("/", response_model=Arrival)
async def create_arrival(
request: Request,
arrival_in: ArrivalCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Create a new arrival record"""
arrival = crud_arrival.create(db, obj_in=arrival_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": "arrival_booked_in",
"data": {
"id": arrival.id,
"registration": arrival.registration,
"in_from": arrival.in_from,
"status": arrival.status.value
}
})
return arrival
@router.get("/{arrival_id}", response_model=Arrival)
async def get_arrival(
arrival_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get a specific arrival record"""
arrival = crud_arrival.get(db, arrival_id=arrival_id)
if not arrival:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Arrival record not found"
)
return arrival
@router.put("/{arrival_id}", response_model=Arrival)
async def update_arrival(
request: Request,
arrival_id: int,
arrival_in: ArrivalUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update an arrival record"""
db_arrival = crud_arrival.get(db, arrival_id=arrival_id)
if not db_arrival:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Arrival record not found"
)
arrival = crud_arrival.update(db, db_obj=db_arrival, obj_in=arrival_in)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "arrival_updated",
"data": {
"id": arrival.id,
"registration": arrival.registration,
"status": arrival.status.value
}
})
return arrival
@router.patch("/{arrival_id}/status", response_model=Arrival)
async def update_arrival_status(
request: Request,
arrival_id: int,
status_update: ArrivalStatusUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update arrival status"""
arrival = crud_arrival.update_status(
db,
arrival_id=arrival_id,
status=status_update.status,
timestamp=status_update.timestamp
)
if not arrival:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Arrival record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "arrival_status_update",
"data": {
"id": arrival.id,
"registration": arrival.registration,
"status": arrival.status.value,
"landed_dt": arrival.landed_dt.isoformat() if arrival.landed_dt else None
}
})
return arrival
@router.delete("/{arrival_id}", response_model=Arrival)
async def cancel_arrival(
request: Request,
arrival_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Cancel an arrival record"""
arrival = crud_arrival.cancel(db, arrival_id=arrival_id)
if not arrival:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Arrival record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "arrival_cancelled",
"data": {
"id": arrival.id,
"registration": arrival.registration
}
})
return arrival

View File

@@ -0,0 +1,167 @@
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_departure import departure as crud_departure
from app.schemas.departure import Departure, DepartureCreate, DepartureUpdate, DepartureStatus, DepartureStatusUpdate
from app.models.ppr import User
from app.core.utils import get_client_ip
router = APIRouter()
@router.get("/", response_model=List[Departure])
async def get_departures(
request: Request,
skip: int = 0,
limit: int = 100,
status: Optional[DepartureStatus] = 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 departure records with optional filtering"""
departures = crud_departure.get_multi(
db, skip=skip, limit=limit, status=status,
date_from=date_from, date_to=date_to
)
return departures
@router.post("/", response_model=Departure)
async def create_departure(
request: Request,
departure_in: DepartureCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Create a new departure record"""
departure = crud_departure.create(db, obj_in=departure_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": "departure_booked_out",
"data": {
"id": departure.id,
"registration": departure.registration,
"out_to": departure.out_to,
"status": departure.status.value
}
})
return departure
@router.get("/{departure_id}", response_model=Departure)
async def get_departure(
departure_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user)
):
"""Get a specific departure record"""
departure = crud_departure.get(db, departure_id=departure_id)
if not departure:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Departure record not found"
)
return departure
@router.put("/{departure_id}", response_model=Departure)
async def update_departure(
request: Request,
departure_id: int,
departure_in: DepartureUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update a departure record"""
db_departure = crud_departure.get(db, departure_id=departure_id)
if not db_departure:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Departure record not found"
)
departure = crud_departure.update(db, db_obj=db_departure, obj_in=departure_in)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "departure_updated",
"data": {
"id": departure.id,
"registration": departure.registration,
"status": departure.status.value
}
})
return departure
@router.patch("/{departure_id}/status", response_model=Departure)
async def update_departure_status(
request: Request,
departure_id: int,
status_update: DepartureStatusUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Update departure status"""
departure = crud_departure.update_status(
db,
departure_id=departure_id,
status=status_update.status,
timestamp=status_update.timestamp
)
if not departure:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Departure record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "departure_status_update",
"data": {
"id": departure.id,
"registration": departure.registration,
"status": departure.status.value,
"departure_dt": departure.departure_dt.isoformat() if departure.departure_dt else None
}
})
return departure
@router.delete("/{departure_id}", response_model=Departure)
async def cancel_departure(
request: Request,
departure_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user)
):
"""Cancel a departure record"""
departure = crud_departure.cancel(db, departure_id=departure_id)
if not departure:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Departure record not found"
)
# Send real-time update
if hasattr(request.app.state, 'connection_manager'):
await request.app.state.connection_manager.broadcast({
"type": "departure_cancelled",
"data": {
"id": departure.id,
"registration": departure.registration
}
})
return departure

View File

@@ -4,8 +4,12 @@ from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.crud.crud_ppr import ppr as crud_ppr
from app.crud.crud_local_flight import local_flight as crud_local_flight
from app.crud.crud_departure import departure as crud_departure
from app.crud.crud_arrival import arrival as crud_arrival
from app.schemas.ppr import PPRPublic
from app.models.local_flight import LocalFlightStatus
from app.models.departure import DepartureStatus
from app.models.arrival import ArrivalStatus
from datetime import date
router = APIRouter()
@@ -56,7 +60,7 @@ async def get_public_arrivals(db: Session = Depends(get_db)):
@router.get("/departures")
async def get_public_departures(db: Session = Depends(get_db)):
"""Get today's departures for public display (PPR and local flights)"""
"""Get today's departures for public display (PPR, local flights, and departures to other airports)"""
departures = crud_ppr.get_departures_today(db)
# Convert PPR departures to dictionaries
@@ -70,7 +74,8 @@ async def get_public_departures(db: Session = Depends(get_db)):
'etd': departure.etd,
'departed_dt': departure.departed_dt,
'status': departure.status.value,
'isLocalFlight': False
'isLocalFlight': False,
'isDeparture': False
})
# Add local flights with BOOKED_OUT status
@@ -91,7 +96,29 @@ async def get_public_departures(db: Session = Depends(get_db)):
'departed_dt': None,
'status': 'BOOKED_OUT',
'isLocalFlight': True,
'flight_type': flight.flight_type.value
'flight_type': flight.flight_type.value,
'isDeparture': False
})
# Add departures to other airports with BOOKED_OUT status
departures_to_airports = crud_departure.get_multi(
db,
status=DepartureStatus.BOOKED_OUT,
limit=1000
)
# Convert departures to match the format for display
for dep in departures_to_airports:
departures_list.append({
'ac_call': dep.callsign or dep.registration,
'ac_reg': dep.registration,
'ac_type': dep.type,
'out_to': dep.out_to,
'etd': dep.booked_out_dt,
'departed_dt': None,
'status': 'BOOKED_OUT',
'isLocalFlight': False,
'isDeparture': True
})
return departures_list

View File

@@ -0,0 +1,104 @@
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.arrival import Arrival, ArrivalStatus
from app.schemas.arrival import ArrivalCreate, ArrivalUpdate, ArrivalStatusUpdate
class CRUDArrival:
def get(self, db: Session, arrival_id: int) -> Optional[Arrival]:
return db.query(Arrival).filter(Arrival.id == arrival_id).first()
def get_multi(
self,
db: Session,
skip: int = 0,
limit: int = 100,
status: Optional[ArrivalStatus] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None
) -> List[Arrival]:
query = db.query(Arrival)
if status:
query = query.filter(Arrival.status == status)
if date_from:
query = query.filter(func.date(Arrival.booked_in_dt) >= date_from)
if date_to:
query = query.filter(func.date(Arrival.booked_in_dt) <= date_to)
return query.order_by(desc(Arrival.booked_in_dt)).offset(skip).limit(limit).all()
def get_arrivals_today(self, db: Session) -> List[Arrival]:
"""Get today's arrivals (booked in or landed)"""
today = date.today()
return db.query(Arrival).filter(
and_(
func.date(Arrival.booked_in_dt) == today,
or_(
Arrival.status == ArrivalStatus.BOOKED_IN,
Arrival.status == ArrivalStatus.LANDED
)
)
).order_by(Arrival.booked_in_dt).all()
def create(self, db: Session, obj_in: ArrivalCreate, created_by: str) -> Arrival:
db_obj = Arrival(
**obj_in.dict(),
created_by=created_by,
status=ArrivalStatus.BOOKED_IN
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, db_obj: Arrival, obj_in: ArrivalUpdate) -> Arrival:
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,
arrival_id: int,
status: ArrivalStatus,
timestamp: Optional[datetime] = None
) -> Optional[Arrival]:
db_obj = self.get(db, arrival_id)
if not db_obj:
return None
db_obj.status = status
if status == ArrivalStatus.LANDED and timestamp:
db_obj.landed_dt = timestamp
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def cancel(self, db: Session, arrival_id: int) -> Optional[Arrival]:
db_obj = self.get(db, arrival_id)
if not db_obj:
return None
db_obj.status = ArrivalStatus.CANCELLED
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
arrival = CRUDArrival()

View File

@@ -0,0 +1,104 @@
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.departure import Departure, DepartureStatus
from app.schemas.departure import DepartureCreate, DepartureUpdate, DepartureStatusUpdate
class CRUDDeparture:
def get(self, db: Session, departure_id: int) -> Optional[Departure]:
return db.query(Departure).filter(Departure.id == departure_id).first()
def get_multi(
self,
db: Session,
skip: int = 0,
limit: int = 100,
status: Optional[DepartureStatus] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None
) -> List[Departure]:
query = db.query(Departure)
if status:
query = query.filter(Departure.status == status)
if date_from:
query = query.filter(func.date(Departure.booked_out_dt) >= date_from)
if date_to:
query = query.filter(func.date(Departure.booked_out_dt) <= date_to)
return query.order_by(desc(Departure.booked_out_dt)).offset(skip).limit(limit).all()
def get_departures_today(self, db: Session) -> List[Departure]:
"""Get today's departures (booked out or departed)"""
today = date.today()
return db.query(Departure).filter(
and_(
func.date(Departure.booked_out_dt) == today,
or_(
Departure.status == DepartureStatus.BOOKED_OUT,
Departure.status == DepartureStatus.DEPARTED
)
)
).order_by(Departure.booked_out_dt).all()
def create(self, db: Session, obj_in: DepartureCreate, created_by: str) -> Departure:
db_obj = Departure(
**obj_in.dict(),
created_by=created_by,
status=DepartureStatus.BOOKED_OUT
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def update(self, db: Session, db_obj: Departure, obj_in: DepartureUpdate) -> Departure:
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,
departure_id: int,
status: DepartureStatus,
timestamp: Optional[datetime] = None
) -> Optional[Departure]:
db_obj = self.get(db, departure_id)
if not db_obj:
return None
db_obj.status = status
if status == DepartureStatus.DEPARTED and timestamp:
db_obj.departure_dt = timestamp
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
def cancel(self, db: Session, departure_id: int) -> Optional[Departure]:
db_obj = self.get(db, departure_id)
if not db_obj:
return None
db_obj.status = DepartureStatus.CANCELLED
db.add(db_obj)
db.commit()
db.refresh(db_obj)
return db_obj
departure = CRUDDeparture()

View File

@@ -11,6 +11,8 @@ 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
from app.models.departure import Departure
from app.models.arrival import Arrival
# Set up logging
logging.basicConfig(level=logging.INFO)

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, BigInteger, String, Integer, Text, DateTime, Enum as SQLEnum, func
from sqlalchemy.ext.declarative import declarative_base
from enum import Enum
from datetime import datetime
Base = declarative_base()
class ArrivalStatus(str, Enum):
BOOKED_IN = "BOOKED_IN"
LANDED = "LANDED"
CANCELLED = "CANCELLED"
class Arrival(Base):
__tablename__ = "arrivals"
id = Column(BigInteger, primary_key=True, autoincrement=True)
registration = Column(String(16), nullable=False, index=True)
type = Column(String(32), nullable=True)
callsign = Column(String(16), nullable=True)
pob = Column(Integer, nullable=False)
in_from = Column(String(4), nullable=False, index=True)
status = Column(SQLEnum(ArrivalStatus), default=ArrivalStatus.BOOKED_IN, nullable=False, index=True)
notes = Column(Text, nullable=True)
booked_in_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
landed_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)

View File

@@ -0,0 +1,29 @@
from sqlalchemy import Column, BigInteger, String, Integer, Text, DateTime, Enum as SQLEnum, func
from sqlalchemy.ext.declarative import declarative_base
from enum import Enum
from datetime import datetime
Base = declarative_base()
class DepartureStatus(str, Enum):
BOOKED_OUT = "BOOKED_OUT"
DEPARTED = "DEPARTED"
CANCELLED = "CANCELLED"
class Departure(Base):
__tablename__ = "departures"
id = Column(BigInteger, primary_key=True, autoincrement=True)
registration = Column(String(16), nullable=False, index=True)
type = Column(String(32), nullable=True)
callsign = Column(String(16), nullable=True)
pob = Column(Integer, nullable=False)
out_to = Column(String(4), nullable=False, index=True)
status = Column(SQLEnum(DepartureStatus), default=DepartureStatus.BOOKED_OUT, nullable=False, index=True)
notes = Column(Text, nullable=True)
booked_out_dt = Column(DateTime, server_default=func.now(), nullable=False, index=True)
departure_dt = Column(DateTime, nullable=True)
created_by = Column(String(16), nullable=True, index=True)
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)

View File

@@ -0,0 +1,66 @@
from pydantic import BaseModel, validator
from datetime import datetime
from typing import Optional
from enum import Enum
class ArrivalStatus(str, Enum):
BOOKED_IN = "BOOKED_IN"
LANDED = "LANDED"
CANCELLED = "CANCELLED"
class ArrivalBase(BaseModel):
registration: str
type: Optional[str] = None
callsign: Optional[str] = None
pob: int
in_from: str
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('in_from')
def validate_in_from(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Origin airport 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 ArrivalCreate(ArrivalBase):
pass
class ArrivalUpdate(BaseModel):
type: Optional[str] = None
callsign: Optional[str] = None
pob: Optional[int] = None
in_from: Optional[str] = None
notes: Optional[str] = None
class ArrivalStatusUpdate(BaseModel):
status: ArrivalStatus
timestamp: Optional[datetime] = None
class Arrival(ArrivalBase):
id: int
status: ArrivalStatus
booked_in_dt: datetime
landed_dt: Optional[datetime] = None
created_by: Optional[str] = None
updated_at: datetime
class Config:
from_attributes = True

View File

@@ -0,0 +1,66 @@
from pydantic import BaseModel, validator
from datetime import datetime
from typing import Optional
from enum import Enum
class DepartureStatus(str, Enum):
BOOKED_OUT = "BOOKED_OUT"
DEPARTED = "DEPARTED"
CANCELLED = "CANCELLED"
class DepartureBase(BaseModel):
registration: str
type: Optional[str] = None
callsign: Optional[str] = None
pob: int
out_to: str
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('out_to')
def validate_out_to(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Destination airport 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 DepartureCreate(DepartureBase):
pass
class DepartureUpdate(BaseModel):
type: Optional[str] = None
callsign: Optional[str] = None
pob: Optional[int] = None
out_to: Optional[str] = None
notes: Optional[str] = None
class DepartureStatusUpdate(BaseModel):
status: DepartureStatus
timestamp: Optional[datetime] = None
class Departure(DepartureBase):
id: int
status: DepartureStatus
booked_out_dt: datetime
departure_dt: Optional[datetime] = None
created_by: Optional[str] = None
updated_at: datetime
class Config:
from_attributes = True

View File

@@ -19,7 +19,7 @@ class LocalFlightStatus(str, Enum):
class LocalFlightBase(BaseModel):
registration: str
type: str # Aircraft type
type: Optional[str] = None # Aircraft type - optional, can be looked up later
callsign: Optional[str] = None
pob: int
flight_type: LocalFlightType
@@ -31,11 +31,13 @@ class LocalFlightBase(BaseModel):
raise ValueError('Aircraft registration is required')
return v.strip().upper()
@validator('type')
@validator('type', pre=True, always=False)
def validate_type(cls, v):
if not v or len(v.strip()) == 0:
raise ValueError('Aircraft type is required')
return v.strip()
if v is None or (isinstance(v, str) and len(v.strip()) == 0):
return None
if isinstance(v, str):
return v.strip()
return v
@validator('pob')
def validate_pob(cls, v):