source: uc_approve.py@ 527c097

main
Last change on this file since 527c097 was 527c097, checked in by teo <teodorbogoeski@…>, 3 months ago

Add connection pooling and transaction management

  • Property mode set to 100644
File size: 6.7 KB
Line 
1from db import get_connection, get_transaction, pick_from_list, print_table
2
3
4def approve_reservations(admin_user_id):
5 """UC0003: Approve or Reject Reservations."""
6 while True:
7 pending = _fetch_pending()
8 if not pending:
9 print("\n No pending reservations.")
10 return
11
12 print(f"\n=== Pending Reservations ({len(pending)}) ===\n")
13 items = []
14 for p in pending:
15 loc = f"{p['building']} / {p['room']}" if p["building"] else "Digital"
16 items.append(
17 f"[#{p['reservation_id']}] {p['purpose'][:40]}\n"
18 f" by {p['requested_by']} ({p['requester_role']})\n"
19 f" {p['resource_name']} ({loc})\n"
20 f" {p['start_time'].strftime('%Y-%m-%d %H:%M')} - {p['end_time'].strftime('%Y-%m-%d %H:%M')}"
21 )
22 idx = pick_from_list(" Select a reservation to review: ", items)
23 if idx is None:
24 return
25
26 reservation = pending[idx]
27 _review_reservation(reservation, admin_user_id)
28
29
30def _fetch_pending():
31 with get_connection() as conn:
32 with conn.cursor() as cur:
33 cur.execute("""
34 SELECT res.reservation_id, res.start_time, res.end_time,
35 res.purpose, res.created_at, res.recurrence_group_id, res.resource_id,
36 u.first_name || ' ' || u.last_name AS requested_by,
37 u.email AS requester_email,
38 ut.type_name AS requester_role,
39 r.name AS resource_name,
40 rt.type_name AS resource_type,
41 l.building, l.room
42 FROM reservations res
43 JOIN users u ON res.user_id = u.user_id
44 JOIN user_types ut ON u.type_id = ut.type_id
45 JOIN resources r ON res.resource_id = r.resource_id
46 JOIN resource_types rt ON r.type_id = rt.type_id
47 LEFT JOIN locations l ON r.location_id = l.location_id
48 WHERE res.status = 'pending'
49 ORDER BY res.created_at ASC
50 """)
51 cols = [desc[0] for desc in cur.description]
52 return [dict(zip(cols, row)) for row in cur.fetchall()]
53
54
55def _review_reservation(res, admin_user_id):
56 print(f"\n === Reservation #{res['reservation_id']} ===")
57 print(f" Purpose: {res['purpose']}")
58 print(f" Requested by: {res['requested_by']} ({res['requester_role']})")
59 print(f" Email: {res['requester_email']}")
60 print(f" Resource: {res['resource_name']} ({res['resource_type']})")
61 if res["building"]:
62 print(f" Location: {res['building']} / {res['room']}")
63 print(f" Time: {res['start_time'].strftime('%Y-%m-%d %H:%M')} - {res['end_time'].strftime('%Y-%m-%d %H:%M')}")
64 print(f" Created: {res['created_at'].strftime('%Y-%m-%d %H:%M')}")
65
66 # Show recurrence series if applicable
67 if res["recurrence_group_id"]:
68 print(f"\n Part of recurring series:")
69 with get_connection() as conn:
70 with conn.cursor() as cur:
71 cur.execute(
72 """
73 SELECT reservation_id, start_time, end_time, status
74 FROM reservations
75 WHERE recurrence_group_id = %s
76 ORDER BY start_time
77 """,
78 (str(res["recurrence_group_id"]),),
79 )
80 series = cur.fetchall()
81 print_table(
82 ["ID", "Start", "End", "Status"],
83 [(r[0], r[1].strftime("%Y-%m-%d %H:%M"), r[2].strftime("%Y-%m-%d %H:%M"), r[3]) for r in series],
84 )
85
86 # Check conflicts
87 with get_connection() as conn:
88 with conn.cursor() as cur:
89 cur.execute(
90 """
91 SELECT res2.reservation_id, res2.start_time, res2.end_time,
92 res2.purpose,
93 u.first_name || ' ' || u.last_name AS reserved_by
94 FROM reservations res2
95 JOIN users u ON res2.user_id = u.user_id
96 WHERE res2.resource_id = %s
97 AND res2.reservation_id != %s
98 AND res2.status = 'approved'
99 AND res2.start_time < %s
100 AND res2.end_time > %s
101 """,
102 (res["resource_id"], res["reservation_id"], res["end_time"], res["start_time"]),
103 )
104 conflicts = cur.fetchall()
105
106 if conflicts:
107 print(f"\n WARNING: {len(conflicts)} conflict(s) with approved reservations:")
108 print_table(
109 ["ID", "Start", "End", "Purpose", "Reserved by"],
110 [(c[0], c[1].strftime("%Y-%m-%d %H:%M"), c[2].strftime("%Y-%m-%d %H:%M"), c[3][:30], c[4]) for c in conflicts],
111 )
112 else:
113 print("\n No conflicts with approved reservations.")
114
115 # Action
116 print()
117 action = pick_from_list(" Choose action: ", ["Approve", "Reject", "Skip"])
118 if action is None or action == 2:
119 return
120
121 new_status = "approved" if action == 0 else "rejected"
122 try:
123 with get_transaction() as conn:
124 with conn.cursor() as cur:
125 # For approvals, re-check conflicts within the transaction
126 # to prevent approving overlapping reservations
127 if new_status == "approved":
128 cur.execute(
129 """
130 SELECT COUNT(*) FROM reservations
131 WHERE resource_id = %s
132 AND reservation_id != %s
133 AND status = 'approved'
134 AND start_time < %s AND end_time > %s
135 """,
136 (res["resource_id"], res["reservation_id"],
137 res["end_time"], res["start_time"]),
138 )
139 if cur.fetchone()[0] > 0:
140 raise RuntimeError("Conflict: another reservation was approved for this time slot")
141
142 cur.execute(
143 """
144 UPDATE reservations
145 SET status = %s, approved_by = %s
146 WHERE reservation_id = %s AND status = 'pending'
147 RETURNING reservation_id, status
148 """,
149 (new_status, admin_user_id, res["reservation_id"]),
150 )
151 result = cur.fetchone()
152 # Transaction committed automatically
153 if result:
154 print(f"\n Reservation #{result[0]} has been {result[1]}.")
155 else:
156 print("\n Reservation was already processed by another administrator.")
157 except Exception as e:
158 # Transaction rolled back automatically
159 print(f"\n Error: {e}")
Note: See TracBrowser for help on using the repository browser.