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 18119a0 Move docs, download, and draft routes to the new layout
18119a0 is described below
commit 18119a03c28b80ff7513d7a81cb33645b4eae71f
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Oct 28 14:32:30 2025 +0000
Move docs, download, and draft routes to the new layout
---
atr/get/__init__.py | 15 ++-
atr/{routes => get}/docs.py | 11 +-
atr/{routes => get}/download.py | 32 +++---
atr/get/draft.py | 102 ++++++++++++++++++
atr/post/__init__.py | 3 +-
atr/{routes => post}/draft.py | 138 ++++---------------------
atr/routes/__init__.py | 6 --
atr/routes/keys.py | 2 +-
atr/routes/start.py | 3 +-
atr/shared/__init__.py | 2 +-
atr/{get/__init__.py => shared/draft.py} | 24 +++--
atr/templates/check-selected-path-table.html | 8 +-
atr/templates/check-selected-release-info.html | 4 +-
atr/templates/check-selected.html | 4 +-
atr/templates/download-all.html | 10 +-
atr/templates/draft-tools.html | 6 +-
atr/templates/file-selected-path.html | 2 +-
atr/templates/finish-selected.html | 2 +-
atr/templates/includes/sidebar.html | 2 +-
atr/templates/project-view.html | 2 +-
atr/templates/releases-finished.html | 2 +-
atr/templates/upload-selected.html | 2 +-
atr/templates/voting-selected-revision.html | 2 +-
atr/web.py | 4 +
24 files changed, 206 insertions(+), 182 deletions(-)
diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index 352ab48..d3b96cc 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -22,8 +22,21 @@ import atr.get.candidate as candidate
import atr.get.committees as committees
import atr.get.compose as compose
import atr.get.distribution as distribution
+import atr.get.docs as docs
+import atr.get.download as download
+import atr.get.draft as draft
import atr.get.vote as vote
ROUTES_MODULE: Final[Literal[True]] = True
-__all__ = ["announce", "candidate", "committees", "compose", "distribution",
"vote"]
+__all__ = [
+ "announce",
+ "candidate",
+ "committees",
+ "compose",
+ "distribution",
+ "docs",
+ "download",
+ "draft",
+ "vote",
+]
diff --git a/atr/routes/docs.py b/atr/get/docs.py
similarity index 92%
rename from atr/routes/docs.py
rename to atr/get/docs.py
index 5ee86c1..40e6047 100644
--- a/atr/routes/docs.py
+++ b/atr/get/docs.py
@@ -23,9 +23,10 @@ import aiofiles.os
import markupsafe
import quart
+import atr.blueprints.get as get
import atr.config as config
-import atr.route as route
import atr.template as template
+import atr.web as web
class H1Parser(HTMLParser):
@@ -47,13 +48,13 @@ class H1Parser(HTMLParser):
self.h1_content = data.strip()
[email protected]("/docs/")
-async def index(session: route.CommitterSession | None) -> str:
[email protected]("/docs/")
+async def index(session: web.Committer | None) -> str:
return await _serve_docs_page("index")
[email protected]("/docs/<path:page>")
-async def page(session: route.CommitterSession | None, page: str) -> str:
[email protected]("/docs/<path:page>")
+async def page(session: web.Committer | None, page: str) -> str:
return await _serve_docs_page(page)
diff --git a/atr/routes/download.py b/atr/get/download.py
similarity index 89%
rename from atr/routes/download.py
rename to atr/get/download.py
index d92c727..c5c7225 100644
--- a/atr/routes/download.py
+++ b/atr/get/download.py
@@ -15,8 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-"""download.py"""
-
import pathlib
from collections.abc import AsyncGenerator
@@ -27,11 +25,11 @@ import quart
import werkzeug.wrappers.response as response
import zipstream
+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.routes.mapping as mapping
import atr.routes.root as root
import atr.template as template
@@ -39,10 +37,8 @@ import atr.util as util
import atr.web as web
[email protected]("/download/all/<project_name>/<version_name>")
-async def all_selected(
- session: route.CommitterSession, project_name: str, version_name: str
-) -> response.Response | str:
[email protected]("/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."""
async with db.session() as data:
release = await session.release(project_name=project_name,
version_name=version_name, phase=None, data=data)
@@ -66,25 +62,25 @@ async def all_selected(
)
[email protected]("/download/path/<project_name>/<version_name>/<path:file_path>")
[email protected]("/download/path/<project_name>/<version_name>/<path:file_path>")
async def 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 | quart.Response:
"""Download a file or list a directory from a release in any phase."""
return await _download_or_list(project_name, version_name, file_path)
[email protected]("/download/path/<project_name>/<version_name>/")
[email protected]("/download/path/<project_name>/<version_name>/")
async def path_empty(
- session: route.CommitterSession | None, project_name: str, version_name:
str
+ session: web.Committer | None, project_name: str, version_name: str
) -> response.Response | quart.Response:
"""List files at the root of a release directory for download."""
return await _download_or_list(project_name, version_name, ".")
[email protected]("/download/sh/<project_name>/<version_name>")
[email protected]("/download/sh/<project_name>/<version_name>")
async def sh_selected(
- session: route.CommitterSession | None, project_name: str, version_name:
str
+ session: web.Committer | None, project_name: str, version_name: str
) -> response.Response | quart.Response:
"""Shell script to download a release."""
conf = config.get()
@@ -99,9 +95,9 @@ async def sh_selected(
return web.ShellResponse(content)
[email protected]("/download/urls/<project_name>/<version_name>")
[email protected]("/download/urls/<project_name>/<version_name>")
async def urls_selected(
- session: route.CommitterSession | None, project_name: str, version_name:
str
+ session: web.Committer | None, project_name: str, version_name: str
) -> response.Response | quart.Response:
try:
async with db.session() as data:
@@ -116,9 +112,9 @@ async def urls_selected(
return web.TextResponse(f"Internal server error: {e}", status=500)
[email protected]("/download/zip/<project_name>/<version_name>")
[email protected]("/download/zip/<project_name>/<version_name>")
async def zip_selected(
- session: route.CommitterSession, project_name: str, version_name: str
+ session: web.Committer, project_name: str, version_name: str
) -> response.Response | quart.wrappers.response.Response:
try:
release = await session.release(project_name=project_name,
version_name=version_name, phase=None)
@@ -156,7 +152,7 @@ async def _download_or_list(project_name: str,
version_name: str, file_path: str
# Check that path is relative
original_path = pathlib.Path(file_path)
if (file_path != ".") and (not
original_path.is_relative_to(original_path.anchor)):
- raise route.FlashError("Path must be relative")
+ raise web.FlashError("Path must be relative")
# We allow downloading files from any phase
async with db.session() as data:
diff --git a/atr/get/draft.py b/atr/get/draft.py
new file mode 100644
index 0000000..0d2a513
--- /dev/null
+++ b/atr/get/draft.py
@@ -0,0 +1,102 @@
+# 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 datetime
+import pathlib
+from typing import TYPE_CHECKING
+
+import aiofiles.os
+import asfquart.base as base
+
+import atr.blueprints.get as get
+import atr.forms as forms
+import atr.template as template
+import atr.util as util
+import atr.web as web
+
+if TYPE_CHECKING:
+ import werkzeug.wrappers.response as response
+
+
[email protected]("/draft/tools/<project_name>/<version_name>/<path:file_path>")
+async def tools(session: web.Committer, project_name: str, version_name: str,
file_path: str) -> str:
+ """Show the tools for a specific file."""
+ await session.check_access(project_name)
+
+ release = await session.release(project_name, version_name)
+ full_path = str(util.release_directory(release) / file_path)
+
+ # Check that the file exists
+ if not await aiofiles.os.path.exists(full_path):
+ raise base.ASFQuartException("File does not exist", errorcode=404)
+
+ modified = int(await aiofiles.os.path.getmtime(full_path))
+ file_size = await aiofiles.os.path.getsize(full_path)
+
+ file_data = {
+ "filename": pathlib.Path(file_path).name,
+ "bytes_size": file_size,
+ "uploaded": datetime.datetime.fromtimestamp(modified, tz=datetime.UTC),
+ }
+
+ return await template.render(
+ "draft-tools.html",
+ asf_id=session.uid,
+ project_name=project_name,
+ version_name=version_name,
+ file_path=file_path,
+ file_data=file_data,
+ release=release,
+ format_file_size=util.format_file_size,
+ empty_form=await forms.Empty.create_form(),
+ )
+
+
+# TODO: Should we deprecate this and ensure compose covers it all?
+# If we did that, we'd lose the exhaustive use of the abstraction
[email protected]("/draft/view/<project_name>/<version_name>")
+async def view(session: web.Committer, project_name: str, version_name: str)
-> response.Response | str:
+ """View all the files in the rsync upload directory for a release."""
+ await session.check_access(project_name)
+
+ release = await session.release(project_name, version_name)
+
+ # Convert async generator to list
+ revision_number = release.latest_revision_number
+ file_stats = []
+ if revision_number is not None:
+ file_stats = [
+ stat
+ async for stat in util.content_list(util.get_unfinished_dir(),
project_name, version_name, revision_number)
+ ]
+ # Sort the files by FileStat.path
+ file_stats.sort(key=lambda fs: fs.path)
+
+ return await template.render(
+ # TODO: Move to somewhere appropriate
+ "phase-view.html",
+ file_stats=file_stats,
+ release=release,
+ format_datetime=util.format_datetime,
+ format_file_size=util.format_file_size,
+ format_permissions=util.format_permissions,
+ phase="release candidate draft",
+ phase_key="draft",
+ )
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index ca6989d..9a396cb 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -20,8 +20,9 @@ from typing import Final, Literal
import atr.post.announce as announce
import atr.post.candidate as candidate
import atr.post.distribution as distribution
+import atr.post.draft as draft
import atr.post.vote as vote
ROUTES_MODULE: Final[Literal[True]] = True
-__all__ = ["announce", "candidate", "distribution", "vote"]
+__all__ = ["announce", "candidate", "distribution", "draft", "vote"]
diff --git a/atr/routes/draft.py b/atr/post/draft.py
similarity index 71%
rename from atr/routes/draft.py
rename to atr/post/draft.py
index f1b6932..341f7e4 100644
--- a/atr/routes/draft.py
+++ b/atr/post/draft.py
@@ -15,29 +15,26 @@
# specific language governing permissions and limitations
# under the License.
-"""draft.py"""
-
from __future__ import annotations
-import datetime
import pathlib
-from typing import TYPE_CHECKING, TypeVar
+from typing import TYPE_CHECKING
import aiofiles.os
import aioshutil
import asfquart.base as base
import quart
+import atr.blueprints.post as post
import atr.construct as construct
import atr.forms as forms
import atr.get.compose as compose
import atr.log as log
import atr.models.sql as sql
-import atr.route as route
import atr.routes.root as root
import atr.routes.upload as upload
+import atr.shared as shared
import atr.storage as storage
-import atr.template as template
import atr.util as util
import atr.web as web
@@ -45,26 +42,6 @@ if TYPE_CHECKING:
import werkzeug.wrappers.response as response
-T = TypeVar("T")
-
-
-class DeleteFileForm(forms.Typed):
- """Form for deleting a file."""
-
- file_path = forms.string("File path")
- submit = forms.submit("Delete file")
-
-
-class DeleteForm(forms.Typed):
- """Form for deleting a candidate draft."""
-
- release_name = forms.hidden()
- project_name = forms.hidden()
- version_name = forms.hidden()
- confirm_delete = forms.string("Confirmation",
validators=forms.constant("DELETE"))
- submit = forms.submit("Delete candidate draft")
-
-
class VotePreviewForm(forms.Typed):
body = forms.textarea("Body")
# TODO: Validate the vote duration again?
@@ -73,10 +50,10 @@ class VotePreviewForm(forms.Typed):
vote_duration = forms.integer("Vote duration")
[email protected]("/draft/delete", methods=["POST"])
-async def delete(session: route.CommitterSession) -> response.Response:
[email protected]("/draft/delete")
+async def delete(session: web.Committer) -> response.Response:
"""Delete a candidate draft and all its associated files."""
- form = await DeleteForm.create_form(data=await quart.request.form)
+ 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:
@@ -117,12 +94,12 @@ async def delete(session: route.CommitterSession) ->
response.Response:
return await session.redirect(root.index, success="Candidate draft deleted
successfully")
[email protected]("/draft/delete-file/<project_name>/<version_name>",
methods=["POST"])
-async def delete_file(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
[email protected]("/draft/delete-file/<project_name>/<version_name>")
+async def delete_file(session: web.Committer, project_name: str, version_name:
str) -> response.Response:
"""Delete a specific file from the release candidate, creating a new
revision."""
await session.check_access(project_name)
- form = await DeleteFileForm.create_form(data=await quart.request.form)
+ form = await shared.draft.DeleteFileForm.create_form(data=await
quart.request.form)
if not await form.validate_on_submit():
error_summary = []
for key, value in form.errors.items():
@@ -152,8 +129,8 @@ async def delete_file(session: route.CommitterSession,
project_name: str, versio
)
[email protected]("/draft/fresh/<project_name>/<version_name>",
methods=["POST"])
-async def fresh(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response:
[email protected]("/draft/fresh/<project_name>/<version_name>")
+async def fresh(session: web.Committer, project_name: str, version_name: str)
-> response.Response:
"""Restart all checks for a whole release candidate draft."""
# Admin only button, but it's okay if users find and use this manually
await session.check_access(project_name)
@@ -178,10 +155,8 @@ async def fresh(session: route.CommitterSession,
project_name: str, version_name
)
[email protected]("/draft/hashgen/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
-async def hashgen(
- session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
-) -> response.Response:
[email protected]("/draft/hashgen/<project_name>/<version_name>/<path:file_path>")
+async def hashgen(session: web.Committer, project_name: str, version_name:
str, file_path: str) -> response.Response:
"""Generate an sha256 or sha512 hash file for a candidate draft file,
creating a new revision."""
await session.check_access(project_name)
@@ -213,10 +188,8 @@ async def hashgen(
)
[email protected]("/draft/sbomgen/<project_name>/<version_name>/<path:file_path>",
methods=["POST"])
-async def sbomgen(
- session: route.CommitterSession, project_name: str, version_name: str,
file_path: str
-) -> response.Response:
[email protected]("/draft/sbomgen/<project_name>/<version_name>/<path:file_path>")
+async def sbomgen(session: web.Committer, project_name: str, version_name:
str, file_path: str) -> response.Response:
"""Generate a CycloneDX SBOM file for a candidate draft file, creating a
new revision."""
await session.check_access(project_name)
@@ -244,14 +217,14 @@ async def sbomgen(
# Check that the source file exists in the new revision
if not await aiofiles.os.path.exists(path_in_new_revision):
log.error(f"Source file {rel_path} not found in new
revision for SBOM generation.")
- raise route.FlashError("Source artifact file not found in
the new revision.")
+ raise web.FlashError("Source artifact file not found in
the new revision.")
# Check that the SBOM file does not already exist in the new
revision
if await aiofiles.os.path.exists(sbom_path_in_new_revision):
raise base.ASFQuartException("SBOM file already exists",
errorcode=400)
if creating.new is None:
- raise route.FlashError("Internal error: New revision not
found")
+ raise web.FlashError("Internal error: New revision not found")
# Create and queue the task, using paths within the new revision
sbom_task = await wacp.sbom.generate_cyclonedx(
@@ -272,8 +245,8 @@ async def sbomgen(
)
[email protected]("/draft/svnload/<project_name>/<version_name>",
methods=["POST"])
-async def svnload(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
[email protected]("/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."""
await session.check_access(project_name)
@@ -316,76 +289,9 @@ async def svnload(session: route.CommitterSession,
project_name: str, version_na
)
[email protected]("/draft/tools/<project_name>/<version_name>/<path:file_path>")
-async def tools(session: route.CommitterSession, project_name: str,
version_name: str, file_path: str) -> str:
- """Show the tools for a specific file."""
- await session.check_access(project_name)
-
- release = await session.release(project_name, version_name)
- full_path = str(util.release_directory(release) / file_path)
-
- # Check that the file exists
- if not await aiofiles.os.path.exists(full_path):
- raise base.ASFQuartException("File does not exist", errorcode=404)
-
- modified = int(await aiofiles.os.path.getmtime(full_path))
- file_size = await aiofiles.os.path.getsize(full_path)
-
- file_data = {
- "filename": pathlib.Path(file_path).name,
- "bytes_size": file_size,
- "uploaded": datetime.datetime.fromtimestamp(modified, tz=datetime.UTC),
- }
-
- return await template.render(
- "draft-tools.html",
- asf_id=session.uid,
- project_name=project_name,
- version_name=version_name,
- file_path=file_path,
- file_data=file_data,
- release=release,
- format_file_size=util.format_file_size,
- empty_form=await forms.Empty.create_form(),
- )
-
-
-# TODO: Should we deprecate this and ensure compose covers it all?
-# If we did that, we'd lose the exhaustive use of the abstraction
[email protected]("/draft/view/<project_name>/<version_name>")
-async def view(session: route.CommitterSession, project_name: str,
version_name: str) -> response.Response | str:
- """View all the files in the rsync upload directory for a release."""
- await session.check_access(project_name)
-
- release = await session.release(project_name, version_name)
-
- # Convert async generator to list
- revision_number = release.latest_revision_number
- file_stats = []
- if revision_number is not None:
- file_stats = [
- stat
- async for stat in util.content_list(util.get_unfinished_dir(),
project_name, version_name, revision_number)
- ]
- # Sort the files by FileStat.path
- file_stats.sort(key=lambda fs: fs.path)
-
- return await template.render(
- # TODO: Move to somewhere appropriate
- "phase-view.html",
- file_stats=file_stats,
- release=release,
- format_datetime=util.format_datetime,
- format_file_size=util.format_file_size,
- format_permissions=util.format_permissions,
- phase="release candidate draft",
- phase_key="draft",
- )
-
-
[email protected]("/draft/vote/preview/<project_name>/<version_name>",
methods=["POST"])
[email protected]("/draft/vote/preview/<project_name>/<version_name>")
async def vote_preview(
- session: route.CommitterSession, project_name: str, version_name: str
+ session: web.Committer, project_name: str, version_name: str
) -> quart.wrappers.response.Response | response.Response | str:
"""Show the vote email preview for a release."""
@@ -395,7 +301,7 @@ async def vote_preview(
release = await session.release(project_name, version_name)
if release.committee is None:
- raise route.FlashError("Release has no associated committee")
+ raise web.FlashError("Release has no associated committee")
form_body: str = util.unwrap(form.body.data)
asfuid = session.uid
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index a0b4998..4ab4b88 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.docs as docs
-import atr.routes.download as download
-import atr.routes.draft as draft
import atr.routes.file as file
import atr.routes.finish as finish
import atr.routes.ignores as ignores
@@ -39,9 +36,6 @@ import atr.routes.user as user
import atr.routes.voting as voting
__all__ = [
- "docs",
- "download",
- "draft",
"file",
"finish",
"ignores",
diff --git a/atr/routes/keys.py b/atr/routes/keys.py
index 6cbda29..71e0565 100644
--- a/atr/routes/keys.py
+++ b/atr/routes/keys.py
@@ -174,7 +174,7 @@ async def add(session: route.CommitterSession) -> str:
form = await AddOpenPGPKeyForm.create_form()
forms.choices(form.selected_committees, committee_choices)
- except route.FlashError as e:
+ except (route.FlashError, web.FlashError) as e:
log.warning("FlashError adding OpenPGP key: %s", e)
await quart.flash(str(e), "error")
except Exception as e:
diff --git a/atr/routes/start.py b/atr/routes/start.py
index acae711..a1c9917 100644
--- a/atr/routes/start.py
+++ b/atr/routes/start.py
@@ -28,6 +28,7 @@ import atr.models.sql as sql
import atr.route as route
import atr.storage as storage
import atr.template as template
+import atr.web as web
class StartReleaseForm(forms.Typed):
@@ -71,7 +72,7 @@ async def selected(session: route.CommitterSession,
project_name: str) -> respon
version_name=new_release.version,
success="Release candidate draft created successfully",
)
- except (route.FlashError, base.ASFQuartException) as e:
+ except (route.FlashError, web.FlashError, base.ASFQuartException) as e:
# Flash the error and let the code fall through to render the
template below
await quart.flash(str(e), "error")
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 16dd241..352c86b 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -25,9 +25,9 @@ import atr.db.interaction as interaction
import atr.forms as forms
import atr.models.results as results
import atr.models.sql as sql
-import atr.routes.draft as draft
import atr.shared.announce as announce
import atr.shared.distribution as distribution
+import atr.shared.draft as draft
import atr.shared.vote as vote
import atr.storage as storage
import atr.template as template
diff --git a/atr/get/__init__.py b/atr/shared/draft.py
similarity index 61%
copy from atr/get/__init__.py
copy to atr/shared/draft.py
index 352ab48..150cc6e 100644
--- a/atr/get/__init__.py
+++ b/atr/shared/draft.py
@@ -15,15 +15,21 @@
# specific language governing permissions and limitations
# under the License.
-from typing import Final, Literal
+import atr.forms as forms
-import atr.get.announce as announce
-import atr.get.candidate as candidate
-import atr.get.committees as committees
-import atr.get.compose as compose
-import atr.get.distribution as distribution
-import atr.get.vote as vote
-ROUTES_MODULE: Final[Literal[True]] = True
+class DeleteFileForm(forms.Typed):
+ """Form for deleting a file."""
-__all__ = ["announce", "candidate", "committees", "compose", "distribution",
"vote"]
+ file_path = forms.string("File path")
+ submit = forms.submit("Delete file")
+
+
+class DeleteForm(forms.Typed):
+ """Form for deleting a candidate draft."""
+
+ release_name = forms.hidden()
+ project_name = forms.hidden()
+ version_name = forms.hidden()
+ confirm_delete = forms.string("Confirmation",
validators=forms.constant("DELETE"))
+ submit = forms.submit("Delete candidate draft")
diff --git a/atr/templates/check-selected-path-table.html
b/atr/templates/check-selected-path-table.html
index 5470f1d..b67368b 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -93,7 +93,7 @@
title="Show more actions for {{ path }}"
onclick="this.innerHTML = (this.innerHTML.trim() ===
'More') ? 'Less' : 'More';">More</button>
{% elif phase == "release_candidate" %}
- <a href="{{ as_url(routes.download.path,
project_name=release.project.name, version_name=release.version,
file_path=path) }}"
+ <a href="{{ as_url(get.download.path,
project_name=release.project.name, version_name=release.version,
file_path=path) }}"
title="Download file {{ path }}"
class="btn btn-sm btn-outline-secondary">Download</a>
{% endif %}
@@ -109,10 +109,10 @@
<div class="btn-group btn-group-sm"
role="group"
aria-label="More file actions for {{ path }}">
- <a href="{{ as_url(routes.download.path,
project_name=release.project.name, version_name=release.version,
file_path=path) }}"
+ <a href="{{ as_url(get.download.path,
project_name=release.project.name, version_name=release.version,
file_path=path) }}"
title="Download file {{ path }}"
class="btn btn-outline-secondary">Download</a>
- <a href="{{ as_url(routes.draft.tools,
project_name=project_name, version_name=version_name, file_path=path) }}"
+ <a href="{{ as_url(get.draft.tools,
project_name=project_name, version_name=version_name, file_path=path) }}"
title="Tools for file {{ path }}"
class="btn btn-outline-secondary">Tools</a>
<button class="btn btn-outline-danger"
@@ -120,7 +120,7 @@
data-bs-target="#delete-{{ row_id }}"
title="Delete file {{ path }}">Delete</button>
</div>
- {{ dialog.delete_modal(path, "Delete file", "file, and any
associated metadata files", as_url(routes.draft.delete_file,
project_name=project_name, version_name=version_name) , delete_file_form,
"file_path") }}
+ {{ dialog.delete_modal(path, "Delete file", "file, and any
associated metadata files", as_url(post.draft.delete_file,
project_name=project_name, version_name=version_name) , delete_file_form,
"file_path") }}
</div>
</div>
</td>
diff --git a/atr/templates/check-selected-release-info.html
b/atr/templates/check-selected-release-info.html
index 1e84a78..643d441 100644
--- a/atr/templates/check-selected-release-info.html
+++ b/atr/templates/check-selected-release-info.html
@@ -45,7 +45,7 @@
title="Upload files to this draft"
class="btn btn-primary"><i class="bi bi-upload me-1"></i> Upload
files</a>
- <a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
title="Download {%- if has_files -%}files{%- else -%}links{%- endif
-%}"
class="btn btn-primary"><i class="bi bi-download me-1"></i> Download
{% if has_files %}
@@ -73,7 +73,7 @@
{% endif %}
<a href="#more-actions" class="btn btn-outline-secondary"><i class="bi
bi-gear me-1"></i> More actions</a>
{% elif phase == "release_candidate" %}
- <a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-primary"><i class="bi bi-download me-1"></i>
Download files</a>
<a href="{{ as_url(get.candidate.view,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-secondary"><i class="bi bi-eye me-1"></i> View
files</a>
diff --git a/atr/templates/check-selected.html
b/atr/templates/check-selected.html
index e2ac6ab..37567ee 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -171,7 +171,7 @@
</div>
<div class="mb-3">
<form method="post"
- action="{{ as_url(routes.draft.fresh,
project_name=release.project.name, version_name=release.version) }}"
+ action="{{ as_url(post.draft.fresh,
project_name=release.project.name, version_name=release.version) }}"
class="mb-0">
{{ empty_form.hidden_tag() }}
@@ -182,7 +182,7 @@
<h3 id="delete-draft" class="mt-4">Delete this draft</h3>
<div>
<form method="post"
- action="{{ as_url(routes.draft.delete,
project_name=release.project.name, version_name=release.version) }}"
+ action="{{ as_url(post.draft.delete,
project_name=release.project.name, version_name=release.version) }}"
class="mb-0">
{{ delete_form.hidden_tag() }}
diff --git a/atr/templates/download-all.html b/atr/templates/download-all.html
index 13346fa..f8f1976 100644
--- a/atr/templates/download-all.html
+++ b/atr/templates/download-all.html
@@ -67,7 +67,7 @@
The archive is generated on the fly, which may take a while for very large
releases.
</p>
<p>
- <a href="{{ as_url(routes.download.zip_selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.download.zip_selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-primary btn-lg">
<i class="bi bi-file-earmark-zip me-2"></i>Download {{ release.name
}}.zip
</a>
@@ -92,17 +92,17 @@
<h3 id="download-browser" class="mt-4">Using your browser</h3>
<p>
- You can download the files one by one using your browser from the <a
href="{{ as_url(routes.download.path_empty, project_name=release.project.name,
version_name=release.version) }}">download folder</a>. Clicking a link to any
file will download it, as it is served as <code>application/octet-stream</code>.
+ You can download the files one by one using your browser from the <a
href="{{ as_url(get.download.path_empty, project_name=release.project.name,
version_name=release.version) }}">download folder</a>. Clicking a link to any
file will download it, as it is served as <code>application/octet-stream</code>.
</p>
<h3 id="download-curl" class="mt-4">Using curl</h3>
<p>You can download all of the files in this release using curl with the
following command:</p>
<!-- TODO: Add a button to copy the command to the clipboard -->
<pre class="bg-light border rounded p-3 mb-3">
-curl{% if server_domain == "localhost.apache.org" %} --insecure{% endif %}
-fsS https://{{ server_host }}{{ as_url(routes.download.sh_selected,
project_name=release.project.name, version_name=release.version) }} |{% if
server_domain == "localhost.apache.org" %} CURL_EXTRA=--insecure{% endif %} sh
+curl{% if server_domain == "localhost.apache.org" %} --insecure{% endif %}
-fsS https://{{ server_host }}{{ as_url(get.download.sh_selected,
project_name=release.project.name, version_name=release.version) }} |{% if
server_domain == "localhost.apache.org" %} CURL_EXTRA=--insecure{% endif %} sh
</pre>
<p>
- This downloads the files into the <em>current directory</em>. Ensure that
you create a new empty directory, and change to it, before running the command.
The script requires curl and a POSIX compliant version of sh. It works by
downloading a POSIX complaint shell script straight into your shell. You can of
course <a href="{{ as_url(routes.download.sh_selected,
project_name=release.project.name, version_name=release.version) }}">download
the script</a> and audit it before running it.
+ This downloads the files into the <em>current directory</em>. Ensure that
you create a new empty directory, and change to it, before running the command.
The script requires curl and a POSIX compliant version of sh. It works by
downloading a POSIX complaint shell script straight into your shell. You can of
course <a href="{{ as_url(get.download.sh_selected,
project_name=release.project.name, version_name=release.version) }}">download
the script</a> and audit it before running it.
</p>
<h3 id="download-rsync" class="mt-4">Using rsync</h3>
@@ -116,7 +116,7 @@ rsync -av -e 'ssh -p 2222' {{ asf_id }}@{{ server_domain
}}:/{{ release.project.
<h3 id="download-wget" class="mt-4">Using wget</h3>
<p>You can download all of the files in this release using wget with the
following command:</p>
<pre class="bg-light border rounded p-3 mb-3">
-wget -r -np -nH --cut-dirs=4 --default-page=.index.html{% if server_domain ==
"localhost.apache.org" %} --no-check-certificate{% endif %} https://{{
server_host }}{{ as_url(routes.download.path_empty,
project_name=release.project.name, version_name=release.version) }}
+wget -r -np -nH --cut-dirs=4 --default-page=.index.html{% if server_domain ==
"localhost.apache.org" %} --no-check-certificate{% endif %} https://{{
server_host }}{{ as_url(get.download.path_empty,
project_name=release.project.name, version_name=release.version) }}
</pre>
<p>
This downloads the files into the <em>current directory</em>. Ensure that
you create a new empty directory, and change to it, before running the command.
diff --git a/atr/templates/draft-tools.html b/atr/templates/draft-tools.html
index e474d6e..a7607da 100644
--- a/atr/templates/draft-tools.html
+++ b/atr/templates/draft-tools.html
@@ -35,14 +35,14 @@
</div>
<div class="d-flex gap-2 mb-4">
<form method="post"
- action="{{ as_url(routes.draft.hashgen, project_name=project_name,
version_name=version_name, file_path=file_path) }}">
+ action="{{ as_url(post.draft.hashgen, project_name=project_name,
version_name=version_name, file_path=file_path) }}">
{{ empty_form.hidden_tag() }}
<input type="hidden" name="hash_type" value="sha256" />
<button type="submit" class="btn btn-outline-secondary">Generate
SHA256</button>
</form>
<form method="post"
- action="{{ as_url(routes.draft.hashgen, project_name=project_name,
version_name=version_name, file_path=file_path) }}">
+ action="{{ as_url(post.draft.hashgen, project_name=project_name,
version_name=version_name, file_path=file_path) }}">
{{ empty_form.hidden_tag() }}
<input type="hidden" name="hash_type" value="sha512" />
@@ -55,7 +55,7 @@
<p>NOTE: This functionality is currently not available.</p>
<p>Generate a CycloneDX Software Bill of Materials (SBOM) file for this
artifact.</p>
<form method="post"
- action="{{ as_url(routes.draft.sbomgen, project_name=project_name,
version_name=version_name, file_path=file_path) }}">
+ action="{{ as_url(post.draft.sbomgen, project_name=project_name,
version_name=version_name, file_path=file_path) }}">
{{ empty_form.hidden_tag() }}
<button type="submit" class="btn btn-outline-secondary">Generate
CycloneDX SBOM (.cdx.json)</button>
diff --git a/atr/templates/file-selected-path.html
b/atr/templates/file-selected-path.html
index 5cf8dcd..d3cee26 100644
--- a/atr/templates/file-selected-path.html
+++ b/atr/templates/file-selected-path.html
@@ -11,7 +11,7 @@
{% block content %}
{# Generate back link based on phase_key #}
{% if phase_key == "draft" %}
- {% set back_url = as_url(routes.draft.view,
project_name=release.project.name, version_name=release.version) %}
+ {% set back_url = as_url(get.draft.view,
project_name=release.project.name, version_name=release.version) %}
{% elif phase_key == "candidate" %}
{% set back_url = as_url(get.candidate.view,
project_name=release.project.name, version_name=release.version) %}
{% elif phase_key == "preview" %}
diff --git a/atr/templates/finish-selected.html
b/atr/templates/finish-selected.html
index bea1604..3722886 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -69,7 +69,7 @@
</div>
<div>
<a title="Download all files"
- href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
+ href="{{ as_url(get.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-primary me-2">
<i class="bi bi-download"></i>
Download all files
diff --git a/atr/templates/includes/sidebar.html
b/atr/templates/includes/sidebar.html
index 2967c55..13a95e6 100644
--- a/atr/templates/includes/sidebar.html
+++ b/atr/templates/includes/sidebar.html
@@ -121,7 +121,7 @@
</li>
<li>
<i class="bi bi-book"></i>
- <a href="{{ as_url(routes.docs.index) }}">Documentation</a>
+ <a href="{{ as_url(get.docs.index) }}">Documentation</a>
</li>
<li>
<i class="bi bi-book"></i>
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index c745561..12455c8 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -413,7 +413,7 @@
<h2>Draft candidate releases</h2>
<div class="d-flex flex-wrap gap-2 mb-4">
{% for draft in candidate_drafts %}
- <a href="{{ as_url(routes.draft.view, project_name=project.name,
version_name=draft.version) }}"
+ <a href="{{ as_url(get.draft.view, project_name=project.name,
version_name=draft.version) }}"
class="btn btn-sm btn-outline-secondary py-2 px-3"
title="View draft {{ project.name }} {{ draft.version }}">
{{ project.name }} {{ draft.version }}
diff --git a/atr/templates/releases-finished.html
b/atr/templates/releases-finished.html
index ba64e4c..606faa1 100644
--- a/atr/templates/releases-finished.html
+++ b/atr/templates/releases-finished.html
@@ -25,7 +25,7 @@
<strong class="card-title fs-5">{{ release.version }}</strong>
<p class="card-text text-muted">Released on {{
format_datetime(release.created) }}</p>
<div class="mt-auto">
- <a href="{{ as_url(routes.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
+ <a href="{{ as_url(get.download.all_selected,
project_name=release.project.name, version_name=release.version) }}"
class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-download me-1"></i> Download files
</a>
diff --git a/atr/templates/upload-selected.html
b/atr/templates/upload-selected.html
index f5ff081..cc6cfd2 100644
--- a/atr/templates/upload-selected.html
+++ b/atr/templates/upload-selected.html
@@ -75,7 +75,7 @@
{{ forms.errors_summary(svn_form) }}
<div class="row">
<div class="col-md-8 w-100">
- <form action="{{ as_url(routes.draft.svnload, project_name=project_name,
version_name=version_name) }}"
+ <form action="{{ as_url(post.draft.svnload, project_name=project_name,
version_name=version_name) }}"
method="post"
novalidate
class="atr-canary py-4 px-5">
diff --git a/atr/templates/voting-selected-revision.html
b/atr/templates/voting-selected-revision.html
index c3b5686..76e59f4 100644
--- a/atr/templates/voting-selected-revision.html
+++ b/atr/templates/voting-selected-revision.html
@@ -178,7 +178,7 @@
return;
}
- const previewUrl = "{{ as_url(routes.draft.vote_preview,
project_name=release.project.name, version_name=release.version) }}";
+ const previewUrl = "{{ as_url(post.draft.vote_preview,
project_name=release.project.name, version_name=release.version) }}";
const csrfTokenInput =
voteForm.querySelector('input[name="csrf_token"]');
if (!previewUrl || !csrfTokenInput) {
diff --git a/atr/web.py b/atr/web.py
index 32ae7e7..05f05cb 100644
--- a/atr/web.py
+++ b/atr/web.py
@@ -179,6 +179,10 @@ class ElementResponse(quart.Response):
super().__init__(str(element), status=status, mimetype="text/html")
+class FlashError(RuntimeError):
+ """Error that triggers a flash message."""
+
+
class HeaderValue:
# TODO: There does not appear to be a general HTTP header construction
package in Python
# The existence of one would help us and others to adhere to the HTTP
component of ASVS v5 1.2.1
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]