Drone req handling update
This commit is contained in:
@@ -17,6 +17,7 @@ from app.schemas.drone_request import (
|
||||
DroneRequest,
|
||||
DroneRequestComment,
|
||||
DroneRequestCreate,
|
||||
DroneRequestPublicSubmission,
|
||||
DroneRequestStatus,
|
||||
DroneRequestStatusUpdate,
|
||||
DroneRequestUpdate,
|
||||
@@ -75,6 +76,32 @@ async def _send_drone_submitted_email(drone_request):
|
||||
)
|
||||
|
||||
|
||||
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_amsl": drone_request.maximum_elevation_ft_amsl,
|
||||
"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,
|
||||
@@ -94,6 +121,13 @@ async def _send_drone_approved_email(drone_request, message: Optional[str] = Non
|
||||
)
|
||||
|
||||
|
||||
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,
|
||||
@@ -114,7 +148,7 @@ async def get_drone_requests(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/public", response_model=DroneRequest)
|
||||
@router.post("/public", response_model=DroneRequestPublicSubmission)
|
||||
async def create_public_drone_request(
|
||||
request: Request,
|
||||
drone_request_in: DroneRequestCreate,
|
||||
@@ -131,7 +165,8 @@ async def create_public_drone_request(
|
||||
|
||||
await _broadcast(request, "drone_request_created", drone_request)
|
||||
await _send_drone_submitted_email(drone_request)
|
||||
return drone_request
|
||||
await _send_drone_tower_notification(drone_request)
|
||||
return _public_submission_response(drone_request)
|
||||
|
||||
|
||||
@router.get("/public/edit/{token}", response_model=DroneRequest)
|
||||
@@ -155,7 +190,7 @@ async def update_drone_request_public(
|
||||
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.PENDING, DroneRequestStatus.APPROVED]:
|
||||
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}",
|
||||
@@ -182,7 +217,7 @@ async def cancel_drone_request_public(
|
||||
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.PENDING, DroneRequestStatus.APPROVED]:
|
||||
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}",
|
||||
|
||||
@@ -21,6 +21,7 @@ class Settings(BaseSettings):
|
||||
mail_password: str
|
||||
mail_from: str
|
||||
mail_from_name: str
|
||||
drone_request_tower_email: str | None = None
|
||||
|
||||
# Application settings
|
||||
api_v1_str: str = "/api/v1"
|
||||
|
||||
@@ -10,7 +10,6 @@ class DroneRequestStatus(str, Enum):
|
||||
NEW = "NEW"
|
||||
APPROVED = "APPROVED"
|
||||
DENIED = "DENIED"
|
||||
PENDING = "PENDING"
|
||||
CANCELED = "CANCELED"
|
||||
INFLIGHT = "INFLIGHT"
|
||||
COMPLETED = "COMPLETED"
|
||||
|
||||
@@ -9,7 +9,6 @@ class DroneRequestStatus(str, Enum):
|
||||
NEW = "NEW"
|
||||
APPROVED = "APPROVED"
|
||||
DENIED = "DENIED"
|
||||
PENDING = "PENDING"
|
||||
CANCELED = "CANCELED"
|
||||
INFLIGHT = "INFLIGHT"
|
||||
COMPLETED = "COMPLETED"
|
||||
@@ -104,3 +103,8 @@ class DroneRequest(DroneRequestBase):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class DroneRequestPublicSubmission(DroneRequest):
|
||||
request_id: str
|
||||
secure_link: str
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Drone Request Awaiting Review</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #f4f7fa; font-family: Arial, sans-serif; color: #263645;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background: #f4f7fa; padding: 24px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="max-width: 680px; background: #ffffff; border-radius: 8px; overflow: hidden;">
|
||||
<tr>
|
||||
<td style="background: #34495e; color: #ffffff; padding: 24px;">
|
||||
<h1 style="margin: 0; font-size: 24px;">Drone request awaiting review</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 16px;">{{ reference_number }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 24px;">
|
||||
<p style="font-size: 17px; margin: 0 0 18px;">
|
||||
A new drone flight request has been submitted. Please review it and approve or deny it as soon as practical.
|
||||
</p>
|
||||
<p style="margin: 0 0 22px;">
|
||||
<a href="{{ requests_url }}" style="background: #2f93d1; color: #ffffff; display: inline-block; padding: 12px 18px; border-radius: 5px; text-decoration: none; font-weight: bold;">Open drone requests</a>
|
||||
</p>
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse; font-size: 15px;">
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Operator</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ operator_name }}{% if operator_id %} ({{ operator_id }}){% endif %}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Flyer</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ flyer_name or '-' }}{% if flyer_id %} ({{ flyer_id }}){% endif %}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Contact</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ email }}{% if phone %} / {{ phone }}{% endif %}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Takeoff</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ takeoff_time }}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Completion</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ completion_time }}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Location</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ location }}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Inside FRZ</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ inside_frz }}</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Max elevation</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td></tr>
|
||||
<tr><td style="padding: 10px; border: 1px solid #dfe5eb; font-weight: bold;">Applicant notes</td><td style="padding: 10px; border: 1px solid #dfe5eb;">{{ notes or '-' }}</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user