asf-tooling commented on issue #920:
URL:
https://github.com/apache/tooling-trusted-releases/issues/920#issuecomment-4410023899
<!-- gofannon-issue-triage-bot v2 -->
**Automated triage** — analyzed at `main@2da7807a`
**Type:** `new_feature` • **Classification:** `actionable` •
**Confidence:** `medium`
**Application domain(s):** `project_committee_management`,
`cryptographic_keys`
### Summary
Issue #920 requests reorganizing the committee page into sections (Projects
tiles, enhanced Signing Keys table, Roster) similar to the project page. The
signing keys table should be sorted by asf_uid then fingerprint, and gain
columns for artifact signature count, key expiration, and member/committer
status. @dave2wave noted the same signing keys table changes should apply to
the Keys page. The committee view uses a Jinja template (committee-view.html)
not visible in the provided source, but the keys page has a programmatic table
in `_committee_keys` that can be patched directly.
### Where this lives in the code today
#### `atr/get/committees.py` — `view` (lines 35-70)
_needs modification_
The committee view function passes data to a Jinja template; needs
additional roster data and project add URL.
```python
@get.typed
async def view(session: web.Public, _committees: Literal["committees"],
name: safe.CommitteeKey) -> str:
"""
URL: /committees/<name>
"""
# TODO: Could also import this from keys.py
async with db.session() as data:
committee = await data.committee(
key=str(name),
_projects=True,
_public_signing_keys=True,
).demand(base.ASFQuartException(f"Committee {name!s} not found",
errorcode=404))
project_list = list(committee.projects)
committee_member = False
if isinstance(session, web.Committer):
committee_member = await
session.prevent_confusing_ui_display_committee(name, False)
for project in project_list:
# Workaround for the usual loading problem
project.committee = committee
return await template.render(
"committee-view.html",
committee=committee,
projects=project_list,
algorithms=shared.algorithms,
now=datetime.datetime.now(datetime.UTC),
email_from_key=util.email_from_uid,
is_committee_member=committee_member,
update_committee_keys_form=await form.render(
model_cls=shared.keys.UpdateCommitteeKeysForm,
action=util.as_url(post.keys.keys),
submit_label="Regenerate KEYS file",
defaults={"committee_key": committee.key},
empty=True,
),
is_standing=util.committee_is_standing(committee.key),
)
```
#### `atr/get/keys.py` — `_committee_keys` (lines 281-309)
_needs modification_
The committee keys table on the keys page needs sorting by asf_uid then
fingerprint, plus new columns for expiration and member status per @dave2wave's
comment.
```python
async def _committee_keys(page: htm.Block, user_committees_with_keys:
list[sql.Committee]) -> None:
page.h2("#your-committee-keys")["Your committee's keys"]
page.div(".mb-4")[htm.a(".btn.btn-outline-primary",
href=util.as_url(upload))["Upload a KEYS file"]]
for committee in user_committees_with_keys:
if not util.committee_is_standing(committee.key):
page.h3(f"#committee-{committee.key}.mt-3")[committee.display_name or
committee.key]
if committee.public_signing_keys:
thead = htm.thead[
htm.tr[
htm.th(".px-2", scope="col")["Key ID"],
htm.th(".px-2", scope="col")["Email"],
htm.th(".px-2", scope="col")["Apache UID"],
]
]
tbody = htm.Block(htm.tbody)
for key in committee.public_signing_keys:
row = htm.Block(htm.tr)
details_url = util.as_url(details,
fingerprint=key.fingerprint)
row.td(".text-break.font-monospace.px-2")[htm.a(href=details_url)[key.fingerprint[-16:].upper()]]
email = util.email_from_uid(key.primary_declared_uid) if
key.primary_declared_uid else "-"
row.td(".text-break.px-2")[email or "-"]
row.td(".text-break.px-2")[key.apache_uid or "-"]
tbody.append(row.collect())
page.div(".table-responsive.mb-2")[
htm.table(".table.border.table-striped.table-sm")[thead,
tbody.collect()]
]
```
### Where new code would go
- `atr/get/keys.py` — after symbol _committee_keys
A helper function to compute key expiration badge for table cells would
reduce duplication between the details page and the new table columns.
- `atr/get/committees.py` — after symbol view
A helper to build the roster data (members/committers with their signing
key status) for passing to the committee template.
### Proposed approach
The implementation should be split into two main areas:
1. **Keys page table enhancement** (`atr/get/keys.py`): Modify
`_committee_keys` to sort keys by `apache_uid` then `fingerprint`, add an
'Expired' column using the key's `expires` field, and add a 'Status' column
showing whether the key owner is currently a member/committer (using
`committee.committee_members` and `committee.committers`). The artifact
signature count column (from #911) likely requires a new DB query counting
release artifacts signed by each key; this may need to be deferred until #911
is resolved.
2. **Committee page reorganization** (`atr/get/committees.py` and the
`committee-view.html` template): The view function needs to pass roster data
(committee_members + committers with names) and a project-add URL. Since the
template isn't available in the source provided, the Python-side changes focus
on preparing the data. The template would need restructuring into card sections
matching the project page style.
A shared helper for rendering expiration badges would reduce duplication
between the key details page and the new table columns.
### Suggested patches
#### `atr/get/keys.py`
Sort committee keys by asf_uid then fingerprint, add Expired and Member
columns to the committee keys table per issue requirements and @dave2wave's
comment.
````diff
--- a/atr/get/keys.py
+++ b/atr/get/keys.py
@@ -238,6 +238,18 @@ async def upload(_session: web.Committer, _keys_upload:
Literal["keys/upload"])
return await shared.keys.render_upload_page()
+def _expiration_badge(key: sql.PublicSigningKey) -> htm.Element | str:
+ """Return an expiration badge element or empty string."""
+ if not key.expires:
+ return "-"
+ now = datetime.datetime.now(datetime.UTC)
+ days_until_expiry = (key.expires - now).days
+ if days_until_expiry < 0:
+ return htm.span(".badge.bg-danger.text-white")["Expired"]
+ elif days_until_expiry <= 30:
+ return
htm.span(".badge.bg-warning.text-dark")[f"{days_until_expiry}d"]
+ return htm.span(".text-muted")[key.expires.strftime("%Y-%m-%d")]
+
+
async def _committee_keys(page: htm.Block, user_committees_with_keys:
list[sql.Committee]) -> None:
page.h2("#your-committee-keys")["Your committee's keys"]
page.div(".mb-4")[htm.a(".btn.btn-outline-primary",
href=util.as_url(upload))["Upload a KEYS file"]]
@@ -247,21 +259,35 @@ async def _committee_keys(page: htm.Block,
user_committees_with_keys: list[sql.C
page.h3(f"#committee-{committee.key}.mt-3")[committee.display_name or
committee.key]
if committee.public_signing_keys:
+ # Sort by apache_uid then fingerprint
+ sorted_keys = sorted(
+ committee.public_signing_keys,
+ key=lambda k: (k.apache_uid or "", k.fingerprint),
+ )
+
thead = htm.thead[
htm.tr[
htm.th(".px-2", scope="col")["Key ID"],
htm.th(".px-2", scope="col")["Email"],
htm.th(".px-2", scope="col")["Apache UID"],
+ htm.th(".px-2", scope="col")["Expires"],
+ htm.th(".px-2", scope="col")["Status"],
]
]
tbody = htm.Block(htm.tbody)
- for key in committee.public_signing_keys:
+ members_set = set(getattr(committee, "committee_members",
[]))
+ committers_set = set(getattr(committee, "committers", []))
+ for key in sorted_keys:
row = htm.Block(htm.tr)
details_url = util.as_url(details,
fingerprint=key.fingerprint)
row.td(".text-break.font-monospace.px-2")[htm.a(href=details_url)[key.fingerprint[-16:].upper()]]
email = util.email_from_uid(key.primary_declared_uid)
if key.primary_declared_uid else "-"
row.td(".text-break.px-2")[email or "-"]
row.td(".text-break.px-2")[key.apache_uid or "-"]
+ row.td(".text-break.px-2")[_expiration_badge(key)]
+ uid = (key.apache_uid or "").lower()
+ status = "PMC" if uid in members_set else ("Committer"
if uid in committers_set else "-")
+ row.td(".text-break.px-2")[status]
tbody.append(row.collect())
page.div(".table-responsive.mb-2")[
````
#### `atr/get/committees.py`
Pass roster data and add-project URL to the committee view template for the
new Roster section and Projects tile with create button.
````diff
--- a/atr/get/committees.py
+++ b/atr/get/committees.py
@@ -46,6 +46,8 @@ async def view(session: web.Public, _committees:
Literal["committees"], name: sa
"""
URL: /committees/<name>
"""
+ import atr.get.projects as projects
+
# TODO: Could also import this from keys.py
async with db.session() as data:
committee = await data.committee(
@@ -64,6 +66,8 @@ async def view(session: web.Public, _committees:
Literal["committees"], name: sa
"committee-view.html",
committee=committee,
projects=project_list,
+ add_project_url=util.as_url(projects.add_project,
committee_key=committee.key) if committee_member else None,
+ roster=_build_roster(committee),
algorithms=shared.algorithms,
now=datetime.datetime.now(datetime.UTC),
email_from_key=util.email_from_uid,
@@ -77,3 +81,23 @@ async def view(session: web.Public, _committees:
Literal["committees"], name: sa
),
is_standing=util.committee_is_standing(committee.key),
)
+
+
+def _build_roster(committee: sql.Committee) -> list[dict[str, object]]:
+ """Build a roster of members and committers with their roles."""
+ members = set(getattr(committee, "committee_members", []))
+ committers = set(getattr(committee, "committers", []))
+ all_uids = sorted(members | committers)
+
+ # Build set of UIDs that have signing keys
+ signing_key_uids: set[str] = set()
+ if hasattr(committee, "public_signing_keys") and
committee.public_signing_keys:
+ for key in committee.public_signing_keys:
+ if key.apache_uid:
+ signing_key_uids.add(key.apache_uid.lower())
+
+ return [
+ {"uid": uid, "is_pmc_member": uid in members, "has_signing_key":
uid in signing_key_uids}
+ for uid in all_uids
+ ]
````
### Open questions
- The committee view uses a Jinja template (committee-view.html) not
provided in the source - the actual UI restructuring (tiles, roster table, card
sections) must happen there.
- The artifact signature count column (referenced from #911) requires a
database query to count release artifacts signed with each key - the
implementation depends on how #911 is resolved.
- The roster section asks for 'Name' but ASF display names may come from
LDAP cache - need to confirm how to look up display names from UIDs.
- The committee model attributes committee_members and committers may need
to be eagerly loaded (via _committee_members flag or similar) in the view query.
- Should the 'Status' column in the keys table show member/committer status
at the committee level or at the project level?
### Files examined
- `atr/get/committees.py`
- `atr/get/keys.py`
- `atr/shared/keys.py`
- `atr/get/projects.py`
- `atr/post/keys.py`
- `atr/post/projects.py`
- `atr/shared/projects.py`
- `atr/storage/writers/keys.py`
### Related issues
This issue appears related to: #917.
_Both address reorganization of project and committee pages with improved
navigation and sections_
---
*Draft from a triage agent. A human reviewer should validate before merging
any change. The agent did not run tests or verify diffs apply.*
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]