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

tn 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 4a0fe57  cleanup drafts, rename modify route to directory, support 
deleting of candidate files, support multiple file uploads, restructure sidebar
4a0fe57 is described below

commit 4a0fe5793bd1db358905156ef4dd4293806e1b5d
Author: Thomas Neidhart <[email protected]>
AuthorDate: Mon Mar 31 23:23:14 2025 +0200

    cleanup drafts, rename modify route to directory, support deleting of 
candidate files, support multiple file uploads, restructure sidebar
---
 atr/routes/draft.py                                | 120 +++++++++++++--------
 atr/static/js/atr.js                               |   4 +
 ...draft-add-project.html => draft-add-files.html} |  14 +--
 .../{draft-modify.html => draft-directory.html}    |  61 ++---------
 atr/templates/draft-review.html                    |   9 +-
 atr/templates/includes/sidebar.html                |  18 ++--
 atr/templates/index.html                           |  95 ++++++++--------
 atr/templates/layouts/base.html                    |   1 +
 atr/templates/macros/dialog.html                   |  42 ++++++++
 9 files changed, 206 insertions(+), 158 deletions(-)

diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 3394256..19f44e1 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -25,14 +25,12 @@ import hashlib
 import logging
 import pathlib
 import re
-from typing import Protocol, TypeVar
+from typing import TYPE_CHECKING, Protocol, TypeVar
 
 import aiofiles.os
 import aioshutil
 import asfquart.base as base
 import quart
-import werkzeug.datastructures as datastructures
-import werkzeug.wrappers.response as response
 import wtforms
 
 import atr.analysis as analysis
@@ -42,6 +40,12 @@ import atr.routes as routes
 import atr.tasks as tasks
 import atr.util as util
 
+if TYPE_CHECKING:
+    from collections.abc import Sequence
+
+    import werkzeug.datastructures as datastructures
+    import werkzeug.wrappers.response as response
+
 # _CONFIG: Final = config.get()
 # _LOGGER: Final = logging.getLogger(__name__)
 
@@ -72,6 +76,13 @@ class DeleteForm(util.QuartFormTyped):
     submit = wtforms.SubmitField("Delete candidate draft")
 
 
+class DeleteFileForm(util.QuartFormTyped):
+    """Form for deleting a file."""
+
+    file_path = wtforms.StringField("File path", 
validators=[wtforms.validators.InputRequired("File path is required")])
+    submit = wtforms.SubmitField("Delete file")
+
+
 async def _number_of_release_files(release: models.Release) -> int:
     """Return the number of files in the release."""
     path_project = release.project.name
@@ -193,7 +204,7 @@ async def add(session: routes.CommitterSession) -> 
response.Response | str:
             # TODO: Show the form with errors
             return await session.redirect(add, error="Invalid form data")
         await _add(session, form)
-        return await session.redirect(add, success="Release candidate created 
successfully")
+        return await session.redirect(directory, success="Release candidate 
created successfully")
 
     return await quart.render_template(
         "draft-add.html",
@@ -208,38 +219,40 @@ async def add(session: routes.CommitterSession) -> 
response.Response | str:
 
 
 @routes.committer("/draft/add/<project_name>/<version_name>", methods=["GET", 
"POST"])
-async def add_project(
-    session: routes.CommitterSession, project_name: str, version_name: str
-) -> response.Response | str:
+async def add_file(session: routes.CommitterSession, project_name: str, 
version_name: str) -> response.Response | str:
     """Show a page to allow the user to add a single file to a candidate 
draft."""
 
-    class AddProjectForm(util.QuartFormTyped):
-        """Form for adding a single file to a release candidate."""
+    class AddFilesForm(util.QuartFormTyped):
+        """Form for adding file(s) to a release candidate."""
 
-        file_path = wtforms.StringField("File path (optional)", 
validators=[wtforms.validators.Optional()])
-        file_data = wtforms.FileField("File", 
validators=[wtforms.validators.InputRequired("File is required")])
-        submit = wtforms.SubmitField("Add file")
+        file_name = wtforms.StringField("File name (optional)", 
validators=[wtforms.validators.Optional()])
+        file_data = wtforms.MultipleFileField(
+            "File", validators=[wtforms.validators.InputRequired("File(s) are 
required")]
+        )
+        submit = wtforms.SubmitField("Add file(s)")
 
-    form = await AddProjectForm.create_form()
+    form = await AddFilesForm.create_form()
     if await form.validate_on_submit():
         try:
-            file_path = None
-            if isinstance(form.file_path.data, str) and form.file_path.data:
-                file_path = pathlib.Path(form.file_path.data)
+            file_name = None
+            if isinstance(form.file_name.data, str) and form.file_name.data:
+                file_name = pathlib.Path(form.file_name.data)
             file_data = form.file_data.data
-            if not isinstance(file_data, datastructures.FileStorage):
+            if not file_data or len(file_data) == 0:
                 raise routes.FlashError("Invalid file upload")
+            if file_name is not None and len(file_data) > 1:
+                raise routes.FlashError("File name can only be used when 
uploading a single file")
 
-            await _add_one(project_name, version_name, file_path, file_data)
+            await _upload_files(project_name, version_name, file_name, 
file_data)
             return await session.redirect(
-                review, success="File added successfully", 
project_name=project_name, version_name=version_name
+                review, success="File(s) added successfully", 
project_name=project_name, version_name=version_name
             )
         except Exception as e:
-            logging.exception("Error adding file:")
-            await quart.flash(f"Error adding file: {e!s}", "error")
+            logging.exception("Error adding file(s):")
+            await quart.flash(f"Error adding file(s): {e!s}", "error")
 
     return await quart.render_template(
-        "draft-add-project.html",
+        "draft-add-files.html",
         asf_id=session.uid,
         server_domain=session.host,
         project_name=project_name,
@@ -252,12 +265,11 @@ async def add_project(
 async def delete(session: routes.CommitterSession) -> response.Response:
     """Delete a candidate draft and all its associated files."""
     form = await DeleteForm.create_form(data=await quart.request.form)
-
     if not await form.validate_on_submit():
         for _field, errors in form.errors.items():
             for error in errors:
                 await quart.flash(f"{error}", "error")
-        return await session.redirect(promote)
+        return await session.redirect(directory)
 
     candidate_draft_name = form.candidate_draft_name.data
     if not candidate_draft_name:
@@ -289,14 +301,16 @@ async def delete(session: routes.CommitterSession) -> 
response.Response:
         # TODO: Confirm that this is a bug, and report upstream
         await aioshutil.rmtree(draft_dir)  # type: ignore[call-arg]
 
-    return await session.redirect(promote, success="Candidate draft deleted 
successfully")
+    return await session.redirect(directory, success="Candidate draft deleted 
successfully")
 
 
[email protected]("/draft/delete-file/<project_name>/<version_name>/<path:file_path>",
 methods=["POST"])
-async def delete_file(
-    session: routes.CommitterSession, project_name: str, version_name: str, 
file_path: str
-) -> response.Response:
[email protected]("/draft/delete-file/<project_name>/<version_name>", 
methods=["POST"])
+async def delete_file(session: routes.CommitterSession, project_name: str, 
version_name: str) -> response.Response:
     """Delete a specific file from the release candidate."""
+    form = await DeleteFileForm.create_form(data=await quart.request.form)
+    if not await form.validate_on_submit():
+        return await session.redirect(review, project_name=project_name, 
version_name=version_name)
+
     # Check that the user has access to the project
     if not any((p.name == project_name) for p in (await 
session.user_projects)):
         raise base.ASFQuartException("You do not have access to this project", 
errorcode=403)
@@ -307,6 +321,7 @@ async def delete_file(
             base.ASFQuartException("Release does not exist", errorcode=404)
         )
 
+        file_path = str(form.file_path.data)
         full_path = str(util.get_release_candidate_draft_dir() / project_name 
/ version_name / file_path)
 
         # Check that the file exists
@@ -378,11 +393,12 @@ async def hashgen(
     )
 
 
[email protected]("/draft/modify")
-async def modify(session: routes.CommitterSession) -> str:
-    """Allow the user to modify a candidate draft."""
[email protected]("/drafts")
+async def directory(session: routes.CommitterSession) -> str:
+    """Allow the user to view current candidate drafts."""
     # Do them outside of the template rendering call to ensure order
     # The user_candidate_drafts call can use cached results from user_projects
+    # TODO: admin users should be able to view and manipulate all candidates 
if needed
     user_projects = await session.user_projects
     user_candidate_drafts = await session.user_candidate_drafts
 
@@ -390,7 +406,7 @@ async def modify(session: routes.CommitterSession) -> str:
     delete_form = await DeleteForm.create_form()
 
     return await quart.render_template(
-        "draft-modify.html",
+        "draft-directory.html",
         asf_id=session.uid,
         projects=user_projects,
         server_domain=session.host,
@@ -538,6 +554,8 @@ async def review(session: routes.CommitterSession, 
project_name: str, version_na
             release_name=f"{project_name}-{version_name}", path=str(path), 
status=models.CheckResultStatus.FAILURE
         ).all()
 
+    delete_file_form = await DeleteFileForm.create_form()
+
     return await quart.render_template(
         "draft-review.html",
         asf_id=session.uid,
@@ -555,6 +573,7 @@ async def review(session: routes.CommitterSession, 
project_name: str, version_na
         errors=path_errors,
         modified=path_modified,
         models=models,
+        delete_file_form=delete_file_form,
     )
 
 
@@ -736,27 +755,36 @@ async def _add(session: routes.CommitterSession, form: 
AddProtocol) -> None:
             data.add(release)
 
 
-async def _add_one(
+async def _upload_files(
     project_name: str,
     version_name: str,
-    file_path: pathlib.Path | None,
-    file: datastructures.FileStorage,
+    file_name: pathlib.Path | None,
+    files: Sequence[datastructures.FileStorage],
 ) -> None:
-    """Process and save the uploaded file."""
-    # TODO: Rename to upload or file-upload
+    """Process and save the uploaded files."""
     # Create target directory
     target_dir = util.get_release_candidate_draft_dir() / project_name / 
version_name
     target_dir.mkdir(parents=True, exist_ok=True)
 
-    # Use the original filename if no path is specified
-    if not file_path:
-        if not file.filename:
-            raise routes.FlashError("No filename provided")
-        file_path = pathlib.Path(file.filename)
+    def get_filepath(file: datastructures.FileStorage) -> pathlib.Path:
+        # Use the original filename if no path is specified
+        if not file_name:
+            if not file.filename:
+                raise routes.FlashError("No filename provided")
+            return pathlib.Path(file.filename)
+        else:
+            return file_name
+
+    for file in files:
+        # Save file to specified path
+        file_path = get_filepath(file)
+        target_path = target_dir / file_path.relative_to(file_path.anchor)
+        target_path.parent.mkdir(parents=True, exist_ok=True)
+
+        await _save_file(file, target_path)
+
 
-    # Save file to specified path
-    target_path = target_dir / file_path.relative_to(file_path.anchor)
-    target_path.parent.mkdir(parents=True, exist_ok=True)
+async def _save_file(file: datastructures.FileStorage, target_path: 
pathlib.Path) -> None:
     async with aiofiles.open(target_path, "wb") as f:
         while chunk := await asyncio.to_thread(file.stream.read, 8192):
             await f.write(chunk)
diff --git a/atr/static/js/atr.js b/atr/static/js/atr.js
new file mode 100644
index 0000000..5582be2
--- /dev/null
+++ b/atr/static/js/atr.js
@@ -0,0 +1,4 @@
+function updateDeleteButton(inputElement, buttonId) {
+  let button = document.getElementById(buttonId);
+  button.disabled = inputElement.value !== "DELETE";
+}
diff --git a/atr/templates/draft-add-project.html 
b/atr/templates/draft-add-files.html
similarity index 82%
rename from atr/templates/draft-add-project.html
rename to atr/templates/draft-add-files.html
index 76c74d1..33a6ff2 100644
--- a/atr/templates/draft-add-project.html
+++ b/atr/templates/draft-add-files.html
@@ -1,11 +1,11 @@
 {% extends "layouts/base.html" %}
 
 {% block title %}
-  Add file to {{ project_name }} {{ version_name }} ~ ATR
+  Add file(s) to {{ project_name }} {{ version_name }} ~ ATR
 {% endblock title %}
 
 {% block description %}
-  Add a single file to a release candidate.
+  Add file(s) to a release candidate.
 {% endblock description %}
 
 {% block content %}
@@ -28,11 +28,11 @@
         class="striking py-4 px-5">
     {{ form.csrf_token }}
     <div class="mb-3 pb-3 row border-bottom">
-      <label for="{{ form.file_path.id }}"
-             class="col-sm-3 col-form-label text-sm-end">{{ 
form.file_path.label.text }}:</label>
+      <label for="{{ form.file_name.id }}"
+             class="col-sm-3 col-form-label text-sm-end">{{ 
form.file_name.label.text }}:</label>
       <div class="col-sm-8">
-        {{ form.file_path(class_="form-control") }}
-        {% if form.file_path.errors -%}<span class="error-message">{{ 
form.file_path.errors[0] }}</span>{%- endif %}
+        {{ form.file_name(class_="form-control") }}
+        {% if form.file_name.errors -%}<span class="error-message">{{ 
form.file_name.errors[0] }}</span>{%- endif %}
           <span id="file_path-help" class="form-text text-muted">Enter the 
path where the file should be saved in the release candidate</span>
         </div>
       </div>
@@ -43,7 +43,7 @@
         <div class="col-sm-8">
           {{ form.file_data(class_="form-control") }}
           {% if form.file_data.errors -%}<span class="error-message">{{ 
form.file_data.errors[0] }}</span>{%- endif %}
-            <span id="file_data-help" class="form-text text-muted">Select the 
file to upload</span>
+            <span id="file_data-help" class="form-text text-muted">Select the 
file(s) to upload</span>
           </div>
         </div>
 
diff --git a/atr/templates/draft-modify.html 
b/atr/templates/draft-directory.html
similarity index 65%
rename from atr/templates/draft-modify.html
rename to atr/templates/draft-directory.html
index 2eb5d31..6a32c75 100644
--- a/atr/templates/draft-modify.html
+++ b/atr/templates/draft-directory.html
@@ -1,15 +1,17 @@
 {% extends "layouts/base.html" %}
 
 {% block title %}
-  Modify candidate draft ~ ATR
+  Candidate draft directory ~ ATR
 {% endblock title %}
 
 {% block description %}
-  Modify candidate drafts using rsync.
+  Review and modify candidate drafts.
 {% endblock description %}
 
+{% import 'macros/dialog.html' as dialog %}
+
 {% block content %}
-  <h1>Modify a candidate draft</h1>
+  <h1>Current candidate drafts</h1>
   <p class="intro">
     A <strong>candidate draft</strong> is an editable set of files which can 
be <strong>frozen and promoted into a candidate release</strong> for voting on 
by the PMC.
   </p>
@@ -21,7 +23,7 @@
 
   <div class="row row-cols-1 row-cols-md-2 g-4 mb-5">
     {% for release in candidate_drafts %}
-      {% set release_id = release.name.replace('.', '_') %}
+      {% set release_id = release.name %}
       <div class="col" id="{{ release.name }}">
         <div class="card h-100">
           <div class="card-body position-relative">
@@ -37,7 +39,7 @@
                  class="btn btn-sm btn-outline-primary">Review</a>
               <a href="{{ as_url(routes.draft.viewer, 
project_name=release.project.name, version_name=release.version) }}"
                  class="btn btn-sm btn-outline-primary">View files</a>
-              <a href="{{ as_url(routes.draft.add_project, 
project_name=release.project.name, version_name=release.version) }}"
+              <a href="{{ as_url(routes.draft.add_file, 
project_name=release.project.name, version_name=release.version) }}"
                  class="btn btn-sm btn-outline-primary">Upload file</a>
               <button class="btn btn-sm btn-outline-danger"
                       data-bs-toggle="modal"
@@ -63,49 +65,7 @@
         </div>
       </div>
 
-      <div class="modal modal-lg fade"
-           id="delete-{{ release_id }}"
-           data-bs-backdrop="static"
-           data-bs-keyboard="false"
-           tabindex="-1"
-           aria-labelledby="delete-{{ release_id }}-label"
-           aria-hidden="true">
-        <div class="modal-dialog border-primary">
-          <div class="modal-content">
-            <div class="modal-header bg-danger bg-opacity-10 text-danger">
-              <h1 class="modal-title fs-5" id="delete-{{ release_id 
}}-label">Delete candidate draft</h1>
-              <button type="button"
-                      class="btn-close"
-                      data-bs-dismiss="modal"
-                      aria-label="Close"></button>
-            </div>
-            <div class="modal-body">
-              <p class="text-muted mb-3">Warning: This action will permanently 
delete this candidate draft and cannot be undone.</p>
-              <form method="post" action="{{ as_url(routes.draft.delete) }}">
-                {{ delete_form.hidden_tag() }}
-                <input type="hidden" name="candidate_draft_name" value="{{ 
release.name }}" />
-                <div class="mb-3">
-                  <label for="confirm_delete_{{ release_id }}" 
class="form-label">
-                    Type <strong>DELETE</strong> to confirm:
-                  </label>
-                  <input class="form-control mt-2"
-                         id="confirm_delete_{{ release_id }}"
-                         name="confirm_delete"
-                         placeholder="DELETE"
-                         required=""
-                         type="text"
-                         value=""
-                         onkeyup="updateDeleteButton(this, 'delete-button-{{ 
release_id }}')" />
-                </div>
-                <button type="submit"
-                        id="delete-button-{{ release_id }}"
-                        disabled
-                        class="btn btn-danger">Delete candidate draft</button>
-              </form>
-            </div>
-          </div>
-        </div>
-      </div>
+      {{ dialog.delete_modal_with_confirm(release_id, "Delete candidate 
draft", "candidate draft", as_url(routes.draft.delete) , delete_form, 
"candidate_draft_name") }}
     {% endfor %}
     {% if candidate_drafts|length == 0 %}
       <div class="col-12">
@@ -151,10 +111,5 @@
               });
           });
       });
-
-      function updateDeleteButton(inputElement, buttonId) {
-          let button = document.getElementById(buttonId);
-          button.disabled = inputElement.value !== "DELETE";
-      }
   </script>
 {% endblock javascripts %}
diff --git a/atr/templates/draft-review.html b/atr/templates/draft-review.html
index 36b9bd8..85181be 100644
--- a/atr/templates/draft-review.html
+++ b/atr/templates/draft-review.html
@@ -8,6 +8,8 @@
   Review the files for the {{ project_name }} {{ version_name }} candidate 
draft.
 {% endblock description %}
 
+{% import 'macros/dialog.html' as dialog %}
+
 {% block content %}
   <h1>Review of {{ release.project.display_name }} {{ version_name }}</h1>
   <p class="intro">
@@ -96,8 +98,13 @@
                        class="btn btn-sm btn-outline-primary fs-6 small 
ms-2">Tools</a>
                     <a href="{{ as_url(routes.draft.review_path, 
project_name=project_name, version_name=version_name, file_path=path) }}"
                        class="btn btn-sm btn-outline-primary fs-6 small 
ms-2">Review file</a>
+                    <button class="btn btn-sm btn-outline-danger"
+                            data-bs-toggle="modal"
+                            data-bs-target="#delete-{{ path }}">Delete 
file</button>
                   </td>
                 </tr>
+                {% set file_id = path|string %}
+                {{ dialog.delete_modal_with_confirm(file_id, "Delete file", 
"file", as_url(routes.draft.delete_file, project_name=project_name, 
version_name=version_name) , delete_file_form, "file_path") }}
               {% endfor %}
             </tbody>
           </table>
@@ -148,7 +155,7 @@
     </div>
     <div class="card-body">
       <p>
-        <a href="{{ as_url(routes.draft.add_project, 
project_name=release.project.name, version_name=release.version) }}">Upload a 
file in the browser</a>, or use the command below to add or modify files in 
this release using rsync:
+        <a href="{{ as_url(routes.draft.add_file, 
project_name=release.project.name, version_name=release.version) }}">Upload a 
file in the browser</a>, or use the command below to add or modify files in 
this release using rsync:
       </p>
     </div>
     <pre class="card-footer bg-light border-1 pt-4 small">
diff --git a/atr/templates/includes/sidebar.html 
b/atr/templates/includes/sidebar.html
index 925ba38..4a828b3 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -27,14 +27,23 @@
     {% endif %}
   </div>
   <nav>
+    <h3>Home</h3>
+    <ul>
+      <li>
+        <i class="fa-solid fa-house"></i>
+        <a href="{{ as_url(routes.root.index) }}"
+           {% if request.endpoint == 'root' %}class="active"{% endif 
%}>About</a>
+      </li>
+    </ul>
+
     {% if current_user %}
       <h3>Release candidate drafts</h3>
       <ul>
         <li>
-          <a href="{{ as_url(routes.draft.add) }}">Add draft</a>
+          <a href="{{ as_url(routes.draft.directory) }}">Review drafts</a>
         </li>
         <li>
-          <a href="{{ as_url(routes.draft.modify) }}">Modify draft</a>
+          <a href="{{ as_url(routes.draft.add) }}">Add draft</a>
         </li>
         <!-- TODO: Don't show this if the user doesn't have any release 
candidates? -->
         <li>
@@ -88,11 +97,6 @@
 
       <h3>Organisation</h3>
       <ul>
-        <li>
-          <i class="fa-solid fa-house"></i>
-          <a href="{{ as_url(routes.root.index) }}"
-             {% if request.endpoint == 'root' %}class="active"{% endif 
%}>About</a>
-        </li>
         <li>
           <i class="fa-solid fa-diagram-project"></i>
           <a href="{{ as_url(routes.committees.directory) }}">Committees</a>
diff --git a/atr/templates/index.html b/atr/templates/index.html
index a357848..68f96e7 100644
--- a/atr/templates/index.html
+++ b/atr/templates/index.html
@@ -13,64 +13,71 @@
     verify, and track release candidates.
   </p>
 
-  <h2>Quick tutorial</h2>
-  <p>
-    This is a preview of an early version of ATR, and we would like testers to 
try it out and give us feedback. This section provides a quick tutorial for 
using ATR. The basic workflow on ATR is Release Candidate Draft -> Release 
Candidate -> Release Preview -> Release. Note that, as the header says on every 
page, this is a preview and you cannot yet create actual releases with ATR.
-  </p>
+  {% if current_user %}
+    <h2>Quick tutorial</h2>
+    <p>
+      This is a preview of an early version of ATR, and we would like testers 
to try it out and give us feedback. This section provides a quick tutorial for 
using ATR. The basic workflow on ATR is Release Candidate Draft -> Release 
Candidate -> Release Preview -> Release. Note that, as the header says on every 
page, this is a preview and you cannot yet create actual releases with ATR.
+    </p>
 
-  <h3>Release candidate draft</h3>
+    <h3>Release candidate draft</h3>
 
-  <p>
-    We recommend that you start by <a href="{{ as_url(routes.keys.ssh_add) 
}}">uploading your SSH key</a>. This gives you rsync access which makes it 
easier to upload your files. We plan to obtain your SSH key from your ASF 
account via LDAP in the long run.
-  </p>
+    <p>
+      We recommend that you start by <a href="{{ as_url(routes.keys.ssh_add) 
}}">uploading your SSH key</a>. This gives you rsync access which makes it 
easier to upload your files. We plan to obtain your SSH key from your ASF 
account via LDAP in the long run.
+    </p>
 
-  <p>
-    Once you've uploaded your SSH key, you may be able to <a href="{{ 
as_url(routes.draft.add) }}">add a release candidate draft</a>. Only Project 
Management Committee (PMC) members can do this. Once a draft has been created, 
all PMC members and committers can add files to the draft or delete files from 
it.
-  </p>
+    <p>
+      Once you've uploaded your SSH key, you may be able to <a href="{{ 
as_url(routes.draft.add) }}">add a release candidate draft</a>. Only Project 
Management Committee (PMC) members can do this. Once a draft has been created, 
all PMC members and committers can add files to the draft or delete files from 
it.
+    </p>
 
-  <p>
-    When you add files, ATR automatically runs some checks on the files. 
You'll be able to browse the results of those checks. Note that our checks are 
currently very basic, and we'll be adding more checks as we get feedback from 
testers.
-  </p>
+    <p>
+      When you add files, ATR automatically runs some checks on the files. 
You'll be able to browse the results of those checks. Note that our checks are 
currently very basic, and we'll be adding more checks as we get feedback from 
testers.
+    </p>
 
-  <p>
-    When you're happy with the files in the draft, you can <a href="{{ 
as_url(routes.draft.promote) }}">promote the draft</a> to a release candidate.
-  </p>
+    <p>
+      When you're happy with the files in the draft, you can <a href="{{ 
as_url(routes.draft.promote) }}">promote the draft</a> to a release candidate.
+    </p>
 
-  <h3>Release candidate</h3>
+    <h3>Release candidate</h3>
 
-  <p>
-    When you've promoted the draft to a release candidate, you can use ATR to 
<a href="{{ as_url(routes.candidate.vote) }}">start a vote on the release 
candidate</a>. Currently we allow any PMC member to start the vote, but in the 
future we may limit this to designated release managers. The ATR is designed to 
send the vote email itself, but we understand that projects send very detailed 
vote announcement emails. We plan to make it easier for you to send such 
announcement emails. We also  [...]
-  </p>
+    <p>
+      When you've promoted the draft to a release candidate, you can use ATR 
to <a href="{{ as_url(routes.candidate.vote) }}">start a vote on the release 
candidate</a>. Currently we allow any PMC member to start the vote, but in the 
future we may limit this to designated release managers. The ATR is designed to 
send the vote email itself, but we understand that projects send very detailed 
vote announcement emails. We plan to make it easier for you to send such 
announcement emails. We als [...]
+    </p>
 
-  <p>
-    The vote email is not actually sent out, because ATR cannot yet be used to 
create releases. We are only testing the workflow.
-  </p>
+    <p>
+      The vote email is not actually sent out, because ATR cannot yet be used 
to create releases. We are only testing the workflow.
+    </p>
 
-  <p>
-    When you're happy with the release candidate, you can <a href="{{ 
as_url(routes.candidate.resolve) }}">record the vote resolution</a> to promote 
the release candidate to a release preview.
-  </p>
+    <p>
+      When you're happy with the release candidate, you can <a href="{{ 
as_url(routes.candidate.resolve) }}">record the vote resolution</a> to promote 
the release candidate to a release preview.
+    </p>
 
-  <h3>Release preview</h3>
+    <h3>Release preview</h3>
 
-  <p>
-    When you've promoted the release candidate to a release preview, you can 
review the files. We plan to make it possible to adjust the release preview 
before it's promoted to a release.
-  </p>
+    <p>
+      When you've promoted the release candidate to a release preview, you can 
review the files. We plan to make it possible to adjust the release preview 
before it's promoted to a release.
+    </p>
 
-  <p>
-    When you're happy with the release preview, you can <a href="{{ 
as_url(routes.preview.promote) }}">promote the release preview</a> to a 
release. This, again, should be an action limited to designated release 
managers.
-  </p>
+    <p>
+      When you're happy with the release preview, you can <a href="{{ 
as_url(routes.preview.promote) }}">promote the release preview</a> to a 
release. This, again, should be an action limited to designated release 
managers.
+    </p>
 
-  <h3>Release</h3>
+    <h3>Release</h3>
 
-  <p>
-    When you've promoted the release preview to a release, you can <a href="{{ 
as_url(routes.release.review) }}">browse the release</a>.
-  </p>
+    <p>
+      When you've promoted the release preview to a release, you can <a 
href="{{ as_url(routes.release.review) }}">browse the release</a>.
+    </p>
+
+    <h2>Key features</h2>
+    <ul>
+      <li>Support for rsync or HTML form based file uploads</li>
+      <li>Automatic checks of release artifacts</li>
+      <li>Templated email vote announcements</li>
+    </ul>
+  {% else %}
 
-  <h2>Key features</h2>
-  <ul>
-    <li>Support for rsync or HTML form based file uploads</li>
-    <li>Automatic checks of release artifacts</li>
-    <li>Templated email vote announcements</li>
-  </ul>
+    <div class="alert alert-primary d-flex align-items-center" role="alert">
+      <div>You need to login with your ASF account in order to use this 
platform.</div>
+    </div>
+  {% endif %}
 
 {% endblock content %}
diff --git a/atr/templates/layouts/base.html b/atr/templates/layouts/base.html
index b77c432..265e7ee 100644
--- a/atr/templates/layouts/base.html
+++ b/atr/templates/layouts/base.html
@@ -54,6 +54,7 @@
 
     {% block javascripts %}
       <script src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') 
}}"></script>
+      <script src="{{ url_for('static', filename='js/atr.js') }}"></script>
     {% endblock javascripts %}
   </body>
 </html>
diff --git a/atr/templates/macros/dialog.html b/atr/templates/macros/dialog.html
new file mode 100644
index 0000000..07b9b92
--- /dev/null
+++ b/atr/templates/macros/dialog.html
@@ -0,0 +1,42 @@
+{% macro delete_modal_with_confirm(id, title, item, action, form, field_name) 
%}
+  <div class="modal modal-lg fade"
+       id="delete-{{ id }}"
+       data-bs-backdrop="static"
+       data-bs-keyboard="false"
+       tabindex="-1"
+       aria-labelledby="delete-{{ id }}-label"
+       aria-hidden="true">
+    <div class="modal-dialog border-primary">
+      <div class="modal-content">
+        <div class="modal-header bg-danger bg-opacity-10 text-danger">
+          <h1 class="modal-title fs-5" id="delete-{{ id }}-label">{{ title 
}}</h1>
+          <button type="button"
+                  class="btn-close"
+                  data-bs-dismiss="modal"
+                  aria-label="Close"></button>
+        </div>
+        <div class="modal-body">
+          <p class="text-muted mb-3">Warning: This action will permanently 
delete this {{ item }} and cannot be undone.</p>
+          <form method="post" action="{{ action }}">
+            {{ form.hidden_tag() }}
+            {{ form[field_name](value_=id, hidden=True) }}
+            <div class="mb-3">
+              <label for="confirm_delete_{{ id }}" class="form-label">
+                Type <strong>DELETE</strong> to confirm:
+              </label>
+              <input class="form-control mt-2"
+                     id="confirm_delete_{{ id }}"
+                     name="confirm_delete"
+                     placeholder="DELETE"
+                     required=""
+                     type="text"
+                     value=""
+                     onkeyup="updateDeleteButton(this, 'delete-button-{{ id 
}}')" />
+            </div>
+            {{ form.submit(class_="btn btn-danger", id_="delete-button-" + id, 
disabled=True) }}
+          </form>
+        </div>
+      </div>
+    </div>
+  </div>
+{% endmacro %}


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

Reply via email to