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 f0b337e  Move root, sbom, and start routes to the new layout
f0b337e is described below

commit f0b337e44deace48416192115491f1c7e2ab0149
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Oct 28 16:13:07 2025 +0000

    Move root, sbom, and start routes to the new layout
---
 atr/admin/__init__.py                        |   2 +-
 atr/get/__init__.py                          |   6 ++
 atr/get/download.py                          |   5 +-
 atr/{routes => get}/root.py                  |  37 ++++-----
 atr/{routes => get}/sbom.py                  | 107 ++----------------------
 atr/{post/candidate.py => get/start.py}      |  15 ++--
 atr/post/__init__.py                         |   4 +
 atr/post/candidate.py                        |   4 +-
 atr/post/draft.py                            |  16 ++--
 atr/post/preview.py                          |   3 +-
 atr/post/sbom.py                             | 120 +++++++++++++++++++++++++++
 atr/post/{candidate.py => start.py}          |  13 ++-
 atr/routes/__init__.py                       |   6 --
 atr/routes/voting.py                         |   3 +-
 atr/shared/__init__.py                       |   2 +
 atr/shared/finish.py                         |   4 +-
 atr/{routes => shared}/start.py              |   3 +-
 atr/templates/check-selected-path-table.html |   2 +-
 atr/templates/check-selected.html            |   2 +-
 atr/templates/error.html                     |   2 +-
 atr/templates/finish-selected.html           |   2 +-
 atr/templates/includes/sidebar.html          |  12 +--
 atr/templates/index-committer.html           |   2 +-
 atr/templates/notfound.html                  |   2 +-
 atr/templates/project-view.html              |   2 +-
 atr/templates/releases-finished.html         |   2 +-
 atr/templates/revisions-selected.html        |   2 +-
 atr/templates/start-selected.html            |   4 +-
 atr/templates/todo.html                      |  19 -----
 29 files changed, 206 insertions(+), 197 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index c369435..749028b 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -126,7 +126,7 @@ async def browse_as_post(session: web.Committer) -> str | 
response.Response:
 async def _browse_as(session: web.Committer) -> str | response.Response:
     """Allows an admin to browse as another user."""
     # TODO: Enable this in debugging mode only?
-    from atr.routes import root
+    import atr.get.root as root
 
     form = await BrowseAsUserForm.create_form()
     if not (await form.validate_on_submit()):
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index b142cc5..53a0ddd 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -39,6 +39,9 @@ import atr.get.release as release
 import atr.get.report as report
 import atr.get.resolve as resolve
 import atr.get.revisions as revisions
+import atr.get.root as root
+import atr.get.sbom as sbom
+import atr.get.start as start
 import atr.get.vote as vote
 
 ROUTES_MODULE: Final[Literal[True]] = True
@@ -64,5 +67,8 @@ __all__ = [
     "report",
     "resolve",
     "revisions",
+    "root",
+    "sbom",
+    "start",
     "vote",
 ]
diff --git a/atr/get/download.py b/atr/get/download.py
index bcfef95..c1d81ed 100644
--- a/atr/get/download.py
+++ b/atr/get/download.py
@@ -31,7 +31,6 @@ import atr.db as db
 import atr.htm as htm
 import atr.mapping as mapping
 import atr.models.sql as sql
-import atr.routes.root as root
 import atr.template as template
 import atr.util as util
 import atr.web as web
@@ -40,6 +39,8 @@ import atr.web as web
 @get.committer("/download/all/<project_name>/<version_name>")
 async def all_selected(session: web.Committer, project_name: str, 
version_name: str) -> response.Response | str:
     """Display download commands for a release."""
+    import atr.get.root as root
+
     async with db.session() as data:
         release = await session.release(project_name=project_name, 
version_name=version_name, phase=None, data=data)
         if not release:
@@ -147,6 +148,8 @@ async def zip_selected(
 
 async def _download_or_list(project_name: str, version_name: str, file_path: 
str) -> response.Response | quart.Response:
     """Download a file or list a directory from a release in any phase."""
+    import atr.get.root as root
+
     # await session.check_access(project_name)
 
     # Check that path is relative
diff --git a/atr/routes/root.py b/atr/get/root.py
similarity index 87%
rename from atr/routes/root.py
rename to atr/get/root.py
index 2744e15..6fd33b5 100644
--- a/atr/routes/root.py
+++ b/atr/get/root.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""root.py"""
-
 import pathlib
 from typing import Final
 
@@ -28,14 +26,15 @@ import sqlalchemy.orm as orm
 import sqlmodel
 import werkzeug.wrappers.response as response
 
+import atr.blueprints.get as get
 import atr.config as config
 import atr.db as db
 import atr.htm as htm
 import atr.models.sql as sql
-import atr.route as route
 import atr.template as template
 import atr.user as user
 import atr.util as util
+import atr.web as web
 
 _POLICIES: Final = htm.div[
     htm.h1["Release policy"],
@@ -61,14 +60,14 @@ _POLICIES: Final = htm.div[
 ]
 
 
[email protected]("/about")
-async def about(session: route.CommitterSession) -> str:
[email protected]("/about")
+async def about(session: web.Committer) -> str:
     """About page."""
     return await template.render("about.html")
 
 
[email protected]("/")
-async def index(session: route.CommitterSession | None) -> 
quart_response.Response | str:
[email protected]("/")
+async def index(session: web.Committer | None) -> quart_response.Response | 
str:
     """Show public info or an entry portal for participants."""
     session_data = await asfquart.session.read()
     if session_data:
@@ -142,21 +141,21 @@ async def index(session: route.CommitterSession | None) 
-> quart_response.Respon
     return await template.render("index-public.html")
 
 
[email protected]("/miscellaneous/resolved.json")
-async def resolved_json(session: route.CommitterSession | None) -> 
quart_response.Response:
[email protected]("/miscellaneous/resolved.json")
+async def resolved_json(session: web.Committer | None) -> 
quart_response.Response:
     json_path = pathlib.Path(config.get().PROJECT_ROOT) / "atr" / "static" / 
"json" / "resolved.json"
     async with aiofiles.open(json_path) as f:
         content = await f.read()
     return quart_response.Response(content, mimetype="application/json")
 
 
[email protected]("/policies")
-async def policies(session: route.CommitterSession | None) -> str:
[email protected]("/policies")
+async def policies(session: web.Committer | None) -> str:
     return await template.blank("Policies", content=_POLICIES)
 
 
[email protected]("/test-login")
-async def test_login(session: route.CommitterSession | None) -> 
response.Response:
[email protected]("/test-login")
+async def test_login(session: web.Committer | None) -> response.Response:
     if not config.get().ALLOW_TESTS:
         raise base.ASFQuartException("Test login not enabled", errorcode=404)
 
@@ -172,16 +171,10 @@ async def test_login(session: route.CommitterSession | 
None) -> response.Respons
     }
 
     asfquart.session.write(session_data)
-    return await route.redirect(index)
-
-
[email protected]("/todo", methods=["POST"])
-async def todo(session: route.CommitterSession) -> str:
-    """POST target for development."""
-    return await template.render("todo.html")
+    return await web.redirect(index)
 
 
[email protected]("/tutorial")
-async def tutorial(session: route.CommitterSession) -> str:
[email protected]("/tutorial")
+async def tutorial(session: web.Committer) -> str:
     """Tutorial page."""
     return await template.render("tutorial.html")
diff --git a/atr/routes/sbom.py b/atr/get/sbom.py
similarity index 74%
rename from atr/routes/sbom.py
rename to atr/get/sbom.py
index 8ae5245..8e432d2 100644
--- a/atr/routes/sbom.py
+++ b/atr/get/sbom.py
@@ -18,78 +18,29 @@
 from __future__ import annotations
 
 import json
-import pathlib
 from typing import TYPE_CHECKING, Any
 
 import asfquart.base as base
 import markupsafe
-import quart
 
+import atr.blueprints.get as get
 import atr.db as db
 import atr.forms as forms
 import atr.htm as htm
-import atr.log as log
 import atr.models.results as results
 import atr.models.sql as sql
-import atr.route as route
+import atr.post as post
 import atr.sbom as sbom
-import atr.storage as storage
 import atr.template as template
 import atr.util as util
+import atr.web as web
 
 if TYPE_CHECKING:
     import collections.abc
 
-    import werkzeug.wrappers.response as response
 
-
[email protected]("/sbom/augment/<project_name>/<version_name>/<path:file_path>",
 methods=["POST"])
-async def augment(
-    session: route.CommitterSession, project_name: str, version_name: str, 
file_path: str
-) -> response.Response:
-    """Augment a CycloneDX SBOM file."""
-    await session.check_access(project_name)
-
-    await util.validate_empty_form()
-    rel_path = pathlib.Path(file_path)
-
-    # Check that the file is a .cdx.json archive before creating a revision
-    if not (file_path.endswith(".cdx.json")):
-        raise base.ASFQuartException("SBOM augmentation is only supported for 
.cdx.json files", errorcode=400)
-
-    try:
-        async with db.session() as data:
-            release = await data.release(project_name=project_name, 
version=version_name).demand(
-                RuntimeError("Release does not exist for new revision 
creation")
-            )
-            revision_number = release.latest_revision_number
-            if revision_number is None:
-                raise RuntimeError("No revision number found for new revision 
creation")
-            log.info(f"Augmenting SBOM for {project_name} {version_name} 
{revision_number} {rel_path}")
-        async with storage.write_as_project_committee_member(project_name) as 
wacm:
-            sbom_task = await wacm.sbom.augment_cyclonedx(project_name, 
version_name, revision_number, rel_path)
-
-    except Exception as e:
-        log.exception("Error augmenting SBOM:")
-        await quart.flash(f"Error augmenting SBOM: {e!s}", "error")
-        return await session.redirect(
-            report,
-            project=project_name,
-            version=version_name,
-            file_path=str(rel_path),
-        )
-
-    return await session.redirect(
-        report,
-        success=f"SBOM augmentation task queued for {rel_path.name} (task ID: 
{util.unwrap(sbom_task.id)})",
-        project=project_name,
-        version=version_name,
-        file_path=str(rel_path),
-    )
-
-
[email protected]("/sbom/report/<project>/<version>/<path:file_path>")
-async def report(session: route.CommitterSession, project: str, version: str, 
file_path: str) -> str:
[email protected]("/sbom/report/<project>/<version>/<path:file_path>")
+async def report(session: web.Committer, project: str, version: str, 
file_path: str) -> str:
     await session.check_access(project)
     await session.release(project, version)
     async with db.session() as data:
@@ -144,7 +95,7 @@ async def report(session: route.CommitterSession, project: 
str, version: str, fi
     # TODO: Add a field to the SBOM to show that it's been augmented
     # And then don't allow it to be augmented again
     action = util.as_url(
-        augment,
+        post.sbom.augment,
         project_name=project,
         version_name=version,
         file_path=file_path,
@@ -200,50 +151,6 @@ async def report(session: route.CommitterSession, project: 
str, version: str, fi
     return await template.blank("SBOM report", content=block.collect())
 
 
[email protected]("/sbom/scan/<project_name>/<version_name>/<path:file_path>", 
methods=["POST"])
-async def scan(
-    session: route.CommitterSession, project_name: str, version_name: str, 
file_path: str
-) -> response.Response:
-    """Scan a CycloneDX SBOM file for vulnerabilities using OSV."""
-    await session.check_access(project_name)
-
-    await util.validate_empty_form()
-    rel_path = pathlib.Path(file_path)
-
-    if not (file_path.endswith(".cdx.json")):
-        raise base.ASFQuartException("OSV scanning is only supported for 
.cdx.json files", errorcode=400)
-
-    try:
-        async with db.session() as data:
-            release = await data.release(project_name=project_name, 
version=version_name).demand(
-                RuntimeError("Release does not exist for OSV scan")
-            )
-            revision_number = release.latest_revision_number
-            if revision_number is None:
-                raise RuntimeError("No revision number found for OSV scan")
-            log.info(f"Starting OSV scan for {project_name} {version_name} 
{revision_number} {rel_path}")
-        async with storage.write_as_project_committee_member(project_name) as 
wacm:
-            sbom_task = await wacm.sbom.osv_scan_cyclonedx(project_name, 
version_name, revision_number, rel_path)
-
-    except Exception as e:
-        log.exception("Error starting OSV scan:")
-        await quart.flash(f"Error starting OSV scan: {e!s}", "error")
-        return await session.redirect(
-            report,
-            project=project_name,
-            version=version_name,
-            file_path=str(rel_path),
-        )
-
-    return await session.redirect(
-        report,
-        success=f"OSV vulnerability scan queued for {rel_path.name} (task ID: 
{util.unwrap(sbom_task.id)})",
-        project=project_name,
-        version=version_name,
-        file_path=str(rel_path),
-    )
-
-
 def _extract_vulnerability_severity(vuln: dict[str, Any]) -> str:
     """Extract severity information from vulnerability data."""
     db_specific = vuln.get("database_specific", {})
@@ -323,7 +230,7 @@ def _vulnerability_scan_button(
     block.p["No vulnerability scan has been performed for this revision."]
 
     action = util.as_url(
-        scan,
+        post.sbom.scan,
         project_name=project,
         version_name=version,
         file_path=file_path,
diff --git a/atr/post/candidate.py b/atr/get/start.py
similarity index 70%
copy from atr/post/candidate.py
copy to atr/get/start.py
index ca0b011..1ebbb7d 100644
--- a/atr/post/candidate.py
+++ b/atr/get/start.py
@@ -15,16 +15,15 @@
 # specific language governing permissions and limitations
 # under the License.
 
+
 import werkzeug.wrappers.response as response
 
-import atr.blueprints.post as post
+import atr.blueprints.get as get
+import atr.shared as shared
 import atr.web as web
 
 
[email protected]("/candidate/delete")
-async def delete(session: web.Committer) -> response.Response:
-    """Delete a release candidate."""
-    import atr.routes.root as root
-
-    # TODO: We need to never retire revisions, if allowing release deletion
-    return await session.redirect(root.index, error="Not yet implemented")
[email protected]("/start/<project_name>")
+async def selected(session: web.Committer, project_name: str) -> 
response.Response | str:
+    """Allow the user to start a new release draft, or handle its 
submission."""
+    return await shared.start.selected(session, project_name)
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index 5f4d930..e3ce6cf 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -28,6 +28,8 @@ import atr.post.preview as preview
 import atr.post.projects as projects
 import atr.post.resolve as resolve
 import atr.post.revisions as revisions
+import atr.post.sbom as sbom
+import atr.post.start as start
 import atr.post.vote as vote
 
 ROUTES_MODULE: Final[Literal[True]] = True
@@ -44,5 +46,7 @@ __all__ = [
     "projects",
     "resolve",
     "revisions",
+    "sbom",
+    "start",
     "vote",
 ]
diff --git a/atr/post/candidate.py b/atr/post/candidate.py
index ca0b011..1f2a426 100644
--- a/atr/post/candidate.py
+++ b/atr/post/candidate.py
@@ -24,7 +24,7 @@ import atr.web as web
 @post.committer("/candidate/delete")
 async def delete(session: web.Committer) -> response.Response:
     """Delete a release candidate."""
-    import atr.routes.root as root
+    import atr.get as get
 
     # TODO: We need to never retire revisions, if allowing release deletion
-    return await session.redirect(root.index, error="Not yet implemented")
+    return await session.redirect(get.root.index, error="Not yet implemented")
diff --git a/atr/post/draft.py b/atr/post/draft.py
index 0860d3f..11830e3 100644
--- a/atr/post/draft.py
+++ b/atr/post/draft.py
@@ -51,26 +51,26 @@ class VotePreviewForm(forms.Typed):
 @post.committer("/draft/delete")
 async def delete(session: web.Committer) -> response.Response:
     """Delete a candidate draft and all its associated files."""
-    import atr.routes.root as root
+    import atr.get as get
 
     form = await shared.draft.DeleteForm.create_form(data=await 
quart.request.form)
     if not await form.validate_on_submit():
         for _field, errors in form.errors.items():
             for error in errors:
                 await quart.flash(f"{error}", "error")
-        return await session.redirect(root.index)
+        return await session.redirect(get.root.index)
 
     release_name = form.release_name.data
     if not release_name:
-        return await session.redirect(root.index, error="Missing required 
parameters")
+        return await session.redirect(get.root.index, error="Missing required 
parameters")
 
     project_name = form.project_name.data
     if not project_name:
-        return await session.redirect(root.index, error="Missing required 
parameters")
+        return await session.redirect(get.root.index, error="Missing required 
parameters")
 
     version_name = form.version_name.data
     if not version_name:
-        return await session.redirect(root.index, error="Missing required 
parameters")
+        return await session.redirect(get.root.index, error="Missing required 
parameters")
 
     await session.check_access(project_name)
 
@@ -91,7 +91,7 @@ async def delete(session: web.Committer) -> response.Response:
         # Yet it works in preview.py
         await aioshutil.rmtree(draft_dir)  # type: ignore[call-arg]
 
-    return await session.redirect(root.index, success="Candidate draft deleted 
successfully")
+    return await session.redirect(get.root.index, success="Candidate draft 
deleted successfully")
 
 
 @post.committer("/draft/delete-file/<project_name>/<version_name>")
@@ -296,11 +296,11 @@ async def vote_preview(
     session: web.Committer, project_name: str, version_name: str
 ) -> quart.wrappers.response.Response | response.Response | str:
     """Show the vote email preview for a release."""
-    import atr.routes.root as root
+    import atr.get as get
 
     form = await VotePreviewForm.create_form(data=await quart.request.form)
     if not await form.validate_on_submit():
-        return await session.redirect(root.index, error="Invalid form data")
+        return await session.redirect(get.root.index, error="Invalid form 
data")
 
     release = await session.release(project_name, version_name)
     if release.committee is None:
diff --git a/atr/post/preview.py b/atr/post/preview.py
index 9e55944..7bc2f6d 100644
--- a/atr/post/preview.py
+++ b/atr/post/preview.py
@@ -23,7 +23,6 @@ import atr.construct as construct
 import atr.forms as forms
 import atr.log as log
 import atr.models.sql as sql
-import atr.routes.root as root
 import atr.storage as storage
 import atr.web as web
 
@@ -80,6 +79,8 @@ async def announce_preview(
 @post.committer("/preview/delete")
 async def delete(session: web.Committer) -> response.Response:
     """Delete a preview and all its associated files."""
+    import atr.get.root as root
+
     # TODO: Where does this come from? A static template?
     form = await DeleteForm.create_form(data=await quart.request.form)
 
diff --git a/atr/post/sbom.py b/atr/post/sbom.py
new file mode 100644
index 0000000..354407a
--- /dev/null
+++ b/atr/post/sbom.py
@@ -0,0 +1,120 @@
+# 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 pathlib
+from typing import TYPE_CHECKING
+
+import asfquart.base as base
+import quart
+
+import atr.blueprints.post as post
+import atr.db as db
+import atr.get as get
+import atr.log as log
+import atr.storage as storage
+import atr.util as util
+import atr.web as web
+
+if TYPE_CHECKING:
+    import werkzeug.wrappers.response as response
+
+
[email protected]("/sbom/augment/<project_name>/<version_name>/<path:file_path>")
+async def augment(session: web.Committer, project_name: str, version_name: 
str, file_path: str) -> response.Response:
+    """Augment a CycloneDX SBOM file."""
+    await session.check_access(project_name)
+
+    await util.validate_empty_form()
+    rel_path = pathlib.Path(file_path)
+
+    # Check that the file is a .cdx.json archive before creating a revision
+    if not (file_path.endswith(".cdx.json")):
+        raise base.ASFQuartException("SBOM augmentation is only supported for 
.cdx.json files", errorcode=400)
+
+    try:
+        async with db.session() as data:
+            release = await data.release(project_name=project_name, 
version=version_name).demand(
+                RuntimeError("Release does not exist for new revision 
creation")
+            )
+            revision_number = release.latest_revision_number
+            if revision_number is None:
+                raise RuntimeError("No revision number found for new revision 
creation")
+            log.info(f"Augmenting SBOM for {project_name} {version_name} 
{revision_number} {rel_path}")
+        async with storage.write_as_project_committee_member(project_name) as 
wacm:
+            sbom_task = await wacm.sbom.augment_cyclonedx(project_name, 
version_name, revision_number, rel_path)
+
+    except Exception as e:
+        log.exception("Error augmenting SBOM:")
+        await quart.flash(f"Error augmenting SBOM: {e!s}", "error")
+        return await session.redirect(
+            get.sbom.report,
+            project=project_name,
+            version=version_name,
+            file_path=str(rel_path),
+        )
+
+    return await session.redirect(
+        get.sbom.report,
+        success=f"SBOM augmentation task queued for {rel_path.name} (task ID: 
{util.unwrap(sbom_task.id)})",
+        project=project_name,
+        version=version_name,
+        file_path=str(rel_path),
+    )
+
+
[email protected]("/sbom/scan/<project_name>/<version_name>/<path:file_path>")
+async def scan(session: web.Committer, project_name: str, version_name: str, 
file_path: str) -> response.Response:
+    """Scan a CycloneDX SBOM file for vulnerabilities using OSV."""
+    await session.check_access(project_name)
+
+    await util.validate_empty_form()
+    rel_path = pathlib.Path(file_path)
+
+    if not (file_path.endswith(".cdx.json")):
+        raise base.ASFQuartException("OSV scanning is only supported for 
.cdx.json files", errorcode=400)
+
+    try:
+        async with db.session() as data:
+            release = await data.release(project_name=project_name, 
version=version_name).demand(
+                RuntimeError("Release does not exist for OSV scan")
+            )
+            revision_number = release.latest_revision_number
+            if revision_number is None:
+                raise RuntimeError("No revision number found for OSV scan")
+            log.info(f"Starting OSV scan for {project_name} {version_name} 
{revision_number} {rel_path}")
+        async with storage.write_as_project_committee_member(project_name) as 
wacm:
+            sbom_task = await wacm.sbom.osv_scan_cyclonedx(project_name, 
version_name, revision_number, rel_path)
+
+    except Exception as e:
+        log.exception("Error starting OSV scan:")
+        await quart.flash(f"Error starting OSV scan: {e!s}", "error")
+        return await session.redirect(
+            get.sbom.report,
+            project=project_name,
+            version=version_name,
+            file_path=str(rel_path),
+        )
+
+    return await session.redirect(
+        get.sbom.report,
+        success=f"OSV vulnerability scan queued for {rel_path.name} (task ID: 
{util.unwrap(sbom_task.id)})",
+        project=project_name,
+        version=version_name,
+        file_path=str(rel_path),
+    )
diff --git a/atr/post/candidate.py b/atr/post/start.py
similarity index 73%
copy from atr/post/candidate.py
copy to atr/post/start.py
index ca0b011..bc801e5 100644
--- a/atr/post/candidate.py
+++ b/atr/post/start.py
@@ -15,16 +15,15 @@
 # specific language governing permissions and limitations
 # under the License.
 
+
 import werkzeug.wrappers.response as response
 
 import atr.blueprints.post as post
+import atr.shared as shared
 import atr.web as web
 
 
[email protected]("/candidate/delete")
-async def delete(session: web.Committer) -> response.Response:
-    """Delete a release candidate."""
-    import atr.routes.root as root
-
-    # TODO: We need to never retire revisions, if allowing release deletion
-    return await session.redirect(root.index, error="Not yet implemented")
[email protected]("/start/<project_name>")
+async def selected(session: web.Committer, project_name: str) -> 
response.Response | str:
+    """Allow the user to start a new release draft, or handle its 
submission."""
+    return await shared.start.selected(session, project_name)
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index c4dfc96..30b164b 100644
--- a/atr/routes/__init__.py
+++ b/atr/routes/__init__.py
@@ -15,18 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import atr.routes.root as root
-import atr.routes.sbom as sbom
-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.voting as voting
 
 __all__ = [
-    "root",
-    "sbom",
-    "start",
     "tokens",
     "upload",
     "user",
diff --git a/atr/routes/voting.py b/atr/routes/voting.py
index 6c18027..0e84a10 100644
--- a/atr/routes/voting.py
+++ b/atr/routes/voting.py
@@ -31,7 +31,6 @@ 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.root as root
 import atr.storage as storage
 import atr.template as template
 import atr.user as user
@@ -122,6 +121,8 @@ async def start_vote_manual(
         # This verifies the state and sets the phase to RELEASE_CANDIDATE
         error = await wacp.release.promote_to_candidate(release.name, 
selected_revision_number, vote_manual=True)
     if error:
+        import atr.get.root as root
+
         return await session.redirect(root.index, error=error)
     return await session.redirect(
         vote.selected,
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index e958e06..822ed7c 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -33,6 +33,7 @@ import atr.shared.ignores as ignores
 import atr.shared.keys as keys
 import atr.shared.projects as projects
 import atr.shared.resolve as resolve
+import atr.shared.start as start
 import atr.shared.vote as vote
 import atr.storage as storage
 import atr.template as template
@@ -188,5 +189,6 @@ __all__ = [
     "keys",
     "projects",
     "resolve",
+    "start",
     "vote",
 ]
diff --git a/atr/shared/finish.py b/atr/shared/finish.py
index 607ce98..c197946 100644
--- a/atr/shared/finish.py
+++ b/atr/shared/finish.py
@@ -136,10 +136,10 @@ async def selected(
     try:
         source_files_rel, target_dirs = await 
_sources_and_targets(latest_revision_dir)
     except FileNotFoundError:
-        import atr.routes.root as root
+        import atr.get as get
 
         await quart.flash("Preview revision directory not found.", "error")
-        return await session.redirect(root.index)
+        return await session.redirect(get.root.index)
 
     formdata = None
     if quart.request.method == "POST":
diff --git a/atr/routes/start.py b/atr/shared/start.py
similarity index 95%
rename from atr/routes/start.py
rename to atr/shared/start.py
index a1c9917..307dc2c 100644
--- a/atr/routes/start.py
+++ b/atr/shared/start.py
@@ -41,8 +41,7 @@ class StartReleaseForm(forms.Typed):
     submit = forms.submit("Start new release")
 
 
[email protected]("/start/<project_name>", methods=["GET", "POST"])
-async def selected(session: route.CommitterSession, project_name: str) -> 
response.Response | str:
+async def selected(session: web.Committer, project_name: str) -> 
response.Response | str:
     """Allow the user to start a new release draft, or handle its 
submission."""
     await session.check_access(project_name)
 
diff --git a/atr/templates/check-selected-path-table.html 
b/atr/templates/check-selected-path-table.html
index 9f5ed05..42f95d7 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -66,7 +66,7 @@
                 </form>
               {% endif %}
               {% if path.suffixes[-2:] == [".cdx", ".json"] %}
-                <a href="{{ as_url(routes.sbom.report, project=project_name, 
version=version_name, file_path=path) }}"
+                <a href="{{ as_url(get.sbom.report, project=project_name, 
version=version_name, file_path=path) }}"
                    class="btn btn-sm btn-outline-secondary">Show SBOM</a>
               {% endif %}
               {% if has_errors %}
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index d229cd0..1c20499 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -31,7 +31,7 @@
 {% block content %}
   {% set phase = release.phase.value %}
   <p class="d-flex justify-content-between align-items-center">
-    <a href="{{ as_url(routes.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
+    <a href="{{ as_url(get.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
     <span>
       {% if phase == "release_candidate_draft" %}
         <strong class="atr-phase-one atr-phase-symbol">①</strong>
diff --git a/atr/templates/error.html b/atr/templates/error.html
index 3a76089..ba6d8ca 100644
--- a/atr/templates/error.html
+++ b/atr/templates/error.html
@@ -30,7 +30,7 @@
     </div>
 
     <div class="mt-4">
-      <a href="{{ as_url(routes.root.index) }}" class="btn btn-primary">Return 
to Home</a>
+      <a href="{{ as_url(get.root.index) }}" class="btn btn-primary">Return to 
Home</a>
     </div>
   </div>
 {% endblock content %}
diff --git a/atr/templates/finish-selected.html 
b/atr/templates/finish-selected.html
index 34794c5..a381335 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -43,7 +43,7 @@
 
 {% block content %}
   <p class="d-flex justify-content-between align-items-center">
-    <a href="{{ as_url(routes.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
+    <a href="{{ as_url(get.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
     <span>
       <span class="atr-phase-symbol-other">①</span>
       <span class="atr-phase-arrow">→</span>
diff --git a/atr/templates/includes/sidebar.html 
b/atr/templates/includes/sidebar.html
index 882bd8a..c9f860d 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -1,6 +1,6 @@
 <aside class="sidebar">
   <div class="sidebar-header">
-    <a href="{{ as_url(routes.root.index) }}" class="site-title">
+    <a href="{{ as_url(get.root.index) }}" class="site-title">
       <h1>
         <span class="apache">A<span class="rest">pache</span></span>
         <br />
@@ -33,7 +33,7 @@
       <ul>
         <li>
           <i class="bi bi-play-circle"></i>
-          <a href="{{ as_url(routes.root.index) }}">Make releases</a>
+          <a href="{{ as_url(get.root.index) }}">Make releases</a>
         </li>
         {% if current_user %}
           <li>
@@ -88,13 +88,13 @@
         {% for project_name, project_full_name in user_projects[:max_projects] 
%}
           <li>
             <i class="bi bi-plus-circle"></i>
-            <a href="{{ as_url(routes.start.selected, 
project_name=project_name) }}">{{ project_full_name.removeprefix("Apache 
").removesuffix(" (Incubating)") }}</a>
+            <a href="{{ as_url(get.start.selected, project_name=project_name) 
}}">{{ project_full_name.removeprefix("Apache ").removesuffix(" (Incubating)") 
}}</a>
           </li>
         {% endfor %}
         {% if user_projects|length > max_projects %}
           <li>
             <i class="bi bi-three-dots"></i>
-            (<a href="{{ as_url(routes.root.index) }}">{{ user_projects|length 
- max_projects }} more</a>)
+            (<a href="{{ as_url(get.root.index) }}">{{ user_projects|length - 
max_projects }} more</a>)
           </li>
         {% endif %}
       </ul>
@@ -117,7 +117,7 @@
       <ul>
         <li>
           <i class="bi bi-book"></i>
-          <a href="{{ as_url(routes.root.tutorial) }}">Tutorial</a>
+          <a href="{{ as_url(get.root.tutorial) }}">Tutorial</a>
         </li>
         <li>
           <i class="bi bi-book"></i>
@@ -125,7 +125,7 @@
         </li>
         <li>
           <i class="bi bi-book"></i>
-          <a href="{{ as_url(routes.root.policies) }}">ASF Policies</a>
+          <a href="{{ as_url(get.root.policies) }}">ASF Policies</a>
         </li>
         <li>
           <i class="bi bi-file-earmark-code"></i>
diff --git a/atr/templates/index-committer.html 
b/atr/templates/index-committer.html
index 74e7138..e2e1030 100644
--- a/atr/templates/index-committer.html
+++ b/atr/templates/index-committer.html
@@ -143,7 +143,7 @@
             </a>
           {% endfor %}
 
-          <a href="{{ as_url(routes.start.selected, project_name=project.name) 
}}"
+          <a href="{{ as_url(get.start.selected, project_name=project.name) }}"
              title="Start a new {{ project.display_name }} release"
              class="text-decoration-none">
             <div class="card h-100 shadow-sm atr-cursor-pointer page-card">
diff --git a/atr/templates/notfound.html b/atr/templates/notfound.html
index f559638..3e1dde7 100644
--- a/atr/templates/notfound.html
+++ b/atr/templates/notfound.html
@@ -25,7 +25,7 @@
     </div>
 
     <div class="mt-4">
-      <a href="{{ as_url(routes.root.index) }}" class="btn btn-primary">Return 
to Home</a>
+      <a href="{{ as_url(get.root.index) }}" class="btn btn-primary">Return to 
Home</a>
     </div>
   </div>
 {% endblock content %}
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index a57a426..a958388 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -34,7 +34,7 @@
   {{ forms.errors_summary(policy_form) }}
 
   <p class="mb-4">
-    <a href="{{ as_url(routes.start.selected, project_name=project.name) }}"
+    <a href="{{ as_url(get.start.selected, project_name=project.name) }}"
        class="btn btn-sm btn-outline-primary">Start a new release</a>
   </p>
 
diff --git a/atr/templates/releases-finished.html 
b/atr/templates/releases-finished.html
index 6841d74..7d9d456 100644
--- a/atr/templates/releases-finished.html
+++ b/atr/templates/releases-finished.html
@@ -10,7 +10,7 @@
 
 {% block content %}
   <p>
-    <a href="{{ as_url(routes.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
+    <a href="{{ as_url(get.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
   </p>
 
   <h1>Releases of {{ project.display_name }}</h1>
diff --git a/atr/templates/revisions-selected.html 
b/atr/templates/revisions-selected.html
index 98e031a..b660939 100644
--- a/atr/templates/revisions-selected.html
+++ b/atr/templates/revisions-selected.html
@@ -33,7 +33,7 @@
         <span class="atr-phase-three atr-phase-label">FINISH</span>
       </span>
     {% else %}
-      <a href="{{ as_url(routes.root.index) }}" class="atr-back-link">← Back 
to Select a release</a>
+      <a href="{{ as_url(get.root.index) }}" class="atr-back-link">← Back to 
Select a release</a>
     {% endif %}
   </p>
 
diff --git a/atr/templates/start-selected.html 
b/atr/templates/start-selected.html
index a755fbb..9b0d0af 100644
--- a/atr/templates/start-selected.html
+++ b/atr/templates/start-selected.html
@@ -13,7 +13,7 @@
   {{ forms.errors_summary(form) }}
 
   <form method="post"
-        action="{{ as_url(routes.start.selected, project_name=project.name) }}"
+        action="{{ as_url(post.start.selected, project_name=project.name) }}"
         enctype="multipart/form-data"
         class="atr-canary py-4 px-5 border rounded"
         novalidate>
@@ -35,7 +35,7 @@
 
     <div class="mt-4">
       {{ form.submit(class_="btn btn-primary btn-lg") }}
-      <a href="{{ as_url(routes.root.index) }}" class="btn btn-link 
ms-2">Cancel</a>
+      <a href="{{ as_url(get.root.index) }}" class="btn btn-link 
ms-2">Cancel</a>
     </div>
   </form>
 
diff --git a/atr/templates/todo.html b/atr/templates/todo.html
deleted file mode 100644
index 6285d3c..0000000
--- a/atr/templates/todo.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
-  TODO ~ ATR
-{% endblock title %}
-
-{% block description %}
-  TODO.
-{% endblock description %}
-
-{% block content %}
-  <h1>TODO</h1>
-
-  <p>This page is a placeholder for development.</p>
-
-  <p>
-    <a href="{{ as_url(routes.root.index) }}" class="btn btn-primary">← Back 
to home</a>
-  </p>
-{% endblock content %}


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


Reply via email to