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]

Reply via email to