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]