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]

Reply via email to