Coverage for app/refuels/routes.py: 100%
105 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-31 10:13 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-31 10:13 +0000
1from datetime import UTC, date, datetime
3from flask import ( # pyright: ignore[reportMissingImports]
4 Blueprint,
5 abort,
6 flash,
7 redirect,
8 render_template,
9 request,
10 session,
11 url_for,
12)
13from flask.typing import ResponseReturnValue # pyright: ignore[reportMissingImports]
14from flask_babel import gettext as _ # pyright: ignore[reportMissingImports]
15from models import ( # pyright: ignore[reportMissingImports]
16 Aircraft,
17 Refuel,
18 Role,
19 TenantUser,
20 db,
21)
22from utils import ( # pyright: ignore[reportMissingImports]
23 activity,
24 login_required,
25 require_role,
26 user_can_access_aircraft,
27)
29refuels_bp = Blueprint("refuels", __name__)
31_CREW_ROLES = (Role.ADMIN, Role.OWNER, Role.PILOT, Role.MAINTENANCE)
32_UNITS = ["L", "gal"]
35def _tenant_id() -> int:
36 tu = TenantUser.query.filter_by(user_id=session["user_id"]).first()
37 if not tu:
38 abort(403)
39 return int(tu.tenant_id)
42def _get_aircraft_or_404(aircraft_id: int) -> Aircraft:
43 ac = db.session.get(Aircraft, aircraft_id)
44 if (
45 not ac
46 or ac.tenant_id != _tenant_id()
47 or not user_can_access_aircraft(aircraft_id)
48 ):
49 abort(404)
50 return ac
53def _get_refuel_or_404(aircraft: Aircraft, refuel_id: int) -> Refuel:
54 r = db.session.get(Refuel, refuel_id)
55 if not r or r.aircraft_id != aircraft.id:
56 abort(404)
57 return r
60def _parse_required_date(raw: str, label: str) -> tuple[date | None, str | None]:
61 raw = raw.strip()
62 if not raw:
63 return None, str(_("%(label)s is required.", label=label))
64 try:
65 d = date.fromisoformat(raw)
66 except ValueError:
67 return None, str(_("%(label)s must be a valid date (YYYY-MM-DD).", label=label))
68 if d > datetime.now(UTC).date():
69 return None, str(_("%(label)s cannot be in the future.", label=label))
70 return d, None
73# ── Refuel list ───────────────────────────────────────────────────────────────
76@refuels_bp.route("/aircraft/<aircraft_ref:aircraft_id>/refuels")
77@login_required
78def list_refuels(aircraft_id: int) -> ResponseReturnValue:
79 ac = _get_aircraft_or_404(aircraft_id)
80 refuels = (
81 Refuel.query.filter_by(aircraft_id=ac.id)
82 .order_by(Refuel.date.desc(), Refuel.id.desc())
83 .all()
84 )
85 return render_template("refuels/list.html", aircraft=ac, refuels=refuels)
88# ── Add refuel ────────────────────────────────────────────────────────────────
91@refuels_bp.route(
92 "/aircraft/<aircraft_ref:aircraft_id>/refuels/new", methods=["GET", "POST"]
93)
94@login_required
95@require_role(*_CREW_ROLES)
96def new_refuel(aircraft_id: int) -> ResponseReturnValue:
97 ac = _get_aircraft_or_404(aircraft_id)
98 if request.method == "POST":
99 return _save_refuel(ac, None)
100 return render_template(
101 "refuels/refuel_form.html",
102 aircraft=ac,
103 refuel=None,
104 units=_UNITS,
105 today=date.today().isoformat(),
106 )
109# ── Edit refuel ───────────────────────────────────────────────────────────────
112@refuels_bp.route(
113 "/aircraft/<aircraft_ref:aircraft_id>/refuels/<int:refuel_id>/edit",
114 methods=["GET", "POST"],
115)
116@login_required
117@require_role(*_CREW_ROLES)
118def edit_refuel(aircraft_id: int, refuel_id: int) -> ResponseReturnValue:
119 ac = _get_aircraft_or_404(aircraft_id)
120 r = _get_refuel_or_404(ac, refuel_id)
121 if request.method == "POST":
122 return _save_refuel(ac, r)
123 return render_template(
124 "refuels/refuel_form.html",
125 aircraft=ac,
126 refuel=r,
127 units=_UNITS,
128 today=date.today().isoformat(),
129 )
132def _save_refuel(ac: Aircraft, r: Refuel | None) -> ResponseReturnValue:
133 date_raw = request.form.get("date", "").strip()
134 quantity_raw = request.form.get("quantity", "").strip()
135 unit = request.form.get("unit", "").strip()
136 note = request.form.get("note", "").strip() or None
138 errors = []
139 refuel_date, date_error = _parse_required_date(date_raw, str(_("Date")))
140 if date_error:
141 errors.append(date_error)
143 quantity: float | None = None
144 if not quantity_raw:
145 errors.append(_("Quantity is required."))
146 else:
147 try:
148 quantity = float(quantity_raw)
149 if quantity <= 0:
150 raise ValueError
151 except ValueError:
152 errors.append(_("Quantity must be a positive number."))
154 if unit not in _UNITS:
155 unit = "L"
157 if errors:
158 for msg in errors:
159 flash(msg, "danger")
160 return render_template(
161 "refuels/refuel_form.html",
162 aircraft=ac,
163 refuel=r,
164 units=_UNITS,
165 today=date.today().isoformat(),
166 )
168 _refuel_is_new = r is None
169 if r is None:
170 r = Refuel(aircraft_id=ac.id, created_by_id=session.get("user_id"))
171 db.session.add(r)
173 assert refuel_date is not None
174 r.date = refuel_date
175 r.quantity = quantity
176 r.unit = unit
177 r.note = note
178 db.session.commit()
180 activity(
181 "refuel.logged" if _refuel_is_new else "refuel.edited",
182 refuel_id=r.id,
183 aircraft_id=ac.id,
184 quantity=str(r.quantity),
185 unit=r.unit,
186 )
187 flash(_("Refuel recorded."), "success")
188 return redirect(url_for("refuels.list_refuels", aircraft_id=ac.id))
191# ── Delete refuel ─────────────────────────────────────────────────────────────
194@refuels_bp.route(
195 "/aircraft/<aircraft_ref:aircraft_id>/refuels/<int:refuel_id>/delete",
196 methods=["POST"],
197)
198@login_required
199@require_role(*_CREW_ROLES)
200def delete_refuel(aircraft_id: int, refuel_id: int) -> ResponseReturnValue:
201 ac = _get_aircraft_or_404(aircraft_id)
202 r = _get_refuel_or_404(ac, refuel_id)
203 db.session.delete(r)
204 db.session.commit()
205 flash(_("Refuel deleted."), "success")
206 return redirect(url_for("refuels.list_refuels", aircraft_id=ac.id))