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 bdf626b Move the vote resolution form from its own page onto the vote
page
bdf626b is described below
commit bdf626bf9575119c1ff9bb896b41f806395bf4f7
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon May 5 15:12:12 2025 +0100
Move the vote resolution form from its own page onto the vote page
---
atr/routes/compose.py | 3 +
atr/routes/resolve.py | 45 +-------
atr/templates/candidate-resolve-release.html | 154 ---------------------------
atr/templates/check-selected.html | 35 +++++-
atr/templates/phase-view.html | 5 +-
atr/templates/release-select.html | 3 +-
playwright/test.py | 6 +-
7 files changed, 48 insertions(+), 203 deletions(-)
diff --git a/atr/routes/compose.py b/atr/routes/compose.py
index 5673f5e..11820f6 100644
--- a/atr/routes/compose.py
+++ b/atr/routes/compose.py
@@ -28,6 +28,7 @@ import atr.db.models as models
import atr.revision as revision
import atr.routes as routes
import atr.routes.draft as draft
+import atr.routes.resolve as resolve
import atr.util as util
if TYPE_CHECKING:
@@ -96,6 +97,7 @@ async def check(
delete_draft_form = await draft.DeleteForm.create_form()
delete_file_form = await draft.DeleteFileForm.create_form()
+ resolve_form = await resolve.ResolveForm.create_form()
return await quart.render_template(
"check-selected.html",
@@ -123,6 +125,7 @@ async def check(
models=models,
task_mid=task_mid,
form=form,
+ resolve_form=resolve_form,
)
diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index 748adcc..da2b35e 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -29,6 +29,7 @@ import atr.db.models as models
import atr.routes as routes
import atr.routes.compose as compose
import atr.routes.finish as finish
+import atr.routes.vote as vote
import atr.util as util
@@ -62,44 +63,6 @@ def release_latest_vote_task(release: models.Release) ->
models.Task | None:
return None
[email protected]("/resolve/<project_name>/<version_name>",
measure_performance=False)
-async def selected(session: routes.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
- """Resolve the vote on a release candidate."""
- await session.check_access(project_name)
-
- release = await session.release(
- project_name,
- version_name,
- phase=models.ReleasePhase.RELEASE_CANDIDATE,
- with_committee=True,
- with_tasks=True,
- )
-
- form = await ResolveForm.create_form()
-
- latest_vote_task = release_latest_vote_task(release)
- task_mid = None
- archive_url = None
- if latest_vote_task is not None:
- task_mid = task_mid_get(latest_vote_task)
- archive_url = await _task_archive_url_cached(task_mid)
-
- if ("LOCAL_DEBUG" in os.environ) and (latest_vote_task is not None):
- latest_vote_task.status = models.TaskStatus.COMPLETED
- latest_vote_task.result = [json.dumps({"mid":
"[email protected]"})]
-
- return await quart.render_template(
- "candidate-resolve-release.html",
- release=release,
- format_artifact_name=_format_artifact_name,
- form=form,
- format_datetime=util.format_datetime,
- vote_task=latest_vote_task,
- task_mid=task_mid,
- archive_url=archive_url,
- )
-
-
@routes.committer("/resolve/<project_name>/<version_name>", methods=["POST"],
measure_performance=False)
async def selected_post(
session: routes.CommitterSession, project_name: str, version_name: str
@@ -112,14 +75,14 @@ async def selected_post(
for _field, errors in form.errors.items():
for error in errors:
await quart.flash(f"{error}", "error")
- return await session.redirect(selected, project_name=project_name,
version_name=version_name)
+ return await session.redirect(vote.selected,
project_name=project_name, version_name=version_name)
candidate_name = form.candidate_name.data
vote_result = form.vote_result.data
if not candidate_name:
return await session.redirect(
- selected, error="Missing candidate name",
project_name=project_name, version_name=version_name
+ vote.selected, error="Missing candidate name",
project_name=project_name, version_name=version_name
)
# Extract project name
@@ -127,7 +90,7 @@ async def selected_post(
project_name, version_name = candidate_name.rsplit("-", 1)
except ValueError:
return await session.redirect(
- selected, error="Invalid candidate name format",
project_name=project_name, version_name=version_name
+ vote.selected, error="Invalid candidate name format",
project_name=project_name, version_name=version_name
)
# Check that the user has access to the project
diff --git a/atr/templates/candidate-resolve-release.html
b/atr/templates/candidate-resolve-release.html
deleted file mode 100644
index a0a4f6e..0000000
--- a/atr/templates/candidate-resolve-release.html
+++ /dev/null
@@ -1,154 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
- Resolve vote for {{ release.short_display_name }} ~ ATR
-{% endblock title %}
-
-{% block description %}
- Resolve the vote for the {{ release.project.display_name }} {{
release.version }} release candidate.
-{% endblock description %}
-
-{% block content %}
- <p class="d-flex justify-content-between align-items-center">
- <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="atr-back-link">← Back to Vote on {{
release.project.short_display_name }} {{ release.version }}</a>
- <span>
- <span class="atr-phase-symbol-other">①</span>
- <span class="atr-phase-arrow">→</span>
- <strong class="atr-phase-two atr-phase-symbol">②</strong>
- <span class="atr-phase-two atr-phase-label">VOTE</span>
- <span class="atr-phase-arrow">→</span>
- <span class="atr-phase-symbol-other">③</span>
- </span>
- </p>
-
- <h1>
- Resolve vote for <strong>{{ release.project.short_display_name }}</strong>
<em>{{ release.version }}</em>
- </h1>
- <p>
- Resolve the vote for the <strong>release candidate</strong> for {{
release.project.display_name }} {{ release.version }}. If you resolve a vote as
passed, the release candidate will be promoted to the next stage, making it a
release preview. If you resolve a vote as failed, the release candidate will be
returned to the draft stage.
- </p>
-
- <div class="card mb-4">
- <div class="card-header d-flex justify-content-between align-items-center">
- <h5 class="mb-0">Release information</h5>
- </div>
- <div class="card-body pb-0 mb-1">
- <div class="row">
- <div class="col-md-6">
- <p>
- <strong>Project:</strong>
- <a href="{{ as_url(routes.projects.view,
name=release.project.name) }}">{{ release.project.display_name }}</a>
- </p>
- <p>
- <strong>Label:</strong> {{ release.name }}
- </p>
- </div>
- <div class="col-md-6">
- <p>
- <strong>Created:</strong> {{ format_datetime(release.created) }}
- </p>
- {% if release.vote_started %}
- <p>
- <strong>Vote Started:</strong> {{
format_datetime(release.vote_started) }}
- </p>
- {% endif %}
- </div>
- </div>
- </div>
- <div class="card-body">
- <a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-primary me-2"><i class="bi bi-download me-1"></i>
Download files</a>
- <a href="{{ as_url(routes.candidate.view,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-secondary me-2"><i class="bi bi-eye me-1"></i> View
files</a>
- <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-success"><i class="bi bi-check-circle me-1"></i> Vote
on release</a>
- </div>
- </div>
-
- <div class="card mb-4">
- <div class="card-header">
- <h5 class="mb-0">Vote email details</h5>
- </div>
- <div class="card-body">
- <div class="p-3 border rounded bg-white mb-3">
- {% if vote_task %}
- {% if vote_task.status.value == "completed" %}
- <p class="mb-0 text-success fw-semibold">
- <i class="bi bi-check-circle me-1"></i> Vote email sent: {{
format_datetime(vote_task.completed) }}
- </p>
- {% if task_mid %}
- <p class="mt-2 mb-0 text-muted ps-4">
- Message-ID: <code class="user-select-all">{{ task_mid }}</code>
- </p>
- {% endif %}
- {% elif vote_task.status.value == "failed" %}
- <p class="mb-1 text-danger fw-semibold">
- <i class="bi bi-x-circle me-1"></i> Vote email failed: {{
format_datetime(vote_task.completed) }}
- </p>
- <div class="alert alert-danger mt-2 mb-0 p-2" role="alert">
- <p class="mb-0 p-2 text-danger">{{ vote_task.error }}</p>
- </div>
- {% else %}
- <p class="mb-0 text-warning fw-semibold">
- <i class="bi bi-hourglass-split me-1"></i> Vote email task
status: {{ vote_task.status.value.upper() }}
- {% if vote_task.started %}
- (Started: {{ format_datetime(vote_task.started) }})
- {% else %}
- (Added: {{ format_datetime(vote_task.added) }})
- {% endif %}
- </p>
- {% endif %}
- {% if archive_url %}
- <p class="mt-2 mb-0 text-muted ps-4">
- <a href="{{ archive_url }}"
- rel="noopener"
- target="_blank"
- title="View vote email thread in the archive (opens in new
window)">View vote email thread in the archive <i class="bi
bi-box-arrow-up-right ms-1"></i></a>
- </p>
- {% elif task_mid %}
- <p class="mt-2 mb-0 text-muted ps-4">Could not retrieve archive
URL for this message.</p>
- {% endif %}
- {% else %}
- <p class="mb-0 text-muted">
- <i class="bi bi-question-circle me-1"></i> Vote email: No vote
initiation task found for this release.
- </p>
- {% endif %}
- </div>
- </div>
- </div>
-
- <div class="card mb-4">
- <div class="card-header">
- <h5 class="mb-0">Resolve vote</h5>
- </div>
- <div class="card-body">
- <form method="post"
- action="{{ as_url(routes.resolve.selected_post,
project_name=release.project.name, version_name=release.version) }}"
- class="atr-canary py-4 px-5"
- novalidate>
- <input type="hidden" name="candidate_name" value="{{ release.name }}"
/>
- {{ form.csrf_token }}
-
- <div class="mb-3 pb-3 row border-bottom">
- <label class="col-sm-3 col-form-label text-sm-end fw-semibold">{{
form.vote_result.label.text }}:</label>
- <div class="col-sm-9 pt-2">
- {% for subfield in form.vote_result %}
- <div class="form-check form-check-inline">
- {{ subfield(class="form-check-input" + (" is-invalid" if
form.vote_result.errors else "") , id=subfield.id ~ "_" ~ loop.index) }}
- <label class="form-check-label" for="{{ subfield.id }}_{{
loop.index }}">{{ subfield.label.text }}</label>
- </div>
- {% endfor %}
- {% if form.vote_result.errors %}
- <div class="invalid-feedback d-block">{{
form.vote_result.errors[0] }}</div>
- {% endif %}
- </div>
- </div>
-
- <div class="row">
- <div class="col-sm-9 offset-sm-3">{{ form.submit(class_="btn
btn-primary mt-3") }}</div>
- </div>
- </form>
- </div>
- </div>
-{% endblock content %}
diff --git a/atr/templates/check-selected.html
b/atr/templates/check-selected.html
index 564286d..4ba6e9a 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -102,8 +102,7 @@
class="btn btn-primary me-2"><i class="bi bi-download me-1"></i>
Download files</a>
<a href="{{ as_url(routes.candidate.view,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-secondary me-2"><i class="bi bi-eye me-1"></i> View
files</a>
- <a href="{{ as_url(routes.resolve.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="btn btn-success"><i class="bi bi-clipboard-check me-1"></i>
Resolve vote</a>
+ <a href="#resolve-vote" class="btn btn-success"><i class="bi
bi-clipboard-check me-1"></i> Resolve vote</a>
</div>
{% endif %}
</div>
@@ -329,6 +328,38 @@
<div class="col-md-9 offset-md-3">{{ form.submit(class_="btn
btn-primary") }}</div>
</div>
</form>
+
+ <h2 id="resolve-vote">Resolve vote</h2>
+ <div class="border rounded bg-warning-subtle p-3 mb-3">
+ <i class="bi bi-info-circle me-1"></i>
+ <strong>NOTE:</strong> We are allowing a vote to be resolved early in
order to facilitate testing. This is not the final behaviour.
+ </div>
+ <form method="post"
+ action="{{ as_url(routes.resolve.selected_post,
project_name=release.project.name, version_name=release.version) }}"
+ class="atr-canary py-4 px-5"
+ novalidate>
+ <input type="hidden" name="candidate_name" value="{{ release.name }}" />
+ {{ resolve_form.csrf_token }}
+
+ <div class="mb-3 pb-3 row border-bottom">
+ <label class="col-sm-3 col-form-label text-sm-end fw-semibold">{{
resolve_form.vote_result.label.text }}:</label>
+ <div class="col-sm-9 pt-2">
+ {% for subfield in resolve_form.vote_result %}
+ <div class="form-check form-check-inline">
+ {{ subfield(class="form-check-input" + (" is-invalid" if
resolve_form.vote_result.errors else "") , id=subfield.id ~ "_" ~ loop.index) }}
+ <label class="form-check-label" for="{{ subfield.id }}_{{
loop.index }}">{{ subfield.label.text }}</label>
+ </div>
+ {% endfor %}
+ {% if resolve_form.vote_result.errors %}
+ <div class="invalid-feedback d-block">{{
resolve_form.vote_result.errors[0] }}</div>
+ {% endif %}
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-sm-9 offset-sm-3">{{ resolve_form.submit(class_="btn
btn-primary mt-3") }}</div>
+ </div>
+ </form>
{% endif %}
{% endblock content %}
diff --git a/atr/templates/phase-view.html b/atr/templates/phase-view.html
index c3d1121..e098fd0 100644
--- a/atr/templates/phase-view.html
+++ b/atr/templates/phase-view.html
@@ -10,6 +10,7 @@
{% block content %}
<p class="d-flex justify-content-between align-items-center">
+ {# TODO: Use mappings.py #}
{% if phase_key == "draft" %}
<a href="{{ as_url(routes.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-back-link">← Back to Compose {{ release.short_display_name
}}</a>
@@ -22,8 +23,8 @@
<span class="atr-phase-symbol-other">③</span>
</span>
{% elif phase_key == "candidate" %}
- <a href="{{ as_url(routes.resolve.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="atr-back-link">← Back to Resolve vote for {{
release.short_display_name }}</a>
+ <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
+ class="atr-back-link">← Back to Vote for {{
release.short_display_name }}</a>
<span>
<span class="atr-phase-symbol-other">①</span>
<span class="atr-phase-arrow">→</span>
diff --git a/atr/templates/release-select.html
b/atr/templates/release-select.html
index 0c7f8a5..9ec8b91 100644
--- a/atr/templates/release-select.html
+++ b/atr/templates/release-select.html
@@ -28,11 +28,12 @@
{% for release in releases %}
{% set phase = release.phase.value %}
{% set target_url = None %}
+ {# TODO: Use mappings.py #}
{% if phase == "release_candidate_draft" %}
{% set target_url = as_url(routes.compose.selected,
project_name=project.name, version_name=release.version) %}
{% set badge_class = "bg-primary" %}
{% elif phase == "release_candidate" %}
- {% set target_url = as_url(routes.resolve.selected,
project_name=project.name, version_name=release.version) %}
+ {% set target_url = as_url(routes.vote.selected,
project_name=project.name, version_name=release.version) %}
{% set badge_class = "bg-warning text-dark" %}
{% elif phase == "release_preview" %}
{% set target_url = as_url(routes.announce.selected,
project_name=project.name, version_name=release.version) %}
diff --git a/playwright/test.py b/playwright/test.py
index 3d0ae2d..6bccd3c 100644
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -182,9 +182,9 @@ def lifecycle_04_start_vote(page: sync_api.Page,
credentials: Credentials, versi
def lifecycle_05_resolve_vote(page: sync_api.Page, credentials: Credentials,
version_name: str) -> None:
- logging.info(f"Navigating to the candidate resolve page for
tooling-test-example {version_name}")
- go_to_path(page, f"/resolve/tooling-test-example/{version_name}")
- logging.info("Candidate resolve page loaded successfully")
+ logging.info(f"Navigating to the vote page for tooling-test-example
{version_name}")
+ go_to_path(page, f"/vote/tooling-test-example/{version_name}")
+ logging.info("Vote page loaded successfully")
logging.info(f"Locating the form to resolve the vote for
tooling-test-example {version_name}")
form_locator = page.locator(
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]