Coverage for app/airworthiness/routes.py: 100%
352 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 15:29 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-17 15:29 +0000
1import os
2from datetime import date
4from flask import ( # pyright: ignore[reportMissingImports]
5 Blueprint,
6 abort,
7 flash,
8 redirect,
9 render_template,
10 request,
11 session,
12 url_for,
13)
14from flask.typing import ResponseReturnValue # pyright: ignore[reportMissingImports]
15from flask_babel import gettext as _ # pyright: ignore[reportMissingImports]
16from flask_babel import ngettext
17from models import ( # pyright: ignore[reportMissingImports]
18 AdSbItem,
19 AdSbStatus,
20 Aircraft,
21 AirworthinessDocStatus,
22 AirworthinessDocType,
23 AirworthinessDocument,
24 AirworthinessDocumentStatus,
25 AvionicsUnit,
26 Component,
27 EASASourceNode,
28 EquipmentStatus,
29 EquipmentWishlistItem,
30 InstalledSTC,
31 Role,
32 TenantUser,
33 db,
34)
35from utils import ( # pyright: ignore[reportMissingImports]
36 login_required,
37 require_role,
38 user_can_access_aircraft,
39)
41airworthiness_bp = Blueprint("airworthiness", __name__)
43_OWNER_ROLES = (Role.ADMIN, Role.OWNER)
44_CREW_ROLES = (Role.ADMIN, Role.OWNER, Role.PILOT, Role.MAINTENANCE)
47def _tenant_id() -> int:
48 tu = TenantUser.query.filter_by(user_id=session["user_id"]).first()
49 if not tu:
50 abort(403)
51 return int(tu.tenant_id)
54def _get_aircraft_or_404(aircraft_id: int) -> Aircraft:
55 ac = db.session.get(Aircraft, aircraft_id)
56 if (
57 not ac
58 or ac.tenant_id != _tenant_id()
59 or not user_can_access_aircraft(aircraft_id)
60 ):
61 abort(404)
62 return ac
65def _get_node_or_404(aircraft: Aircraft, node_id: int) -> EASASourceNode:
66 node = db.session.get(EASASourceNode, node_id)
67 if not node or node.component.aircraft_id != aircraft.id:
68 abort(404)
69 return node
72def _get_doc_or_404(aircraft: Aircraft, doc_id: int) -> AirworthinessDocument:
73 doc = db.session.get(AirworthinessDocument, doc_id)
74 if not doc:
75 abort(404)
76 # Document belongs to this aircraft if its component belongs to it
77 component = doc.component or (
78 doc.source_node.component if doc.source_node else None
79 )
80 if not component or component.aircraft_id != aircraft.id:
81 abort(404)
82 return doc
85def _get_stc_or_404(aircraft: Aircraft, stc_id: int) -> InstalledSTC:
86 stc = db.session.get(InstalledSTC, stc_id)
87 if not stc or stc.aircraft_id != aircraft.id:
88 abort(404)
89 return stc
92def _get_ad_sb_item_or_404(aircraft: Aircraft, item_id: int) -> AdSbItem:
93 item = db.session.get(AdSbItem, item_id)
94 if not item or item.aircraft_id != aircraft.id:
95 abort(404)
96 return item
99def _get_avionics_unit_or_404(aircraft: Aircraft, unit_id: int) -> AvionicsUnit:
100 unit = db.session.get(AvionicsUnit, unit_id)
101 if not unit or unit.aircraft_id != aircraft.id:
102 abort(404)
103 return unit
106def _get_wishlist_item_or_404(
107 aircraft: Aircraft, item_id: int
108) -> EquipmentWishlistItem:
109 item = db.session.get(EquipmentWishlistItem, item_id)
110 if not item or item.aircraft_id != aircraft.id:
111 abort(404)
112 return item
115def _status_for(aircraft_id: int, doc_id: int) -> AirworthinessDocumentStatus | None:
116 return AirworthinessDocumentStatus.query.filter_by( # type: ignore[no-any-return]
117 aircraft_id=aircraft_id, document_id=doc_id
118 ).first()
121# ── Dashboard ─────────────────────────────────────────────────────────────────
124@airworthiness_bp.route("/aircraft/<aircraft_ref:aircraft_id>/airworthiness/")
125@login_required
126@require_role(*_CREW_ROLES)
127def dashboard(aircraft_id: int) -> ResponseReturnValue:
128 ac = _get_aircraft_or_404(aircraft_id)
130 # Gather all documents for this aircraft through its components
131 component_ids = [c.id for c in ac.components] # type: ignore[attr-defined]
133 # Documents via EASA source nodes
134 synced_docs = (
135 AirworthinessDocument.query.join(EASASourceNode)
136 .filter(EASASourceNode.component_id.in_(component_ids))
137 .all()
138 if component_ids
139 else []
140 )
141 # Manual documents attached directly to components
142 manual_docs = (
143 AirworthinessDocument.query.filter(
144 AirworthinessDocument.component_id.in_(component_ids),
145 AirworthinessDocument.source_node_id.is_(None),
146 ).all()
147 if component_ids
148 else []
149 )
150 all_docs = synced_docs + manual_docs
152 # Build status map {doc_id: AirworthinessDocumentStatus}
153 existing_statuses = {
154 s.document_id: s
155 for s in AirworthinessDocumentStatus.query.filter_by(
156 aircraft_id=aircraft_id
157 ).all()
158 }
160 # Attach status to each doc (or None if not yet created)
161 doc_rows = []
162 for doc in all_docs:
163 st = existing_statuses.get(doc.id)
164 component = doc.component or (
165 doc.source_node.component if doc.source_node else None
166 )
167 doc_rows.append({"doc": doc, "status": st, "component": component})
169 # Sort: pending first, then by doc_type, then reference
170 _STATUS_ORDER = {
171 AirworthinessDocStatus.PENDING_REVIEW: 0,
172 AirworthinessDocStatus.QUESTION: 1,
173 AirworthinessDocStatus.DEFERRED: 2,
174 AirworthinessDocStatus.COMPLIED: 3,
175 AirworthinessDocStatus.NOT_APPLICABLE: 4,
176 None: 0,
177 }
178 doc_rows.sort(
179 key=lambda r: (
180 _STATUS_ORDER.get(r["status"].status if r["status"] else None, 0),
181 r["doc"].doc_type,
182 r["doc"].reference,
183 )
184 )
186 # Summary counts
187 counts: dict[str, int] = {s: 0 for s in AirworthinessDocStatus.ALL}
188 counts["total"] = len(all_docs)
189 for row in doc_rows:
190 st_val = (
191 row["status"].status
192 if row["status"]
193 else AirworthinessDocStatus.PENDING_REVIEW
194 )
195 counts[st_val] = counts.get(st_val, 0) + 1
197 # Source nodes grouped by component
198 nodes_by_component: dict[int, list[EASASourceNode]] = {}
199 for comp in ac.components: # type: ignore[attr-defined]
200 if comp.easa_source_nodes:
201 nodes_by_component[comp.id] = comp.easa_source_nodes
203 is_production = os.environ.get("OPENHANGAR_ENV", "production") == "production"
204 return render_template(
205 "airworthiness/dashboard.html",
206 aircraft=ac,
207 doc_rows=doc_rows,
208 counts=counts,
209 nodes_by_component=nodes_by_component,
210 doc_types=AirworthinessDocType,
211 statuses=AirworthinessDocStatus,
212 installed_stcs=ac.installed_stcs,
213 is_production=is_production,
214 )
217# ── EASA sync (manual trigger) ────────────────────────────────────────────────
220@airworthiness_bp.route(
221 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/sync", methods=["POST"]
222)
223@login_required
224@require_role(*_OWNER_ROLES)
225def trigger_sync(aircraft_id: int) -> ResponseReturnValue:
226 if os.environ.get("OPENHANGAR_ENV", "production") != "production":
227 abort(403)
228 ac = _get_aircraft_or_404(aircraft_id)
229 from airworthiness_sync import (
230 sync_aircraft, # pyright: ignore[reportMissingImports]
231 )
233 added, errors = sync_aircraft(ac)
234 if errors:
235 flash(
236 ngettext(
237 "Sync completed with errors on one node. Check logs.",
238 "Sync completed with errors on %(n)s nodes. Check logs.",
239 errors,
240 n=errors,
241 ),
242 "warning",
243 )
244 if added:
245 flash(
246 ngettext(
247 "One new document discovered.",
248 "%(n)s new documents discovered.",
249 added,
250 n=added,
251 ),
252 "success",
253 )
254 else:
255 flash(_("No new documents found."), "info")
256 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
259# ── EASA source nodes ─────────────────────────────────────────────────────────
262@airworthiness_bp.route(
263 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/nodes/new",
264 methods=["GET", "POST"],
265)
266@login_required
267@require_role(*_OWNER_ROLES)
268def add_node(aircraft_id: int) -> ResponseReturnValue:
269 ac = _get_aircraft_or_404(aircraft_id)
270 components = [c for c in ac.components if not c.removed_at] # type: ignore[attr-defined]
272 if request.method == "POST":
273 component_id = request.form.get("component_id", type=int)
274 comp = db.session.get(Component, component_id)
275 if not comp or comp.aircraft_id != aircraft_id:
276 abort(400)
278 node = EASASourceNode(
279 component_id=component_id,
280 tc_holder_node_id=request.form["tc_holder_node_id"].strip(),
281 tc_holder_name=request.form["tc_holder_name"].strip(),
282 type_node_id=request.form["type_node_id"].strip(),
283 type_name=request.form["type_name"].strip(),
284 model_node_id=request.form["model_node_id"].strip(),
285 model_name=request.form["model_name"].strip(),
286 )
287 db.session.add(node)
288 db.session.commit()
289 flash(_("EASA source node added."), "success")
290 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
292 preselect_component_id = request.args.get("component_id", type=int)
293 return render_template(
294 "airworthiness/node_form.html",
295 aircraft=ac,
296 components=components,
297 preselect_component_id=preselect_component_id,
298 )
301@airworthiness_bp.route(
302 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/nodes/<int:node_id>/delete",
303 methods=["POST"],
304)
305@login_required
306@require_role(*_OWNER_ROLES)
307def delete_node(aircraft_id: int, node_id: int) -> ResponseReturnValue:
308 ac = _get_aircraft_or_404(aircraft_id)
309 node = _get_node_or_404(ac, node_id)
310 db.session.delete(node)
311 db.session.commit()
312 flash(_("EASA source node removed."), "success")
313 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
316# ── Documents ─────────────────────────────────────────────────────────────────
319@airworthiness_bp.route(
320 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/documents/new",
321 methods=["GET", "POST"],
322)
323@login_required
324@require_role(*_OWNER_ROLES)
325def add_document(aircraft_id: int) -> ResponseReturnValue:
326 ac = _get_aircraft_or_404(aircraft_id)
327 components = [c for c in ac.components if not c.removed_at] # type: ignore[attr-defined]
329 if request.method == "POST":
330 component_id = request.form.get("component_id", type=int)
331 comp = db.session.get(Component, component_id)
332 if not comp or comp.aircraft_id != aircraft_id:
333 abort(400)
335 expiry_raw = request.form.get("expiry_date", "").strip()
336 expiry = date.fromisoformat(expiry_raw) if expiry_raw else None
338 doc = AirworthinessDocument(
339 doc_type=request.form["doc_type"].strip(),
340 reference=request.form["reference"].strip(),
341 title=request.form.get("title", "").strip() or None,
342 component_id=component_id,
343 doc_url=request.form.get("doc_url", "").strip() or None,
344 expiry_date=expiry,
345 )
346 db.session.add(doc)
347 db.session.flush()
349 status = AirworthinessDocumentStatus(
350 aircraft_id=aircraft_id,
351 document_id=doc.id,
352 status=AirworthinessDocStatus.PENDING_REVIEW,
353 )
354 db.session.add(status)
355 db.session.commit()
356 flash(_("Document added."), "success")
357 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
359 return render_template(
360 "airworthiness/document_form.html",
361 aircraft=ac,
362 components=components,
363 doc_types=AirworthinessDocType,
364 )
367@airworthiness_bp.route(
368 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/documents/<int:doc_id>/delete",
369 methods=["POST"],
370)
371@login_required
372@require_role(*_OWNER_ROLES)
373def delete_document(aircraft_id: int, doc_id: int) -> ResponseReturnValue:
374 ac = _get_aircraft_or_404(aircraft_id)
375 doc = _get_doc_or_404(ac, doc_id)
376 if not doc.is_manual:
377 flash(_("Only manually added documents can be deleted."), "danger")
378 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
379 db.session.delete(doc)
380 db.session.commit()
381 flash(_("Document deleted."), "success")
382 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
385# ── Status updates ────────────────────────────────────────────────────────────
388@airworthiness_bp.route(
389 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/documents/<int:doc_id>/status",
390 methods=["GET", "POST"],
391)
392@login_required
393@require_role(*_CREW_ROLES)
394def update_status(aircraft_id: int, doc_id: int) -> ResponseReturnValue:
395 ac = _get_aircraft_or_404(aircraft_id)
396 doc = _get_doc_or_404(ac, doc_id)
397 st = _status_for(aircraft_id, doc_id)
399 if request.method == "POST":
400 new_status = request.form.get("status", "").strip()
401 if new_status not in AirworthinessDocStatus.ALL:
402 abort(400)
404 compliance_raw = request.form.get("compliance_date", "").strip()
405 review_raw = request.form.get("next_review_date", "").strip()
407 if st is None:
408 st = AirworthinessDocumentStatus(
409 aircraft_id=aircraft_id, document_id=doc_id
410 )
411 db.session.add(st)
413 st.status = new_status
414 st.notes = request.form.get("notes", "").strip() or None
415 st.compliance_date = (
416 date.fromisoformat(compliance_raw) if compliance_raw else None
417 )
418 st.next_review_date = date.fromisoformat(review_raw) if review_raw else None
419 db.session.commit()
420 flash(_("Status updated."), "success")
421 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
423 return render_template(
424 "airworthiness/status_form.html",
425 aircraft=ac,
426 doc=doc,
427 current_status=st,
428 statuses=AirworthinessDocStatus,
429 )
432# ── Installed STCs ────────────────────────────────────────────────────────────
435@airworthiness_bp.route(
436 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/stcs/new",
437 methods=["GET", "POST"],
438)
439@login_required
440@require_role(*_OWNER_ROLES)
441def add_stc(aircraft_id: int) -> ResponseReturnValue:
442 ac = _get_aircraft_or_404(aircraft_id)
444 if request.method == "POST":
445 install_raw = request.form.get("installation_date", "").strip()
446 stc = InstalledSTC(
447 aircraft_id=aircraft_id,
448 stc_number=request.form["stc_number"].strip(),
449 title=request.form.get("title", "").strip() or None,
450 tc_holder=request.form.get("tc_holder", "").strip() or None,
451 installation_date=date.fromisoformat(install_raw) if install_raw else None,
452 notes=request.form.get("notes", "").strip() or None,
453 )
454 db.session.add(stc)
455 db.session.commit()
456 flash(_("Installed STC added."), "success")
457 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
459 return render_template("airworthiness/stc_form.html", aircraft=ac)
462@airworthiness_bp.route(
463 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/stcs/<int:stc_id>/delete",
464 methods=["POST"],
465)
466@login_required
467@require_role(*_OWNER_ROLES)
468def delete_stc(aircraft_id: int, stc_id: int) -> ResponseReturnValue:
469 ac = _get_aircraft_or_404(aircraft_id)
470 stc = _get_stc_or_404(ac, stc_id)
471 db.session.delete(stc)
472 db.session.commit()
473 flash(_("Installed STC removed."), "success")
474 return redirect(url_for("airworthiness.dashboard", aircraft_id=aircraft_id))
477# ── AD/SB compliance board ────────────────────────────────────────────────────
478# Backlog item: an owner's working AD/SB summary — explicitly not the
479# authoritative FAA/EASA record (see the disclaimer on the board itself).
482@airworthiness_bp.route("/aircraft/<aircraft_ref:aircraft_id>/airworthiness/adsb/")
483@login_required
484@require_role(*_CREW_ROLES)
485def adsb_board(aircraft_id: int) -> ResponseReturnValue:
486 ac = _get_aircraft_or_404(aircraft_id)
487 items = list(ac.ad_sb_items)
488 checklist_items = [i for i in items if i.status in AdSbStatus.OPEN]
489 return render_template(
490 "airworthiness/adsb_board.html",
491 aircraft=ac,
492 items=items,
493 checklist_items=checklist_items,
494 statuses=AdSbStatus,
495 )
498@airworthiness_bp.route(
499 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/adsb/new",
500 methods=["GET", "POST"],
501)
502@login_required
503@require_role(*_OWNER_ROLES)
504def add_ad_sb_item(aircraft_id: int) -> ResponseReturnValue:
505 ac = _get_aircraft_or_404(aircraft_id)
507 if request.method == "POST":
508 status = request.form.get("status", "").strip()
509 if status not in AdSbStatus.ALL:
510 abort(400)
511 item = AdSbItem(
512 aircraft_id=aircraft_id,
513 reference=request.form["reference"].strip(),
514 title=request.form.get("title", "").strip() or None,
515 status=status,
516 notes=request.form.get("notes", "").strip() or None,
517 )
518 db.session.add(item)
519 db.session.commit()
520 flash(_("AD/SB item added."), "success")
521 return redirect(url_for("airworthiness.adsb_board", aircraft_id=aircraft_id))
523 return render_template(
524 "airworthiness/adsb_form.html", aircraft=ac, item=None, statuses=AdSbStatus
525 )
528@airworthiness_bp.route(
529 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/adsb/<int:item_id>/edit",
530 methods=["GET", "POST"],
531)
532@login_required
533@require_role(*_OWNER_ROLES)
534def edit_ad_sb_item(aircraft_id: int, item_id: int) -> ResponseReturnValue:
535 ac = _get_aircraft_or_404(aircraft_id)
536 item = _get_ad_sb_item_or_404(ac, item_id)
538 if request.method == "POST":
539 status = request.form.get("status", "").strip()
540 if status not in AdSbStatus.ALL:
541 abort(400)
542 item.reference = request.form["reference"].strip()
543 item.title = request.form.get("title", "").strip() or None
544 item.status = status
545 item.notes = request.form.get("notes", "").strip() or None
546 db.session.commit()
547 flash(_("AD/SB item updated."), "success")
548 return redirect(url_for("airworthiness.adsb_board", aircraft_id=aircraft_id))
550 return render_template(
551 "airworthiness/adsb_form.html", aircraft=ac, item=item, statuses=AdSbStatus
552 )
555@airworthiness_bp.route(
556 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/adsb/<int:item_id>/delete",
557 methods=["POST"],
558)
559@login_required
560@require_role(*_OWNER_ROLES)
561def delete_ad_sb_item(aircraft_id: int, item_id: int) -> ResponseReturnValue:
562 ac = _get_aircraft_or_404(aircraft_id)
563 item = _get_ad_sb_item_or_404(ac, item_id)
564 db.session.delete(item)
565 db.session.commit()
566 flash(_("AD/SB item removed."), "success")
567 return redirect(url_for("airworthiness.adsb_board", aircraft_id=aircraft_id))
570@airworthiness_bp.route("/aircraft/<aircraft_ref:aircraft_id>/airworthiness/adsb/print")
571@login_required
572@require_role(*_CREW_ROLES)
573def adsb_print(aircraft_id: int) -> ResponseReturnValue:
574 ac = _get_aircraft_or_404(aircraft_id)
575 items = list(ac.ad_sb_items)
576 checklist_items = [i for i in items if i.status in AdSbStatus.OPEN]
577 return render_template(
578 "airworthiness/adsb_print.html",
579 aircraft=ac,
580 items=items,
581 checklist_items=checklist_items,
582 statuses=AdSbStatus,
583 today=date.today(),
584 )
587# ── Avionics/equipment inventory ─────────────────────────────────────────────
588# Backlog item: per-unit status + a separate upgrade wish list, kept distinct
589# from the generic Component/maintenance-trigger model (no TBO/life-limit
590# tracking here — this is a simple inventory).
593@airworthiness_bp.route("/aircraft/<aircraft_ref:aircraft_id>/airworthiness/equipment/")
594@login_required
595@require_role(*_CREW_ROLES)
596def equipment_board(aircraft_id: int) -> ResponseReturnValue:
597 ac = _get_aircraft_or_404(aircraft_id)
598 return render_template(
599 "airworthiness/equipment_board.html",
600 aircraft=ac,
601 units=list(ac.avionics_units),
602 wishlist_items=list(ac.equipment_wishlist_items),
603 statuses=EquipmentStatus,
604 )
607@airworthiness_bp.route(
608 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/equipment/new",
609 methods=["GET", "POST"],
610)
611@login_required
612@require_role(*_OWNER_ROLES)
613def add_avionics_unit(aircraft_id: int) -> ResponseReturnValue:
614 ac = _get_aircraft_or_404(aircraft_id)
616 if request.method == "POST":
617 status = request.form.get("status", "").strip()
618 if status not in EquipmentStatus.ALL:
619 abort(400)
620 unit = AvionicsUnit(
621 aircraft_id=aircraft_id,
622 role=request.form["role"].strip(),
623 make=request.form.get("make", "").strip() or None,
624 model=request.form.get("model", "").strip() or None,
625 serial_number=request.form.get("serial_number", "").strip() or None,
626 status=status,
627 certification_notes=request.form.get("certification_notes", "").strip()
628 or None,
629 notes=request.form.get("notes", "").strip() or None,
630 )
631 db.session.add(unit)
632 db.session.commit()
633 flash(_("Equipment unit added."), "success")
634 return redirect(
635 url_for("airworthiness.equipment_board", aircraft_id=aircraft_id)
636 )
638 return render_template(
639 "airworthiness/avionics_unit_form.html",
640 aircraft=ac,
641 unit=None,
642 statuses=EquipmentStatus,
643 )
646@airworthiness_bp.route(
647 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/equipment/<int:unit_id>/edit",
648 methods=["GET", "POST"],
649)
650@login_required
651@require_role(*_OWNER_ROLES)
652def edit_avionics_unit(aircraft_id: int, unit_id: int) -> ResponseReturnValue:
653 ac = _get_aircraft_or_404(aircraft_id)
654 unit = _get_avionics_unit_or_404(ac, unit_id)
656 if request.method == "POST":
657 status = request.form.get("status", "").strip()
658 if status not in EquipmentStatus.ALL:
659 abort(400)
660 unit.role = request.form["role"].strip()
661 unit.make = request.form.get("make", "").strip() or None
662 unit.model = request.form.get("model", "").strip() or None
663 unit.serial_number = request.form.get("serial_number", "").strip() or None
664 unit.status = status
665 unit.certification_notes = (
666 request.form.get("certification_notes", "").strip() or None
667 )
668 unit.notes = request.form.get("notes", "").strip() or None
669 db.session.commit()
670 flash(_("Equipment unit updated."), "success")
671 return redirect(
672 url_for("airworthiness.equipment_board", aircraft_id=aircraft_id)
673 )
675 return render_template(
676 "airworthiness/avionics_unit_form.html",
677 aircraft=ac,
678 unit=unit,
679 statuses=EquipmentStatus,
680 )
683@airworthiness_bp.route(
684 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/equipment/<int:unit_id>/delete",
685 methods=["POST"],
686)
687@login_required
688@require_role(*_OWNER_ROLES)
689def delete_avionics_unit(aircraft_id: int, unit_id: int) -> ResponseReturnValue:
690 ac = _get_aircraft_or_404(aircraft_id)
691 unit = _get_avionics_unit_or_404(ac, unit_id)
692 db.session.delete(unit)
693 db.session.commit()
694 flash(_("Equipment unit removed."), "success")
695 return redirect(url_for("airworthiness.equipment_board", aircraft_id=aircraft_id))
698@airworthiness_bp.route(
699 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/equipment/wishlist/new",
700 methods=["GET", "POST"],
701)
702@login_required
703@require_role(*_OWNER_ROLES)
704def add_wishlist_item(aircraft_id: int) -> ResponseReturnValue:
705 ac = _get_aircraft_or_404(aircraft_id)
707 if request.method == "POST":
708 cost_raw = request.form.get("rough_cost", "").strip()
709 rough_cost = None
710 if cost_raw:
711 try:
712 rough_cost = float(cost_raw)
713 if rough_cost < 0:
714 raise ValueError
715 except ValueError:
716 flash(_("Estimated cost must be a non-negative number."), "danger")
717 return render_template("airworthiness/wishlist_form.html", aircraft=ac)
718 item = EquipmentWishlistItem(
719 aircraft_id=aircraft_id,
720 title=request.form["title"].strip(),
721 rough_cost=rough_cost,
722 requirements=request.form.get("requirements", "").strip() or None,
723 notes=request.form.get("notes", "").strip() or None,
724 )
725 db.session.add(item)
726 db.session.commit()
727 flash(_("Wish-list item added."), "success")
728 return redirect(
729 url_for("airworthiness.equipment_board", aircraft_id=aircraft_id)
730 )
732 return render_template("airworthiness/wishlist_form.html", aircraft=ac)
735@airworthiness_bp.route(
736 "/aircraft/<aircraft_ref:aircraft_id>/airworthiness/equipment/wishlist/<int:item_id>/delete",
737 methods=["POST"],
738)
739@login_required
740@require_role(*_OWNER_ROLES)
741def delete_wishlist_item(aircraft_id: int, item_id: int) -> ResponseReturnValue:
742 ac = _get_aircraft_or_404(aircraft_id)
743 item = _get_wishlist_item_or_404(ac, item_id)
744 db.session.delete(item)
745 db.session.commit()
746 flash(_("Wish-list item removed."), "success")
747 return redirect(url_for("airworthiness.equipment_board", aircraft_id=aircraft_id))