"""GET /flights (list), GET/POST /flights/ (detail/edit/requery), and the approve/reject callbacks used both by the review page's own buttons and by ntfy's action buttons. Approve/reject require a signed token (see tokens.py) with NO special-casing for "this came from the authenticated review page" - the reverse-proxy bypass needed for ntfy's unauthenticated callback applies to the whole path, so the token is the only thing standing between the internet and these two routes. """ from datetime import date, datetime, timedelta, timezone from flask import Blueprint, abort, redirect, render_template, request, url_for import airtrail_client import models import reference_data import scheduler import tokens bp = Blueprint("review", __name__) REVIEWABLE_STATUSES = ["pending_review", "track_unavailable", "decode_failed"] # A flight can only be approved once the FlightAware fetch has actually been # attempted (daily job or manual re-query) - never straight from 'queued', # so nothing reaches AirTrail before we know whether a track exists. APPROVABLE_STATUSES = ("pending_review", "track_unavailable") ALL_STATUSES = [ "queued", "pending_review", "track_unavailable", "approved", "rejected", "decode_failed", ] EDITABLE_FIELDS = [ "passenger_name", "pnr", "from_iata", "to_iata", "operating_carrier_iata", "flight_number", "flight_date", "compartment", "seat", ] def _get_or_404(flight_id: int) -> dict: row = models.get_flight(flight_id) if not row: abort(404) return row @bp.route("/flights") def list_flights(): status = request.args.get("status") if status and status in ALL_STATUSES: rows = models.list_flights([status]) elif status == "all": rows = models.list_flights() else: rows = models.list_flights(REVIEWABLE_STATUSES) status = None return render_template( "review_list.html", flights=rows, active_status=status, statuses=ALL_STATUSES ) @bp.route("/flights/") def detail(flight_id: int): row = _get_or_404(flight_id) approve_token = tokens.sign_token(flight_id, "approve") reject_token = tokens.sign_token(flight_id, "reject") return render_template( "review_detail.html", flight=row, approve_token=approve_token, reject_token=reject_token ) @bp.route("/flights//edit", methods=["POST"]) def edit(flight_id: int): row = _get_or_404(flight_id) updates = {} for f in EDITABLE_FIELDS: if f in request.form: value = request.form[f].strip() updates[f] = value or None from_iata = updates.get("from_iata", row.get("from_iata")) to_iata = updates.get("to_iata", row.get("to_iata")) carrier_iata = updates.get("operating_carrier_iata", row.get("operating_carrier_iata")) updates["from_icao"] = reference_data.iata_to_icao_airport(from_iata) updates["to_icao"] = reference_data.iata_to_icao_airport(to_iata) updates["operating_carrier_icao"] = reference_data.iata_to_icao_airline(carrier_iata) flight_date_str = updates.get("flight_date", row.get("flight_date")) if flight_date_str: try: flight_date = date.fromisoformat(flight_date_str) updates["process_after"] = datetime.combine( flight_date + timedelta(days=1), datetime.min.time(), tzinfo=timezone.utc ).isoformat() except ValueError: pass models.update_flight(flight_id, **updates) return redirect(url_for("review.detail", flight_id=flight_id)) @bp.route("/flights//requery", methods=["POST"]) def requery(flight_id: int): row = _get_or_404(flight_id) models.update_flight(flight_id, status="queued", error_message=None) scheduler.process_one_flight(models.get_flight(flight_id)) return redirect(url_for("review.detail", flight_id=flight_id)) @bp.route("/flights//approve", methods=["POST"]) def approve(flight_id: int): token = request.values.get("token", "") if not tokens.verify_token(flight_id, "approve", token): abort(403) row = _get_or_404(flight_id) if row["status"] not in APPROVABLE_STATUSES and row["status"] != "approved": abort(409, "flight hasn't been fetched from FlightAware yet") ok, message = airtrail_client.approve_flight(flight_id) if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: return {"success": ok, "message": message} return redirect(url_for("review.detail", flight_id=flight_id)) @bp.route("/flights//reject", methods=["POST"]) def reject(flight_id: int): token = request.values.get("token", "") if not tokens.verify_token(flight_id, "reject", token): abort(403) ok, message = airtrail_client.reject_flight(flight_id) if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html: return {"success": ok, "message": message} return redirect(url_for("review.detail", flight_id=flight_id))