diff --git a/informed_decision/review.py b/informed_decision/review.py
index d45dc57..49d718d 100644
--- a/informed_decision/review.py
+++ b/informed_decision/review.py
@@ -47,6 +47,7 @@ class OverviewRow:
engine_status: str | None = None
reason: str | None = None
approved_at: str | None = None
+ requesting_party: str | None = None
history: tuple = () # ({"verb", "memo_version", "at", "state"}, ...), the person's own
@@ -160,11 +161,12 @@ class ReviewController:
if exc.code == "session_expired":
raise
return OverviewRow(memo.id, "unavailable", reason=exc.code)
- status, reason = None, None
+ status, reason, requesting_party = None, None, None
try:
current = self.approval_factory(session).get_approval(memo.approval_id)
if current["binding"]["digest"] == memo.approval_binding_digest:
status = current["status"]
+ requesting_party = current["binding"].get("principal")
else:
reason = "binding_changed"
except (ApprovalEngineError, ValueError):
@@ -192,7 +194,7 @@ class ReviewController:
else:
group = "closed"
return OverviewRow(memo.id, group, memo.question, memo.version, status, reason,
- intent.get("approved_at") if intent else None,
+ intent.get("approved_at") if intent else None, requesting_party,
tuple({"verb": d.verb.value, "memo_version": d.memo_version, "at": d.at,
"state": s["state"] if s else None} for d, s in history))
diff --git a/informed_decision/ui.py b/informed_decision/ui.py
index 930d839..4e185a5 100644
--- a/informed_decision/ui.py
+++ b/informed_decision/ui.py
@@ -148,6 +148,8 @@ def _overview_row(row):
return (f'
{text(row.memo_id)}'
f'
{text(reason)}
')
facts = [f'Memo {text(row.memo_id)} ยท version {row.version}']
+ if row.requesting_party:
+ facts.append(f'Requested by: {text(row.requesting_party)}')
if row.engine_status:
facts.append(f'Approval status: {text(row.engine_status)}')
elif row.reason:
diff --git a/tests/test_review_controller.py b/tests/test_review_controller.py
index adc69b9..2d701c1 100644
--- a/tests/test_review_controller.py
+++ b/tests/test_review_controller.py
@@ -346,9 +346,29 @@ def test_overview_degrades_one_row_when_engine_is_unreachable(review):
[row]=controller.overview(session)
assert (row.group,row.engine_status,row.reason)==('open',None,'engine_unavailable')
assert row.question==memo.question
+ assert row.requesting_party is None
transport.request=original
+def test_overview_shows_the_requesting_party(review):
+ """approval-engine's binding.principal is the party the approval was
+ issued on behalf of -- a different role from memo.binding.principal, the
+ reviewer (docs/memo.py's `_binding_document` docstring). The overview
+ surfaces the former alongside status so a reviewer can tell who asked."""
+ controller,session,app,memo,engine,transport=review
+ [row]=controller.overview(session)
+ assert row.requesting_party=='factory-fixture'
+
+
+def test_overview_does_not_surface_a_requesting_party_when_binding_changed(review):
+ controller,session,app,memo,engine,transport=review
+ stale=replace(memo,version=memo.version+1,approval_binding_digest='sha256:'+'0'*64)
+ controller.store.save_memo(stale)
+ [row]=controller.overview(session)
+ assert row.reason=='binding_changed'
+ assert row.requesting_party is None
+
+
def test_overview_surfaces_unconfirmed_submission(review):
controller,session,app,memo,engine,transport=review;p=opened(review).presentation
controller.acknowledge(session,p.id,['h-1'])
@@ -379,6 +399,7 @@ def test_home_lists_open_then_accepted_memos_and_keeps_the_manual_form(review):
assert r['status']==200 and 'Open for you (1)' in r['body'] and 'Accepted (' not in r['body']
assert 'Approve the synthetic factory delivery?' in r['body']
assert 'Approval status: requested' in r['body'] and 'Open a decision review' in r['body']
+ assert 'Requested by: factory-fixture' in r['body']
assert 'private-brief-sentinel' not in r['body'] and 'private-packet-sentinel' not in r['body']
assert len(controller.store.evidence())==0
p=opened(review).presentation;controller.acknowledge(session,p.id,['h-1'])