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]

Reply via email to