Add FRZ
This commit is contained in:
@@ -7,6 +7,7 @@ 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
|
||||
@@ -56,6 +57,43 @@ async def _send_drone_email(drone_request, subject: str, message: str):
|
||||
)
|
||||
|
||||
|
||||
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_amsl": drone_request.maximum_elevation_ft_amsl,
|
||||
"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_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_amsl": drone_request.maximum_elevation_ft_amsl,
|
||||
"edit_url": f"{settings.base_url}/drone-request.html?token={drone_request.public_token}" if drone_request.public_token else None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DroneRequest])
|
||||
async def get_drone_requests(
|
||||
skip: int = 0,
|
||||
@@ -92,11 +130,7 @@ async def create_public_drone_request(
|
||||
)
|
||||
|
||||
await _broadcast(request, "drone_request_created", drone_request)
|
||||
await _send_drone_email(
|
||||
drone_request,
|
||||
f"Drone flight request received {drone_request.reference_number}",
|
||||
"We have received your drone flight request. We will email you when the approval status changes or if we need more information.",
|
||||
)
|
||||
await _send_drone_submitted_email(drone_request)
|
||||
return drone_request
|
||||
|
||||
|
||||
@@ -172,6 +206,11 @@ async def cancel_drone_request_public(
|
||||
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,
|
||||
@@ -230,11 +269,14 @@ async def update_drone_request_status(
|
||||
|
||||
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}."
|
||||
await _send_drone_email(
|
||||
drone_request,
|
||||
f"Drone request {drone_request.reference_number} {drone_request.status.value}",
|
||||
message,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
SWANSEA_FRZ_POLYGONS = [
|
||||
{
|
||||
"name": "EGR1U003A SWANSEA",
|
||||
"coordinates": [
|
||||
[51.6385691199, -4.0677777778],
|
||||
[51.6383925433, -4.0732789905],
|
||||
[51.6378646894, -4.0787217619],
|
||||
[51.6369911656, -4.0840482758],
|
||||
[51.6357812508, -4.0892019586],
|
||||
[51.6342477966, -4.0941280843],
|
||||
[51.6324070894, -4.0987743581],
|
||||
[51.6302786768, -4.1030914745],
|
||||
[51.6278851591, -4.1070336417],
|
||||
[51.6252519474, -4.1105590688],
|
||||
[51.6224069933, -4.1136304084],
|
||||
[51.6193804903, -4.1162151518],
|
||||
[51.6162045529, -4.1182859716],
|
||||
[51.6129128741, -4.1198210079],
|
||||
[51.6095403677, -4.1208040969],
|
||||
[51.606122797, -4.1212249367],
|
||||
[51.6026963949, -4.1210791924],
|
||||
[51.5992974802, -4.120368536],
|
||||
[51.595962072, -4.1191006239],
|
||||
[51.592725509, -4.1172890099],
|
||||
[51.5896220749, -4.1149529973],
|
||||
[51.5866846367, -4.1121174298],
|
||||
[51.5839442973, -4.1088124245],
|
||||
[51.5814300673, -4.1050730509],
|
||||
[51.5791685587, -4.1009389566],
|
||||
[51.5771837053, -4.0964539477],
|
||||
[51.5754965099, -4.0916655245],
|
||||
[51.5741248235, -4.0866243798],
|
||||
[51.5730831575, -4.0813838645],
|
||||
[51.572382531, -4.0759994259],
|
||||
[51.5720303553, -4.0705280237],
|
||||
[51.5720303553, -4.0650275318],
|
||||
[51.572382531, -4.0595561297],
|
||||
[51.5730831575, -4.054171691],
|
||||
[51.5741248235, -4.0489311758],
|
||||
[51.5754965099, -4.0438900311],
|
||||
[51.5771837053, -4.0391016078],
|
||||
[51.5791685587, -4.0346165989],
|
||||
[51.5814300673, -4.0304825047],
|
||||
[51.5839442973, -4.026743131],
|
||||
[51.5866846367, -4.0234381258],
|
||||
[51.5896220749, -4.0206025582],
|
||||
[51.592725509, -4.0182665456],
|
||||
[51.595962072, -4.0164549317],
|
||||
[51.5992974802, -4.0151870195],
|
||||
[51.6026963949, -4.0144763632],
|
||||
[51.606122797, -4.0143306189],
|
||||
[51.6095403677, -4.0147514587],
|
||||
[51.6129128741, -4.0157345476],
|
||||
[51.6162045529, -4.017269584],
|
||||
[51.6193804903, -4.0193404037],
|
||||
[51.6224069933, -4.0219251472],
|
||||
[51.6252519474, -4.0249964868],
|
||||
[51.6278851591, -4.0285219138],
|
||||
[51.6302786768, -4.0324640811],
|
||||
[51.6324070894, -4.0367811974],
|
||||
[51.6342477966, -4.0414274713],
|
||||
[51.6357812508, -4.0463535969],
|
||||
[51.6369911656, -4.0515072798],
|
||||
[51.6378646894, -4.0568337937],
|
||||
[51.6383925433, -4.0622765651],
|
||||
[51.6385691199, -4.0677777778],
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "EGR1U003B SWANSEA RWY 04",
|
||||
"coordinates": [
|
||||
[51.5614305556, -4.1105694444],
|
||||
[51.5760447778, -4.0933516667],
|
||||
[51.5775992074, -4.0974772875],
|
||||
[51.5793789018, -4.1013615228],
|
||||
[51.5813693889, -4.1049727778],
|
||||
[51.5667527778, -4.1221888889],
|
||||
[51.5614305556, -4.1105694444],
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "EGR1U003C SWANSEA RWY 22",
|
||||
"coordinates": [
|
||||
[51.6483027778, -4.0259555556],
|
||||
[51.6345286389, -4.0422406389],
|
||||
[51.632975828, -4.0381074205],
|
||||
[51.631197314, -4.0342163026],
|
||||
[51.6292076111, -4.030599],
|
||||
[51.6429805556, -4.0143111111],
|
||||
[51.6483027778, -4.0259555556],
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "EGR1U003D SWANSEA RWY 10",
|
||||
"coordinates": [
|
||||
[51.6016305556, -4.1483194444],
|
||||
[51.5997253611, -4.1204896111],
|
||||
[51.602737017, -4.1210842429],
|
||||
[51.605769878, -4.1212361072],
|
||||
[51.60879875, -4.1209438611],
|
||||
[51.6105638889, -4.1467305556],
|
||||
[51.6016305556, -4.1483194444],
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "EGR1U003E SWANSEA RWY 28",
|
||||
"coordinates": [
|
||||
[51.5998777778, -3.9918916667],
|
||||
[51.6014628333, -4.0146683056],
|
||||
[51.5984676719, -4.015448363],
|
||||
[51.5955291688, -4.0166629251],
|
||||
[51.5926717222, -4.0183018333],
|
||||
[51.5909444444, -3.9934777778],
|
||||
[51.5998777778, -3.9918916667],
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def point_inside_swansea_frz(lat: float, lng: float) -> bool:
|
||||
"""Return whether a point is inside the Swansea UAS FRZ polygons from the KML source."""
|
||||
return any(_point_inside_polygon(lat, lng, polygon["coordinates"]) for polygon in SWANSEA_FRZ_POLYGONS)
|
||||
|
||||
|
||||
def swansea_frz_geojson() -> dict:
|
||||
return {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {"name": polygon["name"]},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[
|
||||
[lng, lat] for lat, lng in polygon["coordinates"]
|
||||
]],
|
||||
},
|
||||
}
|
||||
for polygon in SWANSEA_FRZ_POLYGONS
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _point_inside_polygon(lat: float, lng: float, coordinates: list[list[float]]) -> bool:
|
||||
inside = False
|
||||
j = len(coordinates) - 1
|
||||
for i, (point_lat, point_lng) in enumerate(coordinates):
|
||||
previous_lat, previous_lng = coordinates[j]
|
||||
intersects = (
|
||||
(point_lat > lat) != (previous_lat > lat)
|
||||
and lng < (previous_lng - point_lng) * (lat - point_lat) / (previous_lat - point_lat) + point_lng
|
||||
)
|
||||
if intersects:
|
||||
inside = not inside
|
||||
j = i
|
||||
return inside
|
||||
@@ -5,6 +5,7 @@ from typing import List, Optional
|
||||
from sqlalchemy import desc, func
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.frz import point_inside_swansea_frz
|
||||
from app.crud.crud_journal import journal as crud_journal
|
||||
from app.models.drone_request import DroneRequest, DroneRequestStatus
|
||||
from app.models.journal import EntityType
|
||||
@@ -52,10 +53,13 @@ class CRUDDroneRequest:
|
||||
reference_number = self._generate_reference(db)
|
||||
payload = obj_in.dict()
|
||||
notes = payload.pop("notes", None)
|
||||
payload.pop("prototype_overlay", None)
|
||||
payload.pop("location_inside_frz", None)
|
||||
|
||||
db_obj = DroneRequest(
|
||||
**payload,
|
||||
applicant_notes=notes,
|
||||
location_inside_frz=point_inside_swansea_frz(payload["location_latitude"], payload["location_longitude"]),
|
||||
reference_number=reference_number,
|
||||
public_token=secrets.token_urlsafe(64),
|
||||
status=DroneRequestStatus.NEW,
|
||||
@@ -88,6 +92,13 @@ class CRUDDroneRequest:
|
||||
update_data = obj_in.dict(exclude_unset=True)
|
||||
if "notes" in update_data:
|
||||
update_data["applicant_notes"] = update_data.pop("notes")
|
||||
update_data.pop("prototype_overlay", None)
|
||||
update_data.pop("location_inside_frz", None)
|
||||
|
||||
if "location_latitude" in update_data or "location_longitude" in update_data:
|
||||
lat = update_data.get("location_latitude", db_obj.location_latitude)
|
||||
lng = update_data.get("location_longitude", db_obj.location_longitude)
|
||||
update_data["location_inside_frz"] = point_inside_swansea_frz(lat, lng)
|
||||
changes = []
|
||||
|
||||
for field, value in update_data.items():
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Drone Flight Request Approved</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #f4f6f8; color: #24313d; font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.55;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f4f6f8; padding: 24px 12px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width: 680px; background: #ffffff; border-collapse: collapse; border-radius: 8px; overflow: hidden; border: 1px solid #dfe5eb;">
|
||||
<tr>
|
||||
<td style="background: #1f7a4d; color: #ffffff; padding: 24px 28px;">
|
||||
<h1 style="margin: 0; font-size: 24px; line-height: 1.25;">Drone Flight Request Approved</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 17px;">Reference {{ reference_number }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 28px;">
|
||||
<p style="margin: 0 0 16px;">Hello {{ name }},</p>
|
||||
<p style="margin: 0 0 20px;">Your drone flight request has been approved.</p>
|
||||
|
||||
{% if message %}
|
||||
<div style="border-left: 5px solid #3498db; background: #eef7ff; padding: 16px 18px; margin: 0 0 22px;">
|
||||
<p style="margin: 0;"><strong>Airport comment:</strong> {{ message }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="border-left: 5px solid #f39c12; background: #fff7e6; padding: 16px 18px; margin: 0 0 22px;">
|
||||
<p style="margin: 0; font-size: 17px;"><strong>Before you fly:</strong> you must call the tower approximately 20 minutes before commencing flight. Do not commence unless you have made this pre-flight call and can comply with any tower instructions given at the time.</p>
|
||||
</div>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse; margin: 0 0 22px;">
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc; width: 38%;"><strong>Reference</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ reference_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Status</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Takeoff</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ takeoff_time }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Completion</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ completion_time }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Location</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ location }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if edit_url %}
|
||||
<p style="margin: 0 0 22px;">
|
||||
<a href="{{ edit_url }}" style="background: #3498db; color: #ffffff; display: inline-block; padding: 12px 18px; border-radius: 5px; text-decoration: none; font-weight: bold;">View, update, or cancel request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin: 0; color: #5d6d7e;">Please quote your reference number in any replies or phone calls.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Drone Flight Request Submitted</title>
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background: #f4f6f8; color: #24313d; font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.55;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f4f6f8; padding: 24px 12px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width: 680px; background: #ffffff; border-collapse: collapse; border-radius: 8px; overflow: hidden; border: 1px solid #dfe5eb;">
|
||||
<tr>
|
||||
<td style="background: #263645; color: #ffffff; padding: 24px 28px;">
|
||||
<h1 style="margin: 0; font-size: 24px; line-height: 1.25;">Drone Flight Request Submitted</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 17px;">Reference {{ reference_number }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 28px;">
|
||||
<p style="margin: 0 0 16px;">Hello {{ name }},</p>
|
||||
<p style="margin: 0 0 20px;">We have received your drone flight request. We will email you when the approval status changes or if we need more information.</p>
|
||||
|
||||
<div style="border-left: 5px solid #f39c12; background: #fff7e6; padding: 16px 18px; margin: 0 0 22px;">
|
||||
<p style="margin: 0; font-size: 17px;"><strong>Before you fly:</strong> if you have not received approval on the day of flight, you must contact the tower to chase your request before commencing flight, and at least 20 minutes before your planned takeoff time.</p>
|
||||
</div>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse; margin: 0 0 22px;">
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc; width: 38%;"><strong>Reference</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ reference_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Status</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Takeoff</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ takeoff_time }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Completion</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ completion_time }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Location</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ location }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if edit_url %}
|
||||
<p style="margin: 0 0 22px;">
|
||||
<a href="{{ edit_url }}" style="background: #3498db; color: #ffffff; display: inline-block; padding: 12px 18px; border-radius: 5px; text-decoration: none; font-weight: bold;">View, update, or cancel request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin: 0; color: #5d6d7e;">Please quote your reference number in any replies.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,24 +4,65 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Drone Flight Request Update</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; line-height: 1.5; color: #222;">
|
||||
<h2>Drone Flight Request Update</h2>
|
||||
<p>Hello {{ name }},</p>
|
||||
<p>{{ message }}</p>
|
||||
<body style="margin: 0; padding: 0; background: #f4f6f8; color: #24313d; font-family: Arial, Helvetica, sans-serif; font-size: 16px; line-height: 1.55;">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background: #f4f6f8; padding: 24px 12px;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="max-width: 680px; background: #ffffff; border-collapse: collapse; border-radius: 8px; overflow: hidden; border: 1px solid #dfe5eb;">
|
||||
<tr>
|
||||
<td style="background: #263645; color: #ffffff; padding: 24px 28px;">
|
||||
<h1 style="margin: 0; font-size: 24px; line-height: 1.25;">Drone Flight Request Update</h1>
|
||||
<p style="margin: 8px 0 0; font-size: 17px;">Reference {{ reference_number }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 28px;">
|
||||
<p style="margin: 0 0 16px;">Hello {{ name }},</p>
|
||||
<p style="margin: 0 0 20px;">{{ message }}</p>
|
||||
|
||||
<table cellpadding="6" cellspacing="0" style="border-collapse: collapse;">
|
||||
<tr><td><strong>Reference</strong></td><td>{{ reference_number }}</td></tr>
|
||||
<tr><td><strong>Status</strong></td><td>{{ status }}</td></tr>
|
||||
<tr><td><strong>Takeoff</strong></td><td>{{ takeoff_time }}</td></tr>
|
||||
<tr><td><strong>Completion</strong></td><td>{{ completion_time }}</td></tr>
|
||||
<tr><td><strong>Location</strong></td><td>{{ location }}</td></tr>
|
||||
<tr><td><strong>Max elevation</strong></td><td>{{ maximum_elevation_ft_amsl }} ft AMSL</td></tr>
|
||||
<div style="border-left: 5px solid #f39c12; background: #fff7e6; padding: 16px 18px; margin: 0 0 22px;">
|
||||
<p style="margin: 0; font-size: 17px;"><strong>Before you fly:</strong> if you have not received approval on the day of flight, you must contact the tower to chase your request before commencing flight, and at least 20 minutes before your planned takeoff time.</p>
|
||||
</div>
|
||||
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="border-collapse: collapse; margin: 0 0 22px;">
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc; width: 38%;"><strong>Reference</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ reference_number }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Status</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ status }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Takeoff</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ takeoff_time }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Completion</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ completion_time }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Location</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ location }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb; background: #f8fafc;"><strong>Max elevation</strong></td>
|
||||
<td style="padding: 12px; border: 1px solid #dfe5eb;">{{ maximum_elevation_ft_amsl }} ft AMSL</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if edit_url %}
|
||||
<p style="margin: 0 0 22px;">
|
||||
<a href="{{ edit_url }}" style="background: #3498db; color: #ffffff; display: inline-block; padding: 12px 18px; border-radius: 5px; text-decoration: none; font-weight: bold;">View, update, or cancel request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<p style="margin: 0; color: #5d6d7e;">Please quote your reference number in any replies.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{% if edit_url %}
|
||||
<p>You can <a href="{{ edit_url }}">view, update, or cancel your drone request</a> using this secure link.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>Please quote your reference number in any replies.</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -188,8 +188,8 @@ echo ""
|
||||
|
||||
# Start the application with appropriate settings
|
||||
if [ "${ENVIRONMENT}" = "production" ]; then
|
||||
echo "Starting in PRODUCTION mode with multiple workers..."
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers ${WORKERS:-4}
|
||||
echo "Starting in PRODUCTION mode with a single worker for in-process WebSocket broadcasts..."
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||
else
|
||||
echo "Starting in DEVELOPMENT mode with auto-reload..."
|
||||
exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from app.core.frz import point_inside_swansea_frz
|
||||
from app.models.drone_request import DroneRequest
|
||||
|
||||
|
||||
@@ -16,11 +17,14 @@ def drone_payload(**overrides):
|
||||
"estimated_completion_at": "2026-06-20T10:30:00",
|
||||
"maximum_elevation_ft_amsl": 250,
|
||||
"location_description": "North apron",
|
||||
"location_latitude": 51.47,
|
||||
"location_longitude": -0.45,
|
||||
"location_inside_frz": "yes",
|
||||
"location_latitude": 51.623389,
|
||||
"location_longitude": -4.069231,
|
||||
"location_inside_frz": "no",
|
||||
"notes": "Survey flight",
|
||||
"prototype_overlay": {"radius_nm": 1},
|
||||
"prototype_overlay": {
|
||||
"airport_reference_point": {"lat": 0, "lng": 0},
|
||||
"frz_radius_metres": 1,
|
||||
},
|
||||
}
|
||||
payload.update(overrides)
|
||||
return payload
|
||||
@@ -142,3 +146,11 @@ def test_drone_request_not_found_and_validation_paths(auth_client, client):
|
||||
== 404
|
||||
)
|
||||
assert auth_client.get("/api/v1/drone-requests/404/journal").status_code == 404
|
||||
|
||||
|
||||
def test_swansea_frz_runway_extensions_start_at_thresholds():
|
||||
assert point_inside_swansea_frz(51.626825, -4.037672) is True
|
||||
assert point_inside_swansea_frz(51.583775, -4.097928) is True
|
||||
assert point_inside_swansea_frz(51.603007, -4.025604) is True
|
||||
assert point_inside_swansea_frz(51.607593, -4.109996) is True
|
||||
assert point_inside_swansea_frz(51.68000, -4.06780) is False
|
||||
|
||||
Reference in New Issue
Block a user