Files
2026-06-29 06:26:37 -04:00

357 lines
14 KiB
Python

from datetime import date
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status
from sqlalchemy.orm import Session
from app.api.deps import get_current_operator_user, get_current_read_user, get_db
from app.core.email import email_service
from app.core.config import settings
from app.core.frz import swansea_frz_geojson
from app.core.utils import get_client_ip
from app.crud.crud_drone_request import drone_request as crud_drone_request
from app.crud.crud_journal import journal as crud_journal
from app.models.journal import EntityType
from app.models.ppr import User
from app.schemas.drone_request import (
DroneRequest,
DroneRequestComment,
DroneRequestCreate,
DroneRequestPublicSubmission,
DroneRequestStatus,
DroneRequestStatusUpdate,
DroneRequestUpdate,
)
from app.schemas.journal import JournalEntryResponse
router = APIRouter()
async def _broadcast(request: Request, event_type: str, drone_request: DroneRequest):
if hasattr(request.app.state, "connection_manager"):
await request.app.state.connection_manager.broadcast({
"type": event_type,
"data": {
"id": drone_request.id,
"reference_number": drone_request.reference_number,
"status": drone_request.status.value,
},
})
async def _send_drone_email(drone_request, subject: str, message: str):
await email_service.send_email(
to_email=drone_request.email,
subject=subject,
template_name="drone_request_update.html",
template_vars={
"name": drone_request.operator_name,
"reference_number": drone_request.reference_number,
"status": drone_request.status.value,
"message": message,
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
},
)
async def _send_drone_submitted_email(drone_request):
await email_service.send_email(
to_email=drone_request.email,
subject=f"Drone flight request received {drone_request.reference_number}",
template_name="drone_request_submitted.html",
template_vars={
"name": drone_request.operator_name,
"reference_number": drone_request.reference_number,
"status": drone_request.status.value,
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
},
)
async def _send_drone_tower_notification(drone_request):
tower_email = settings.drone_request_tower_email or settings.mail_from
await email_service.send_email(
to_email=tower_email,
subject=f"Drone flight request awaiting review {drone_request.reference_number}",
template_name="drone_request_tower_notification.html",
template_vars={
"reference_number": drone_request.reference_number,
"operator_name": drone_request.operator_name,
"operator_id": drone_request.operator_id,
"flyer_name": drone_request.flyer_name,
"flyer_id": drone_request.flyer_id,
"email": drone_request.email,
"phone": drone_request.phone,
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"inside_frz": "Yes" if drone_request.location_inside_frz else "No",
"notes": drone_request.applicant_notes,
"requests_url": f"{settings.base_url}/drone-requests",
},
reply_to=f"{drone_request.operator_name} <{drone_request.email}>",
)
async def _send_drone_approved_email(drone_request, message: Optional[str] = None):
await email_service.send_email(
to_email=drone_request.email,
subject=f"Drone request {drone_request.reference_number} APPROVED",
template_name="drone_request_approved.html",
template_vars={
"name": drone_request.operator_name,
"reference_number": drone_request.reference_number,
"status": drone_request.status.value,
"message": message,
"takeoff_time": drone_request.estimated_takeoff_at.strftime("%Y-%m-%d %H:%M"),
"completion_time": drone_request.estimated_completion_at.strftime("%Y-%m-%d %H:%M"),
"location": drone_request.location_description or f"{drone_request.location_latitude}, {drone_request.location_longitude}",
"maximum_elevation_ft_agl": drone_request.maximum_elevation_ft_agl,
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
},
)
def _public_submission_response(drone_request):
payload = DroneRequest.model_validate(drone_request, from_attributes=True).model_dump(mode="json")
payload["request_id"] = drone_request.reference_number
payload["secure_link"] = f"{settings.base_url}/drone-request.html?token={drone_request.public_token}"
return payload
@router.get("/", response_model=List[DroneRequest])
async def get_drone_requests(
skip: int = 0,
limit: int = 100,
status: Optional[DroneRequestStatus] = None,
date_from: Optional[date] = None,
date_to: Optional[date] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user),
):
return crud_drone_request.get_multi(
db,
skip=skip,
limit=limit,
status=status,
date_from=date_from,
date_to=date_to,
)
@router.post("/public", response_model=DroneRequestPublicSubmission)
async def create_public_drone_request(
request: Request,
drone_request_in: DroneRequestCreate,
db: Session = Depends(get_db),
):
client_ip = get_client_ip(request)
drone_request = crud_drone_request.create(
db,
obj_in=drone_request_in,
created_by="public",
user_ip=client_ip,
submitted_via="PUBLIC",
)
await _broadcast(request, "drone_request_created", drone_request)
await _send_drone_submitted_email(drone_request)
await _send_drone_tower_notification(drone_request)
return _public_submission_response(drone_request)
@router.get("/public/edit/{token}", response_model=DroneRequest)
async def get_drone_request_for_edit(
token: str,
db: Session = Depends(get_db),
):
drone_request = crud_drone_request.get_by_public_token(db, token)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token")
return drone_request
@router.patch("/public/edit/{token}", response_model=DroneRequest)
async def update_drone_request_public(
token: str,
drone_request_in: DroneRequestUpdate,
request: Request,
db: Session = Depends(get_db),
):
drone_request = crud_drone_request.get_by_public_token(db, token)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token")
if drone_request.status not in [DroneRequestStatus.NEW, DroneRequestStatus.APPROVED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Drone request cannot be edited while {drone_request.status.value}",
)
client_ip = get_client_ip(request)
updated_request = crud_drone_request.update(
db,
db_obj=drone_request,
obj_in=drone_request_in,
user="public",
user_ip=client_ip,
)
await _broadcast(request, "drone_request_updated", updated_request)
return updated_request
@router.delete("/public/cancel/{token}", response_model=DroneRequest)
async def cancel_drone_request_public(
token: str,
request: Request,
db: Session = Depends(get_db),
):
drone_request = crud_drone_request.get_by_public_token(db, token)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Invalid or expired token")
if drone_request.status not in [DroneRequestStatus.NEW, DroneRequestStatus.APPROVED]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Drone request cannot be cancelled while {drone_request.status.value}",
)
client_ip = get_client_ip(request)
cancelled_request = crud_drone_request.update_status(
db,
request_id=drone_request.id,
status=DroneRequestStatus.CANCELED,
comment="Cancelled by operator using secure link",
user="public",
user_ip=client_ip,
)
await _broadcast(request, "drone_request_status_update", cancelled_request)
await _send_drone_email(
cancelled_request,
f"Drone request {cancelled_request.reference_number} CANCELED",
"Your drone flight request has been cancelled.",
)
return cancelled_request
@router.get("/frz")
async def get_swansea_drone_frz():
return swansea_frz_geojson()
@router.get("/{request_id}", response_model=DroneRequest)
async def get_drone_request(
request_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user),
):
drone_request = crud_drone_request.get(db, request_id)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found")
return drone_request
@router.patch("/{request_id}", response_model=DroneRequest)
async def update_drone_request(
request: Request,
request_id: int,
drone_request_in: DroneRequestUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user),
):
db_request = crud_drone_request.get(db, request_id)
if not db_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found")
client_ip = get_client_ip(request)
drone_request = crud_drone_request.update(
db,
db_obj=db_request,
obj_in=drone_request_in,
user=current_user.username,
user_ip=client_ip,
)
await _broadcast(request, "drone_request_updated", drone_request)
return drone_request
@router.patch("/{request_id}/status", response_model=DroneRequest)
async def update_drone_request_status(
request: Request,
request_id: int,
status_update: DroneRequestStatusUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user),
):
client_ip = get_client_ip(request)
drone_request = crud_drone_request.update_status(
db,
request_id=request_id,
status=status_update.status,
comment=status_update.comment,
user=current_user.username,
user_ip=client_ip,
)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found")
await _broadcast(request, "drone_request_status_update", drone_request)
message = status_update.comment or f"Your drone flight request status is now {drone_request.status.value}."
if drone_request.status == DroneRequestStatus.APPROVED:
await _send_drone_approved_email(drone_request, status_update.comment)
else:
await _send_drone_email(
drone_request,
f"Drone request {drone_request.reference_number} {drone_request.status.value}",
message,
)
return drone_request
@router.post("/{request_id}/comments", response_model=DroneRequest)
async def add_drone_request_comment(
request: Request,
request_id: int,
comment_in: DroneRequestComment,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_operator_user),
):
client_ip = get_client_ip(request)
drone_request = crud_drone_request.add_comment(
db,
request_id=request_id,
comment=comment_in.comment,
user=current_user.username,
user_ip=client_ip,
)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found")
await _broadcast(request, "drone_request_comment_added", drone_request)
if comment_in.email_applicant:
await _send_drone_email(
drone_request,
f"Drone request {drone_request.reference_number} update",
comment_in.comment,
)
return drone_request
@router.get("/{request_id}/journal", response_model=List[JournalEntryResponse])
async def get_drone_request_journal(
request_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_read_user),
):
drone_request = crud_drone_request.get(db, request_id)
if not drone_request:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Drone request not found")
return crud_journal.get_entity_journal(db, EntityType.DRONE_REQUEST, request_id)