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 6cfc782 Make the form to revert to an older revision more type safe
6cfc782 is described below
commit 6cfc7826ae8d2eb2029e3000f6c88bb5f42b4fe2
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Nov 10 20:47:39 2025 +0000
Make the form to revert to an older revision more type safe
---
atr/get/revisions.py | 210 +++++++++++++++++++++++++++++++---
atr/htm.py | 3 +-
atr/post/revisions.py | 15 +--
atr/post/sbom.py | 2 +-
atr/shared/__init__.py | 2 +
atr/shared/revisions.py | 22 ++++
atr/templates/revisions-selected.html | 136 ----------------------
7 files changed, 228 insertions(+), 162 deletions(-)
diff --git a/atr/get/revisions.py b/atr/get/revisions.py
index ebcf31a..321972a 100644
--- a/atr/get/revisions.py
+++ b/atr/get/revisions.py
@@ -25,14 +25,25 @@ import sqlmodel
import atr.blueprints.get as get
import atr.db as db
-import atr.forms as forms
+import atr.form as form
+import atr.get.compose as compose
+import atr.get.finish as finish
+import atr.get.root as root
+import atr.htm as htm
import atr.models.schema as schema
import atr.models.sql as sql
+import atr.shared as shared
import atr.template as template
import atr.util as util
import atr.web as web
+class FilesDiff(schema.Strict):
+ added: list[pathlib.Path]
+ removed: list[pathlib.Path]
+ modified: list[pathlib.Path]
+
+
@get.committer("/revisions/<project_name>/<version_name>")
async def selected(session: web.Committer, project_name: str, version_name:
str) -> str:
"""Show the revision history for a release candidate draft or release
preview."""
@@ -50,7 +61,7 @@ async def selected(session: web.Committer, project_name: str,
version_name: str)
latest_revision_number = release.latest_revision_number
if latest_revision_number is None:
# TODO: Set an error message, and redirect to the release page?
- ...
+ pass
# Oldest to newest, to build diffs relative to previous revision
async with db.session() as data_for_revisions:
@@ -77,22 +88,191 @@ async def selected(session: web.Committer, project_name:
str, version_name: str)
loop_prev_revision_files = current_files_for_diff
loop_prev_revision_number = current_db_revision.number
- return await template.render(
- "revisions-selected.html",
- project_name=project_name,
- version_name=version_name,
- release=release,
- phase_key=phase_key,
- revision_history=list(reversed(revision_history)),
- latest_revision_number=latest_revision_number,
- empty_form=await forms.Empty.create_form(),
+ content = await _render_page(
+ release,
+ phase_key,
+ list(reversed(revision_history)),
+ latest_revision_number,
+ project_name,
+ version_name,
)
+ return await template.blank(
+ title=f"Revisions of {release.short_display_name}",
+ content=content,
+ )
-class FilesDiff(schema.Strict):
- added: list[pathlib.Path]
- removed: list[pathlib.Path]
- modified: list[pathlib.Path]
+
+def _render_back_link(release: sql.Release, phase_key: str) -> htm.Element:
+ if phase_key == "draft":
+ back_url = util.as_url(compose.selected,
project_name=release.project.name, version_name=release.version)
+ return htm.a(".atr-back-link", href=back_url)[f"← Back to Compose
{release.short_display_name}"]
+ elif phase_key == "preview":
+ back_url = util.as_url(finish.selected,
project_name=release.project.name, version_name=release.version)
+ return htm.a(".atr-back-link", href=back_url)[f"← Back to Finish
{release.short_display_name}"]
+ else:
+ return htm.a(".atr-back-link", href=util.as_url(root.index))["← Back
to Select a release"]
+
+
+def _render_files_diff(body: htm.Block, files_diff: "FilesDiff") -> None:
+ if not (files_diff.added or files_diff.removed or files_diff.modified):
+ body.p(".fst-italic.text-muted.mt-2")["No file changes detected in
this revision."]
+ return
+
+ if files_diff.added:
+ body.h3(".fs-6.fw-semibold.mt-3.atr-sans")[
+ "Added files ",
+
htm.span(".font-monospace.fw-normal")[f"({len(files_diff.added)})"],
+ ]
+ with body.block(htm.ul, classes=".list-group.list-group-flush.mb-2")
as ul:
+ for file in files_diff.added:
+
ul.li(".list-group-item.list-group-item-success.py-1.px-3.small.rounded-2")[str(file)]
+
+ if files_diff.removed:
+ body.h3(".fs-6.fw-semibold.mt-3.atr-sans")[
+ "Removed files ",
+
htm.span(".font-monospace.fw-normal")[f"({len(files_diff.removed)})"],
+ ]
+ with body.block(htm.ul, classes=".list-group.list-group-flush.mb-2")
as ul:
+ for file in files_diff.removed:
+
ul.li(".list-group-item.list-group-item-danger.py-1.px-3.small.rounded-2")[str(file)]
+
+ if files_diff.modified:
+ body.h3(".fs-6.fw-semibold.mt-3.atr-sans")[
+ "Modified files ",
+
htm.span(".font-monospace.fw-normal")[f"({len(files_diff.modified)})"],
+ ]
+ with body.block(htm.ul, classes=".list-group.list-group-flush.mb-2")
as ul:
+ for file in files_diff.modified:
+
ul.li(".list-group-item.list-group-item-warning.py-1.px-3.small.rounded-2")[str(file)]
+
+
+async def _render_page(
+ release: sql.Release,
+ phase_key: str,
+ revision_history: list[tuple[sql.Revision, "FilesDiff"]],
+ latest_revision_number: str | None,
+ project_name: str,
+ version_name: str,
+) -> htm.Element:
+ page = htm.Block()
+
+ page.p(".d-flex.justify-content-between.align-items-center")[
+ _render_back_link(release, phase_key),
+ _render_phase_indicator(phase_key),
+ ]
+
+ page.h1[
+ "Revisions of ",
+ htm.strong[release.project.short_display_name],
+ " ",
+ htm.em[release.version],
+ ]
+
+ if revision_history:
+ for revision, files_diff in revision_history:
+ _render_revision_card(
+ page, revision, files_diff, latest_revision_number, phase_key,
project_name, version_name
+ )
+ else:
+ page.div(".alert.alert-info")["No revision history found for this
candidate draft."]
+
+ return page.collect()
+
+
+def _render_phase_indicator(phase_key: str) -> htm.Element:
+ span = htm.Block(htm.span)
+
+ if phase_key == "draft":
+ span.strong(".atr-phase-one.atr-phase-symbol")["①"]
+ span.span(".atr-phase-one.atr-phase-label")["COMPOSE"]
+ span.span(".atr-phase-arrow")["→"]
+ span.span(".atr-phase-symbol-other")["②"]
+ span.span(".atr-phase-arrow")["→"]
+ span.span(".atr-phase-symbol-other")["③"]
+ elif phase_key == "preview":
+ span.span(".atr-phase-symbol-other")["①"]
+ span.span(".atr-phase-arrow")["→"]
+ span.span(".atr-phase-symbol-other")["②"]
+ span.span(".atr-phase-arrow")["→"]
+ span.strong(".atr-phase-three.atr-phase-symbol")["③"]
+ span.span(".atr-phase-three.atr-phase-label")["FINISH"]
+
+ return span.collect(separator=" ")
+
+
+def _render_revision_actions(body: htm.Block, revision: sql.Revision,
project_name: str, version_name: str) -> None:
+ body.h3(".fs-6.fw-semibold.mt-3.atr-sans")["Actions"]
+ body.div(".mt-3")[
+ form.render(
+ model_cls=shared.revisions.SetRevisionForm,
+ form_classes="",
+ submit_classes="btn-sm btn-outline-danger",
+ submit_label="Create a new revision from this one",
+ defaults={"revision_number": revision.number},
+ empty=True,
+ )
+ ]
+
+
+def _render_revision_card(
+ page: htm.Block,
+ revision: sql.Revision,
+ files_diff: "FilesDiff",
+ latest_revision_number: str | None,
+ phase_key: str,
+ project_name: str,
+ version_name: str,
+) -> None:
+ with page.block(htm.div, classes=".card.mb-3") as card:
+
card.div(".card-header.d-flex.justify-content-between.align-items-center")[
+ _render_revision_header(revision, latest_revision_number),
+ _render_revision_timestamp(revision),
+ ]
+ with card.block(htm.div, classes=".card-body") as card_body:
+ if revision.description:
+ card_body.p(".mb-2")[htm.strong[revision.description]]
+
+ if revision.parent:
+ card_body.p(".small.text-muted.mb-2")[
+ "Changes from ",
+ htm.a(href=f"#{revision.parent.number}", title=f"Revision
{revision.parent.number}")[
+ "previous revision"
+ ],
+ ":",
+ ]
+ else:
+ card_body.p(".small.text-muted.mb-2")["Initial revision"]
+
+ _render_files_diff(card_body, files_diff)
+
+ is_draft = phase_key == "draft"
+ revision_is_preview = revision.phase.value.lower() ==
"release_preview"
+ if (revision.number != latest_revision_number) and (is_draft or
revision_is_preview):
+ _render_revision_actions(card_body, revision, project_name,
version_name)
+
+
+def _render_revision_header(revision: sql.Revision, latest_revision_number:
str | None) -> htm.Element:
+ revision_phase_key = revision.phase.value.lower().split("_")[-1]
+ badges = [
+ htm.span(".badge.bg-secondary.ms-2")[revision_phase_key.upper()],
+ ]
+ if revision.number == latest_revision_number:
+ badges.append(htm.span(".badge.bg-primary.ms-2")["Current"])
+
+ return htm.h2(".fs-6.my-2.mx-0.p-0.border-0.atr-sans")[
+ htm.a(".fw-bold.text-decoration-none.text-body",
href=f"#{revision.number}")[revision.number],
+ *badges,
+ ]
+
+
+def _render_revision_timestamp(revision: sql.Revision) -> htm.Element:
+ if revision.created:
+ timestamp = revision.created.strftime("%Y-%m-%d %H:%M:%S UTC")
+ else:
+ timestamp = "Invalid timestamp"
+
+ return htm.span(".fs-6.text-muted")[f"{timestamp} by {revision.asfuid}"]
async def _revision_files_diff(
diff --git a/atr/htm.py b/atr/htm.py
index 6df198a..240ccaf 100644
--- a/atr/htm.py
+++ b/atr/htm.py
@@ -136,7 +136,8 @@ class Block:
) -> Generator[Block, Any, Any]:
block = Block(element, classes=classes)
yield block
- self.append(block.collect(separator=separator))
+ # If you use depth=2, you get the context manager
+ self.append(block.collect(separator=separator, depth=3))
def collect(self, separator: Element | VoidElement | str | None = None,
depth: int = 1) -> Element:
src = log.caller_name(depth=depth)
diff --git a/atr/post/revisions.py b/atr/post/revisions.py
index effc0be..0c6a7ad 100644
--- a/atr/post/revisions.py
+++ b/atr/post/revisions.py
@@ -15,31 +15,28 @@
# specific language governing permissions and limitations
# under the License.
-
import aioshutil
import asfquart.base as base
-import quart
import atr.blueprints.post as post
import atr.db as db
import atr.get as get
import atr.models.sql as sql
+import atr.shared as shared
import atr.storage as storage
import atr.util as util
import atr.web as web
@post.committer("/revisions/<project_name>/<version_name>")
-async def selected_post(session: web.Committer, project_name: str,
version_name: str) -> web.WerkzeugResponse:
[email protected](shared.revisions.SetRevisionForm)
+async def selected_post(
+ session: web.Committer, set_revision_form:
shared.revisions.SetRevisionForm, project_name: str, version_name: str
+) -> web.WerkzeugResponse:
"""Set a specific revision as the latest for a candidate draft or release
preview."""
await session.check_access(project_name)
- # TODO: This is not truly empty, so make a form object for this
- await util.validate_empty_form()
- form_data = await quart.request.form
- selected_revision_number = form_data.get("revision_number")
- if not selected_revision_number:
- raise base.ASFQuartException("Missing revision number", errorcode=400)
+ selected_revision_number = set_revision_form.revision_number
async with db.session() as data:
release = await session.release(project_name, version_name,
phase=None, data=data)
diff --git a/atr/post/sbom.py b/atr/post/sbom.py
index 4e1bd22..65f7c0e 100644
--- a/atr/post/sbom.py
+++ b/atr/post/sbom.py
@@ -53,6 +53,7 @@ async def _augment(
"""Augment a CycloneDX SBOM file."""
rel_path = pathlib.Path(file_path)
+ # Check that the file is a .cdx.json archive before creating a revision
if not (file_path.endswith(".cdx.json")):
raise base.ASFQuartException("SBOM augmentation is only supported for
.cdx.json files", errorcode=400)
@@ -96,7 +97,6 @@ async def _scan(session: web.Committer, project_name: str,
version_name: str, fi
"""Scan a CycloneDX SBOM file for vulnerabilities using OSV."""
rel_path = pathlib.Path(file_path)
- # Check that the file is a .cdx.json archive before creating a revision
if not (file_path.endswith(".cdx.json")):
raise base.ASFQuartException("OSV scanning is only supported for
.cdx.json files", errorcode=400)
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 1403e43..d1a6112 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -33,6 +33,7 @@ import atr.shared.ignores as ignores
import atr.shared.keys as keys
import atr.shared.projects as projects
import atr.shared.resolve as resolve
+import atr.shared.revisions as revisions
import atr.shared.sbom as sbom
import atr.shared.start as start
import atr.shared.test as test
@@ -195,6 +196,7 @@ __all__ = [
"keys",
"projects",
"resolve",
+ "revisions",
"sbom",
"start",
"test",
diff --git a/atr/shared/revisions.py b/atr/shared/revisions.py
new file mode 100644
index 0000000..451bd4c
--- /dev/null
+++ b/atr/shared/revisions.py
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import atr.form as form
+
+
+class SetRevisionForm(form.Form):
+ revision_number: str = form.label("Revision number",
widget=form.Widget.HIDDEN)
diff --git a/atr/templates/revisions-selected.html
b/atr/templates/revisions-selected.html
deleted file mode 100644
index b660939..0000000
--- a/atr/templates/revisions-selected.html
+++ /dev/null
@@ -1,136 +0,0 @@
-{% extends "layouts/base.html" %}
-
-{% block title %}
- Revisions of {{ release.short_display_name }} ~ ATR
-{% endblock title %}
-
-{% block description %}
- View the revision history for the {{ release.short_display_name }} candidate
draft.
-{% endblock description %}
-
-{% block content %}
- <p class="d-flex justify-content-between align-items-center">
- {% if phase_key == "draft" %}
- <a href="{{ as_url(get.compose.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="atr-back-link">← Back to Compose {{ release.short_display_name
}}</a>
- <span>
- <strong class="atr-phase-one atr-phase-symbol">①</strong>
- <span class="atr-phase-one atr-phase-label">COMPOSE</span>
- <span class="atr-phase-arrow">→</span>
- <span class="atr-phase-symbol-other">②</span>
- <span class="atr-phase-arrow">→</span>
- <span class="atr-phase-symbol-other">③</span>
- </span>
- {% elif phase_key == "preview" %}
- <a href="{{ as_url(get.finish.selected,
project_name=release.project.name, version_name=release.version) }}"
- class="atr-back-link">← Back to Finish {{ release.short_display_name
}}</a>
- <span>
- <span class="atr-phase-symbol-other">①</span>
- <span class="atr-phase-arrow">→</span>
- <span class="atr-phase-symbol-other">②</span>
- <span class="atr-phase-arrow">→</span>
- <strong class="atr-phase-three atr-phase-symbol">③</strong>
- <span class="atr-phase-three atr-phase-label">FINISH</span>
- </span>
- {% else %}
- <a href="{{ as_url(get.root.index) }}" class="atr-back-link">← Back to
Select a release</a>
- {% endif %}
- </p>
-
- <h1>
- Revisions of <strong>{{ release.project.short_display_name }}</strong>
<em>{{ release.version }}</em>
- </h1>
-
- {% if revision_history %}
- {% for revision, files_diff in revision_history %}
- <div id="{{ revision.number }}" class="card mb-3">
- <div class="card-header d-flex justify-content-between
align-items-center">
- <h2 class="fs-6 my-2 mx-0 p-0 border-0 atr-sans">
- <a href="#{{ revision.number }}"
- class="fw-bold text-decoration-none text-body">{{
revision.number }}</a>
- {% set revision_phase_key =
revision.phase.value.lower().split("_")[-1] %}
- <span class="badge bg-secondary ms-2">{{
revision_phase_key.upper() }}</span>
- {% if revision.number == latest_revision_number %}<span
class="badge bg-primary ms-2">Current</span>{% endif %}
- </h2>
- <span class="fs-6 text-muted">
- {% if revision.created %}
- {{ revision.created.strftime("%Y-%m-%d %H:%M:%S UTC") }}
- {% else %}
- Invalid timestamp
- {% endif %}
- by {{ revision.asfuid }}
- </span>
- </div>
- <div class="card-body">
- {% if revision.description %}
- <p class="mb-2">
- <strong><!-- Description: -->{{ revision.description }}</strong>
- </p>
- {% endif %}
- {% if revision.parent %}
- <p class="small text-muted mb-2">
- Changes from <a href="#{{ revision.parent.number }}"
- title="Revision {{ revision.parent.number }}">previous revision</a>:
- </p>
- {% else %}
- <p class="small text-muted mb-2">Initial revision</p>
- {% endif %}
-
- {% if (not files_diff.added) and (not files_diff.removed) and (not
files_diff.modified) %}
- <p class="fst-italic text-muted mt-2">No file changes detected in
this revision.</p>
- {% else %}
- {% if files_diff.added %}
- <h3 class="fs-6 fw-semibold mt-3 atr-sans">
- Added files <span class="font-monospace fw-normal">({{
files_diff.added|length }})</span>
- </h3>
- <ul class="list-group list-group-flush mb-2">
- {% for file in files_diff.added %}
- <li class="list-group-item list-group-item-success py-1 px-3
small rounded-2">{{ file }}</li>
- {% endfor %}
- </ul>
- {% endif %}
-
- {% if files_diff.removed %}
- <h3 class="fs-6 fw-semibold mt-3 atr-sans">
- Removed files <span class="font-monospace fw-normal">({{
files_diff.removed|length }})</span>
- </h3>
- <ul class="list-group list-group-flush mb-2">
- {% for file in files_diff.removed %}
- <li class="list-group-item list-group-item-danger py-1 px-3
small rounded-2">{{ file }}</li>
- {% endfor %}
- </ul>
- {% endif %}
-
- {% if files_diff.modified %}
- <h3 class="fs-6 fw-semibold mt-3 atr-sans">
- Modified files <span class="font-monospace fw-normal">({{
files_diff.modified|length }})</span>
- </h3>
- <ul class="list-group list-group-flush mb-2">
- {% for file in files_diff.modified %}
- <li class="list-group-item list-group-item-warning py-1 px-3
small rounded-2">{{ file }}</li>
- {% endfor %}
- </ul>
- {% endif %}
- {% endif %}
-
- {% set is_draft = phase_key == "draft" %}
- {% set revision_is_preview = revision.phase.value.lower() ==
"release_preview" %}
- {% if (revision.number != latest_revision_number) and (is_draft or
revision_is_preview) %}
- <h3 class="fs-6 fw-semibold mt-3 atr-sans">Actions</h3>
- <div class="mt-3">
- <form method="post"
- action="{{ as_url(post.revisions.selected_post,
project_name=project_name, version_name=version_name) }}">
- {{ empty_form.hidden_tag() }}
-
- <input type="hidden" name="revision_number" value="{{
revision.number }}" />
- <button type="submit" class="btn btn-sm
btn-outline-danger">Create a new revision from this one</button>
- </form>
- </div>
- {% endif %}
- </div>
- </div>
- {% endfor %}
- {% else %}
- <div class="alert alert-info">No revision history found for this candidate
draft.</div>
- {% endif %}
-{% endblock content %}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]