Pilot self-bookout
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals, circuits, journal, overflights
|
||||
from app.api.endpoints import auth, pprs, public, aircraft, airport, local_flights, departures, arrivals, circuits, journal, overflights, public_book
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -12,5 +12,6 @@ api_router.include_router(overflights.router, prefix="/overflights", tags=["over
|
||||
api_router.include_router(circuits.router, prefix="/circuits", tags=["circuits"])
|
||||
api_router.include_router(journal.router, prefix="/journal", tags=["journal"])
|
||||
api_router.include_router(public.router, prefix="/public", tags=["public"])
|
||||
api_router.include_router(public_book.router, prefix="/public-book", tags=["public_booking"])
|
||||
api_router.include_router(aircraft.router, prefix="/aircraft", tags=["aircraft"])
|
||||
api_router.include_router(airport.router, prefix="/airport", tags=["airport"])
|
||||
207
backend/app/api/endpoints/public_book.py
Normal file
207
backend/app/api/endpoints/public_book.py
Normal file
@@ -0,0 +1,207 @@
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.deps import get_db
|
||||
from app.core.config import settings
|
||||
from app.schemas.public_book import (
|
||||
PublicLocalFlightCreate,
|
||||
PublicCircuitCreate,
|
||||
PublicDepartureCreate,
|
||||
PublicArrivalCreate,
|
||||
)
|
||||
from app.schemas.local_flight import LocalFlight as LocalFlightSchema
|
||||
from app.schemas.circuit import Circuit as CircuitSchema
|
||||
from app.schemas.departure import Departure as DepartureSchema
|
||||
from app.schemas.arrival import Arrival as ArrivalSchema
|
||||
from app.crud.crud_local_flight import local_flight as crud_local_flight
|
||||
from app.crud.crud_circuit import crud_circuit
|
||||
from app.crud.crud_departure import departure as crud_departure
|
||||
from app.crud.crud_arrival import arrival as crud_arrival
|
||||
from app.models.local_flight import SubmissionSource
|
||||
from app.models.departure import SubmissionSource as DepartureSubmissionSource
|
||||
from app.models.arrival import SubmissionSource as ArrivalSubmissionSource
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def check_public_booking_enabled():
|
||||
"""Check if public booking is enabled"""
|
||||
if not settings.allow_public_booking:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Public booking is currently disabled"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/local-flights", response_model=LocalFlightSchema)
|
||||
async def public_book_local_flight(
|
||||
request: Request,
|
||||
flight_in: PublicLocalFlightCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Book a local flight via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
# Create the flight with public submission source
|
||||
from app.schemas.local_flight import LocalFlightCreate
|
||||
|
||||
flight_create = LocalFlightCreate(
|
||||
registration=flight_in.registration,
|
||||
type=flight_in.type,
|
||||
callsign=flight_in.callsign,
|
||||
pob=flight_in.pob,
|
||||
flight_type=flight_in.flight_type,
|
||||
duration=flight_in.duration,
|
||||
etd=flight_in.etd,
|
||||
notes=flight_in.notes,
|
||||
)
|
||||
|
||||
flight = crud_local_flight.create(db, obj_in=flight_create, created_by="PUBLIC_PILOT")
|
||||
|
||||
# Update with submission source and pilot email
|
||||
db.query(type(flight)).filter(type(flight).id == flight.id).update({
|
||||
type(flight).submitted_via: SubmissionSource.PUBLIC,
|
||||
type(flight).pilot_email: flight_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
db.refresh(flight)
|
||||
|
||||
# Send real-time update via WebSocket if available
|
||||
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,
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return flight
|
||||
|
||||
|
||||
@router.post("/circuits", response_model=CircuitSchema)
|
||||
async def public_record_circuit(
|
||||
request: Request,
|
||||
circuit_in: PublicCircuitCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Record a circuit (touch and go) via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
from app.schemas.circuit import CircuitCreate
|
||||
|
||||
circuit_create = CircuitCreate(
|
||||
local_flight_id=circuit_in.local_flight_id,
|
||||
circuit_timestamp=circuit_in.circuit_timestamp,
|
||||
)
|
||||
|
||||
circuit = crud_circuit.create(db, obj_in=circuit_create)
|
||||
|
||||
# Send real-time update via WebSocket
|
||||
if hasattr(request.app.state, 'connection_manager'):
|
||||
await request.app.state.connection_manager.broadcast({
|
||||
"type": "circuit_recorded",
|
||||
"data": {
|
||||
"id": circuit.id,
|
||||
"local_flight_id": circuit.local_flight_id,
|
||||
"circuit_timestamp": circuit.circuit_timestamp.isoformat(),
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return circuit
|
||||
|
||||
|
||||
@router.post("/departures", response_model=DepartureSchema)
|
||||
async def public_book_departure(
|
||||
request: Request,
|
||||
departure_in: PublicDepartureCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Book a departure via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
from app.schemas.departure import DepartureCreate
|
||||
|
||||
departure_create = DepartureCreate(
|
||||
registration=departure_in.registration,
|
||||
type=departure_in.type,
|
||||
callsign=departure_in.callsign,
|
||||
pob=departure_in.pob,
|
||||
out_to=departure_in.out_to,
|
||||
etd=departure_in.etd,
|
||||
notes=departure_in.notes,
|
||||
)
|
||||
|
||||
departure = crud_departure.create(db, obj_in=departure_create, created_by="PUBLIC_PILOT")
|
||||
|
||||
# Update with submission source and pilot email
|
||||
db.query(type(departure)).filter(type(departure).id == departure.id).update({
|
||||
type(departure).submitted_via: DepartureSubmissionSource.PUBLIC,
|
||||
type(departure).pilot_email: departure_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
db.refresh(departure)
|
||||
|
||||
# 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,
|
||||
"status": departure.status.value,
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return departure
|
||||
|
||||
|
||||
@router.post("/arrivals", response_model=ArrivalSchema)
|
||||
async def public_book_arrival(
|
||||
request: Request,
|
||||
arrival_in: PublicArrivalCreate,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Book an arrival via public portal"""
|
||||
check_public_booking_enabled()
|
||||
|
||||
from app.schemas.arrival import ArrivalCreate
|
||||
|
||||
arrival_create = ArrivalCreate(
|
||||
registration=arrival_in.registration,
|
||||
type=arrival_in.type,
|
||||
callsign=arrival_in.callsign,
|
||||
pob=arrival_in.pob,
|
||||
in_from=arrival_in.in_from,
|
||||
eta=arrival_in.eta,
|
||||
notes=arrival_in.notes,
|
||||
)
|
||||
|
||||
arrival = crud_arrival.create(db, obj_in=arrival_create, created_by="PUBLIC_PILOT")
|
||||
|
||||
# Update with submission source and pilot email
|
||||
db.query(type(arrival)).filter(type(arrival).id == arrival.id).update({
|
||||
type(arrival).submitted_via: ArrivalSubmissionSource.PUBLIC,
|
||||
type(arrival).pilot_email: arrival_in.pilot_email,
|
||||
})
|
||||
db.commit()
|
||||
db.refresh(arrival)
|
||||
|
||||
# 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,
|
||||
"status": arrival.status.value,
|
||||
"submitted_via": "PUBLIC"
|
||||
}
|
||||
})
|
||||
|
||||
return arrival
|
||||
@@ -33,6 +33,9 @@ class Settings(BaseSettings):
|
||||
top_bar_base_color: str = "#2c3e50"
|
||||
environment: str = "production" # production, development, staging, etc.
|
||||
|
||||
# Public booking settings
|
||||
allow_public_booking: bool = False # Enable/disable public flight booking
|
||||
|
||||
# Redis settings (for future use)
|
||||
redis_url: Optional[str] = None
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@ from datetime import datetime
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class ArrivalStatus(str, Enum):
|
||||
BOOKED_IN = "BOOKED_IN"
|
||||
LANDED = "LANDED"
|
||||
@@ -27,4 +32,6 @@ class Arrival(Base):
|
||||
eta = Column(DateTime, nullable=True, index=True)
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
@@ -6,6 +6,11 @@ from datetime import datetime
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class DepartureStatus(str, Enum):
|
||||
BOOKED_OUT = "BOOKED_OUT"
|
||||
DEPARTED = "DEPARTED"
|
||||
@@ -27,4 +32,6 @@ class Departure(Base):
|
||||
etd = Column(DateTime, nullable=True, index=True) # Estimated Time of Departure
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual departure time
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
@@ -4,6 +4,11 @@ from enum import Enum
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class LocalFlightType(str, Enum):
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUITS = "CIRCUITS"
|
||||
@@ -35,4 +40,6 @@ class LocalFlight(Base):
|
||||
departed_dt = Column(DateTime, nullable=True) # Actual takeoff time
|
||||
landed_dt = Column(DateTime, nullable=True)
|
||||
created_by = Column(String(16), nullable=True, index=True)
|
||||
submitted_via = Column(SQLEnum(SubmissionSource), nullable=False, default=SubmissionSource.ADMIN, index=True)
|
||||
pilot_email = Column(String(128), nullable=True) # For public submissions
|
||||
updated_at = Column(DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp())
|
||||
|
||||
@@ -10,6 +10,11 @@ class ArrivalStatus(str, Enum):
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class ArrivalBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
@@ -63,6 +68,8 @@ class Arrival(ArrivalBase):
|
||||
landed_dt: Optional[datetime] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -10,6 +10,11 @@ class DepartureStatus(str, Enum):
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class DepartureBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
@@ -63,3 +68,9 @@ class Departure(DepartureBase):
|
||||
created_dt: datetime
|
||||
etd: Optional[datetime] = None
|
||||
departed_dt: Optional[datetime] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -17,6 +17,11 @@ class LocalFlightStatus(str, Enum):
|
||||
CANCELLED = "CANCELLED"
|
||||
|
||||
|
||||
class SubmissionSource(str, Enum):
|
||||
ADMIN = "ADMIN"
|
||||
PUBLIC = "PUBLIC"
|
||||
|
||||
|
||||
class LocalFlightBase(BaseModel):
|
||||
registration: str
|
||||
type: Optional[str] = None # Aircraft type - optional, can be looked up later
|
||||
@@ -81,6 +86,8 @@ class LocalFlightInDBBase(LocalFlightBase):
|
||||
circuits: Optional[int] = None
|
||||
created_by: Optional[str] = None
|
||||
updated_at: datetime
|
||||
submitted_via: Optional[SubmissionSource] = None
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
129
backend/app/schemas/public_book.py
Normal file
129
backend/app/schemas/public_book.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from pydantic import BaseModel, validator, EmailStr
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class LocalFlightType(str, Enum):
|
||||
LOCAL = "LOCAL"
|
||||
CIRCUITS = "CIRCUITS"
|
||||
DEPARTURE = "DEPARTURE"
|
||||
|
||||
|
||||
class PublicLocalFlightCreate(BaseModel):
|
||||
"""Schema for public local flight booking"""
|
||||
registration: str
|
||||
type: Optional[str] = None # Aircraft type - optional
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
flight_type: LocalFlightType
|
||||
duration: Optional[int] = 45 # Duration in minutes, default 45
|
||||
etd: Optional[datetime] = None # Estimated Time of Departure
|
||||
notes: Optional[str] = None
|
||||
pilot_email: Optional[str] = None # Pilot's email for contact (optional)
|
||||
pilot_name: Optional[str] = None # Pilot's name
|
||||
|
||||
@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('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
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class PublicCircuitCreate(BaseModel):
|
||||
"""Schema for public circuit (touch and go) recording"""
|
||||
local_flight_id: int
|
||||
circuit_timestamp: datetime
|
||||
pilot_email: Optional[str] = None
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class PublicDepartureCreate(BaseModel):
|
||||
"""Schema for public departure booking"""
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
out_to: str
|
||||
etd: Optional[datetime] = None # Estimated Time of Departure
|
||||
notes: Optional[str] = None
|
||||
pilot_email: Optional[str] = None
|
||||
pilot_name: 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
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
|
||||
|
||||
class PublicArrivalCreate(BaseModel):
|
||||
"""Schema for public arrival booking"""
|
||||
registration: str
|
||||
type: Optional[str] = None
|
||||
callsign: Optional[str] = None
|
||||
pob: int
|
||||
in_from: str
|
||||
eta: Optional[datetime] = None
|
||||
notes: Optional[str] = None
|
||||
pilot_email: Optional[str] = None
|
||||
pilot_name: 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
|
||||
|
||||
@validator('pilot_email', pre=True, always=False)
|
||||
def validate_pilot_email(cls, v):
|
||||
if v is None or v == '':
|
||||
return None
|
||||
return v.strip().lower()
|
||||
Reference in New Issue
Block a user