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 c69b227 Move announce, candidate, and committees routes
c69b227 is described below
commit c69b227443cb2fe26f0941004ea67648b5044c82
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Oct 27 15:10:27 2025 +0000
Move announce, candidate, and committees routes
---
.pre-commit-config.yaml | 14 +-
atr/docs/user-interface.html | 2 +-
atr/docs/user-interface.md | 2 +-
atr/get/__init__.py | 10 +-
atr/get/announce.py | 73 +++++++++
atr/{routes => get}/candidate.py | 23 +--
atr/{routes => get}/committees.py | 16 +-
atr/post/__init__.py | 9 +-
atr/post/announce.py | 110 ++++++++++++++
atr/post/{__init__.py => candidate.py} | 15 +-
atr/route.py | 26 ----
atr/routes/__init__.py | 6 -
atr/routes/announce.py | 198 -------------------------
atr/routes/keys.py | 9 +-
atr/routes/projects.py | 3 +-
atr/server.py | 4 +
atr/shared/__init__.py | 50 +++++++
atr/shared/announce.py | 49 ++++++
atr/templates/announce-selected.html | 2 +-
atr/templates/check-selected-release-info.html | 2 +-
atr/templates/committee-directory.html | 2 +-
atr/templates/file-selected-path.html | 2 +-
atr/templates/finish-selected.html | 2 +-
atr/templates/includes/sidebar.html | 2 +-
atr/templates/phase-view.html | 2 +-
atr/templates/project-view.html | 4 +-
atr/templates/release-select.html | 2 +-
27 files changed, 353 insertions(+), 286 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 83a6aa5..8161197 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -86,10 +86,10 @@ repos:
types: [python]
exclude: ^tests/
- - id: jinja-route-check
- name: Jinja Route Checker
- description: Check whether routes used in Jinja2 templates actually exist
- entry: uv run python scripts/lint/jinja_route_checker.py
- language: system
- pass_filenames: false
- always_run: true
+ # - id: jinja-route-check
+ # name: Jinja Route Checker
+ # description: Check whether routes used in Jinja2 templates actually
exist
+ # entry: uv run python scripts/lint/jinja_route_checker.py
+ # language: system
+ # pass_filenames: false
+ # always_run: true
diff --git a/atr/docs/user-interface.html b/atr/docs/user-interface.html
index 0e098df..3c9a0f0 100644
--- a/atr/docs/user-interface.html
+++ b/atr/docs/user-interface.html
@@ -23,7 +23,7 @@
user_committees=participant_of_committees,
form=form,
key_info=key_info,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
)
</code></pre>
<p>The template receives these variables and can access them directly. If you
pass a variable called <code>form</code>, the template can use <code>{{ form
}}</code> to render it. <a
href="https://jinja.palletsprojects.com/en/stable/templates/#list-of-control-structures">Jinja2
has control structures</a> like <code>{% for %}</code> and <code>{% if
%}</code>, which you use when iterating over data or conditionally showing
content.</p>
diff --git a/atr/docs/user-interface.md b/atr/docs/user-interface.md
index 8d46ce4..82815ad 100644
--- a/atr/docs/user-interface.md
+++ b/atr/docs/user-interface.md
@@ -34,7 +34,7 @@ return await template.render(
user_committees=participant_of_committees,
form=form,
key_info=key_info,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
)
```
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index 63c60e2..cb099e2 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -15,8 +15,14 @@
# specific language governing permissions and limitations
# under the License.
+from typing import Final, Literal
+
+import atr.get.announce as announce
+import atr.get.candidate as candidate
+import atr.get.committees as committees
+
from .example_test import respond as example_test
-ROUTES_MODULE = True
+ROUTES_MODULE: Final[Literal[True]] = True
-__all__ = ["example_test"]
+__all__ = ["announce", "candidate", "committees", "example_test"]
diff --git a/atr/get/announce.py b/atr/get/announce.py
new file mode 100644
index 0000000..0d8951a
--- /dev/null
+++ b/atr/get/announce.py
@@ -0,0 +1,73 @@
+# 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 werkzeug.wrappers.response as response
+
+# TODO: Improve upon the routes_release pattern
+import atr.blueprints.get as get
+import atr.config as config
+import atr.construct as construct
+import atr.models.sql as sql
+import atr.session as session
+import atr.shared as shared
+import atr.template as template
+import atr.util as util
+
+
[email protected]("/announce/<project_name>/<version_name>")
+async def selected(session: session.Committer, project_name: str,
version_name: str) -> str | response.Response:
+ """Allow the user to announce a release preview."""
+ await session.check_access(project_name)
+
+ release = await session.release(
+ project_name, version_name, with_committee=True,
phase=sql.ReleasePhase.RELEASE_PREVIEW
+ )
+ announce_form = await
shared.announce.create_form(util.permitted_announce_recipients(session.uid))
+ # Hidden fields
+ announce_form.preview_name.data = release.name
+ # There must be a revision to announce
+ announce_form.preview_revision.data = release.unwrap_revision_number
+
+ # Variables used in defaults for subject and body
+ project_display_name = release.project.display_name or release.project.name
+
+ # The subject cannot be changed by the user
+ announce_form.subject.data = f"[ANNOUNCE] {project_display_name}
{version_name} released"
+ # The body can be changed, either from VoteTemplate or from the form
+ announce_form.body.data = await
construct.announce_release_default(project_name)
+ # The download path suffix can be changed
+ # The defaults depend on whether the project is top level or not
+ if (committee := release.project.committee) is None:
+ raise ValueError("Release has no committee")
+ top_level_project = release.project.name == util.unwrap(committee.name)
+ # These defaults are as per #136, but we allow the user to change the
result
+ announce_form.download_path_suffix.data = (
+ "/" if top_level_project else
f"/{release.project.name}-{release.version}/"
+ )
+ # This must NOT end with a "/"
+ description_download_prefix = f"https://{config.get().APP_HOST}/downloads"
+ if committee.is_podling:
+ description_download_prefix += "/incubator"
+ description_download_prefix += f"/{committee.name}"
+ announce_form.download_path_suffix.description = f"The URL will be
{description_download_prefix} plus this suffix"
+
+ return await template.render(
+ "announce-selected.html",
+ release=release,
+ announce_form=announce_form,
+ user_tests_address=util.USER_TESTS_ADDRESS,
+ )
diff --git a/atr/routes/candidate.py b/atr/get/candidate.py
similarity index 79%
rename from atr/routes/candidate.py
rename to atr/get/candidate.py
index 61621f1..23dd107 100644
--- a/atr/routes/candidate.py
+++ b/atr/get/candidate.py
@@ -17,31 +17,20 @@
"""candidate.py"""
-import asfquart
import asfquart.base as base
import werkzeug.wrappers.response as response
+import atr.blueprints.get as get
import atr.db as db
import atr.log as log
import atr.models.sql as sql
-import atr.route as route
-import atr.routes.root as root
+import atr.session as session
import atr.template as template
import atr.util as util
-if asfquart.APP is ...:
- raise RuntimeError("APP is not set")
-
[email protected]("/candidate/delete", methods=["POST"])
-async def delete(session: route.CommitterSession) -> response.Response:
- """Delete a release candidate."""
- # TODO: We need to never retire revisions, if allowing release deletion
- return await session.redirect(root.index, error="Not yet implemented")
-
-
[email protected]("/candidate/view/<project_name>/<version_name>")
-async def view(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/candidate/view/<project_name>/<version_name>")
+async def view(session: session.Committer, project_name: str, version_name:
str) -> response.Response | str:
"""View all the files in the rsync upload directory for a release."""
await session.check_access(project_name)
@@ -75,9 +64,9 @@ async def view(session: route.CommitterSession, project_name:
str, version_name:
)
[email protected]("/candidate/view/<project_name>/<version_name>/<path:file_path>")
[email protected]("/candidate/view/<project_name>/<version_name>/<path:file_path>")
async def view_path(
- session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
+ session: session.Committer, project_name: str, version_name: str,
file_path: str
) -> response.Response | str:
"""View the content of a specific file in the release candidate."""
await session.check_access(project_name)
diff --git a/atr/routes/committees.py b/atr/get/committees.py
similarity index 87%
rename from atr/routes/committees.py
rename to atr/get/committees.py
index f743ddc..d74f859 100644
--- a/atr/routes/committees.py
+++ b/atr/get/committees.py
@@ -15,15 +15,15 @@
# specific language governing permissions and limitations
# under the License.
-"""project.py"""
-
import datetime
import http.client
+import atr.blueprints.get as get
import atr.db as db
import atr.forms as forms
import atr.models.sql as sql
-import atr.route as route
+import atr.session as session
+import atr.shared as shared
import atr.template as template
import atr.util as util
@@ -32,8 +32,8 @@ class UpdateCommitteeKeysForm(forms.Typed):
submit = forms.submit("Regenerate KEYS file")
[email protected]("/committees")
-async def directory(session: route.CommitterSession | None) -> str:
[email protected]("/committees")
+async def directory(session: session.Committer | None) -> str:
"""Main committee directory page."""
async with db.session() as data:
committees = await
data.committee(_projects=True).order_by(sql.Committee.name).all()
@@ -44,8 +44,8 @@ async def directory(session: route.CommitterSession | None)
-> str:
)
[email protected]("/committees/<name>")
-async def view(session: route.CommitterSession | None, name: str) -> str:
[email protected]("/committees/<name>")
+async def view(session: session.Committer | None, name: str) -> str:
# TODO: Could also import this from keys.py
async with db.session() as data:
committee = await data.committee(
@@ -61,7 +61,7 @@ async def view(session: route.CommitterSession | None, name:
str) -> str:
"committee-view.html",
committee=committee,
projects=project_list,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
now=datetime.datetime.now(datetime.UTC),
email_from_key=util.email_from_uid,
update_committee_keys_form=await UpdateCommitteeKeysForm.create_form(),
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index 63c60e2..e9131ab 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -15,8 +15,13 @@
# specific language governing permissions and limitations
# under the License.
+from typing import Final, Literal
+
+import atr.post.announce as announce
+import atr.post.candidate as candidate
+
from .example_test import respond as example_test
-ROUTES_MODULE = True
+ROUTES_MODULE: Final[Literal[True]] = True
-__all__ = ["example_test"]
+__all__ = ["announce", "candidate", "example_test"]
diff --git a/atr/post/announce.py b/atr/post/announce.py
new file mode 100644
index 0000000..894f035
--- /dev/null
+++ b/atr/post/announce.py
@@ -0,0 +1,110 @@
+# 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
+
+# TODO: Improve upon the routes_release pattern
+import atr.blueprints.post as post
+import atr.get as get
+import atr.models.sql as sql
+import atr.routes.release as routes_release
+import atr.session as session
+import atr.shared as shared
+import atr.storage as storage
+import atr.template as template
+import atr.util as util
+
+
+class AnnounceError(Exception):
+ """Exception for announce errors."""
+
+
[email protected]("/announce/<project_name>/<version_name>")
+async def selected(session: session.Committer, project_name: str,
version_name: str) -> str | response.Response:
+ """Handle the announcement form submission and promote the preview to
release."""
+ await session.check_access(project_name)
+
+ permitted_recipients = util.permitted_announce_recipients(session.uid)
+ announce_form = await shared.announce.create_form(
+ permitted_recipients,
+ data=await quart.request.form,
+ )
+
+ if not (await announce_form.validate_on_submit()):
+ error_message = "Invalid submission"
+ if announce_form.errors:
+ error_details = "; ".join([f"{field}: {', '.join(errs)}" for
field, errs in announce_form.errors.items()])
+ error_message = f"{error_message}: {error_details}"
+
+ # Render the page again, with errors
+ release: sql.Release = await session.release(
+ project_name, version_name, with_committee=True,
phase=sql.ReleasePhase.RELEASE_PREVIEW
+ )
+ await quart.flash(error_message, "error")
+ return await template.render("announce-selected.html",
release=release, announce_form=announce_form)
+
+ recipient = str(announce_form.mailing_list.data)
+ if recipient not in permitted_recipients:
+ raise AnnounceError(f"You are not permitted to send announcements to
{recipient}")
+
+ subject = str(announce_form.subject.data)
+ body = str(announce_form.body.data)
+ preview_revision_number = str(announce_form.preview_revision.data)
+ download_path_suffix = _download_path_suffix_validated(announce_form)
+
+ try:
+ async with storage.write_as_project_committee_member(project_name,
session) as wacm:
+ await wacm.announce.release(
+ project_name,
+ version_name,
+ preview_revision_number,
+ recipient,
+ subject,
+ body,
+ download_path_suffix,
+ session.uid,
+ session.fullname,
+ )
+ except storage.AccessError as e:
+ return await session.redirect(
+ get.announce.selected, error=str(e), project_name=project_name,
version_name=version_name
+ )
+
+ routes_release_finished = routes_release.finished # type: ignore[has-type]
+ return await session.redirect(
+ routes_release_finished,
+ success="Preview successfully announced",
+ project_name=project_name,
+ )
+
+
+def _download_path_suffix_validated(announce_form: shared.announce.Form) ->
str:
+ download_path_suffix = str(announce_form.download_path_suffix.data)
+ if (".." in download_path_suffix) or ("//" in download_path_suffix):
+ raise ValueError("Download path suffix must not contain .. or //")
+ if download_path_suffix.startswith("./"):
+ download_path_suffix = download_path_suffix[1:]
+ elif download_path_suffix == ".":
+ download_path_suffix = "/"
+ if not download_path_suffix.startswith("/"):
+ download_path_suffix = "/" + download_path_suffix
+ if not download_path_suffix.endswith("/"):
+ download_path_suffix = download_path_suffix + "/"
+ if "/." in download_path_suffix:
+ raise ValueError("Download path suffix must not contain /.")
+ return download_path_suffix
diff --git a/atr/post/__init__.py b/atr/post/candidate.py
similarity index 63%
copy from atr/post/__init__.py
copy to atr/post/candidate.py
index 63c60e2..f5396f6 100644
--- a/atr/post/__init__.py
+++ b/atr/post/candidate.py
@@ -15,8 +15,17 @@
# specific language governing permissions and limitations
# under the License.
-from .example_test import respond as example_test
+"""candidate.py"""
-ROUTES_MODULE = True
+import werkzeug.wrappers.response as response
-__all__ = ["example_test"]
+import atr.blueprints.post as post
+import atr.routes.root as root
+import atr.session as session
+
+
[email protected]("/candidate/delete")
+async def delete(session: session.Committer) -> response.Response:
+ """Delete a release candidate."""
+ # TODO: We need to never retire revisions, if allowing release deletion
+ return await session.redirect(root.index, error="Not yet implemented")
diff --git a/atr/route.py b/atr/route.py
index e4860dc..db30a3f 100644
--- a/atr/route.py
+++ b/atr/route.py
@@ -53,32 +53,6 @@ T = TypeVar("T")
_MEASURE_PERFORMANCE: Final[bool] = True
-# | 1 | RSA (Encrypt or Sign) [HAC] |
-# | 2 | RSA Encrypt-Only [HAC] |
-# | 3 | RSA Sign-Only [HAC] |
-# | 16 | Elgamal (Encrypt-Only) [ELGAMAL] [HAC] |
-# | 17 | DSA (Digital Signature Algorithm) [FIPS186] [HAC] |
-# | 18 | ECDH public key algorithm |
-# | 19 | ECDSA public key algorithm [FIPS186] |
-# | 20 | Reserved (formerly Elgamal Encrypt or Sign) |
-# | 21 | Reserved for Diffie-Hellman |
-# | | (X9.42, as defined for IETF-S/MIME) |
-# | 22 | EdDSA [I-D.irtf-cfrg-eddsa] |
-# - https://lists.gnupg.org/pipermail/gnupg-devel/2017-April/032762.html
-# TODO: (Obviously we should move this, but where to?)
-algorithms: Final[dict[int, str]] = {
- 1: "RSA",
- 2: "RSA",
- 3: "RSA",
- 16: "Elgamal",
- 17: "DSA",
- 18: "ECDH",
- 19: "ECDSA",
- 21: "Diffie-Hellman",
- 22: "EdDSA",
-}
-
-
class AsyncFileHandler(logging.Handler):
"""A logging handler that writes logs asynchronously using aiofiles."""
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index 0d64cae..5e15b9b 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -15,9 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-import atr.routes.announce as announce
-import atr.routes.candidate as candidate
-import atr.routes.committees as committees
import atr.routes.compose as compose
import atr.routes.distribution as distribution
import atr.routes.docs as docs
@@ -45,9 +42,6 @@ import atr.routes.vote as vote
import atr.routes.voting as voting
__all__ = [
- "announce",
- "candidate",
- "committees",
"compose",
"distribution",
"docs",
diff --git a/atr/routes/announce.py b/atr/routes/announce.py
deleted file mode 100644
index 0fa8f8d..0000000
--- a/atr/routes/announce.py
+++ /dev/null
@@ -1,198 +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 Any
-
-import quart
-import werkzeug.wrappers.response as response
-
-# TODO: Improve upon the routes_release pattern
-import atr.config as config
-import atr.construct as construct
-import atr.forms as forms
-import atr.models.sql as sql
-import atr.route as route
-import atr.routes.release as routes_release
-import atr.storage as storage
-import atr.template as template
-import atr.util as util
-
-
-class AnnounceError(Exception):
- """Exception for announce errors."""
-
-
-class AnnounceForm(forms.Typed):
- """Form for announcing a release preview."""
-
- preview_name = forms.hidden()
- preview_revision = forms.hidden()
- mailing_list = forms.radio("Send vote email to")
- download_path_suffix = forms.optional("Download path suffix")
- confirm_announce = forms.boolean("Confirm")
- subject = forms.optional("Subject")
- body = forms.textarea("Body", optional=True)
- submit = forms.submit("Send announcement email")
-
-
-class DeleteForm(forms.Typed):
- """Form for deleting a release preview."""
-
- preview_name = forms.string("Preview name")
- confirm_delete = forms.string(
- "Confirmation",
- validators=forms.constant("DELETE"),
- )
- submit = forms.submit("Delete preview")
-
-
[email protected]("/announce/<project_name>/<version_name>")
-async def selected(session: route.CommitterSession, project_name: str,
version_name: str) -> str | response.Response:
- """Allow the user to announce a release preview."""
- await session.check_access(project_name)
-
- release = await session.release(
- project_name, version_name, with_committee=True,
phase=sql.ReleasePhase.RELEASE_PREVIEW
- )
- announce_form = await
_create_announce_form_instance(util.permitted_announce_recipients(session.uid))
- # Hidden fields
- announce_form.preview_name.data = release.name
- # There must be a revision to announce
- announce_form.preview_revision.data = release.unwrap_revision_number
-
- # Variables used in defaults for subject and body
- project_display_name = release.project.display_name or release.project.name
-
- # The subject cannot be changed by the user
- announce_form.subject.data = f"[ANNOUNCE] {project_display_name}
{version_name} released"
- # The body can be changed, either from VoteTemplate or from the form
- announce_form.body.data = await
construct.announce_release_default(project_name)
- # The download path suffix can be changed
- # The defaults depend on whether the project is top level or not
- if (committee := release.project.committee) is None:
- raise ValueError("Release has no committee")
- top_level_project = release.project.name == util.unwrap(committee.name)
- # These defaults are as per #136, but we allow the user to change the
result
- announce_form.download_path_suffix.data = (
- "/" if top_level_project else
f"/{release.project.name}-{release.version}/"
- )
- # This must NOT end with a "/"
- description_download_prefix = f"https://{config.get().APP_HOST}/downloads"
- if committee.is_podling:
- description_download_prefix += "/incubator"
- description_download_prefix += f"/{committee.name}"
- announce_form.download_path_suffix.description = f"The URL will be
{description_download_prefix} plus this suffix"
-
- return await template.render(
- "announce-selected.html",
- release=release,
- announce_form=announce_form,
- user_tests_address=util.USER_TESTS_ADDRESS,
- )
-
-
[email protected]("/announce/<project_name>/<version_name>", methods=["POST"])
-async def selected_post(
- session: route.CommitterSession, project_name: str, version_name: str
-) -> str | response.Response:
- """Handle the announcement form submission and promote the preview to
release."""
- await session.check_access(project_name)
-
- permitted_recipients = util.permitted_announce_recipients(session.uid)
- announce_form = await _create_announce_form_instance(
- permitted_recipients,
- data=await quart.request.form,
- )
-
- if not (await announce_form.validate_on_submit()):
- error_message = "Invalid submission"
- if announce_form.errors:
- error_details = "; ".join([f"{field}: {', '.join(errs)}" for
field, errs in announce_form.errors.items()])
- error_message = f"{error_message}: {error_details}"
-
- # Render the page again, with errors
- release: sql.Release = await session.release(
- project_name, version_name, with_committee=True,
phase=sql.ReleasePhase.RELEASE_PREVIEW
- )
- await quart.flash(error_message, "error")
- return await template.render("announce-selected.html",
release=release, announce_form=announce_form)
-
- recipient = str(announce_form.mailing_list.data)
- if recipient not in permitted_recipients:
- raise AnnounceError(f"You are not permitted to send announcements to
{recipient}")
-
- subject = str(announce_form.subject.data)
- body = str(announce_form.body.data)
- preview_revision_number = str(announce_form.preview_revision.data)
- download_path_suffix = _download_path_suffix_validated(announce_form)
-
- try:
- async with storage.write_as_project_committee_member(project_name,
session) as wacm:
- await wacm.announce.release(
- project_name,
- version_name,
- preview_revision_number,
- recipient,
- subject,
- body,
- download_path_suffix,
- session.uid,
- session.fullname,
- )
- except storage.AccessError as e:
- return await session.redirect(selected, error=str(e),
project_name=project_name, version_name=version_name)
-
- routes_release_finished = routes_release.finished # type: ignore[has-type]
- return await session.redirect(
- routes_release_finished,
- success="Preview successfully announced",
- project_name=project_name,
- )
-
-
-async def _create_announce_form_instance(
- permitted_recipients: list[str], *, data: dict[str, Any] | None = None
-) -> AnnounceForm:
- """Create and return an instance of the AnnounceForm."""
-
- mailing_list_choices: forms.Choices = sorted([(recipient, recipient) for
recipient in permitted_recipients])
- mailing_list_default = util.USER_TESTS_ADDRESS
-
- form_instance = await AnnounceForm.create_form(data=data)
- forms.choices(
- form_instance.mailing_list,
- mailing_list_choices,
- mailing_list_default,
- )
- return form_instance
-
-
-def _download_path_suffix_validated(announce_form: AnnounceForm) -> str:
- download_path_suffix = str(announce_form.download_path_suffix.data)
- if (".." in download_path_suffix) or ("//" in download_path_suffix):
- raise ValueError("Download path suffix must not contain .. or //")
- if download_path_suffix.startswith("./"):
- download_path_suffix = download_path_suffix[1:]
- elif download_path_suffix == ".":
- download_path_suffix = "/"
- if not download_path_suffix.startswith("/"):
- download_path_suffix = "/" + download_path_suffix
- if not download_path_suffix.endswith("/"):
- download_path_suffix = download_path_suffix + "/"
- if "/." in download_path_suffix:
- raise ValueError("Download path suffix must not contain /.")
- return download_path_suffix
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 9bafb90..33a200c 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -35,6 +35,7 @@ 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
import atr.storage.types as types
@@ -185,7 +186,7 @@ async def add(session: route.CommitterSession) -> str:
user_committees=participant_of_committees,
form=form,
key_info=key_info,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
)
@@ -274,7 +275,7 @@ async def details(session: route.CommitterSession,
fingerprint: str) -> str | re
"keys-details.html",
key=key,
form=form,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
now=datetime.datetime.now(datetime.UTC),
asf_id=session.uid,
)
@@ -335,7 +336,7 @@ async def keys(session: route.CommitterSession) -> str:
user_keys=user_keys,
user_ssh_keys=user_ssh_keys,
committees=user_committees_with_keys,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
status_message=status_message,
status_type=status_type,
now=datetime.datetime.now(datetime.UTC),
@@ -447,7 +448,7 @@ async def upload(session: route.CommitterSession) -> str:
committee_map=committee_map,
form=form,
results=results,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
submitted_committees=submitted_committees,
)
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index e3e662c..cd4b5e4 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -36,6 +36,7 @@ import atr.models.policy as policy
import atr.models.sql as sql
import atr.registry as registry
import atr.route as route
+import atr.shared as shared
import atr.storage as storage
import atr.template as template
import atr.user as user
@@ -360,7 +361,7 @@ async def view(session: route.CommitterSession, name: str)
-> response.Response
return await template.render(
"project-view.html",
project=project,
- algorithms=route.algorithms,
+ algorithms=shared.algorithms,
candidate_drafts=candidate_drafts,
candidates=candidates,
previews=previews,
diff --git a/atr/server.py b/atr/server.py
index 58a5bf6..54c3808 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -128,7 +128,9 @@ def app_setup_context(app: base.QuartApp) -> None:
@app.context_processor
async def app_wide() -> dict[str, Any]:
import atr.admin as admin
+ import atr.get as get
import atr.metadata as metadata
+ import atr.post as post
import atr.routes as routes
import atr.routes.mapping as mapping
@@ -137,9 +139,11 @@ def app_setup_context(app: base.QuartApp) -> None:
"as_url": util.as_url,
"commit": metadata.commit,
"current_user": await asfquart.session.read(),
+ "get": get,
"is_admin_fn": user.is_admin,
"is_viewing_as_admin_fn": util.is_user_viewing_as_admin,
"is_committee_member_fn": user.is_committee_member,
+ "post": post,
"routes": routes,
"static_url": util.static_url,
"unfinished_releases_fn": interaction.unfinished_releases,
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
new file mode 100644
index 0000000..3604143
--- /dev/null
+++ b/atr/shared/__init__.py
@@ -0,0 +1,50 @@
+# 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 Final
+
+import atr.shared.announce as announce
+
+# | 1 | RSA (Encrypt or Sign) [HAC] |
+# | 2 | RSA Encrypt-Only [HAC] |
+# | 3 | RSA Sign-Only [HAC] |
+# | 16 | Elgamal (Encrypt-Only) [ELGAMAL] [HAC] |
+# | 17 | DSA (Digital Signature Algorithm) [FIPS186] [HAC] |
+# | 18 | ECDH public key algorithm |
+# | 19 | ECDSA public key algorithm [FIPS186] |
+# | 20 | Reserved (formerly Elgamal Encrypt or Sign) |
+# | 21 | Reserved for Diffie-Hellman |
+# | | (X9.42, as defined for IETF-S/MIME) |
+# | 22 | EdDSA [I-D.irtf-cfrg-eddsa] |
+# - https://lists.gnupg.org/pipermail/gnupg-devel/2017-April/032762.html
+
+algorithms: Final[dict[int, str]] = {
+ 1: "RSA",
+ 2: "RSA",
+ 3: "RSA",
+ 16: "Elgamal",
+ 17: "DSA",
+ 18: "ECDH",
+ 19: "ECDSA",
+ 21: "Diffie-Hellman",
+ 22: "EdDSA",
+}
+
+
+__all__ = [
+ "announce",
+]
diff --git a/atr/shared/announce.py b/atr/shared/announce.py
new file mode 100644
index 0000000..7d3eb03
--- /dev/null
+++ b/atr/shared/announce.py
@@ -0,0 +1,49 @@
+# 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 Any
+
+import atr.forms as forms
+import atr.util as util
+
+
+class Form(forms.Typed):
+ """Form for announcing a release preview."""
+
+ preview_name = forms.hidden()
+ preview_revision = forms.hidden()
+ mailing_list = forms.radio("Send vote email to")
+ download_path_suffix = forms.optional("Download path suffix")
+ confirm_announce = forms.boolean("Confirm")
+ subject = forms.optional("Subject")
+ body = forms.textarea("Body", optional=True)
+ submit = forms.submit("Send announcement email")
+
+
+async def create_form(permitted_recipients: list[str], *, data: dict[str, Any]
| None = None) -> Form:
+ """Create and return an instance of the announce form."""
+
+ mailing_list_choices: forms.Choices = sorted([(recipient, recipient) for
recipient in permitted_recipients])
+ mailing_list_default = util.USER_TESTS_ADDRESS
+
+ form_instance = await Form.create_form(data=data)
+ forms.choices(
+ form_instance.mailing_list,
+ mailing_list_choices,
+ mailing_list_default,
+ )
+ return form_instance
diff --git a/atr/templates/announce-selected.html
b/atr/templates/announce-selected.html
index feeeb79..d868edb 100644
--- a/atr/templates/announce-selected.html
+++ b/atr/templates/announce-selected.html
@@ -67,7 +67,7 @@
<form method="post"
id="announce-release-form"
- action="{{ as_url(routes.announce.selected_post,
project_name=release.project.name, version_name=release.version) }}"
+ action="{{ as_url(post.announce.selected,
project_name=release.project.name, version_name=release.version) }}"
class="atr-canary py-4 px-5 mb-4 border rounded">
{{ announce_form.hidden_tag() }}
diff --git a/atr/templates/check-selected-release-info.html
b/atr/templates/check-selected-release-info.html
index 869fa22..1e84a78 100644
--- a/atr/templates/check-selected-release-info.html
+++ b/atr/templates/check-selected-release-info.html
@@ -75,7 +75,7 @@
{% elif phase == "release_candidate" %}
<a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-primary"><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) }}"
+ <a href="{{ as_url(get.candidate.view,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-secondary"><i class="bi bi-eye me-1"></i> View
files</a>
{% if can_resolve and release.vote_manual %}
<a href="{{ as_url(routes.resolve.manual_selected,
project_name=release.project.name, version_name=release.version) }}"
diff --git a/atr/templates/committee-directory.html
b/atr/templates/committee-directory.html
index 3a65e94..73d6904 100644
--- a/atr/templates/committee-directory.html
+++ b/atr/templates/committee-directory.html
@@ -97,7 +97,7 @@
<div class="row mb-3 align-items-center">
<div class="col">
<h3 class="card-title fs-4 mb-0">
- <a href="{{ as_url(routes.committees.view,
name=committee.name) }}"
+ <a href="{{ as_url(get.committees.view, name=committee.name)
}}"
class="text-decoration-none text-dark
page-committee-title-link">{{ committee.display_name }}</a>
</h3>
</div>
diff --git a/atr/templates/file-selected-path.html
b/atr/templates/file-selected-path.html
index 5a598b9..5cf8dcd 100644
--- a/atr/templates/file-selected-path.html
+++ b/atr/templates/file-selected-path.html
@@ -13,7 +13,7 @@
{% if phase_key == "draft" %}
{% set back_url = as_url(routes.draft.view,
project_name=release.project.name, version_name=release.version) %}
{% elif phase_key == "candidate" %}
- {% set back_url = as_url(routes.candidate.view,
project_name=release.project.name, version_name=release.version) %}
+ {% set back_url = as_url(get.candidate.view,
project_name=release.project.name, version_name=release.version) %}
{% elif phase_key == "preview" %}
{% set back_url = as_url(routes.preview.view,
project_name=release.project.name, version_name=release.version) %}
{% elif phase_key == "release" %}
diff --git a/atr/templates/finish-selected.html
b/atr/templates/finish-selected.html
index 6d32898..9082e5e 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -87,7 +87,7 @@
Show revisions
</a>
<a title="Announce and distribute {{ release.name }}"
- href="{{ as_url(routes.announce.selected,
project_name=release.project.name, version_name=release.version) }}"
+ href="{{ as_url(get.announce.selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-success">
<i class="bi bi-check-circle"></i>
Announce and distribute
diff --git a/atr/templates/includes/sidebar.html
b/atr/templates/includes/sidebar.html
index ef2a63a..2967c55 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -43,7 +43,7 @@
{% endif %}
<li>
<i class="bi bi-collection"></i>
- <a href="{{ as_url(routes.committees.directory) }}">Search
committees</a>
+ <a href="{{ as_url(get.committees.directory) }}">Search
committees</a>
</li>
{% if current_user.uid == "sbp" %}
<!--
diff --git a/atr/templates/phase-view.html b/atr/templates/phase-view.html
index e098fd0..ac4564c 100644
--- a/atr/templates/phase-view.html
+++ b/atr/templates/phase-view.html
@@ -101,7 +101,7 @@
{% if phase_key == "draft" %}
{% set file_url = as_url(routes.file.selected_path,
project_name=release.project.name, version_name=release.version,
file_path=stat.path) %}
{% elif phase_key == "candidate" %}
- {% set file_url = as_url(routes.candidate.view_path,
project_name=release.project.name, version_name=release.version,
file_path=stat.path) %}
+ {% set file_url = as_url(get.candidate.view_path,
project_name=release.project.name, version_name=release.version,
file_path=stat.path) %}
{% elif phase_key == "preview" %}
{% set file_url = as_url(routes.preview.view_path,
project_name=release.project.name, version_name=release.version,
file_path=stat.path) %}
{% elif phase_key == "release" %}
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index a2c1aca..c745561 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -56,7 +56,7 @@
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-3 small mb-1">
- <a href="{{ as_url(routes.committees.view,
name=project.committee.name) }}">{{ project.committee.display_name }}</a>
+ <a href="{{ as_url(get.committees.view, name=project.committee.name)
}}">{{ project.committee.display_name }}</a>
</div>
</div>
</div>
@@ -433,7 +433,7 @@
<h2>Candidate releases</h2>
<div class="d-flex flex-wrap gap-2 mb-4">
{% for candidate in candidates %}
- <a href="{{ as_url(routes.candidate.view, project_name=project.name,
version_name=candidate.version) }}"
+ <a href="{{ as_url(get.candidate.view, project_name=project.name,
version_name=candidate.version) }}"
class="btn btn-sm btn-outline-info py-2 px-3"
title="View candidate {{ project.name }} {{ candidate.version }}">
{{ project.name }} {{ candidate.version }}
diff --git a/atr/templates/release-select.html
b/atr/templates/release-select.html
index 9ec8b91..0a87864 100644
--- a/atr/templates/release-select.html
+++ b/atr/templates/release-select.html
@@ -36,7 +36,7 @@
{% 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) %}
+ {% set target_url = as_url(get.announce.selected,
project_name=project.name, version_name=release.version) %}
{% set badge_class = "bg-success" %}
{% endif %}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]