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-release.git
The following commit(s) were added to refs/heads/main by this push:
new e0bad35 Allow multiple committees to be selected when uploading a
KEYS file
e0bad35 is described below
commit e0bad359b1dc74461f4c3f5a46ff1cbc7c690981
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Apr 24 15:38:34 2025 +0100
Allow multiple committees to be selected when uploading a KEYS file
---
atr/blueprints/admin/admin.py | 2 +-
atr/routes/keys.py | 30 ++++++----
atr/templates/keys-upload.html | 127 +++++++++++++++++++++++++++++++++--------
3 files changed, 124 insertions(+), 35 deletions(-)
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index 3ff7f2a..7daf663 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -558,7 +558,7 @@ async def _update_committees() -> tuple[int, int]: # noqa:
C901
# We add a special entry for Tooling, pretending to be a PMC, for
debugging and testing
tooling_committee = await data.committee(name="tooling").get()
if not tooling_committee:
- tooling_committee = models.Committee(name="tooling")
+ tooling_committee = models.Committee(name="tooling",
full_name="Tooling")
data.add(tooling_committee)
tooling_project = models.Project(
name="tooling", full_name="Apache Tooling",
committee=tooling_committee
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 725d8f0..6f51c44 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -357,7 +357,14 @@ async def upload(session: routes.CommitterSession) -> str:
class UploadKeyForm(util.QuartFormTyped):
key = wtforms.FileField("KEYS file")
submit = wtforms.SubmitField("Upload KEYS file")
- selected_committee = wtforms.SelectField("PMCs", choices=[(c.name,
c.name) for c in user_committees])
+ selected_committees = wtforms.SelectMultipleField(
+ "Associate keys with committees",
+ choices=[(c.name, c.display_name) for c in user_committees],
+ coerce=str,
+ option_widget=widgets.CheckboxInput(),
+ widget=widgets.ListWidget(prefix_label=False),
+ validators=[wtforms.validators.InputRequired("You must select at
least one committee")],
+ )
form = await UploadKeyForm.create_form()
results: list[dict] = []
@@ -386,17 +393,20 @@ async def upload(session: routes.CommitterSession) -> str:
if not key_blocks:
return await render(error="No valid GPG keys found in the uploaded
file")
- # Get selected committee from the form
- selected_committee = form.selected_committee.data
- if not selected_committee:
+ # Get selected committee list from the form
+ selected_committees = form.selected_committees.data
+ if not selected_committees:
return await render(error="You must select at least one committee")
- # Ensure that the selected committee is one of which the user is
actually a member
- if selected_committee not in (session.committees + session.projects):
- return await render(error=f"You are not a member of
{selected_committee}")
+ # Ensure that the selected committees are ones of which the user is
actually a member
+ invalid_committees = [
+ committee for committee in selected_committees if (committee not
in (session.committees + session.projects))
+ ]
+ if invalid_committees:
+ return await render(error=f"Invalid committee selection: {',
'.join(invalid_committees)}")
# Process each key block
- results = await _upload_process_key_blocks(key_blocks,
selected_committee)
+ results = await _upload_process_key_blocks(key_blocks,
selected_committees)
if not results:
return await render(error="No keys were added")
@@ -476,14 +486,14 @@ async def _upload_key_blocks(key_file:
datastructures.FileStorage) -> list[str]:
return key_blocks
-async def _upload_process_key_blocks(key_blocks: list[str],
selected_committee: str) -> list[dict]:
+async def _upload_process_key_blocks(key_blocks: list[str],
selected_committees: list[str]) -> list[dict]:
"""Process GPG key blocks and add them to the user's account."""
results: list[dict] = []
# Process each key block
for i, key_block in enumerate(key_blocks):
try:
- key_info = await key_user_add(None, key_block,
[selected_committee])
+ key_info = await key_user_add(None, key_block, selected_committees)
if key_info:
key_info["status"] = "success"
key_info["message"] = "Key added successfully"
diff --git a/atr/templates/keys-upload.html b/atr/templates/keys-upload.html
index 3bdd370..1a10a47 100644
--- a/atr/templates/keys-upload.html
+++ b/atr/templates/keys-upload.html
@@ -77,35 +77,114 @@
{% endfor %}
{% endif %}
- <form method="post" class="atr-canary" enctype="multipart/form-data">
- {{ form.csrf_token }}
+ <form method="post"
+ class="atr-canary py-4 px-5"
+ enctype="multipart/form-data">
+ {# {{ form.csrf_token }} #}
+ {{ form.hidden_tag() if form.hidden_tag }}
<div class="mb-4">
- <div class="mb-3">
- <label for="key" class="form-label">KEYS file:</label>
+ <div class="row mb-3 pb-3 border-bottom">
+ <div class="col-md-2 text-md-end fw-medium pt-2">{{
form.key.label(class="form-label") }}</div>
+ <div class="col-md-9">
+ {{ form.key(class="form-control", aria_describedby="keys-help") }}
+ <small id="keys-help" class="form-text text-muted mt-2">
+ Upload a KEYS file containing multiple PGP public keys. The file
should contain keys in ASCII-armored format, starting with "-----BEGIN PGP
PUBLIC KEY BLOCK-----".
+ </small>
+ {% if form.key.errors %}
+ <div class="invalid-feedback d-block">
+ {% for error in form.key.errors %}{{ error }}{% endfor %}
+ </div>
+ {% endif %}
+ </div>
</div>
- {{ form.key(class="form-control mb-2", aria_describedby="keys-help") }}
- <small id="keys-help" class="form-text text-muted">
- Upload a KEYS file containing multiple PGP public keys. The file
should contain keys in ASCII-armored format, starting with "-----BEGIN PGP
PUBLIC KEY BLOCK-----".
- </small>
- </div>
- {% if user_committees %}
- <div class="mb-4">
- <div class="mb-3">
- <label for="selected_committee" class="form-label">Associate with
project:</label>
+ {% if user_committees %}
+ <div class="row mb-3 pb-3 border-bottom">
+ <div class="col-md-2 text-md-end fw-medium pt-2">Associate keys with
committee</div>
+ <div class="col-md-9">
+ <div class="row">
+ {% for value, label in form.selected_committees.choices %}
+ <div class="col-sm-12 col-md-6 col-lg-4">
+ <div class="form-check mb-2">
+ <input type="checkbox"
+ name="selected_committees"
+ value="{{ value }}"
+ id="committee_{{ loop.index }}"
+ class="form-check-input" />
+ <label for="committee_{{ loop.index }}"
class="form-check-label">{{ label }}</label>
+ </div>
+ </div>
+ {% else %}
+ <p class="text-muted fst-italic">No committees available for
association.</p>
+ {% endfor %}
+ </div>
+ <div class="mt-2 mb-2">
+ <button type="button"
+ id="toggleCommitteesBtn"
+ class="btn btn-sm btn-outline-secondary">Select
all</button>
+ </div>
+ <small class="form-text text-muted mt-2">
+ Select the committee with which to associate these keys. You
must be a member of the selected committee.
+ </small>
+ {% if form.selected_committees.errors %}
+ <div class="invalid-feedback d-block">
+ {% for error in form.selected_committees.errors %}{{ error
}}{% endfor %}
+ </div>
+ {% endif %}
+ </div>
</div>
- {{ form.selected_committee(class="form-select",
aria_describedby="committees-help") }}
- <small id="committees-help" class="form-text text-muted mt-2">
- Select the committee with which to associate these keys. You must be
a member of the selected committee.
- </small>
- </div>
- {% else %}
- <div class="text-danger mt-1">
- <p>You must be a member of at least one committee to add signing
keys.</p>
- </div>
- {% endif %}
+ {% else %}
+ <div class="row mb-3 pb-3 border-bottom">
+ <div class="col-md-9 offset-md-2">
+ <p class="text-danger">You must be a member of at least one
committee to add signing keys.</p>
+ </div>
+ </div>
+ {% endif %}
+ </div>
- {{ form.submit(class="btn btn-primary") }}
+ <div class="mt-4 col-md-9 offset-md-2">
+ {{ form.submit(class="btn btn-primary") }}
+ <a href="{{ as_url(routes.keys.keys) }}"
+ class="btn btn-link text-secondary">Cancel</a>
+ </div>
</form>
{% endblock content %}
+
+{% block javascripts %}
+ {{ super() }}
+ <script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const btn = document.getElementById("toggleCommitteesBtn");
+ const checkboxes =
document.querySelectorAll("input[name='selected_committees']");
+
+ if (!btn || checkboxes.length === 0) return;
+
+ function updateButtonText() {
+ let allChecked = true;
+ checkboxes.forEach(cb => {
+ if (!cb.checked) allChecked = false;
+ });
+ btn.textContent = allChecked ? "Select none" : "Select all";
+ }
+
+ btn.addEventListener("click", () => {
+ let allChecked = true;
+ checkboxes.forEach(cb => {
+ if (!cb.checked) allChecked = false;
+ });
+ const shouldCheck = !allChecked;
+ checkboxes.forEach(cb => {
+ cb.checked = shouldCheck;
+ });
+ updateButtonText();
+ });
+
+ checkboxes.forEach(cb => {
+ cb.addEventListener("change", updateButtonText);
+ });
+
+ updateButtonText();
+ });
+ </script>
+{% endblock javascripts %}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]