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-releases.git
The following commit(s) were added to refs/heads/main by this push:
new 8f643e0 Move compose, distribution, and vote routes
8f643e0 is described below
commit 8f643e0d9d78c53f0f39c832341b1a80525147c5
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Oct 27 15:57:00 2025 +0000
Move compose, distribution, and vote routes
---
atr/get/__init__.py | 5 +-
atr/get/compose.py | 43 ++++
atr/get/distribution.py | 128 ++++++++++++
atr/{routes => get}/vote.py | 82 +-------
atr/post/__init__.py | 4 +-
atr/post/distribution.py | 95 +++++++++
atr/post/vote.py | 85 ++++++++
atr/routes/__init__.py | 6 -
atr/routes/compose.py | 162 ---------------
atr/routes/draft.py | 2 +-
atr/routes/keys.py | 2 +-
atr/routes/mapping.py | 13 +-
atr/routes/resolve.py | 4 +-
atr/routes/start.py | 2 +-
atr/routes/upload.py | 2 +-
atr/routes/voting.py | 4 +-
atr/shared/__init__.py | 133 +++++++++++-
atr/{routes => shared}/distribution.py | 237 ++++------------------
atr/{post/__init__.py => shared/vote.py} | 13 +-
atr/templates/check-selected-candidate-forms.html | 2 +-
atr/templates/check-selected.html | 2 +-
atr/templates/draft-tools.html | 2 +-
atr/templates/finish-selected.html | 2 +-
atr/templates/phase-view.html | 4 +-
atr/templates/release-select.html | 4 +-
atr/templates/report-selected-path.html | 4 +-
atr/templates/resolve-manual.html | 2 +-
atr/templates/resolve-tabulated.html | 2 +-
atr/templates/revisions-selected.html | 2 +-
atr/templates/upload-selected.html | 2 +-
atr/templates/voting-selected-revision.html | 4 +-
31 files changed, 575 insertions(+), 479 deletions(-)
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index cb099e2..729f0db 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -20,9 +20,12 @@ from typing import Final, Literal
import atr.get.announce as announce
import atr.get.candidate as candidate
import atr.get.committees as committees
+import atr.get.compose as compose
+import atr.get.distribution as distribution
+import atr.get.vote as vote
from .example_test import respond as example_test
ROUTES_MODULE: Final[Literal[True]] = True
-__all__ = ["announce", "candidate", "committees", "example_test"]
+__all__ = ["announce", "candidate", "committees", "compose", "distribution",
"example_test", "vote"]
diff --git a/atr/get/compose.py b/atr/get/compose.py
new file mode 100644
index 0000000..7c51a69
--- /dev/null
+++ b/atr/get/compose.py
@@ -0,0 +1,43 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import asfquart.base as base
+import werkzeug.wrappers.response as response
+
+import atr.blueprints.get as get
+import atr.db as db
+import atr.models.sql as sql
+import atr.routes.mapping as mapping
+import atr.shared as shared
+import atr.web as web
+
+
[email protected]("/compose/<project_name>/<version_name>")
+async def selected(session: web.Committer, project_name: str, version_name:
str) -> response.Response | str:
+ """Show the contents of the release candidate draft."""
+ await session.check_access(project_name)
+
+ async with db.session() as data:
+ release = await data.release(
+ project_name=project_name,
+ version=version_name,
+ _committee=True,
+ _project_release_policy=True,
+ ).demand(base.ASFQuartException("Release does not exist",
errorcode=404))
+ if release.phase != sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
+ return await mapping.release_as_redirect(session, release)
+ return await shared.check(session, release)
diff --git a/atr/get/distribution.py b/atr/get/distribution.py
new file mode 100644
index 0000000..80738a2
--- /dev/null
+++ b/atr/get/distribution.py
@@ -0,0 +1,128 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+import htpy
+
+import atr.blueprints.get as get
+import atr.db as db
+import atr.forms as forms
+import atr.htm as htm
+import atr.models.sql as sql
+import atr.post as post
+import atr.shared as shared
+import atr.template as template
+import atr.util as util
+import atr.web as web
+
+
[email protected]("/distributions/list/<project>/<version>")
+async def list_get(session: web.Committer, project: str, version: str) -> str:
+ async with db.session() as data:
+ distributions = await data.distribution(
+ release_name=sql.release_name(project, version),
+ ).all()
+
+ block = htm.Block()
+
+ release = await shared.distribution.release_validated(project, version,
staging=None)
+ staging = release.phase == sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
+ shared.distribution.html_nav_phase(block, project, version, staging)
+
+ record_a_distribution = htpy.a(
+ ".btn.btn-primary",
+ href=util.as_url(
+ stage if staging else record,
+ project=project,
+ version=version,
+ ),
+ )["Record a distribution"]
+
+ # Distribution list for project-version
+ block.h1["Distribution list for ", htpy.em[f"{project}-{version}"]]
+ if not distributions:
+ block.p["No distributions found."]
+ block.p[record_a_distribution]
+ return await template.blank(
+ "Distribution list",
+ content=block.collect(),
+ )
+ block.p["Here are all of the distributions recorded for this release."]
+ block.p[record_a_distribution]
+ # Table of contents
+ ul_toc = htm.Block(htpy.ul)
+ for dist in distributions:
+ a = htpy.a(href=f"#distribution-{dist.identifier}")[dist.title]
+ ul_toc.li[a]
+ block.append(ul_toc)
+
+ ## Distributions
+ block.h2["Distributions"]
+ for dist in distributions:
+ delete_form = await shared.distribution.DeleteForm.create_form(
+ data={
+ "release_name": dist.release_name,
+ "platform": dist.platform.name,
+ "owner_namespace": dist.owner_namespace,
+ "package": dist.package,
+ "version": dist.version,
+ }
+ )
+
+ ### Platform package version
+ block.h3(
+ # Cannot use "#id" here, because the ID contains "."
+ # If an ID contains ".", htpy parses that as a class
+ id=f"distribution-{dist.identifier}"
+ )[dist.title]
+ tbody = htpy.tbody[
+ shared.distribution.html_tr("Release name", dist.release_name),
+ shared.distribution.html_tr("Platform", dist.platform.value.name),
+ shared.distribution.html_tr("Owner or Namespace",
dist.owner_namespace or "-"),
+ shared.distribution.html_tr("Package", dist.package),
+ shared.distribution.html_tr("Version", dist.version),
+ shared.distribution.html_tr("Staging", "Yes" if dist.staging else
"No"),
+ shared.distribution.html_tr("Upload date", str(dist.upload_date)),
+ shared.distribution.html_tr_a("API URL", dist.api_url),
+ shared.distribution.html_tr_a("Web URL", dist.web_url),
+ ]
+ block.table(".table.table-striped.table-bordered")[tbody]
+ form_action = util.as_url(post.distribution.delete, project=project,
version=version)
+ delete_form_element = forms.render_simple(
+ delete_form,
+ action=form_action,
+ submit_classes="btn-danger",
+ )
+ block.append(htpy.div(".mb-3")[delete_form_element])
+
+ title = f"Distribution list for {project} {version}"
+ return await template.blank(title, content=block.collect())
+
+
[email protected]("/distribution/record/<project>/<version>")
+async def record(session: web.Committer, project: str, version: str) -> str:
+ form = await
shared.distribution.DistributeForm.create_form(data={"package": project,
"version": version})
+ fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
+ return await shared.distribution.record_form_page(fpv)
+
+
[email protected]("/distribution/stage/<project>/<version>")
+async def stage(session: web.Committer, project: str, version: str) -> str:
+ form = await
shared.distribution.DistributeForm.create_form(data={"package": project,
"version": version})
+ fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
+ return await shared.distribution.record_form_page(fpv, staging=True)
diff --git a/atr/routes/vote.py b/atr/get/vote.py
similarity index 59%
rename from atr/routes/vote.py
rename to atr/get/vote.py
index 29b9fd1..e5eeadc 100644
--- a/atr/routes/vote.py
+++ b/atr/get/vote.py
@@ -16,35 +16,25 @@
# under the License.
import asfquart.base as base
-import quart
import werkzeug.wrappers.response as response
+import atr.blueprints.get as get
import atr.db as db
import atr.db.interaction as interaction
import atr.forms as forms
import atr.log as log
import atr.models.results as results
import atr.models.sql as sql
-import atr.route as route
-import atr.routes.compose as compose
import atr.routes.mapping as mapping
+import atr.shared as shared
import atr.storage as storage
import atr.user as user
import atr.util as util
+import atr.web as web
-class CastVoteForm(forms.Typed):
- """Form for casting a vote."""
-
- vote_value = forms.radio("Your vote")
- vote_comment = forms.textarea("Comment (optional)", optional=True)
- submit = forms.submit("Submit vote")
-
-
[email protected]("/vote/<project_name>/<version_name>")
-async def selected(
- session: route.CommitterSession | None, project_name: str, version_name:
str
-) -> response.Response | str:
[email protected]("/vote/<project_name>/<version_name>")
+async def selected(session: web.Committer | None, project_name: str,
version_name: str) -> response.Response | str:
"""Show the contents of the release candidate draft."""
async with db.session() as data:
release = await data.release(
@@ -102,7 +92,7 @@ async def selected(
form = None
if can_vote:
- form = await CastVoteForm.create_form()
+ form = await shared.vote.CastVoteForm.create_form()
async with storage.write() as write:
try:
if release.committee.is_podling:
@@ -121,7 +111,7 @@ async def selected(
],
)
- return await compose.check(
+ return await shared.check(
session,
release,
task_mid=task_mid,
@@ -132,61 +122,3 @@ async def selected(
can_vote=can_vote,
can_resolve=can_resolve,
)
-
-
[email protected]("/vote/<project_name>/<version_name>", methods=["POST"])
-async def selected_post(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
- """Handle submission of a vote."""
- await session.check_access(project_name)
-
- # Ensure the release exists and is in the correct phase
- release = await session.release(project_name, version_name,
phase=sql.ReleasePhase.RELEASE_CANDIDATE)
-
- if release.committee is None:
- raise ValueError("Release has no committee")
-
- # Set up form choices
- async with storage.write() as write:
- try:
- if release.committee.is_podling:
- _wacm = write.as_committee_member("incubator")
- else:
- _wacm = write.as_committee_member(release.committee.name)
- potency = "Binding"
- except storage.AccessError:
- # Participant, due to session.check_access above
- potency = "Non-binding"
-
- form = await CastVoteForm.create_form(data=await quart.request.form)
- forms.choices(
- form.vote_value,
- choices=[
- ("+1", f"+1 ({potency})"),
- ("0", "0"),
- ("-1", f"-1 ({potency})"),
- ],
- )
-
- if await form.validate_on_submit():
- vote = str(form.vote_value.data)
- comment = str(form.vote_comment.data)
- async with
storage.write_as_committee_participant(release.committee.name) as wacm:
- email_recipient, error_message = await
wacm.vote.send_user_vote(release, vote, comment, session.fullname)
- if error_message:
- return await session.redirect(
- selected, project_name=project_name,
version_name=version_name, error=error_message
- )
-
- success_message = f"Sending your vote to {email_recipient}."
- return await session.redirect(
- selected, project_name=project_name, version_name=version_name,
success=success_message
- )
- else:
- error_message = "Invalid vote submission"
- if form.errors:
- error_details = "; ".join([f"{field}: {', '.join(errs)}" for
field, errs in form.errors.items()])
- error_message = f"{error_message}: {error_details}"
-
- return await session.redirect(
- selected, project_name=project_name, version_name=version_name,
error=error_message
- )
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index e9131ab..768113b 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -19,9 +19,11 @@ from typing import Final, Literal
import atr.post.announce as announce
import atr.post.candidate as candidate
+import atr.post.distribution as distribution
+import atr.post.vote as vote
from .example_test import respond as example_test
ROUTES_MODULE: Final[Literal[True]] = True
-__all__ = ["announce", "candidate", "example_test"]
+__all__ = ["announce", "candidate", "distribution", "example_test", "vote"]
diff --git a/atr/post/distribution.py b/atr/post/distribution.py
new file mode 100644
index 0000000..dbf1744
--- /dev/null
+++ b/atr/post/distribution.py
@@ -0,0 +1,95 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import quart
+
+import atr.blueprints.post as post
+import atr.db as db
+import atr.get as get
+import atr.models.distribution as distribution
+import atr.shared as shared
+import atr.storage as storage
+import atr.web as web
+
+if TYPE_CHECKING:
+ import werkzeug.wrappers.response as response
+
+
[email protected]("/distribution/delete/<project>/<version>")
+async def delete(session: web.Committer, project: str, version: str) ->
response.Response:
+ form = await shared.distribution.DeleteForm.create_form(data=await
quart.request.form)
+ dd = distribution.DeleteData.model_validate(form.data)
+
+ # Validate the submitted data, and obtain the committee for its name
+ async with db.session() as data:
+ release = await
data.release(name=dd.release_name).demand(RuntimeError(f"Release
{dd.release_name} not found"))
+ committee = release.committee
+ if committee is None:
+ raise RuntimeError(f"Release {dd.release_name} has no committee")
+
+ # Delete the distribution
+ async with
storage.write_as_committee_member(committee_name=committee.name) as wacm:
+ await wacm.distributions.delete_distribution(
+ release_name=dd.release_name,
+ platform=dd.platform,
+ owner_namespace=dd.owner_namespace,
+ package=dd.package,
+ version=dd.version,
+ )
+ return await session.redirect(
+ get.distribution.list_get,
+ project=project,
+ version=version,
+ success="Distribution deleted",
+ )
+
+
[email protected]("/distribution/record/<project>/<version>")
+async def record_post(session: web.Committer, project: str, version: str) ->
str:
+ form = await shared.distribution.DistributeForm.create_form(data=await
quart.request.form)
+ fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
+ if await form.validate():
+ return await shared.distribution.record_form_process_page(fpv)
+ match len(form.errors):
+ case 0:
+ # Should not happen
+ await quart.flash("Ambiguous submission errors",
category="warning")
+ case 1:
+ await quart.flash("There was 1 submission error", category="error")
+ case _ as n:
+ await quart.flash(f"There were {n} submission errors",
category="error")
+ return await shared.distribution.record_form_page(fpv)
+
+
[email protected]("/distribution/stage/<project>/<version>")
+async def stage_post(session: web.Committer, project: str, version: str) ->
str:
+ form = await shared.distribution.DistributeForm.create_form(data=await
quart.request.form)
+ fpv = shared.distribution.FormProjectVersion(form=form, project=project,
version=version)
+ if await form.validate():
+ return await shared.distribution.record_form_process_page(fpv,
staging=True)
+ match len(form.errors):
+ case 0:
+ await quart.flash("Ambiguous submission errors",
category="warning")
+ case 1:
+ await quart.flash("There was 1 submission error", category="error")
+ case _ as n:
+ await quart.flash(f"There were {n} submission errors",
category="error")
+ return await shared.distribution.record_form_page(fpv, staging=True)
diff --git a/atr/post/vote.py b/atr/post/vote.py
new file mode 100644
index 0000000..a03da6d
--- /dev/null
+++ b/atr/post/vote.py
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import quart
+import werkzeug.wrappers.response as response
+
+import atr.blueprints.post as post
+import atr.forms as forms
+import atr.get.vote as get_vote
+import atr.models.sql as sql
+import atr.shared.vote as shared_vote
+import atr.storage as storage
+import atr.web as web
+
+
[email protected]("/vote/<project_name>/<version_name>")
+async def selected_post(session: web.Committer, project_name: str,
version_name: str) -> response.Response:
+ """Handle submission of a vote."""
+ await session.check_access(project_name)
+
+ # Ensure the release exists and is in the correct phase
+ release = await session.release(project_name, version_name,
phase=sql.ReleasePhase.RELEASE_CANDIDATE)
+
+ if release.committee is None:
+ raise ValueError("Release has no committee")
+
+ # Set up form choices
+ async with storage.write() as write:
+ try:
+ if release.committee.is_podling:
+ _wacm = write.as_committee_member("incubator")
+ else:
+ _wacm = write.as_committee_member(release.committee.name)
+ potency = "Binding"
+ except storage.AccessError:
+ # Participant, due to session.check_access above
+ potency = "Non-binding"
+
+ form = await shared_vote.CastVoteForm.create_form(data=await
quart.request.form)
+ forms.choices(
+ form.vote_value,
+ choices=[
+ ("+1", f"+1 ({potency})"),
+ ("0", "0"),
+ ("-1", f"-1 ({potency})"),
+ ],
+ )
+
+ if await form.validate_on_submit():
+ vote = str(form.vote_value.data)
+ comment = str(form.vote_comment.data)
+ async with
storage.write_as_committee_participant(release.committee.name) as wacm:
+ email_recipient, error_message = await
wacm.vote.send_user_vote(release, vote, comment, session.fullname)
+ if error_message:
+ return await session.redirect(
+ get_vote.selected, project_name=project_name,
version_name=version_name, error=error_message
+ )
+
+ success_message = f"Sending your vote to {email_recipient}."
+ return await session.redirect(
+ get_vote.selected, project_name=project_name,
version_name=version_name, success=success_message
+ )
+ else:
+ error_message = "Invalid vote submission"
+ if form.errors:
+ error_details = "; ".join([f"{field}: {', '.join(errs)}" for
field, errs in form.errors.items()])
+ error_message = f"{error_message}: {error_details}"
+
+ return await session.redirect(
+ get_vote.selected, project_name=project_name,
version_name=version_name, error=error_message
+ )
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index 5e15b9b..a0b4998 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -15,8 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-import atr.routes.compose as compose
-import atr.routes.distribution as distribution
import atr.routes.docs as docs
import atr.routes.download as download
import atr.routes.draft as draft
@@ -38,12 +36,9 @@ import atr.routes.start as start
import atr.routes.tokens as tokens
import atr.routes.upload as upload
import atr.routes.user as user
-import atr.routes.vote as vote
import atr.routes.voting as voting
__all__ = [
- "compose",
- "distribution",
"docs",
"download",
"draft",
@@ -65,7 +60,6 @@ __all__ = [
"tokens",
"upload",
"user",
- "vote",
"voting",
]
diff --git a/atr/routes/compose.py b/atr/routes/compose.py
deleted file mode 100644
index 3fc4d77..0000000
--- a/atr/routes/compose.py
+++ /dev/null
@@ -1,162 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one
-# or more contributor license agreements. See the NOTICE file
-# distributed with this work for additional information
-# regarding copyright ownership. The ASF licenses this file
-# to you under the Apache License, Version 2.0 (the
-# "License"); you may not use this file except in compliance
-# with the License. You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing,
-# software distributed under the License is distributed on an
-# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-# KIND, either express or implied. See the License for the
-# specific language governing permissions and limitations
-# under the License.
-
-from typing import TYPE_CHECKING
-
-import asfquart.base as base
-import werkzeug.wrappers.response as response
-import wtforms
-
-import atr.db as db
-import atr.db.interaction as interaction
-import atr.forms as forms
-import atr.models.results as results
-import atr.models.sql as sql
-import atr.route as route
-import atr.routes.draft as draft
-import atr.routes.mapping as mapping
-import atr.storage as storage
-import atr.template as template
-import atr.util as util
-
-if TYPE_CHECKING:
- from collections.abc import Sequence
-
-
-async def check(
- session: route.CommitterSession | None,
- release: sql.Release,
- task_mid: str | None = None,
- form: wtforms.Form | None = None,
- hidden_form: wtforms.Form | None = None,
- archive_url: str | None = None,
- vote_task: sql.Task | None = None,
- can_vote: bool = False,
- can_resolve: bool = False,
-) -> response.Response | str:
- base_path = util.release_directory(release)
-
- # TODO: This takes 180ms for providers
- # We could cache it
- paths = [path async for path in util.paths_recursive(base_path)]
- paths.sort()
-
- async with storage.read(session) as read:
- ragp = read.as_general_public()
- info = await ragp.releases.path_info(release, paths)
-
- user_ssh_keys: Sequence[sql.SSHKey] = []
- asf_id: str | None = None
- server_domain: str | None = None
- server_host: str | None = None
-
- if session is not None:
- asf_id = session.uid
- server_domain = session.app_host.split(":", 1)[0]
- server_host = session.app_host
- async with db.session() as data:
- user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
-
- # Get the number of ongoing tasks for the current revision
- ongoing_tasks_count = 0
- match await interaction.latest_info(release.project.name, release.version):
- case (revision_number, revision_editor, revision_timestamp):
- ongoing_tasks_count = await interaction.tasks_ongoing(
- release.project.name,
- release.version,
- revision_number, # type: ignore[arg-type]
- )
- case None:
- revision_number = None # type: ignore[assignment]
- revision_editor = None # type: ignore[assignment]
- revision_timestamp = None # type: ignore[assignment]
-
- delete_draft_form = await draft.DeleteForm.create_form(
- data={"release_name": release.name, "project_name":
release.project.name, "version_name": release.version}
- )
- delete_file_form = await draft.DeleteFileForm.create_form()
- empty_form = await forms.Empty.create_form()
- vote_task_warnings = _warnings_from_vote_result(vote_task)
- has_files = await util.has_files(release)
-
- has_any_errors = any(info.errors.get(path, []) for path in paths) if info
else False
- strict_checking = release.project.policy_strict_checking
- strict_checking_errors = strict_checking and has_any_errors
-
- return await template.render(
- "check-selected.html",
- project_name=release.project.name,
- version_name=release.version,
- release=release,
- paths=paths,
- info=info,
- revision_editor=revision_editor,
- revision_time=revision_timestamp,
- revision_number=revision_number,
- ongoing_tasks_count=ongoing_tasks_count,
- delete_form=delete_draft_form,
- delete_file_form=delete_file_form,
- asf_id=asf_id,
- server_domain=server_domain,
- server_host=server_host,
- user_ssh_keys=user_ssh_keys,
- format_datetime=util.format_datetime,
- models=sql,
- task_mid=task_mid,
- form=form,
- vote_task=vote_task,
- archive_url=archive_url,
- vote_task_warnings=vote_task_warnings,
- empty_form=empty_form,
- hidden_form=hidden_form,
- has_files=has_files,
- strict_checking_errors=strict_checking_errors,
- can_vote=can_vote,
- can_resolve=can_resolve,
- )
-
-
[email protected]("/compose/<project_name>/<version_name>")
-async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
- """Show the contents of the release candidate draft."""
- await session.check_access(project_name)
-
- async with db.session() as data:
- release = await data.release(
- project_name=project_name,
- version=version_name,
- _committee=True,
- _project_release_policy=True,
- ).demand(base.ASFQuartException("Release does not exist",
errorcode=404))
- if release.phase != sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
- return await mapping.release_as_redirect(session, release)
- return await check(session, release)
-
-
-def _warnings_from_vote_result(vote_task: sql.Task | None) -> list[str]:
- # TODO: Replace this with a schema.Strict model
- # But we'd still need to do some of this parsing and validation
- # We should probably rethink how to send data through tasks
-
- if not vote_task or (not vote_task.result):
- return ["No vote task result found."]
-
- vote_task_result = vote_task.result
- if not isinstance(vote_task_result, results.VoteInitiate):
- return ["Vote task result is not a results.VoteInitiate instance."]
-
- return vote_task_result.mail_send_warnings
diff --git a/atr/routes/draft.py b/atr/routes/draft.py
index 265156d..a6a944f 100644
--- a/atr/routes/draft.py
+++ b/atr/routes/draft.py
@@ -30,10 +30,10 @@ import quart
import atr.construct as construct
import atr.forms as forms
+import atr.get.compose as compose
import atr.log as log
import atr.models.sql as sql
import atr.route as route
-import atr.routes.compose as compose
import atr.routes.root as root
import atr.routes.upload as upload
import atr.storage as storage
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 33a200c..738a6b0 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -31,10 +31,10 @@ import wtforms
import atr.db as db
import atr.forms as forms
+import atr.get.compose as compose
import atr.log as log
import atr.models.sql as sql
import atr.route as route
-import atr.routes.compose as compose
import atr.shared as shared
import atr.storage as storage
import atr.storage.outcome as outcome
diff --git a/atr/routes/mapping.py b/atr/routes/mapping.py
index c26ddcf..5adf040 100644
--- a/atr/routes/mapping.py
+++ b/atr/routes/mapping.py
@@ -19,16 +19,19 @@ from collections.abc import Callable
import werkzeug.wrappers.response as response
+import atr.get as get
import atr.models.sql as sql
import atr.route as route
-import atr.routes.compose as compose
import atr.routes.finish as finish
import atr.routes.release as routes_release
-import atr.routes.vote as vote
import atr.util as util
+import atr.web as web
-async def release_as_redirect(session: route.CommitterSession, release:
sql.Release) -> response.Response:
+async def release_as_redirect(
+ session: route.CommitterSession | web.Committer,
+ release: sql.Release,
+) -> response.Response:
route = release_as_route(release)
if route is routes_release.finished:
return await session.redirect(route, project_name=release.project.name)
@@ -38,9 +41,9 @@ async def release_as_redirect(session:
route.CommitterSession, release: sql.Rele
def release_as_route(release: sql.Release) -> Callable:
match release.phase:
case sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT:
- return compose.selected
+ return get.compose.selected
case sql.ReleasePhase.RELEASE_CANDIDATE:
- return vote.selected
+ return get.vote.selected
case sql.ReleasePhase.RELEASE_PREVIEW:
return finish.selected
case sql.ReleasePhase.RELEASE:
diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index 9a255c1..023926b 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -20,11 +20,11 @@ import quart
import werkzeug.wrappers.response as response
import atr.forms as forms
+import atr.get.compose as compose
+import atr.get.vote as vote
import atr.models.sql as sql
import atr.route as route
-import atr.routes.compose as compose
import atr.routes.finish as finish
-import atr.routes.vote as vote
import atr.storage as storage
import atr.tabulate as tabulate
import atr.template as template
diff --git a/atr/routes/start.py b/atr/routes/start.py
index 99ff31a..acae711 100644
--- a/atr/routes/start.py
+++ b/atr/routes/start.py
@@ -23,9 +23,9 @@ import werkzeug.wrappers.response as response
import atr.db as db
import atr.db.interaction as interaction
import atr.forms as forms
+import atr.get.compose as compose
import atr.models.sql as sql
import atr.route as route
-import atr.routes.compose as compose
import atr.storage as storage
import atr.template as template
diff --git a/atr/routes/upload.py b/atr/routes/upload.py
index 65785e5..e8fa43a 100644
--- a/atr/routes/upload.py
+++ b/atr/routes/upload.py
@@ -23,9 +23,9 @@ import wtforms
import atr.db as db
import atr.forms as forms
+import atr.get.compose as compose
import atr.log as log
import atr.route as route
-import atr.routes.compose as compose
import atr.storage as storage
import atr.template as template
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index 5a30456..6c18027 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -26,12 +26,12 @@ import atr.construct as construct
import atr.db as db
import atr.db.interaction as interaction
import atr.forms as forms
+import atr.get.compose as compose
+import atr.get.vote as vote
import atr.log as log
import atr.models.sql as sql
import atr.route as route
-import atr.routes.compose as compose
import atr.routes.root as root
-import atr.routes.vote as vote
import atr.storage as storage
import atr.template as template
import atr.user as user
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 3604143..16dd241 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -15,9 +15,28 @@
# specific language governing permissions and limitations
# under the License.
-from typing import Final
+from typing import TYPE_CHECKING, Final
+import werkzeug.wrappers.response as response
+import wtforms
+
+import atr.db as db
+import atr.db.interaction as interaction
+import atr.forms as forms
+import atr.models.results as results
+import atr.models.sql as sql
+import atr.routes.draft as draft
import atr.shared.announce as announce
+import atr.shared.distribution as distribution
+import atr.shared.vote as vote
+import atr.storage as storage
+import atr.template as template
+import atr.util as util
+import atr.web as web
+
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+
# | 1 | RSA (Encrypt or Sign) [HAC] |
# | 2 | RSA Encrypt-Only [HAC] |
@@ -45,6 +64,118 @@ algorithms: Final[dict[int, str]] = {
}
+async def check(
+ session: web.Committer | None,
+ release: sql.Release,
+ task_mid: str | None = None,
+ form: wtforms.Form | None = None,
+ hidden_form: wtforms.Form | None = None,
+ archive_url: str | None = None,
+ vote_task: sql.Task | None = None,
+ can_vote: bool = False,
+ can_resolve: bool = False,
+) -> response.Response | str:
+ base_path = util.release_directory(release)
+
+ # TODO: This takes 180ms for providers
+ # We could cache it
+ paths = [path async for path in util.paths_recursive(base_path)]
+ paths.sort()
+
+ async with storage.read(session) as read:
+ ragp = read.as_general_public()
+ info = await ragp.releases.path_info(release, paths)
+
+ user_ssh_keys: Sequence[sql.SSHKey] = []
+ asf_id: str | None = None
+ server_domain: str | None = None
+ server_host: str | None = None
+
+ if session is not None:
+ asf_id = session.uid
+ server_domain = session.app_host.split(":", 1)[0]
+ server_host = session.app_host
+ async with db.session() as data:
+ user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all()
+
+ # Get the number of ongoing tasks for the current revision
+ ongoing_tasks_count = 0
+ match await interaction.latest_info(release.project.name, release.version):
+ case (revision_number, revision_editor, revision_timestamp):
+ ongoing_tasks_count = await interaction.tasks_ongoing(
+ release.project.name,
+ release.version,
+ revision_number, # type: ignore[arg-type]
+ )
+ case None:
+ revision_number = None # type: ignore[assignment]
+ revision_editor = None # type: ignore[assignment]
+ revision_timestamp = None # type: ignore[assignment]
+
+ delete_draft_form = await draft.DeleteForm.create_form(
+ data={"release_name": release.name, "project_name":
release.project.name, "version_name": release.version}
+ )
+ delete_file_form = await draft.DeleteFileForm.create_form()
+ empty_form = await forms.Empty.create_form()
+ vote_task_warnings = _warnings_from_vote_result(vote_task)
+ has_files = await util.has_files(release)
+
+ has_any_errors = any(info.errors.get(path, []) for path in paths) if info
else False
+ strict_checking = release.project.policy_strict_checking
+ strict_checking_errors = strict_checking and has_any_errors
+
+ return await template.render(
+ "check-selected.html",
+ project_name=release.project.name,
+ version_name=release.version,
+ release=release,
+ paths=paths,
+ info=info,
+ revision_editor=revision_editor,
+ revision_time=revision_timestamp,
+ revision_number=revision_number,
+ ongoing_tasks_count=ongoing_tasks_count,
+ delete_form=delete_draft_form,
+ delete_file_form=delete_file_form,
+ asf_id=asf_id,
+ server_domain=server_domain,
+ server_host=server_host,
+ user_ssh_keys=user_ssh_keys,
+ format_datetime=util.format_datetime,
+ models=sql,
+ task_mid=task_mid,
+ form=form,
+ vote_task=vote_task,
+ archive_url=archive_url,
+ vote_task_warnings=vote_task_warnings,
+ empty_form=empty_form,
+ hidden_form=hidden_form,
+ has_files=has_files,
+ strict_checking_errors=strict_checking_errors,
+ can_vote=can_vote,
+ can_resolve=can_resolve,
+ )
+
+
+def _warnings_from_vote_result(vote_task: sql.Task | None) -> list[str]:
+ # TODO: Replace this with a schema.Strict model
+ # But we'd still need to do some of this parsing and validation
+ # We should probably rethink how to send data through tasks
+
+ if not vote_task or (not vote_task.result):
+ return ["No vote task result found."]
+
+ vote_task_result = vote_task.result
+ if not isinstance(vote_task_result, results.VoteInitiate):
+ return ["Vote task result is not a results.VoteInitiate instance."]
+
+ return vote_task_result.mail_send_warnings
+
+
__all__ = [
+ "algorithms",
"announce",
+ "check",
+ "distribution",
+ "vote",
]
diff --git a/atr/routes/distribution.py b/atr/shared/distribution.py
similarity index 50%
rename from atr/routes/distribution.py
rename to atr/shared/distribution.py
index 23508b8..4266e06 100644
--- a/atr/routes/distribution.py
+++ b/atr/shared/distribution.py
@@ -19,26 +19,22 @@ from __future__ import annotations
import dataclasses
import json
-from typing import TYPE_CHECKING, Literal
+from typing import Literal
import htpy
import quart
import atr.db as db
import atr.forms as forms
+import atr.get as get
import atr.htm as htm
import atr.models.distribution as distribution
import atr.models.sql as sql
-import atr.route as route
-import atr.routes.compose as compose
import atr.routes.finish as finish
import atr.storage as storage
import atr.template as template
import atr.util as util
-if TYPE_CHECKING:
- import werkzeug.wrappers.response as response
-
type Phase = Literal["COMPOSE", "VOTE", "FINISH"]
@@ -93,167 +89,8 @@ class FormProjectVersion:
version: str
[email protected]("/distribution/delete/<project>/<version>", methods=["POST"])
-async def delete(session: route.CommitterSession, project: str, version: str)
-> response.Response:
- form = await DeleteForm.create_form(data=await quart.request.form)
- dd = distribution.DeleteData.model_validate(form.data)
-
- # Validate the submitted data, and obtain the committee for its name
- async with db.session() as data:
- release = await
data.release(name=dd.release_name).demand(RuntimeError(f"Release
{dd.release_name} not found"))
- committee = release.committee
- if committee is None:
- raise RuntimeError(f"Release {dd.release_name} has no committee")
-
- # Delete the distribution
- async with
storage.write_as_committee_member(committee_name=committee.name) as wacm:
- await wacm.distributions.delete_distribution(
- release_name=dd.release_name,
- platform=dd.platform,
- owner_namespace=dd.owner_namespace,
- package=dd.package,
- version=dd.version,
- )
- return await session.redirect(
- list_get,
- project=project,
- version=version,
- success="Distribution deleted",
- )
-
-
[email protected]("/distributions/list/<project>/<version>", methods=["GET"])
-async def list_get(session: route.CommitterSession, project: str, version:
str) -> str:
- async with db.session() as data:
- distributions = await data.distribution(
- release_name=sql.release_name(project, version),
- ).all()
-
- block = htm.Block()
-
- release = await _release_validated(project, version, staging=None)
- staging = release.phase == sql.ReleasePhase.RELEASE_CANDIDATE_DRAFT
- _html_nav_phase(block, project, version, staging)
-
- record_a_distribution = htpy.a(
- ".btn.btn-primary",
- href=util.as_url(
- stage if staging else record,
- project=project,
- version=version,
- ),
- )["Record a distribution"]
-
- # Distribution list for project-version
- block.h1["Distribution list for ", htpy.em[f"{project}-{version}"]]
- if not distributions:
- block.p["No distributions found."]
- block.p[record_a_distribution]
- return await template.blank(
- "Distribution list",
- content=block.collect(),
- )
- block.p["Here are all of the distributions recorded for this release."]
- block.p[record_a_distribution]
- # Table of contents
- ul_toc = htm.Block(htpy.ul)
- for dist in distributions:
- a = htpy.a(href=f"#distribution-{dist.identifier}")[dist.title]
- ul_toc.li[a]
- block.append(ul_toc)
-
- ## Distributions
- block.h2["Distributions"]
- for dist in distributions:
- delete_form = await DeleteForm.create_form(
- data={
- "release_name": dist.release_name,
- "platform": dist.platform.name,
- "owner_namespace": dist.owner_namespace,
- "package": dist.package,
- "version": dist.version,
- }
- )
-
- ### Platform package version
- block.h3(
- # Cannot use "#id" here, because the ID contains "."
- # If an ID contains ".", htpy parses that as a class
- id=f"distribution-{dist.identifier}"
- )[dist.title]
- tbody = htpy.tbody[
- _html_tr("Release name", dist.release_name),
- _html_tr("Platform", dist.platform.value.name),
- _html_tr("Owner or Namespace", dist.owner_namespace or "-"),
- _html_tr("Package", dist.package),
- _html_tr("Version", dist.version),
- _html_tr("Staging", "Yes" if dist.staging else "No"),
- _html_tr("Upload date", str(dist.upload_date)),
- _html_tr_a("API URL", dist.api_url),
- _html_tr_a("Web URL", dist.web_url),
- ]
- block.table(".table.table-striped.table-bordered")[tbody]
- form_action = util.as_url(delete, project=project, version=version)
- delete_form_element = forms.render_simple(
- delete_form,
- action=form_action,
- submit_classes="btn-danger",
- )
- block.append(htpy.div(".mb-3")[delete_form_element])
-
- title = f"Distribution list for {project} {version}"
- return await template.blank(title, content=block.collect())
-
-
[email protected]("/distribution/record/<project>/<version>", methods=["GET"])
-async def record(session: route.CommitterSession, project: str, version: str)
-> str:
- form = await DistributeForm.create_form(data={"package": project,
"version": version})
- fpv = FormProjectVersion(form=form, project=project, version=version)
- return await _record_form_page(fpv)
-
-
[email protected]("/distribution/record/<project>/<version>", methods=["POST"])
-async def record_post(session: route.CommitterSession, project: str, version:
str) -> str:
- form = await DistributeForm.create_form(data=await quart.request.form)
- fpv = FormProjectVersion(form=form, project=project, version=version)
- if await form.validate():
- return await _record_form_process_page(fpv)
- match len(form.errors):
- case 0:
- # Should not happen
- await quart.flash("Ambiguous submission errors",
category="warning")
- case 1:
- await quart.flash("There was 1 submission error", category="error")
- case _ as n:
- await quart.flash(f"There were {n} submission errors",
category="error")
- return await _record_form_page(fpv)
-
-
[email protected]("/distribution/stage/<project>/<version>", methods=["GET"])
-async def stage(session: route.CommitterSession, project: str, version: str)
-> str:
- form = await DistributeForm.create_form(data={"package": project,
"version": version})
- fpv = FormProjectVersion(form=form, project=project, version=version)
- return await _record_form_page(fpv, staging=True)
-
-
[email protected]("/distribution/stage/<project>/<version>", methods=["POST"])
-async def stage_post(session: route.CommitterSession, project: str, version:
str) -> str:
- form = await DistributeForm.create_form(data=await quart.request.form)
- fpv = FormProjectVersion(form=form, project=project, version=version)
- if await form.validate():
- return await _record_form_process_page(fpv, staging=True)
- match len(form.errors):
- case 0:
- await quart.flash("Ambiguous submission errors",
category="warning")
- case 1:
- await quart.flash("There was 1 submission error", category="error")
- case _ as n:
- await quart.flash(f"There were {n} submission errors",
category="error")
- return await _record_form_page(fpv, staging=True)
-
-
# TODO: Move this to an appropriate module
-def _html_nav(container: htm.Block, back_url: str, back_anchor: str, phase:
Phase) -> None:
+def html_nav(container: htm.Block, back_url: str, back_anchor: str, phase:
Phase) -> None:
classes = ".d-flex.justify-content-between.align-items-center"
block = htm.Block(htpy.p(classes))
block.a(".atr-back-link", href=back_url)[f"← Back to {back_anchor}"]
@@ -284,12 +121,12 @@ def _html_nav(container: htm.Block, back_url: str,
back_anchor: str, phase: Phas
container.append(block)
-def _html_nav_phase(block: htm.Block, project: str, version: str, staging:
bool) -> None:
+def html_nav_phase(block: htm.Block, project: str, version: str, staging:
bool) -> None:
label: Phase
- route, label = (compose.selected, "COMPOSE")
+ route, label = (get.compose.selected, "COMPOSE")
if not staging:
route, label = (finish.selected, "FINISH")
- _html_nav(
+ html_nav(
block,
util.as_url(
route,
@@ -301,34 +138,34 @@ def _html_nav_phase(block: htm.Block, project: str,
version: str, staging: bool)
)
-def _html_submitted_values_table(block: htm.Block, dd: distribution.Data) ->
None:
+def html_submitted_values_table(block: htm.Block, dd: distribution.Data) ->
None:
tbody = htpy.tbody[
- _html_tr("Platform", dd.platform.name),
- _html_tr("Owner or Namespace", dd.owner_namespace or "-"),
- _html_tr("Package", dd.package),
- _html_tr("Version", dd.version),
+ html_tr("Platform", dd.platform.name),
+ html_tr("Owner or Namespace", dd.owner_namespace or "-"),
+ html_tr("Package", dd.package),
+ html_tr("Version", dd.version),
]
block.table(".table.table-striped.table-bordered")[tbody]
-def _html_tr(label: str, value: str) -> htpy.Element:
+def html_tr(label: str, value: str) -> htpy.Element:
return htpy.tr[htpy.th[label], htpy.td[value]]
-def _html_tr_a(label: str, value: str | None) -> htpy.Element:
+def html_tr_a(label: str, value: str | None) -> htpy.Element:
return htpy.tr[htpy.th[label], htpy.td[htpy.a(href=value)[value] if value
else "-"]]
# This function is used for COMPOSE (stage) and FINISH (record)
# It's also used whenever there is an error
-async def _record_form_page(
+async def record_form_page(
fpv: FormProjectVersion, *, extra_content: htpy.Element | None = None,
staging: bool = False
) -> str:
- await _release_validated(fpv.project, fpv.version, staging=staging)
+ await release_validated(fpv.project, fpv.version, staging=staging)
# Render the explanation and form
block = htm.Block()
- _html_nav_phase(block, fpv.project, fpv.version, staging)
+ html_nav_phase(block, fpv.project, fpv.version, staging)
# Record a manual distribution
title_and_heading = f"Record a {'staging' if staging else 'manual'}
distribution"
@@ -342,7 +179,9 @@ async def _record_form_page(
]
block.p[
"You can also ",
- htpy.a(href=util.as_url(list_get, project=fpv.project,
version=fpv.version))["view the distribution list"],
+ htpy.a(href=util.as_url(get.distribution.list_get,
project=fpv.project, version=fpv.version))[
+ "view the distribution list"
+ ],
".",
]
block.append(forms.render_columns(fpv.form, action=quart.request.path,
descriptions=True))
@@ -351,9 +190,9 @@ async def _record_form_page(
return await template.blank(title_and_heading, content=block.collect())
-async def _record_form_process_page(fpv: FormProjectVersion, /, staging: bool
= False) -> str:
+async def record_form_process_page(fpv: FormProjectVersion, /, staging: bool =
False) -> str:
dd = distribution.Data.model_validate(fpv.form.data)
- release, committee = await _release_validated_and_committee(
+ release, committee = await release_validated_and_committee(
fpv.project,
fpv.version,
staging=staging,
@@ -364,7 +203,7 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
div = htm.Block(htpy.div(".alert.alert-danger"))
div.p[message]
collected = div.collect()
- return await _record_form_page(fpv, extra_content=collected,
staging=staging)
+ return await record_form_page(fpv, extra_content=collected,
staging=staging)
async with
storage.write_as_committee_member(committee_name=committee.name) as w:
try:
@@ -389,18 +228,22 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
block.p["The distribution was already recorded."]
block.table(".table.table-striped.table-bordered")[
htpy.tbody[
- _html_tr("Release name", dist.release_name),
- _html_tr("Platform", dist.platform.name),
- _html_tr("Owner or Namespace", dist.owner_namespace or "-"),
- _html_tr("Package", dist.package),
- _html_tr("Version", dist.version),
- _html_tr("Staging", "Yes" if dist.staging else "No"),
- _html_tr("Upload date", str(dist.upload_date)),
- _html_tr_a("API URL", dist.api_url),
- _html_tr_a("Web URL", dist.web_url),
+ html_tr("Release name", dist.release_name),
+ html_tr("Platform", dist.platform.name),
+ html_tr("Owner or Namespace", dist.owner_namespace or "-"),
+ html_tr("Package", dist.package),
+ html_tr("Version", dist.version),
+ html_tr("Staging", "Yes" if dist.staging else "No"),
+ html_tr("Upload date", str(dist.upload_date)),
+ html_tr_a("API URL", dist.api_url),
+ html_tr_a("Web URL", dist.web_url),
]
]
- block.p[htpy.a(href=util.as_url(list_get, project=fpv.project,
version=fpv.version))["Back to distribution list"],]
+ block.p[
+ htpy.a(href=util.as_url(get.distribution.list_get,
project=fpv.project, version=fpv.version))[
+ "Back to distribution list"
+ ],
+ ]
if dd.details:
## Details
@@ -408,7 +251,7 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
### Submitted values
block.h3["Submitted values"]
- _html_submitted_values_table(block, dd)
+ html_submitted_values_table(block, dd)
### As JSON
block.h3["As JSON"]
@@ -428,20 +271,20 @@ async def _record_form_process_page(fpv:
FormProjectVersion, /, staging: bool =
return await template.blank("Distribution submitted",
content=block.collect())
-async def _release_validated_and_committee(
+async def release_validated_and_committee(
project: str,
version: str,
*,
staging: bool | None = None,
) -> tuple[sql.Release, sql.Committee]:
- release = await _release_validated(project, version, committee=True,
staging=staging)
+ release = await release_validated(project, version, committee=True,
staging=staging)
committee = release.committee
if committee is None:
raise RuntimeError(f"Release {project} {version} has no committee")
return release, committee
-async def _release_validated(
+async def release_validated(
project: str,
version: str,
committee: bool = False,
diff --git a/atr/post/__init__.py b/atr/shared/vote.py
similarity index 75%
copy from atr/post/__init__.py
copy to atr/shared/vote.py
index e9131ab..0738790 100644
--- a/atr/post/__init__.py
+++ b/atr/shared/vote.py
@@ -15,13 +15,12 @@
# specific language governing permissions and limitations
# under the License.
-from typing import Final, Literal
+import atr.forms as forms
-import atr.post.announce as announce
-import atr.post.candidate as candidate
-from .example_test import respond as example_test
+class CastVoteForm(forms.Typed):
+ """Form for casting a vote."""
-ROUTES_MODULE: Final[Literal[True]] = True
-
-__all__ = ["announce", "candidate", "example_test"]
+ vote_value = forms.radio("Your vote")
+ vote_comment = forms.textarea("Comment (optional)", optional=True)
+ submit = forms.submit("Submit vote")
diff --git a/atr/templates/check-selected-candidate-forms.html
b/atr/templates/check-selected-candidate-forms.html
index 8cc096e..2e2b4e4 100644
--- a/atr/templates/check-selected-candidate-forms.html
+++ b/atr/templates/check-selected-candidate-forms.html
@@ -7,7 +7,7 @@
<h2>Cast your vote</h2>
<form method="post"
- action="{{ as_url(routes.vote.selected_post,
project_name=project_name, version_name=version_name) }}"
+ action="{{ as_url(post.vote.selected_post, project_name=project_name,
version_name=version_name) }}"
class="atr-canary py-4 px-5 mb-4 border rounded">
{{ form.hidden_tag() }}
diff --git a/atr/templates/check-selected.html
b/atr/templates/check-selected.html
index f06fef9..e2ac6ab 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -108,7 +108,7 @@
{% if phase == "release_candidate_draft" %}
<p>
<a class="btn btn-primary"
- href="{{ as_url(routes.distribution.stage,
project=release.project.name, version=release.version) }}">Record a
distribution</a>
+ href="{{ as_url(get.distribution.stage, project=release.project.name,
version=release.version) }}">Record a distribution</a>
</p>
<h2 id="more-actions">More actions</h2>
<h3 id="ignored-checks" class="mt-4">Ignored checks</h3>
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index 42c96f6..e474d6e 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -9,7 +9,7 @@
{% endblock description %}
{% block content %}
- <a href="{{ as_url(routes.compose.selected, project_name=project_name,
version_name=version_name) }}"
+ <a href="{{ as_url(get.compose.selected, project_name=project_name,
version_name=version_name) }}"
class="atr-back-link">← Back to Compose release</a>
<div class="p-3 mb-4 bg-light border rounded">
diff --git a/atr/templates/finish-selected.html
b/atr/templates/finish-selected.html
index 9082e5e..365326a 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -103,7 +103,7 @@
<div class="alert alert-warning mb-4" role="alert">
<p class="fw-semibold mb-1">TODO</p>
<p class="mb-1">
- We plan to add tools to help release managers to distribute release
artifacts on distribution networks. Currently you must do this manually. Once
you've distributed your release artifacts, you can <a href="{{
as_url(routes.distribution.record, project=release.project.name,
version=release.version) }}">record them on the ATR</a>.
+ We plan to add tools to help release managers to distribute release
artifacts on distribution networks. Currently you must do this manually. Once
you've distributed your release artifacts, you can <a href="{{
as_url(get.distribution.record, project=release.project.name,
version=release.version) }}">record them on the ATR</a>.
</p>
</div>
diff --git a/atr/templates/phase-view.html b/atr/templates/phase-view.html
index ac4564c..61ff04f 100644
--- a/atr/templates/phase-view.html
+++ b/atr/templates/phase-view.html
@@ -12,7 +12,7 @@
<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) }}"
+ <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-back-link">← Back to Compose {{ release.short_display_name
}}</a>
<span>
<strong class="atr-phase-one atr-phase-symbol">①</strong>
@@ -23,7 +23,7 @@
<span class="atr-phase-symbol-other">③</span>
</span>
{% elif phase_key == "candidate" %}
- <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.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>
diff --git a/atr/templates/release-select.html
b/atr/templates/release-select.html
index 0a87864..f60b710 100644
--- a/atr/templates/release-select.html
+++ b/atr/templates/release-select.html
@@ -30,10 +30,10 @@
{% 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 target_url = as_url(get.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.vote.selected,
project_name=project.name, version_name=release.version) %}
+ {% set target_url = as_url(get.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(get.announce.selected,
project_name=project.name, version_name=release.version) %}
diff --git a/atr/templates/report-selected-path.html
b/atr/templates/report-selected-path.html
index aae1a66..a8db765 100644
--- a/atr/templates/report-selected-path.html
+++ b/atr/templates/report-selected-path.html
@@ -25,10 +25,10 @@
{% set phase = release.phase.value %}
<p class="d-flex justify-content-between align-items-center">
{% if phase == "release_candidate_draft" %}
- <a href="{{ as_url(routes.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-back-link">← Back to Compose {{
release.project.short_display_name }} {{ release.version }}</a>
{% else %}
- <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.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>
{% endif %}
<span>
diff --git a/atr/templates/resolve-manual.html
b/atr/templates/resolve-manual.html
index 42cfefd..a50be50 100644
--- a/atr/templates/resolve-manual.html
+++ b/atr/templates/resolve-manual.html
@@ -10,7 +10,7 @@
{% block content %}
<p>
- <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.vote.selected, project_name=release.project.name,
version_name=release.version) }}"
class="atr-back-link">← Back to Vote for {{ release.short_display_name
}}</a>
</p>
diff --git a/atr/templates/resolve-tabulated.html
b/atr/templates/resolve-tabulated.html
index e011cf6..dc24d14 100644
--- a/atr/templates/resolve-tabulated.html
+++ b/atr/templates/resolve-tabulated.html
@@ -10,7 +10,7 @@
{% block content %}
<p>
- <a href="{{ as_url(routes.vote.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.vote.selected, project_name=release.project.name,
version_name=release.version) }}"
class="atr-back-link">← Back to Vote for {{ release.short_display_name
}}</a>
</p>
diff --git a/atr/templates/revisions-selected.html
b/atr/templates/revisions-selected.html
index ccec16b..0dddf84 100644
--- a/atr/templates/revisions-selected.html
+++ b/atr/templates/revisions-selected.html
@@ -11,7 +11,7 @@
{% block content %}
<p class="d-flex justify-content-between align-items-center">
{% if phase_key == "draft" %}
- <a href="{{ as_url(routes.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-back-link">← Back to Compose {{ release.short_display_name
}}</a>
<span>
<strong class="atr-phase-one atr-phase-symbol">①</strong>
diff --git a/atr/templates/upload-selected.html
b/atr/templates/upload-selected.html
index dc5e3b8..f5ff081 100644
--- a/atr/templates/upload-selected.html
+++ b/atr/templates/upload-selected.html
@@ -10,7 +10,7 @@
{% block content %}
<p class="d-flex justify-content-between align-items-center">
- <a href="{{ as_url(routes.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-back-link">← Back to Compose {{ release.short_display_name
}}</a>
<span>
<strong class="atr-phase-one atr-phase-symbol">①</strong>
diff --git a/atr/templates/voting-selected-revision.html
b/atr/templates/voting-selected-revision.html
index 05e2c78..c3b5686 100644
--- a/atr/templates/voting-selected-revision.html
+++ b/atr/templates/voting-selected-revision.html
@@ -10,7 +10,7 @@
{% block content %}
<p class="d-flex justify-content-between align-items-center">
- <a href="{{ as_url(routes.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-back-link">← Back to Compose {{ release.short_display_name
}}</a>
<span>
<strong class="atr-phase-one atr-phase-symbol">①</strong>
@@ -136,7 +136,7 @@
<div class="mt-4 col-md-9 offset-md-3 px-1">
{{ form.submit(class_='btn btn-primary') }}
- <a href="{{ as_url(routes.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-link text-secondary">Cancel</a>
</div>
</form>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]