This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 211ca83  Make some key deletion and update forms more type safe
211ca83 is described below

commit 211ca83187572a4d848aa2c345a43357bd78e39e
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Nov 11 19:36:54 2025 +0000

    Make some key deletion and update forms more type safe
---
 atr/get/keys.py                   | 185 ++++++++++++++++++++++++++++++++------
 atr/htm.py                        |  15 ++++
 atr/post/keys.py                  |  93 ++++++++++---------
 atr/shared/keys.py                |  26 +++++-
 atr/templates/committee-view.html |   4 +-
 atr/templates/keys-review.html    | 161 ---------------------------------
 6 files changed, 252 insertions(+), 232 deletions(-)

diff --git a/atr/get/keys.py b/atr/get/keys.py
index a9eb88d..23c4bf3 100644
--- a/atr/get/keys.py
+++ b/atr/get/keys.py
@@ -15,14 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import datetime
-
-import quart
 
 import atr.blueprints.get as get
 import atr.db as db
 import atr.form as form
 import atr.htm as htm
+import atr.models.sql as sql
 import atr.post as post
 import atr.shared as shared
 import atr.storage as storage
@@ -40,9 +38,9 @@ async def add(session: web.Committer) -> str:
     committee_choices = [(c.name, c.display_name or c.name) for c in 
participant_of_committees]
 
     page = htm.Block()
-    page.p[htm.a(href=util.as_url(keys), class_="atr-back-link")["← Back to 
Manage keys"],]
-    page.div(class_="my-4")[
-        htm.h1(class_="mb-4")["Add your OpenPGP key"],
+    page.p[htm.a(".atr-back-link", href=util.as_url(keys))["← Back to Manage 
keys"],]
+    page.div(".my-4")[
+        htm.h1(".mb-4")["Add your OpenPGP key"],
         htm.p["Add your public key to use for signing release artifacts."],
     ]
     form.render_block(
@@ -84,9 +82,6 @@ async def keys(session: web.Committer) -> str:
     """View all keys associated with the user's account."""
     committees_to_query = list(set(session.committees + session.projects))
 
-    delete_form = await shared.keys.DeleteKeyForm.create_form()
-    update_committee_keys_form = await 
shared.keys.UpdateCommitteeKeysForm.create_form()
-
     async with db.session() as data:
         user_keys = await 
data.public_signing_key(apache_uid=session.uid.lower(), _committees=True).all()
         user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
@@ -94,23 +89,28 @@ async def keys(session: web.Committer) -> str:
     for key in user_keys:
         key.committees.sort(key=lambda c: c.name)
 
-    status_message = quart.request.args.get("status_message")
-    status_type = quart.request.args.get("status_type")
-
-    return await template.render(
-        "keys-review.html",
-        asf_id=session.uid,
-        user_keys=user_keys,
-        user_ssh_keys=user_ssh_keys,
-        committees=user_committees_with_keys,
-        algorithms=shared.algorithms,
-        status_message=status_message,
-        status_type=status_type,
-        now=datetime.datetime.now(datetime.UTC),
-        delete_form=delete_form,
-        update_committee_keys_form=update_committee_keys_form,
-        email_from_key=util.email_from_uid,
-        committee_is_standing=util.committee_is_standing,
+    page = htm.Block()
+    page.h1["Manage keys"]
+    page.p(".mb-4")[
+        htm.a(".btn.btn-sm.btn-secondary.me-3", 
href="#your-public-keys")["Your public keys"],
+        htm.a(".btn.btn-sm.btn-secondary", href="#your-committee-keys")["Your 
committee's keys"],
+    ]
+
+    page.h2("#your-public-keys")["Your public keys"]
+    page.p["Review your public keys used for signing release artifacts."]
+    page.div(".d-flex.gap-3.mb-4")[
+        htm.a(".btn.btn-outline-primary", href=util.as_url(add))["Add your 
OpenPGP key"],
+        htm.a(".btn.btn-outline-primary", href=util.as_url(ssh_add))["Add your 
SSH key"],
+    ]
+
+    _openpgp_keys(page, list(user_keys))
+    _ssh_keys(page, list(user_ssh_keys))
+    _committee_keys(page, list(user_committees_with_keys))
+
+    return await template.blank(
+        "Manage keys",
+        content=page.collect(),
+        description="Review your keys.",
     )
 
 
@@ -124,3 +124,136 @@ async def ssh_add(session: web.Committer) -> 
web.WerkzeugResponse | str:
 async def upload(session: web.Committer) -> str:
     """Upload a KEYS file containing multiple OpenPGP keys."""
     return await shared.keys.upload(session)
+
+
+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.name):
+            
page.h3(f"#committee-{committee.name}.mt-3")[committee.display_name or 
committee.name]
+
+            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-hover.table-sm")[thead, 
tbody.collect()]
+                ]
+                page.p(".text-muted")[
+                    "The ",
+                    htm.code["KEYS"],
+                    " file is automatically generated when you add or remove a 
key,"
+                    " but you can also use the form below to manually 
regenerate it.",
+                ]
+
+                form.render_block(
+                    page,
+                    model_cls=shared.keys.UpdateCommitteeKeysForm,
+                    action=util.as_url(post.keys.keys),
+                    form_classes=".mb-4.d-inline-block",
+                    submit_label="Regenerate KEYS file",
+                    submit_classes="btn btn-sm btn-outline-secondary",
+                    defaults={"committee_name": committee.name},
+                    empty=True,
+                )
+            else:
+                page.p(".mb-4")["No keys uploaded for this committee yet."]
+
+
+def _openpgp_keys(page: htm.Block, user_keys: list[sql.PublicSigningKey]) -> 
None:
+    page.h3["Your OpenPGP keys"]
+    if user_keys:
+        thead = htm.thead[
+            htm.tr[
+                htm.th(".px-2", scope="col")["Key ID"],
+                htm.th(".px-2", scope="col")["Committees"],
+                htm.th(".px-2", scope="col")["Action"],
+            ]
+        ]
+
+        tbody = htm.Block(htm.tbody)
+        for key in user_keys:
+            row = htm.Block(htm.tr, classes=".page-user-openpgp-key")
+            row.td(".text-break.px-2.align-middle")[
+                htm.a(href=util.as_url(details, 
fingerprint=key.fingerprint))[key.fingerprint[-16:].upper()]
+            ]
+            if key.committees:
+                committee_names = ", ".join([c.name for c in key.committees])
+                row.td(".text-break.px-2.align-middle")[committee_names]
+            else:
+                row.td(".text-break.px-2.align-middle")["No PMCs associated"]
+            with row.block(htm.td, classes=".px-2") as td:
+                form.render_block(
+                    td,
+                    model_cls=shared.keys.DeleteOpenPGPKeyForm,
+                    action=util.as_url(post.keys.keys),
+                    form_classes=".m-0",
+                    submit_label="Delete key",
+                    submit_classes="btn btn-sm btn-danger",
+                    defaults={"fingerprint": key.fingerprint},
+                    empty=True,
+                )
+            tbody.append(row.collect())
+
+        page.div(".table-responsive.mb-5")[
+            
htm.table(".table.border.table-striped.table-hover.table-sm")[thead, 
tbody.collect()]
+        ]
+    else:
+        page.p[htm.strong["You haven't added any personal OpenPGP keys yet."]]
+
+
+def _ssh_keys(page: htm.Block, user_ssh_keys: list[sql.SSHKey]) -> None:
+    page.h3["Your SSH keys"]
+    if user_ssh_keys:
+        grid = htm.Block(htm.div, classes=".d-grid.gap-4")
+        for key in user_ssh_keys:
+            card_block = htm.Block(htm.div, 
classes=f"#ssh-key-{key.fingerprint}.card.p-3.border")
+
+            key_type = key.key.split()[0] if key.key else ""
+            tbody = htm.tbody[
+                htm.tr[
+                    htm.th(".p-2.text-dark")["Fingerprint"],
+                    htm.td(".text-break")[key.fingerprint],
+                ],
+                htm.tr[
+                    htm.th(".p-2.text-dark")["Type"],
+                    htm.td(".text-break")[key_type],
+                ],
+            ]
+            card_block.table(".mb-0")[tbody]
+            card_block.details(".mt-3.p-3.bg-light.rounded")[
+                htm.summary(".fw-bold")["View whole key"],
+                htm.pre(".mt-3")[key.key],
+            ]
+
+            form.render_block(
+                card_block,
+                model_cls=shared.keys.DeleteSSHKeyForm,
+                action=util.as_url(post.keys.keys),
+                form_classes=".mt-3",
+                submit_label="Delete key",
+                submit_classes="btn btn-danger",
+                defaults={"fingerprint": key.fingerprint},
+                empty=True,
+            )
+            grid.append(card_block.collect())
+
+        page.div(".mb-5.p-4.bg-light.rounded")[grid.collect()]
+    else:
+        page.p[htm.strong["You haven't added any SSH keys yet."]]
diff --git a/atr/htm.py b/atr/htm.py
index 240ccaf..3810099 100644
--- a/atr/htm.py
+++ b/atr/htm.py
@@ -242,9 +242,24 @@ class Block:
         self.__check_parent("table", {"body", "div"})
         return BlockElementCallable(self, table)
 
+    @property
+    def td(self) -> BlockElementCallable:
+        self.__check_parent("td", {"tr"})
+        return BlockElementCallable(self, td)
+
     def text(self, text: str) -> None:
         self.elements.append(text)
 
+    @property
+    def th(self) -> BlockElementCallable:
+        self.__check_parent("th", {"tr"})
+        return BlockElementCallable(self, th)
+
+    @property
+    def thead(self) -> BlockElementCallable:
+        self.__check_parent("thead", {"table"})
+        return BlockElementCallable(self, thead)
+
     @property
     def title(self) -> BlockElementCallable:
         self.__check_parent("title", {"head", "html"})
diff --git a/atr/post/keys.py b/atr/post/keys.py
index 36054cb..28b221f 100644
--- a/atr/post/keys.py
+++ b/atr/post/keys.py
@@ -73,36 +73,69 @@ async def add(session: web.Committer, add_openpgp_key_form: 
shared.keys.AddOpenP
     return await session.redirect(get.keys.keys)
 
 
[email protected]("/keys/delete")
-async def delete(session: web.Committer) -> web.WerkzeugResponse:
-    """Delete a public signing key or SSH key from the user's account."""
-    form = await shared.keys.DeleteKeyForm.create_form(data=await 
quart.request.form)
[email protected]("/keys")
[email protected](shared.keys.KeysForm)
+async def keys(session: web.Committer, keys_form: shared.keys.KeysForm) -> 
web.WerkzeugResponse:
+    """Handle forms on the keys management page."""
+    match keys_form:
+        case shared.keys.DeleteOpenPGPKeyForm() as delete_openpgp_form:
+            return await _delete_openpgp_key(session, delete_openpgp_form)
 
-    if not await form.validate_on_submit():
-        return await session.redirect(get.keys.keys, error="Invalid request 
for key deletion.")
+        case shared.keys.DeleteSSHKeyForm() as delete_ssh_form:
+            return await _delete_ssh_key(session, delete_ssh_form)
 
-    fingerprint = (await quart.request.form).get("fingerprint")
-    if not fingerprint:
-        return await session.redirect(get.keys.keys, error="Missing key 
fingerprint for deletion.")
+        case shared.keys.UpdateCommitteeKeysForm() as update_committee_form:
+            return await _update_committee_keys(session, update_committee_form)
+
+
+async def _delete_openpgp_key(
+    session: web.Committer, delete_form: shared.keys.DeleteOpenPGPKeyForm
+) -> web.WerkzeugResponse:
+    """Delete an OpenPGP key from the user's account."""
+    fingerprint = delete_form.fingerprint
 
-    # Try to delete an SSH key first
-    # Otherwise, delete an OpenPGP key
-    # TODO: Unmerge this, or identify the key type
     async with storage.write() as write:
         wafc = write.as_foundation_committer()
-        try:
-            await wafc.ssh.delete_key(fingerprint)
-        except storage.AccessError:
-            pass
-        else:
-            return await session.redirect(get.keys.keys, success="SSH key 
deleted successfully")
         oc: outcome.Outcome[sql.PublicSigningKey] = await 
wafc.keys.delete_key(fingerprint)
 
     match oc:
         case outcome.Result():
-            return await session.redirect(get.keys.keys, success="Key deleted 
successfully")
+            return await session.redirect(get.keys.keys, success="OpenPGP key 
deleted successfully")
         case outcome.Error(error):
-            return await session.redirect(get.keys.keys, error=f"Error 
deleting key: {error}")
+            return await session.redirect(get.keys.keys, error=f"Error 
deleting OpenPGP key: {error}")
+
+
+async def _delete_ssh_key(session: web.Committer, delete_form: 
shared.keys.DeleteSSHKeyForm) -> web.WerkzeugResponse:
+    """Delete an SSH key from the user's account."""
+    fingerprint = delete_form.fingerprint
+
+    async with storage.write() as write:
+        wafc = write.as_foundation_committer()
+        try:
+            await wafc.ssh.delete_key(fingerprint)
+        except storage.AccessError as e:
+            return await session.redirect(get.keys.keys, error=f"Error 
deleting SSH key: {e}")
+
+    return await session.redirect(get.keys.keys, success="SSH key deleted 
successfully")
+
+
+async def _update_committee_keys(
+    session: web.Committer, update_form: shared.keys.UpdateCommitteeKeysForm
+) -> web.WerkzeugResponse:
+    """Regenerate the KEYS file for a committee."""
+    committee_name = update_form.committee_name
+
+    async with storage.write() as write:
+        wacm = write.as_committee_member(committee_name)
+        match await wacm.keys.autogenerate_keys_file():
+            case outcome.Result():
+                await quart.flash(
+                    f'Successfully regenerated the KEYS file for the 
"{committee_name}" committee.', "success"
+                )
+            case outcome.Error():
+                await quart.flash(f"Error regenerating the KEYS file for the 
{committee_name} committee.", "error")
+
+    return await session.redirect(get.keys.keys)
 
 
 @post.committer("/keys/details/<fingerprint>")
@@ -138,26 +171,6 @@ async def ssh_add(session: web.Committer) -> 
web.WerkzeugResponse | str:
     return await shared.keys.ssh_add(session)
 
 
[email protected]("/keys/update-committee-keys/<committee_name>")
-async def update_committee_keys(session: web.Committer, committee_name: str) 
-> web.WerkzeugResponse:
-    """Generate and save the KEYS file for a specific committee."""
-    form = await shared.keys.UpdateCommitteeKeysForm.create_form()
-    if not await form.validate_on_submit():
-        return await session.redirect(get.keys.keys, error="Invalid request to 
update KEYS file.")
-
-    async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
-        match await wacm.keys.autogenerate_keys_file():
-            case outcome.Result():
-                await quart.flash(
-                    f'Successfully regenerated the KEYS file for the 
"{committee_name}" committee.', "success"
-                )
-            case outcome.Error():
-                await quart.flash(f"Error regenerating the KEYS file for the 
{committee_name} committee.", "error")
-
-    return await session.redirect(get.keys.keys)
-
-
 @post.committer("/keys/upload")
 async def upload(session: web.Committer) -> str:
     """Upload a KEYS file containing multiple OpenPGP keys."""
diff --git a/atr/shared/keys.py b/atr/shared/keys.py
index 8da536f..4ec8484 100644
--- a/atr/shared/keys.py
+++ b/atr/shared/keys.py
@@ -20,6 +20,7 @@
 import asyncio
 import datetime
 from collections.abc import Awaitable, Callable, Sequence
+from typing import Annotated, Literal
 
 import aiohttp
 import asfquart.base as base
@@ -42,6 +43,10 @@ import atr.user as user
 import atr.util as util
 import atr.web as web
 
+type DELETE_OPENPGP_KEY = Literal["delete_openpgp_key"]
+type DELETE_SSH_KEY = Literal["delete_ssh_key"]
+type UPDATE_COMMITTEE_KEYS = Literal["update_committee_keys"]
+
 
 class AddOpenPGPKeyForm(form.Form):
     public_key: str = form.label(
@@ -74,12 +79,25 @@ class AddSSHKeyForm(forms.Typed):
     submit = forms.submit("Add SSH key")
 
 
-class DeleteKeyForm(forms.Typed):
-    submit = forms.submit("Delete key")
+class DeleteOpenPGPKeyForm(form.Form):
+    variant: DELETE_OPENPGP_KEY = form.value(DELETE_OPENPGP_KEY)
+    fingerprint: str = form.label("Fingerprint", widget=form.Widget.HIDDEN)
+
+
+class DeleteSSHKeyForm(form.Form):
+    variant: DELETE_SSH_KEY = form.value(DELETE_SSH_KEY)
+    fingerprint: str = form.label("Fingerprint", widget=form.Widget.HIDDEN)
+
+
+class UpdateCommitteeKeysForm(form.Empty):
+    variant: UPDATE_COMMITTEE_KEYS = form.value(UPDATE_COMMITTEE_KEYS)
+    committee_name: str = form.label("Committee name", 
widget=form.Widget.HIDDEN)
 
 
-class UpdateCommitteeKeysForm(forms.Typed):
-    submit = forms.submit("Regenerate KEYS file")
+type KeysForm = Annotated[
+    DeleteOpenPGPKeyForm | DeleteSSHKeyForm | UpdateCommitteeKeysForm,
+    form.DISCRIMINATOR,
+]
 
 
 class UpdateKeyCommitteesForm(forms.Typed):
diff --git a/atr/templates/committee-view.html 
b/atr/templates/committee-view.html
index f3a0d7f..275f9aa 100644
--- a/atr/templates/committee-view.html
+++ b/atr/templates/committee-view.html
@@ -77,9 +77,11 @@
             The <code>KEYS</code> file is automatically generated when you add 
or remove a key, but you can also use the form below to manually regenerate it.
           </p>
           <form method="post"
-                action="{{ as_url(post.keys.update_committee_keys, 
committee_name=committee.name) }}"
+                action="{{ as_url(post.keys.keys) }}"
                 class="mb-4 d-inline-block">
             {{ update_committee_keys_form.hidden_tag() }}
+            <input type="hidden" name="variant" value="update_committee_keys" 
/>
+            <input type="hidden" name="committee_name" value="{{ 
committee.name }}" />
 
             {{ update_committee_keys_form.submit(class_='btn btn-sm 
btn-outline-secondary') }}
           </form>
diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html
deleted file mode 100644
index ff5d441..0000000
--- a/atr/templates/keys-review.html
+++ /dev/null
@@ -1,161 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
-  Manage keys ~ ATR
-{% endblock title %}
-
-{% block description %}
-  Review your keys.
-{% endblock description %}
-
-{% block content %}
-  <h1>Manage keys</h1>
-
-  <p class="mb-4">
-    <a href="#your-public-keys" class="btn btn-sm btn-secondary me-3">Your 
public keys</a>
-    <a href="#your-committee-keys" class="btn btn-sm btn-secondary">Your 
committee's keys</a>
-  </p>
-
-  <h2 id="your-public-keys">Your public keys</h2>
-  <p>Review your public keys used for signing release artifacts.</p>
-
-  <div class="d-flex gap-3 mb-4">
-    <a href="{{ as_url(get.keys.add) }}" class="btn btn-outline-primary">Add 
your OpenPGP key</a>
-    <a href="{{ as_url(get.keys.ssh_add) }}" class="btn 
btn-outline-primary">Add your SSH key</a>
-  </div>
-
-  <h3>Your OpenPGP keys</h3>
-
-  {% if user_keys %}
-    <div class="table-responsive mb-5">
-      <table class="table border table-striped table-hover table-sm">
-        <thead>
-          <tr>
-            <th class="px-2" scope="col">Key ID</th>
-            <th class="px-2" scope="col">Committees</th>
-            <th class="px-2" scope="col">Action</th>
-          </tr>
-        </thead>
-        <tbody>
-          {% for key in user_keys %}
-            <tr class="page-user-openpgp-key">
-              <td class="text-break px-2 align-middle">
-                <a href="{{ as_url(get.keys.details, 
fingerprint=key.fingerprint) }}">{{ key.fingerprint[-16:]|upper }}</a>
-              </td>
-              <td class="text-break px-2 align-middle">
-                {% if key.committees %}
-                  {{ key.committees|map(attribute='name') |join(', ') }}
-                {% else %}
-                  No PMCs associated
-                {% endif %}
-              </td>
-              <td class="px-2">
-                <form method="post"
-                      action="{{ as_url(post.keys.delete) }}"
-                      class="m-0"
-                      onsubmit="return confirm('Are you sure you want to 
delete this OpenPGP key?');">
-                  {{ delete_form.hidden_tag() }}
-                  <input type="hidden" name="fingerprint" value="{{ 
key.fingerprint }}" />
-                  {{ delete_form.submit(class_='btn btn-sm btn-danger', 
value='Delete key') }}
-                </form>
-              </td>
-            </tr>
-          {% endfor %}
-        </tbody>
-      </table>
-    </div>
-  {% else %}
-    <p>
-      <strong>You haven't added any personal OpenPGP keys yet.</strong>
-    </p>
-  {% endif %}
-
-  <h3>Your SSH keys</h3>
-  {% if user_ssh_keys %}
-    <div class="mb-5 p-4 bg-light rounded">
-      <div class="d-grid gap-4">
-        {% for key in user_ssh_keys %}
-          <div id="ssh-key-{{ key.fingerprint }}" class="card p-3 border">
-            <table class="mb-0">
-              <tbody>
-                <tr>
-                  <th class="p-2 text-dark">Fingerprint</th>
-                  <td class="text-break">{{ key.fingerprint }}</td>
-                </tr>
-                <tr>
-                  <th class="p-2 text-dark">Type</th>
-                  <td class="text-break">{{ key.key.split()[0] }}</td>
-                </tr>
-              </tbody>
-            </table>
-
-            <details class="mt-3 p-3 bg-light rounded">
-              <summary class="fw-bold">View whole key</summary>
-              <pre class="mt-3">{{ key.key }}</pre>
-            </details>
-
-            <form method="post"
-                  action="{{ as_url(post.keys.delete) }}"
-                  class="mt-3"
-                  onsubmit="return confirm('Are you sure you want to delete 
this SSH key?');">
-              {{ delete_form.hidden_tag() }}
-
-              <input type="hidden" name="fingerprint" value="{{ 
key.fingerprint }}" />
-              {{ delete_form.submit(class_='btn btn-danger', value='Delete 
key') }}
-            </form>
-          </div>
-        {% endfor %}
-      </div>
-    </div>
-  {% else %}
-    <p>
-      <strong>You haven't added any SSH keys yet.</strong>
-    </p>
-  {% endif %}
-
-  <h2 id="your-committee-keys">Your committee's keys</h2>
-  <div class="mb-4">
-    <a href="{{ as_url(get.keys.upload) }}" class="btn 
btn-outline-primary">Upload a KEYS file</a>
-  </div>
-  {% for committee in committees %}
-    {% if not committee_is_standing(committee.name) %}
-      <h3 id="committee-{{ committee.name|slugify }}" class="mt-3">{{ 
committee.display_name }}</h3>
-      {% if committee.public_signing_keys %}
-        <div class="table-responsive mb-2">
-          <table class="table border table-striped table-hover table-sm">
-            <thead>
-              <tr>
-                <th class="px-2" scope="col">Key ID</th>
-                <th class="px-2" scope="col">Email</th>
-                <th class="px-2" scope="col">Apache UID</th>
-              </tr>
-            </thead>
-            <tbody>
-              {% for key in committee.public_signing_keys %}
-                <tr>
-                  <td class="text-break font-monospace px-2">
-                    <a href="{{ as_url(get.keys.details, 
fingerprint=key.fingerprint) }}">{{ key.fingerprint[-16:]|upper }}</a>
-                  </td>
-                  <td class="text-break px-2">{{ 
email_from_key(key.primary_declared_uid) or 'Not specified' }}</td>
-                  <td class="text-break px-2">{{ key.apache_uid or "-" }}</td>
-                </tr>
-              {% endfor %}
-            </tbody>
-          </table>
-        </div>
-        <p class="text-muted">
-          The <code>KEYS</code> file is automatically generated when you add 
or remove a key, but you can also use the form below to manually regenerate it.
-        </p>
-        <form method="post"
-              action="{{ as_url(post.keys.update_committee_keys, 
committee_name=committee.name) }}"
-              class="mb-4 d-inline-block">
-          {{ update_committee_keys_form.hidden_tag() }}
-
-          {{ update_committee_keys_form.submit(class_='btn btn-sm 
btn-outline-secondary') }}
-        </form>
-      {% else %}
-        <p class="mb-4">No keys uploaded for this committee yet.</p>
-      {% endif %}
-    {% endif %}
-  {% endfor %}
-{% endblock content %}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to