Reporting and TZ updates

This commit is contained in:
2026-06-28 07:37:41 -04:00
parent 5e12561fb2
commit c2e4d2adeb
18 changed files with 719 additions and 268 deletions
+18 -10
View File
@@ -1,7 +1,8 @@
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.orm import Session
from datetime import date
from datetime import date, timezone
from zoneinfo import ZoneInfo
from app.api.deps import get_db, get_current_read_user, get_current_operator_user
from app.crud.crud_ppr import ppr as crud_ppr
from app.crud.crud_journal import journal as crud_journal
@@ -19,6 +20,14 @@ from app.core.config import settings
router = APIRouter()
def format_local_datetime(dt):
if not dt:
return "N/A"
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(ZoneInfo(settings.local_timezone)).strftime("%Y-%m-%d %H:%M")
@router.get("/", response_model=List[PPR])
async def get_pprs(
request: Request,
@@ -94,8 +103,8 @@ async def create_public_ppr(
template_vars={
"name": ppr_in.captain,
"aircraft": ppr_in.ac_reg,
"arrival_time": ppr_in.eta.strftime("%Y-%m-%d %H:%M"),
"departure_time": ppr_in.etd.strftime("%Y-%m-%d %H:%M") if ppr_in.etd else "N/A",
"arrival_time": format_local_datetime(ppr_in.eta),
"departure_time": format_local_datetime(ppr_in.etd),
"purpose": ppr_in.notes or "N/A",
"public_token": ppr.public_token,
"base_url": settings.base_url
@@ -232,8 +241,8 @@ async def update_ppr_status(
template_vars={
"name": ppr.captain,
"aircraft": ppr.ac_reg,
"arrival_time": ppr.eta.strftime("%Y-%m-%d %H:%M"),
"departure_time": ppr.etd.strftime("%Y-%m-%d %H:%M") if ppr.etd else "N/A"
"arrival_time": format_local_datetime(ppr.eta),
"departure_time": format_local_datetime(ppr.etd)
}
)
@@ -316,11 +325,10 @@ async def get_ppr_for_edit(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invalid or expired token"
)
# Only allow editing if not already processed
if ppr.status in [PPRStatus.CANCELED, PPRStatus.DELETED, PPRStatus.LANDED, PPRStatus.DEPARTED]:
if ppr.status == PPRStatus.DELETED:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="PPR cannot be edited at this stage"
detail="PPR is no longer available"
)
return ppr
@@ -390,8 +398,8 @@ async def cancel_ppr_public(
template_vars={
"name": cancelled_ppr.captain,
"aircraft": cancelled_ppr.ac_reg,
"arrival_time": cancelled_ppr.eta.strftime("%Y-%m-%d %H:%M"),
"departure_time": cancelled_ppr.etd.strftime("%Y-%m-%d %H:%M") if cancelled_ppr.etd else "N/A"
"arrival_time": format_local_datetime(cancelled_ppr.eta),
"departure_time": format_local_datetime(cancelled_ppr.etd)
}
)
+1
View File
@@ -27,6 +27,7 @@ class Settings(BaseSettings):
api_v1_str: str = "/api/v1"
project_name: str = "Airfield PPR API"
base_url: str
local_timezone: str = "Europe/London"
# UI Configuration
tag: str = ""
+3 -3
View File
@@ -10,10 +10,10 @@
<p><strong>PPR Details:</strong></p>
<ul>
<li>Aircraft: {{ aircraft }}</li>
<li>Original Arrival: {{ arrival_time }}</li>
<li>Original Departure: {{ departure_time }}</li>
<li>Original Arrival (local time): {{ arrival_time }}</li>
<li>Original Departure (local time): {{ departure_time }}</li>
</ul>
<p>If this was not intended, please contact us.</p>
<p>Best regards,<br>Swansea Airport Team</p>
</body>
</html>
</html>
+3 -4
View File
@@ -10,12 +10,11 @@
<p><strong>PPR Details:</strong></p>
<ul>
<li>Aircraft: {{ aircraft }}</li>
<li>Arrival: {{ arrival_time }}</li>
<li>Departure: {{ departure_time }}</li>
<li>Purpose: {{ purpose }}</li>
<li>Arrival (local time): {{ arrival_time }}</li>
<li>Departure (local time): {{ departure_time }}</li>
</ul>
<p>You can <a href="{{ base_url }}/edit.html?token={{ public_token }}">edit or cancel</a> your PPR using this secure link.</p>
<p>You will receive further updates via email.</p>
<p>Best regards,<br>Swansea Airport Team</p>
</body>
</html>
</html>
+21 -1
View File
@@ -124,6 +124,8 @@ def test_public_ppr_create_sends_email_and_generates_token(client, db, ppr_paylo
created = create_response.json()
assert created["created_by"] == "public"
assert sent_email["to_email"] == "pilot@example.com"
assert sent_email["template_vars"]["arrival_time"] == "2026-06-20 11:00"
assert sent_email["template_vars"]["departure_time"] == "2026-06-20 13:00"
db_ppr = db.query(PPRRecord).filter(PPRRecord.id == created["id"]).one()
assert db_ppr.public_token
@@ -146,7 +148,9 @@ def test_public_ppr_token_edit_and_cancel_paths(client, ppr_factory, db):
assert cancel_response.status_code == 200
assert cancel_response.json()["status"] == "CANCELED"
assert client.get("/api/v1/pprs/public/edit/public-edit-token").status_code == 400
assert client.get("/api/v1/pprs/public/edit/public-edit-token").status_code == 200
assert client.patch("/api/v1/pprs/public/edit/public-edit-token", json={}).status_code == 400
assert client.delete("/api/v1/pprs/public/cancel/public-edit-token").status_code == 400
assert client.patch("/api/v1/pprs/public/edit/missing-token", json={}).status_code == 404
assert client.delete("/api/v1/pprs/public/cancel/missing-token").status_code == 404
@@ -179,6 +183,22 @@ def test_activate_rejects_processed_ppr(auth_client, ppr_factory):
assert "cannot be activated" in response.json()["detail"]
def test_public_ppr_processed_token_can_view_but_not_edit_or_cancel(client, ppr_factory):
ppr = ppr_factory(status="LANDED", public_token="processed-token")
get_response = client.get("/api/v1/pprs/public/edit/processed-token")
patch_response = client.patch(
"/api/v1/pprs/public/edit/processed-token",
json={"captain": "Too Late"},
)
cancel_response = client.delete("/api/v1/pprs/public/cancel/processed-token")
assert get_response.status_code == 200
assert get_response.json()["id"] == ppr.id
assert patch_response.status_code == 400
assert cancel_response.status_code == 400
def test_invalid_ppr_payload_returns_validation_error(auth_client, ppr_payload):
ppr_payload["pob_in"] = -1