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 3ead693  Move published, ref, and release routes to the new layout
3ead693 is described below

commit 3ead693c61af286603f4a1afd2c2974854d078fa
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Oct 28 15:48:50 2025 +0000

    Move published, ref, and release routes to the new layout
---
 atr/admin/__init__.py                 |   2 +-
 atr/construct.py                      |   4 +-
 atr/get/__init__.py                   |   8 +
 atr/get/compose.py                    |   2 +-
 atr/get/distribution.py               |   2 -
 atr/get/download.py                   |   2 +-
 atr/get/draft.py                      |   1 -
 atr/{routes => get}/published.py      |  12 +-
 atr/{routes => get}/ref.py            |   7 +-
 atr/{routes => get}/release.py        |  29 +-
 atr/get/vote.py                       |   2 +-
 atr/log.py                            |   2 -
 atr/{routes => }/mapping.py           |  10 +-
 atr/models/sql.py                     |   1 -
 atr/post/announce.py                  |  14 +-
 atr/post/candidate.py                 |   5 +-
 atr/post/draft.py                     |   7 +-
 atr/routes/__init__.py                |   6 -
 atr/routes/projects.py                | 599 ----------------------------------
 atr/sbom/__init__.py                  |   2 -
 atr/sbom/__main__.py                  |   2 -
 atr/sbom/cli.py                       |   2 -
 atr/server.py                         |   2 +-
 atr/shared/finish.py                  |   5 +-
 atr/storage/writers/__init__.py       |   2 +
 atr/storage/writers/announce.py       |   3 +-
 atr/templates/file-selected-path.html |   2 +-
 atr/templates/includes/sidebar.html   |   2 +-
 atr/templates/index-committer.html    |   2 +-
 atr/templates/phase-view.html         |   4 +-
 atr/templates/project-select.html     |   4 +-
 atr/templates/project-view.html       |   2 +-
 atr/templates/releases-finished.html  |   2 +-
 atr/templates/releases.html           |   2 +-
 34 files changed, 75 insertions(+), 678 deletions(-)

diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py
index d0835e5..c369435 100644
--- a/atr/admin/__init__.py
+++ b/atr/admin/__init__.py
@@ -43,9 +43,9 @@ import atr.forms as forms
 import atr.get as get
 import atr.ldap as ldap
 import atr.log as log
+import atr.mapping as mapping
 import atr.models.sql as sql
 import atr.principal as principal
-import atr.routes.mapping as mapping
 import atr.storage as storage
 import atr.storage.outcome as outcome
 import atr.storage.types as types
diff --git a/atr/construct.py b/atr/construct.py
index 42a8757..f908bbb 100644
--- a/atr/construct.py
+++ b/atr/construct.py
@@ -46,7 +46,7 @@ class StartVoteOptions:
 async def announce_release_body(body: str, options: AnnounceReleaseOptions) -> 
str:
     # NOTE: The present module is imported by routes
     # Therefore this must be done here to avoid a circular import
-    import atr.routes.release as routes_release
+    import atr.get as get
 
     try:
         host = quart.request.host
@@ -65,7 +65,7 @@ async def announce_release_body(body: str, options: 
AnnounceReleaseOptions) -> s
             raise RuntimeError(f"Release {options.project_name} 
{options.version_name} has no committee")
         committee = release.committee
 
-    routes_release_view = routes_release.view  # type: ignore[has-type]
+    routes_release_view = get.release.view  # type: ignore[has-type]
     download_path = util.as_url(
         routes_release_view, project_name=options.project_name, 
version_name=options.version_name
     )
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index 1c3a872..3694731 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
+
 from typing import Final, Literal
 
 import atr.get.announce as announce
@@ -31,6 +33,9 @@ import atr.get.ignores as ignores
 import atr.get.keys as keys
 import atr.get.preview as preview
 import atr.get.projects as projects
+import atr.get.published as published
+import atr.get.ref as ref
+import atr.get.release as release
 import atr.get.vote as vote
 
 ROUTES_MODULE: Final[Literal[True]] = True
@@ -50,5 +55,8 @@ __all__ = [
     "keys",
     "preview",
     "projects",
+    "published",
+    "ref",
+    "release",
     "vote",
 ]
diff --git a/atr/get/compose.py b/atr/get/compose.py
index 7c51a69..2696b38 100644
--- a/atr/get/compose.py
+++ b/atr/get/compose.py
@@ -20,8 +20,8 @@ import werkzeug.wrappers.response as response
 
 import atr.blueprints.get as get
 import atr.db as db
+import atr.mapping as mapping
 import atr.models.sql as sql
-import atr.routes.mapping as mapping
 import atr.shared as shared
 import atr.web as web
 
diff --git a/atr/get/distribution.py b/atr/get/distribution.py
index 29a737d..971be34 100644
--- a/atr/get/distribution.py
+++ b/atr/get/distribution.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from __future__ import annotations
-
 import atr.blueprints.get as get
 import atr.db as db
 import atr.forms as forms
diff --git a/atr/get/download.py b/atr/get/download.py
index c5c7225..bcfef95 100644
--- a/atr/get/download.py
+++ b/atr/get/download.py
@@ -29,8 +29,8 @@ import atr.blueprints.get as get
 import atr.config as config
 import atr.db as db
 import atr.htm as htm
+import atr.mapping as mapping
 import atr.models.sql as sql
-import atr.routes.mapping as mapping
 import atr.routes.root as root
 import atr.template as template
 import atr.util as util
diff --git a/atr/get/draft.py b/atr/get/draft.py
index 0d2a513..ca9ad87 100644
--- a/atr/get/draft.py
+++ b/atr/get/draft.py
@@ -15,7 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-
 from __future__ import annotations
 
 import datetime
diff --git a/atr/routes/published.py b/atr/get/published.py
similarity index 92%
rename from atr/routes/published.py
rename to atr/get/published.py
index 3db7cab..6d7e572 100644
--- a/atr/routes/published.py
+++ b/atr/get/published.py
@@ -22,14 +22,14 @@ from datetime import datetime
 import aiofiles.os
 import quart
 
+import atr.blueprints.get as get
 import atr.htm as htm
-import atr.route as route
 import atr.util as util
 import atr.web as web
 
 
[email protected]("/published/<path:path>")
-async def path(session: route.CommitterSession, path: str) -> quart.Response:
[email protected]("/published/<path:path>")
+async def path(session: web.Committer, path: str) -> quart.Response:
     """View the content of a specific file in the downloads directory."""
     # This route is for debugging
     # When developing locally, there is no proxy to view the downloads 
directory
@@ -37,8 +37,8 @@ async def path(session: route.CommitterSession, path: str) -> 
quart.Response:
     return await _path(session, path)
 
 
[email protected]("/published/")
-async def root(session: route.CommitterSession) -> quart.Response:
[email protected]("/published/")
+async def root(session: web.Committer) -> quart.Response:
     return await _path(session, "")
 
 
@@ -95,7 +95,7 @@ async def _file_content(full_path: pathlib.Path) -> 
quart.Response:
     return await quart.send_file(full_path)
 
 
-async def _path(session: route.CommitterSession, path: str) -> quart.Response:
+async def _path(session: web.Committer, path: str) -> quart.Response:
     downloads_path = util.get_downloads_dir()
     full_path = downloads_path / path
     if await aiofiles.os.path.isdir(full_path):
diff --git a/atr/routes/ref.py b/atr/get/ref.py
similarity index 95%
rename from atr/routes/ref.py
rename to atr/get/ref.py
index 8bf5772..78dcbef 100644
--- a/atr/routes/ref.py
+++ b/atr/get/ref.py
@@ -21,16 +21,17 @@ import pathlib
 import quart
 import werkzeug.wrappers.response as response
 
+import atr.blueprints.get as get
 import atr.config as config
-import atr.route as route
+import atr.web as web
 
 # Perhaps GitHub will get around to implementing symbol permalinks:
 # https://github.com/orgs/community/discussions/13292
 # Then this code will be easier, but we should still keep our own links
 
 
[email protected]("/ref/<path:ref_path>")
-async def resolve(session: route.CommitterSession | None, ref_path: str) -> 
response.Response:
[email protected]("/ref/<path:ref_path>")
+async def resolve(session: web.Committer | None, ref_path: str) -> 
response.Response:
     project_root = pathlib.Path(config.get().PROJECT_ROOT)
 
     if ":" in ref_path:
diff --git a/atr/routes/release.py b/atr/get/release.py
similarity index 86%
rename from atr/routes/release.py
rename to atr/get/release.py
index 5b2cc4e..dac1e8a 100644
--- a/atr/routes/release.py
+++ b/atr/get/release.py
@@ -15,27 +15,22 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""release.py"""
-
 import datetime
 
-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.db.interaction as interaction
 import atr.models.sql as sql
-import atr.route as route
 import atr.template as template
 import atr.util as util
-
-if asfquart.APP is ...:
-    raise RuntimeError("APP is not set")
+import atr.web as web
 
 
[email protected]("/releases/finished/<project_name>")
-async def finished(session: route.CommitterSession | None, project_name: str) 
-> str:
[email protected]("/releases/finished/<project_name>")
+async def finished(session: web.Committer | None, project_name: str) -> str:
     """View all finished releases for a project."""
     async with db.session() as data:
         project = await data.project(name=project_name, 
status=sql.ProjectStatus.ACTIVE).demand(
@@ -58,8 +53,8 @@ async def finished(session: route.CommitterSession | None, 
project_name: str) ->
     )
 
 
[email protected]("/releases")
-async def releases(session: route.CommitterSession | None) -> str:
[email protected]("/releases")
+async def releases(session: web.Committer | None) -> str:
     """View all releases."""
     # Releases are public, so we don't need to filter by user
     async with db.session() as data:
@@ -83,8 +78,8 @@ async def releases(session: route.CommitterSession | None) -> 
str:
     )
 
 
[email protected]("/release/select/<project_name>")
-async def select(session: route.CommitterSession, project_name: str) -> str:
[email protected]("/release/select/<project_name>")
+async def select(session: web.Committer, project_name: str) -> str:
     """Show releases in progress for a project."""
     await session.check_access(project_name)
 
@@ -98,8 +93,8 @@ async def select(session: route.CommitterSession, 
project_name: str) -> str:
     )
 
 
[email protected]("/release/view/<project_name>/<version_name>")
-async def view(session: route.CommitterSession | None, project_name: str, 
version_name: str) -> response.Response | str:
[email protected]("/release/view/<project_name>/<version_name>")
+async def view(session: web.Committer | None, project_name: str, version_name: 
str) -> response.Response | str:
     """View all the files in the rsync upload directory for a release."""
     async with db.session() as data:
         release_name = sql.release_name(project_name, version_name)
@@ -125,9 +120,9 @@ async def view(session: route.CommitterSession | None, 
project_name: str, versio
     )
 
 
[email protected]("/release/view/<project_name>/<version_name>/<path:file_path>")
[email protected]("/release/view/<project_name>/<version_name>/<path:file_path>")
 async def view_path(
-    session: route.CommitterSession | None, project_name: str, version_name: 
str, file_path: str
+    session: web.Committer | None, project_name: str, version_name: str, 
file_path: str
 ) -> response.Response | str:
     """View the content of a specific file in the final release."""
     async with db.session() as data:
diff --git a/atr/get/vote.py b/atr/get/vote.py
index e5eeadc..6a8304e 100644
--- a/atr/get/vote.py
+++ b/atr/get/vote.py
@@ -23,9 +23,9 @@ import atr.db as db
 import atr.db.interaction as interaction
 import atr.forms as forms
 import atr.log as log
+import atr.mapping as mapping
 import atr.models.results as results
 import atr.models.sql as sql
-import atr.routes.mapping as mapping
 import atr.shared as shared
 import atr.storage as storage
 import atr.user as user
diff --git a/atr/log.py b/atr/log.py
index c2ce1aa..5f9e9ab 100644
--- a/atr/log.py
+++ b/atr/log.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from __future__ import annotations
-
 import inspect
 import logging
 import logging.handlers
diff --git a/atr/routes/mapping.py b/atr/mapping.py
similarity index 88%
rename from atr/routes/mapping.py
rename to atr/mapping.py
index 930f52d..b465272 100644
--- a/atr/routes/mapping.py
+++ b/atr/mapping.py
@@ -21,18 +21,16 @@ 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.release as routes_release
 import atr.util as util
 import atr.web as web
 
 
 async def release_as_redirect(
-    session: route.CommitterSession | web.Committer,
+    session: web.Committer,
     release: sql.Release,
 ) -> response.Response:
     route = release_as_route(release)
-    if route is routes_release.finished:
+    if route is get.release.finished:
         return await session.redirect(route, project_name=release.project.name)
     return await session.redirect(route, project_name=release.project.name, 
version_name=release.version)
 
@@ -46,11 +44,11 @@ def release_as_route(release: sql.Release) -> Callable:
         case sql.ReleasePhase.RELEASE_PREVIEW:
             return get.finish.selected
         case sql.ReleasePhase.RELEASE:
-            return routes_release.finished
+            return get.release.finished
 
 
 def release_as_url(release: sql.Release) -> str:
     route = release_as_route(release)
-    if route is routes_release.finished:
+    if route is get.release.finished:
         return util.as_url(route, project_name=release.project.name)
     return util.as_url(route, project_name=release.project.name, 
version_name=release.version)
diff --git a/atr/models/sql.py b/atr/models/sql.py
index 968ea83..99da4d8 100644
--- a/atr/models/sql.py
+++ b/atr/models/sql.py
@@ -20,7 +20,6 @@
 # NOTE: We can't use symbolic annotations here because sqlmodel doesn't 
support them
 # https://github.com/fastapi/sqlmodel/issues/196
 # https://github.com/fastapi/sqlmodel/pull/778/files
-# from __future__ import annotations
 
 import dataclasses
 import datetime
diff --git a/atr/post/announce.py b/atr/post/announce.py
index c8e4746..176d0af 100644
--- a/atr/post/announce.py
+++ b/atr/post/announce.py
@@ -15,14 +15,18 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
 import quart
-import werkzeug.wrappers.response as response
+
+if TYPE_CHECKING:
+    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.shared as shared
 import atr.storage as storage
 import atr.template as template
@@ -37,6 +41,8 @@ class AnnounceError(Exception):
 @post.committer("/announce/<project_name>/<version_name>")
 async def selected(session: web.Committer, project_name: str, version_name: 
str) -> str | response.Response:
     """Handle the announcement form submission and promote the preview to 
release."""
+    import atr.get as get
+
     await session.check_access(project_name)
 
     permitted_recipients = util.permitted_announce_recipients(session.uid)
@@ -85,7 +91,7 @@ async def selected(session: web.Committer, project_name: str, 
version_name: str)
             get.announce.selected, error=str(e), project_name=project_name, 
version_name=version_name
         )
 
-    routes_release_finished = routes_release.finished  # type: ignore[has-type]
+    routes_release_finished = get.release.finished  # type: ignore[has-type]
     return await session.redirect(
         routes_release_finished,
         success="Preview successfully announced",
diff --git a/atr/post/candidate.py b/atr/post/candidate.py
index 3b56c4d..ca0b011 100644
--- a/atr/post/candidate.py
+++ b/atr/post/candidate.py
@@ -15,17 +15,16 @@
 # specific language governing permissions and limitations
 # under the License.
 
-"""candidate.py"""
-
 import werkzeug.wrappers.response as response
 
 import atr.blueprints.post as post
-import atr.routes.root as root
 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
+
     # 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/post/draft.py b/atr/post/draft.py
index 341f7e4..0860d3f 100644
--- a/atr/post/draft.py
+++ b/atr/post/draft.py
@@ -31,8 +31,6 @@ import atr.forms as forms
 import atr.get.compose as compose
 import atr.log as log
 import atr.models.sql as sql
-import atr.routes.root as root
-import atr.routes.upload as upload
 import atr.shared as shared
 import atr.storage as storage
 import atr.util as util
@@ -53,6 +51,8 @@ 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
+
     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():
@@ -248,6 +248,8 @@ async def sbomgen(session: web.Committer, project_name: 
str, version_name: str,
 @post.committer("/draft/svnload/<project_name>/<version_name>")
 async def svnload(session: web.Committer, project_name: str, version_name: 
str) -> response.Response | str:
     """Import files from SVN into a draft."""
+    import atr.routes.upload as upload
+
     await session.check_access(project_name)
 
     form = await upload.SvnImportForm.create_form()
@@ -294,6 +296,7 @@ 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
 
     form = await VotePreviewForm.create_form(data=await quart.request.form)
     if not await form.validate_on_submit():
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index 9d61ba3..bec11e9 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.published as published
-import atr.routes.ref as ref
-import atr.routes.release as release
 import atr.routes.report as report
 import atr.routes.resolve as resolve
 import atr.routes.revisions as revisions
@@ -30,9 +27,6 @@ import atr.routes.user as user
 import atr.routes.voting as voting
 
 __all__ = [
-    "published",
-    "ref",
-    "release",
     "report",
     "resolve",
     "revisions",
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
deleted file mode 100644
index cd4b5e4..0000000
--- a/atr/routes/projects.py
+++ /dev/null
@@ -1,599 +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.
-
-"""project.py"""
-
-from __future__ import annotations
-
-import datetime
-import http.client
-import re
-from typing import TYPE_CHECKING, Any
-
-import asfquart.base as base
-import quart
-
-import atr.config as config
-import atr.db as db
-import atr.db.interaction as interaction
-import atr.forms as forms
-import atr.log as log
-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
-import atr.util as util
-
-if TYPE_CHECKING:
-    import werkzeug.wrappers.response as response
-
-
-class AddForm(forms.Typed):
-    committee_name = forms.hidden()
-    display_name = forms.string("Display name")
-    label = forms.string("Label")
-    submit = forms.submit("Add project")
-
-
-class ProjectMetadataForm(forms.Typed):
-    project_name = forms.hidden()
-    category_to_add = forms.optional("New category name")
-    language_to_add = forms.optional("New language name")
-
-
-class ReleasePolicyForm(forms.Typed):
-    """
-    A Form to create or edit a ReleasePolicy.
-
-    TODO: Currently only a single mailto_address is supported.
-          see: 
https://stackoverflow.com/questions/49066046/append-entry-to-fieldlist-with-flask-wtforms-using-ajax
-    """
-
-    project_name = forms.hidden()
-
-    # Compose section
-    source_artifact_paths = forms.textarea(
-        "Source artifact paths",
-        optional=True,
-        rows=5,
-        description="Paths to source artifacts to be included in the release.",
-    )
-    binary_artifact_paths = forms.textarea(
-        "Binary artifact paths",
-        optional=True,
-        rows=5,
-        description="Paths to binary artifacts to be included in the release.",
-    )
-    github_repository_name = forms.optional(
-        "GitHub repository name",
-        description="The name of the GitHub repository to use for the release, 
excluding the apache/ prefix.",
-    )
-    github_compose_workflow_path = forms.textarea(
-        "GitHub compose workflow paths",
-        optional=True,
-        rows=5,
-        description="The full paths to the GitHub workflows to use for the 
release,"
-        " including the .github/workflows/ prefix.",
-    )
-    strict_checking = forms.boolean(
-        "Strict checking", description="If enabled, then the release cannot be 
voted upon unless all checks pass."
-    )
-
-    # Vote section
-    github_vote_workflow_path = forms.textarea(
-        "GitHub vote workflow paths",
-        optional=True,
-        rows=5,
-        description="The full paths to the GitHub workflows to use for the 
release,"
-        " including the .github/workflows/ prefix.",
-    )
-    mailto_addresses = forms.string(
-        "Email",
-        validators=[forms.REQUIRED, forms.EMAIL],
-        placeholder="E.g. [email protected]",
-        description=f"The mailing list where vote emails are sent. This is 
usually"
-        "your dev list. ATR will currently only send test announcement emails 
to"
-        f"{util.USER_TESTS_ADDRESS}.",
-    )
-    manual_vote = forms.boolean(
-        "Manual voting process",
-        description="If this is set then the vote will be completely manual 
and following policy is ignored.",
-    )
-    default_min_hours_value_at_render = forms.hidden()
-    min_hours = forms.integer(
-        "Minimum voting period",
-        validators=[util.validate_vote_duration],
-        default=72,
-        description="The minimum time to run the vote, in hours. Must be 0 or 
between 72 and 144 inclusive."
-        " If 0, then wait until 3 +1 votes and more +1 than -1.",
-    )
-    pause_for_rm = forms.boolean(
-        "Pause for RM", description="If enabled, RM can confirm manually if 
the vote has passed."
-    )
-    release_checklist = forms.textarea(
-        "Release checklist",
-        optional=True,
-        rows=10,
-        description="Markdown text describing how to test release candidates.",
-    )
-    default_start_vote_template_hash = forms.hidden()
-    start_vote_template = forms.textarea(
-        "Start vote template",
-        optional=True,
-        rows=10,
-        description="Email template for messages to start a vote on a 
release.",
-    )
-
-    # Finish section
-    default_announce_release_template_hash = forms.hidden()
-    announce_release_template = forms.textarea(
-        "Announce release template",
-        optional=True,
-        rows=10,
-        description="Email template for messages to announce a finished 
release.",
-    )
-    github_finish_workflow_path = forms.textarea(
-        "GitHub finish workflow paths",
-        optional=True,
-        rows=5,
-        description="The full paths to the GitHub workflows to use for the 
release,"
-        " including the .github/workflows/ prefix.",
-    )
-    preserve_download_files = forms.boolean(
-        "Preserve download files",
-        description="If enabled, existing download files will not be 
overwritten.",
-    )
-
-    submit_policy = forms.submit("Save")
-
-    async def validate(self, extra_validators: dict[str, Any] | None = None) 
-> bool:  # noqa: C901
-        await super().validate(extra_validators=extra_validators)
-
-        if self.manual_vote.data:
-            for field_name in (
-                "mailto_addresses",
-                "min_hours",
-                "pause_for_rm",
-                "release_checklist",
-                "start_vote_template",
-            ):
-                field = getattr(self, field_name, None)
-                if field is not None:
-                    forms.clear_errors(field)
-                self.errors.pop(field_name, None)
-
-        if self.manual_vote.data and self.strict_checking.data:
-            msg = "Manual voting process and strict checking cannot be enabled 
simultaneously."
-            forms.error(self.manual_vote, msg)
-            forms.error(self.strict_checking, msg)
-
-        github_repository_name = (self.github_repository_name.data or 
"").strip()
-        compose_raw = self.github_compose_workflow_path.data or ""
-        vote_raw = self.github_vote_workflow_path.data or ""
-        finish_raw = self.github_finish_workflow_path.data or ""
-        compose = [p.strip() for p in compose_raw.split("\n") if p.strip()]
-        vote = [p.strip() for p in vote_raw.split("\n") if p.strip()]
-        finish = [p.strip() for p in finish_raw.split("\n") if p.strip()]
-
-        any_path = bool(compose or vote or finish)
-        if any_path and (not github_repository_name):
-            forms.error(
-                self.github_repository_name,
-                "GitHub repository name is required when any workflow path is 
set.",
-            )
-
-        if github_repository_name and ("/" in github_repository_name):
-            forms.error(self.github_repository_name, "GitHub repository name 
must not contain a slash.")
-
-        if compose:
-            for p in compose:
-                if not p.startswith(".github/workflows/"):
-                    forms.error(
-                        self.github_compose_workflow_path,
-                        "GitHub workflow paths must start with 
'.github/workflows/'.",
-                    )
-                    break
-        if vote:
-            for p in vote:
-                if not p.startswith(".github/workflows/"):
-                    forms.error(
-                        self.github_vote_workflow_path,
-                        "GitHub workflow paths must start with 
'.github/workflows/'.",
-                    )
-                    break
-        if finish:
-            for p in finish:
-                if not p.startswith(".github/workflows/"):
-                    forms.error(
-                        self.github_finish_workflow_path,
-                        "GitHub workflow paths must start with 
'.github/workflows/'.",
-                    )
-                    break
-
-        return not self.errors
-
-
[email protected]("/project/add/<committee_name>", methods=["GET", "POST"])
-async def add_project(session: route.CommitterSession, committee_name: str) -> 
response.Response | str:
-    await session.check_access_committee(committee_name)
-
-    async with db.session() as data:
-        committee = await data.committee(name=committee_name).demand(
-            base.ASFQuartException(f"Committee {committee_name} not found", 
errorcode=404)
-        )
-
-    form = await AddForm.create_form(data={"committee_name": committee_name})
-    form.display_name.description = f"""\
-For example, "Apache {committee.display_name}" or "Apache 
{committee.display_name} Components".
-You must start with "Apache " and you must use title case.
-"""
-    form.label.description = f"""\
-For example, "{committee.name}" or "{committee.name}-components".
-You must start with your committee label, and you must use lower case.
-"""
-
-    if await form.validate_on_submit():
-        return await _project_add(form, session)
-
-    return await template.render("project-add-project.html", form=form, 
committee_name=committee.display_name)
-
-
[email protected]("/project/delete", methods=["POST"])
-async def delete(session: route.CommitterSession) -> response.Response:
-    """Delete a project created by the user."""
-    # TODO: This is not truly empty, so make a form object for this
-    await util.validate_empty_form()
-    form_data = await quart.request.form
-    project_name = form_data.get("project_name")
-    if not project_name:
-        return await session.redirect(projects, error="Missing project name 
for deletion.")
-
-    async with storage.write(session) as write:
-        wacm = await write.as_project_committee_member(project_name)
-        try:
-            await wacm.project.delete(project_name)
-        except storage.AccessError as e:
-            # TODO: Redirect to committees
-            return await session.redirect(projects, error=f"Error deleting 
project: {e}")
-
-    # TODO: Redirect to committees
-    return await session.redirect(projects, success=f"Project '{project_name}' 
deleted successfully.")
-
-
[email protected]("/projects")
-async def projects(session: route.CommitterSession | None) -> str:
-    """Main project directory page."""
-    async with db.session() as data:
-        projects = await 
data.project(_committee=True).order_by(sql.Project.full_name).all()
-        return await template.render("projects.html", projects=projects, 
empty_form=await forms.Empty.create_form())
-
-
[email protected]("/project/select")
-async def select(session: route.CommitterSession) -> str:
-    """Select a project to work on."""
-    user_projects = []
-    if session.uid:
-        async with db.session() as data:
-            # TODO: Move this filtering logic somewhere else
-            # The ALLOW_TESTS line allows test projects to be shown
-            conf = config.get()
-            all_projects = await data.project(status=sql.ProjectStatus.ACTIVE, 
_committee=True).all()
-            user_projects = [
-                p
-                for p in all_projects
-                if p.committee
-                and (
-                    (conf.ALLOW_TESTS and (p.committee.name == "test"))
-                    or (session.uid in p.committee.committee_members)
-                    or (session.uid in p.committee.committers)
-                    or (session.uid in p.committee.release_managers)
-                )
-            ]
-            user_projects.sort(key=lambda p: p.display_name)
-
-    return await template.render("project-select.html", 
user_projects=user_projects)
-
-
[email protected]("/projects/<name>", methods=["GET", "POST"])
-async def view(session: route.CommitterSession, name: str) -> 
response.Response | str:
-    policy_form = None
-    metadata_form = None
-    can_edit = False
-
-    async with db.session() as data:
-        project = await data.project(
-            name=name, _committee=True, _committee_public_signing_keys=True, 
_release_policy=True
-        ).demand(http.client.HTTPException(404))
-
-    is_committee_member = project.committee and 
(user.is_committee_member(project.committee, session.uid))
-    is_privileged = user.is_admin(session.uid)
-    can_edit = is_committee_member or is_privileged
-
-    if can_edit and (quart.request.method == "POST"):
-        form_data = await quart.request.form
-        if "submit_metadata" in form_data:
-            edited_metadata, metadata_form = await _metadata_edit(session, 
project, form_data)
-            if edited_metadata is True:
-                return quart.redirect(util.as_url(view, name=project.name))
-        elif "submit_policy" in form_data:
-            policy_form = await ReleasePolicyForm.create_form(data=form_data)
-            if await policy_form.validate_on_submit():
-                policy_data = 
policy.ReleasePolicyData.model_validate(policy_form.data)
-                async with storage.write(session) as write:
-                    wacm = await 
write.as_project_committee_member(project.name)
-                    try:
-                        await wacm.policy.edit(project, policy_data)
-                    except storage.AccessError as e:
-                        return await session.redirect(view, name=project.name, 
error=f"Error editing policy: {e}")
-                    return quart.redirect(util.as_url(view, name=project.name))
-            else:
-                log.info(f"policy_form.errors: {policy_form.errors}")
-        else:
-            log.info(f"Unknown form data: {form_data}")
-
-    if metadata_form is None:
-        metadata_form = await 
ProjectMetadataForm.create_form(data={"project_name": project.name})
-    if policy_form is None:
-        policy_form = await _policy_form_create(project)
-    candidate_drafts = await interaction.candidate_drafts(project)
-    candidates = await interaction.candidates(project)
-    previews = await interaction.previews(project)
-    full_releases = await interaction.full_releases(project)
-
-    return await template.render(
-        "project-view.html",
-        project=project,
-        algorithms=shared.algorithms,
-        candidate_drafts=candidate_drafts,
-        candidates=candidates,
-        previews=previews,
-        full_releases=full_releases,
-        number_of_release_files=util.number_of_release_files,
-        now=datetime.datetime.now(datetime.UTC),
-        empty_form=await forms.Empty.create_form(),
-        policy_form=policy_form,
-        can_edit=can_edit,
-        metadata_form=metadata_form,
-        forbidden_categories=registry.FORBIDDEN_PROJECT_CATEGORIES,
-    )
-
-
-async def _metadata_category_add(
-    wacm: storage.WriteAsCommitteeMember, project: sql.Project, 
category_to_add: str
-) -> bool:
-    modified = False
-    try:
-        modified = await wacm.project.category_add(project, 
category_to_add.strip())
-    except storage.AccessError as e:
-        await quart.flash(f"Error adding category: {e}", "error")
-    if modified:
-        await quart.flash(f"Category '{category_to_add}' added.", "success")
-    else:
-        await quart.flash(f"Category '{category_to_add}' already exists.", 
"error")
-    return modified
-
-
-async def _metadata_category_remove(
-    wacm: storage.WriteAsCommitteeMember, project: sql.Project, action_value: 
str
-) -> bool:
-    modified = False
-    try:
-        modified = await wacm.project.category_remove(project, action_value)
-    except storage.AccessError as e:
-        await quart.flash(f"Error removing category: {e}", "error")
-    if modified:
-        await quart.flash(f"Category '{action_value}' removed.", "success")
-    else:
-        await quart.flash(f"Category '{action_value}' does not exist.", 
"error")
-    return modified
-
-
-async def _metadata_edit(
-    session: route.CommitterSession, project: sql.Project, form_data: 
dict[str, str]
-) -> tuple[bool, ProjectMetadataForm]:
-    metadata_form = await ProjectMetadataForm.create_form(data=form_data)
-
-    validated = await metadata_form.validate_on_submit()
-    if not validated:
-        return False, metadata_form
-
-    form_data = await quart.request.form
-    action_full = form_data.get("action", "")
-    action_type = ""
-    action_value = ""
-    if ":" in action_full:
-        action_type, action_value = action_full.split(":", 1)
-    else:
-        action_type = action_full
-
-    # TODO: Add error handling
-    modified = False
-    category_to_add = metadata_form.category_to_add.data
-    language_to_add = metadata_form.language_to_add.data
-
-    async with storage.write(session) as write:
-        wacm = await write.as_project_committee_member(project.name)
-
-        if (action_type == "add_category") and category_to_add:
-            modified = await _metadata_category_add(wacm, project, 
category_to_add)
-        elif (action_type == "remove_category") and action_value:
-            modified = await _metadata_category_remove(wacm, project, 
action_value)
-        elif (action_type == "add_language") and language_to_add:
-            modified = await _metadata_language_add(wacm, project, 
language_to_add)
-        elif (action_type == "remove_language") and action_value:
-            modified = await _metadata_language_remove(wacm, project, 
action_value)
-
-    return modified, metadata_form
-
-
-async def _metadata_language_add(
-    wacm: storage.WriteAsCommitteeMember, project: sql.Project, 
language_to_add: str
-) -> bool:
-    modified = False
-    try:
-        modified = await wacm.project.language_add(project, language_to_add)
-    except storage.AccessError as e:
-        await quart.flash(f"Error adding language: {e}", "error")
-    if modified:
-        await quart.flash(f"Language '{language_to_add}' added.", "success")
-    else:
-        await quart.flash(f"Language '{language_to_add}' already exists.", 
"error")
-    return modified
-
-
-async def _metadata_language_remove(
-    wacm: storage.WriteAsCommitteeMember, project: sql.Project, action_value: 
str
-) -> bool:
-    modified = False
-    try:
-        modified = await wacm.project.language_remove(project, action_value)
-    except storage.AccessError as e:
-        await quart.flash(f"Error removing language: {e}", "error")
-    if modified:
-        await quart.flash(f"Language '{action_value}' removed.", "success")
-    else:
-        await quart.flash(f"Language '{action_value}' does not exist.", 
"error")
-    return modified
-
-
-async def _policy_form_create(project: sql.Project) -> ReleasePolicyForm:
-    # TODO: Use form order for all of these fields
-    policy_form = await ReleasePolicyForm.create_form()
-    policy_form.project_name.data = project.name
-    if project.policy_mailto_addresses:
-        policy_form.mailto_addresses.data = project.policy_mailto_addresses[0]
-    else:
-        policy_form.mailto_addresses.data = f"dev@{project.name}.apache.org"
-    policy_form.min_hours.data = project.policy_min_hours
-    policy_form.manual_vote.data = project.policy_manual_vote
-    policy_form.release_checklist.data = project.policy_release_checklist
-    policy_form.start_vote_template.data = project.policy_start_vote_template
-    policy_form.announce_release_template.data = 
project.policy_announce_release_template
-    policy_form.binary_artifact_paths.data = 
"\n".join(project.policy_binary_artifact_paths)
-    policy_form.source_artifact_paths.data = 
"\n".join(project.policy_source_artifact_paths)
-    policy_form.pause_for_rm.data = project.policy_pause_for_rm
-    policy_form.strict_checking.data = project.policy_strict_checking
-    policy_form.github_repository_name.data = 
project.policy_github_repository_name
-    policy_form.github_compose_workflow_path.data = 
"\n".join(project.policy_github_compose_workflow_path)
-    policy_form.github_vote_workflow_path.data = 
"\n".join(project.policy_github_vote_workflow_path)
-    policy_form.github_finish_workflow_path.data = 
"\n".join(project.policy_github_finish_workflow_path)
-    policy_form.preserve_download_files.data = 
project.policy_preserve_download_files
-
-    # Set the hashes and value of the current defaults
-    policy_form.default_start_vote_template_hash.data = util.compute_sha3_256(
-        project.policy_start_vote_default.encode()
-    )
-    policy_form.default_announce_release_template_hash.data = 
util.compute_sha3_256(
-        project.policy_announce_release_default.encode()
-    )
-    policy_form.default_min_hours_value_at_render.data = 
str(project.policy_default_min_hours)
-    return policy_form
-
-
-async def _project_add(form: AddForm, session: route.CommitterSession) -> 
response.Response:
-    form_values = await _project_add_validate(form)
-    if form_values is None:
-        return quart.redirect(util.as_url(add_project, 
committee_name=form.committee_name.data))
-    committee_name, display_name, label = form_values
-
-    async with storage.write(session) as write:
-        wacm = await write.as_project_committee_member(committee_name)
-        try:
-            await wacm.project.create(committee_name, display_name, label)
-        except storage.AccessError as e:
-            await quart.flash(f"Error adding project: {e}", "error")
-            return quart.redirect(util.as_url(add_project, 
committee_name=committee_name))
-
-    return quart.redirect(util.as_url(view, name=label))
-
-
-async def _project_add_validate(form: AddForm) -> tuple[str, str, str] | None:
-    committee_name = str(form.committee_name.data)
-    # Normalise spaces in the display name, then validate
-    display_name = str(form.display_name.data).strip()
-    display_name = re.sub(r"  +", " ", display_name)
-    if not await _project_add_validate_display_name(display_name):
-        return None
-    # Hidden criterion!
-    # $ sqlite3 state/atr.db 'select full_name from project;' | grep -- 
'[^A-Za-z0-9 ]'
-    # Apache .NET Ant Library
-    # Apache Oltu - Parent
-    # Apache Commons Chain (Dormant)
-    # Apache Commons Functor (Dormant)
-    # Apache Commons OGNL (Dormant)
-    # Apache Commons Proxy (Dormant)
-    # Apache Empire-db
-    # Apache mod_ftp
-    # Apache Lucene.Net
-    # Apache mod_perl
-    # Apache Xalan for C++ XSLT Processor
-    # Apache Xerces for C++ XML Parser
-    if not display_name.replace(" ", "").replace(".", "").replace("+", 
"").isalnum():
-        await quart.flash("Display name must be alphanumeric and may include 
spaces or dots or plus signs", "error")
-        return None
-
-    label = str(form.label.data).strip()
-    if not (label.startswith(committee_name + "-") or (label == 
committee_name)):
-        await quart.flash(f"Label must start with '{committee_name}-'", 
"error")
-        return None
-    if not label.islower():
-        await quart.flash("Label must be all lower case", "error")
-        return None
-    # Hidden criterion!
-    if not label.replace("-", "").isalnum():
-        await quart.flash("Label must be alphanumeric and may include 
hyphens", "error")
-        return None
-
-    return (committee_name, display_name, label)
-
-
-async def _project_add_validate_display_name(display_name: str) -> bool:
-    # We have three criteria for display names
-    must_start_apache = "The first display name word must be 'Apache'."
-    must_have_two_words = "The display name must have at least two words."
-    must_use_correct_case = "Display name words must be in PascalCase, 
camelCase, or mod_ case."
-
-    # First criterion, the first word must be "Apache"
-    display_name_words = display_name.split(" ")
-    if display_name_words[0] != "Apache":
-        await quart.flash(must_start_apache, "error")
-        return False
-
-    # Second criterion, the display name must have two or more words
-    if not display_name_words[1:]:
-        await quart.flash(must_have_two_words, "error")
-        return False
-
-    # Third criterion, the display name must use the correct case
-    allowed_irregular_words = {".NET", "C++", "Empire-db", "Lucene.NET", 
"for", "jclouds"}
-    r_pascal_case = re.compile(r"^([A-Z][0-9a-z]*)+$")
-    r_camel_case = re.compile(r"^[a-z]*([A-Z][0-9a-z]*)+$")
-    r_mod_case = re.compile(r"^mod(_[0-9a-z]+)+$")
-    for display_name_word in display_name_words[1:]:
-        if display_name_word in allowed_irregular_words:
-            continue
-        is_pascal_case = r_pascal_case.match(display_name_word)
-        is_camel_case = r_camel_case.match(display_name_word)
-        is_mod_case = r_mod_case.match(display_name_word)
-        if not (is_pascal_case or is_camel_case or is_mod_case):
-            await quart.flash(must_use_correct_case, "error")
-            return False
-    return True
diff --git a/atr/sbom/__init__.py b/atr/sbom/__init__.py
index 8326a4d..b560995 100644
--- a/atr/sbom/__init__.py
+++ b/atr/sbom/__init__.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from __future__ import annotations
-
 from . import cli, conformance, constants, cyclonedx, licenses, maven, models, 
osv, sbomqs, spdx, utilities
 
 __all__ = [
diff --git a/atr/sbom/__main__.py b/atr/sbom/__main__.py
index 03d908d..3f1c454 100644
--- a/atr/sbom/__main__.py
+++ b/atr/sbom/__main__.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from __future__ import annotations
-
 from . import cli
 
 if __name__ == "__main__":
diff --git a/atr/sbom/cli.py b/atr/sbom/cli.py
index cd80cab..4c8721f 100644
--- a/atr/sbom/cli.py
+++ b/atr/sbom/cli.py
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from __future__ import annotations
-
 import asyncio
 import pathlib
 import sys
diff --git a/atr/server.py b/atr/server.py
index 54c3808..1129ed6 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -129,10 +129,10 @@ def app_setup_context(app: base.QuartApp) -> None:
     async def app_wide() -> dict[str, Any]:
         import atr.admin as admin
         import atr.get as get
+        import atr.mapping as mapping
         import atr.metadata as metadata
         import atr.post as post
         import atr.routes as routes
-        import atr.routes.mapping as mapping
 
         return {
             "admin": admin,
diff --git a/atr/shared/finish.py b/atr/shared/finish.py
index 9cc227f..607ce98 100644
--- a/atr/shared/finish.py
+++ b/atr/shared/finish.py
@@ -33,9 +33,8 @@ import atr.analysis as analysis
 import atr.db as db
 import atr.forms as forms
 import atr.log as log
+import atr.mapping as mapping
 import atr.models.sql as sql
-import atr.routes.mapping as mapping
-import atr.routes.root as root
 import atr.storage as storage
 import atr.template as template
 import atr.util as util
@@ -137,6 +136,8 @@ async def selected(
     try:
         source_files_rel, target_dirs = await 
_sources_and_targets(latest_revision_dir)
     except FileNotFoundError:
+        import atr.routes.root as root
+
         await quart.flash("Preview revision directory not found.", "error")
         return await session.redirect(root.index)
 
diff --git a/atr/storage/writers/__init__.py b/atr/storage/writers/__init__.py
index b8243d7..8c98552 100644
--- a/atr/storage/writers/__init__.py
+++ b/atr/storage/writers/__init__.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
+
 import atr.storage.writers.announce as announce
 import atr.storage.writers.cache as cache
 import atr.storage.writers.checks as checks
diff --git a/atr/storage/writers/announce.py b/atr/storage/writers/announce.py
index 0f3abcd..9506aef 100644
--- a/atr/storage/writers/announce.py
+++ b/atr/storage/writers/announce.py
@@ -27,7 +27,6 @@ import aiofiles.os
 import aioshutil
 import sqlmodel
 
-import atr.construct as construct
 import atr.db as db
 import atr.models.sql as sql
 import atr.storage as storage
@@ -112,6 +111,8 @@ class CommitteeMember(CommitteeParticipant):
         asf_uid: str,
         fullname: str,
     ) -> None:
+        import atr.construct as construct
+
         if recipient not in util.permitted_announce_recipients(asf_uid):
             raise storage.AccessError(f"You are not permitted to send 
announcements to {recipient}")
 
diff --git a/atr/templates/file-selected-path.html 
b/atr/templates/file-selected-path.html
index 38d56ea..6e27df7 100644
--- a/atr/templates/file-selected-path.html
+++ b/atr/templates/file-selected-path.html
@@ -17,7 +17,7 @@
   {% elif phase_key == "preview" %}
     {% set back_url = as_url(get.preview.view, 
project_name=release.project.name, version_name=release.version) %}
   {% elif phase_key == "release" %}
-    {% set back_url = as_url(routes.release.view, 
project_name=release.project.name, version_name=release.version) %}
+    {% set back_url = as_url(get.release.view, 
project_name=release.project.name, version_name=release.version) %}
   {% endif %}
   <a href="{{ back_url }}" class="atr-back-link">← Back to {{ phase_key }}</a>
 
diff --git a/atr/templates/includes/sidebar.html 
b/atr/templates/includes/sidebar.html
index 210465c..882bd8a 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -38,7 +38,7 @@
         {% if current_user %}
           <li>
             <i class="bi bi-view-list"></i>
-            <a href="{{ as_url(routes.release.releases) }}">Browse catalog</a>
+            <a href="{{ as_url(get.release.releases) }}">Browse catalog</a>
           </li>
         {% endif %}
         <li>
diff --git a/atr/templates/index-committer.html 
b/atr/templates/index-committer.html
index 7bcbb94..74e7138 100644
--- a/atr/templates/index-committer.html
+++ b/atr/templates/index-committer.html
@@ -94,7 +94,7 @@
              class="text-decoration-none me-2">Create a sibling project</a>
           {% if completed_releases %}
             <span class="text-muted me-2">/</span>
-            <a href="{{ as_url(routes.release.finished, 
project_name=project.name) }}"
+            <a href="{{ as_url(get.release.finished, 
project_name=project.name) }}"
                class="text-decoration-none">Finished releases</a>
           {% endif %}
         </p>
diff --git a/atr/templates/phase-view.html b/atr/templates/phase-view.html
index 375e785..946a676 100644
--- a/atr/templates/phase-view.html
+++ b/atr/templates/phase-view.html
@@ -45,7 +45,7 @@
         <span class="atr-phase-three atr-phase-label">FINISH</span>
       </span>
     {% else %}
-      <a href="{{ as_url(routes.release.releases) }}" class="atr-back-link">← 
Back to Releases</a>
+      <a href="{{ as_url(get.release.releases) }}" class="atr-back-link">← 
Back to Releases</a>
     {% endif %}
   </p>
 
@@ -105,7 +105,7 @@
                       {% elif phase_key == "preview" %}
                         {% set file_url = as_url(get.preview.view_path, 
project_name=release.project.name, version_name=release.version, 
file_path=stat.path) %}
                       {% elif phase_key == "release" %}
-                        {% set file_url = as_url(routes.release.view_path, 
project_name=release.project.name, version_name=release.version, 
file_path=stat.path) %}
+                        {% set file_url = as_url(get.release.view_path, 
project_name=release.project.name, version_name=release.version, 
file_path=stat.path) %}
                       {% else %}
                         {# TODO: Should probably disable the link here #}
                         {% set file_url = "#" %}
diff --git a/atr/templates/project-select.html 
b/atr/templates/project-select.html
index 63abb38..5830db9 100644
--- a/atr/templates/project-select.html
+++ b/atr/templates/project-select.html
@@ -11,11 +11,11 @@
     <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
       {% for project in user_projects %}
         <div class="col">
-          <a href="{{ as_url(routes.release.select, project_name=project.name) 
}}"
+          <a href="{{ as_url(get.release.select, project_name=project.name) }}"
              class="text-decoration-none">
             {# TODO: We're only setting data-project-url for style #}
             <div class="card h-100 shadow-sm atr-cursor-pointer"
-                 data-project-url="{{ as_url(routes.release.select, 
project_name=project.name) }}">
+                 data-project-url="{{ as_url(get.release.select, 
project_name=project.name) }}">
               <div class="card-body">
                 <h3 class="card-title fs-5 mb-2">{{ project.display_name 
}}</h3>
                 <h4 class="card-subtitle mb-2 text-muted fs-6">{{ project.name 
}}</h4>
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index 633153f..a57a426 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -472,7 +472,7 @@
         <h2>Full releases</h2>
         <div class="d-flex flex-wrap gap-2 mb-4">
           {% for release in full_releases %}
-            <a href="{{ as_url(routes.release.view, project_name=project.name, 
version_name=release.version) }}"
+            <a href="{{ as_url(get.release.view, project_name=project.name, 
version_name=release.version) }}"
                class="btn btn-sm btn-outline-success py-2 px-3"
                title="View release {{ project.name }} {{ release.version }}">
               {{ project.name }} {{ release.version }}
diff --git a/atr/templates/releases-finished.html 
b/atr/templates/releases-finished.html
index 606faa1..6841d74 100644
--- a/atr/templates/releases-finished.html
+++ b/atr/templates/releases-finished.html
@@ -29,7 +29,7 @@
                    class="btn btn-outline-primary w-100 mb-2">
                   <i class="bi bi-download me-1"></i> Download files
                 </a>
-                <a href="{{ as_url(routes.release.view, 
project_name=release.project.name, version_name=release.version) }}"
+                <a href="{{ as_url(get.release.view, 
project_name=release.project.name, version_name=release.version) }}"
                    class="btn btn-outline-secondary w-100">
                   <i class="bi bi-folder2-open me-1"></i> View files
                 </a>
diff --git a/atr/templates/releases.html b/atr/templates/releases.html
index 86ea1cb..2a09235 100644
--- a/atr/templates/releases.html
+++ b/atr/templates/releases.html
@@ -33,7 +33,7 @@
         <div class="card-body">
           <h3 class="card-title mb-3">{{ project_display_name }}</h3>
           <p class="card-text">
-            <a href="{{ as_url(routes.release.finished, 
project_name=project.name) }}"
+            <a href="{{ as_url(get.release.finished, 
project_name=project.name) }}"
                class="btn btn-outline-primary">
               <i class="bi bi-folder2-open me-1"></i> Finished releases 
(<code>{{ count }}</code>)
             </a>


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

Reply via email to