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]