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 47ef22c  Move file, finish, and ignores routes to the new layout
47ef22c is described below

commit 47ef22caaa69d8def3e96251bc9fa5a36780a632
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Oct 28 14:53:30 2025 +0000

    Move file, finish, and ignores routes to the new layout
---
 atr/get/__init__.py                          |   6 +
 atr/{routes => get}/file.py                  |   7 +-
 atr/{post/__init__.py => get/finish.py}      |  20 +-
 atr/get/ignores.py                           | 132 +++++++++++++
 atr/post/__init__.py                         |  12 +-
 atr/{get/__init__.py => post/finish.py}      |  37 ++--
 atr/post/ignores.py                          | 121 ++++++++++++
 atr/routes/__init__.py                       |   6 -
 atr/routes/ignores.py                        | 272 ---------------------------
 atr/routes/mapping.py                        |   3 +-
 atr/routes/resolve.py                        |  16 +-
 atr/shared/__init__.py                       |   5 +
 atr/shared/distribution.py                   |   3 +-
 atr/{routes => shared}/finish.py             |  13 +-
 atr/shared/ignores.py                        |  68 +++++++
 atr/templates/announce-selected.html         |   2 +-
 atr/templates/check-selected-path-table.html |   2 +-
 atr/templates/check-selected.html            |   2 +-
 atr/templates/phase-view.html                |   4 +-
 atr/templates/revisions-selected.html        |   2 +-
 20 files changed, 395 insertions(+), 338 deletions(-)

diff --git a/atr/get/__init__.py b/atr/get/__init__.py
index d3b96cc..5c2c1b4 100644
--- a/atr/get/__init__.py
+++ b/atr/get/__init__.py
@@ -25,6 +25,9 @@ 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.file as file
+import atr.get.finish as finish
+import atr.get.ignores as ignores
 import atr.get.vote as vote
 
 ROUTES_MODULE: Final[Literal[True]] = True
@@ -38,5 +41,8 @@ __all__ = [
     "docs",
     "download",
     "draft",
+    "file",
+    "finish",
+    "ignores",
     "vote",
 ]
diff --git a/atr/routes/file.py b/atr/get/file.py
similarity index 90%
rename from atr/routes/file.py
rename to atr/get/file.py
index 1879ab6..545ab9e 100644
--- a/atr/routes/file.py
+++ b/atr/get/file.py
@@ -17,14 +17,15 @@
 
 import werkzeug.wrappers.response as response
 
-import atr.route as route
+import atr.blueprints.get as get
 import atr.template as template
 import atr.util as util
+import atr.web as web
 
 
[email protected]("/file/<project_name>/<version_name>/<path:file_path>")
[email protected]("/file/<project_name>/<version_name>/<path:file_path>")
 async def selected_path(
-    session: route.CommitterSession, project_name: str, version_name: str, 
file_path: str
+    session: web.Committer, project_name: str, version_name: str, file_path: 
str
 ) -> response.Response | str:
     """View the content of a specific file in the release candidate draft."""
     # TODO: Make this independent of the release phase
diff --git a/atr/post/__init__.py b/atr/get/finish.py
similarity index 61%
copy from atr/post/__init__.py
copy to atr/get/finish.py
index 9a396cb..9530bad 100644
--- a/atr/post/__init__.py
+++ b/atr/get/finish.py
@@ -15,14 +15,18 @@
 # specific language governing permissions and limitations
 # under the License.
 
-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
+import quart.wrappers.response as quart_response
+import werkzeug.wrappers.response as response
 
-ROUTES_MODULE: Final[Literal[True]] = True
+import atr.blueprints.get as get
+import atr.shared as shared
+import atr.web as web
 
-__all__ = ["announce", "candidate", "distribution", "draft", "vote"]
+
[email protected]("/finish/<project_name>/<version_name>")
+async def selected(
+    session: web.Committer, project_name: str, version_name: str
+) -> tuple[quart_response.Response, int] | response.Response | str:
+    """Finish a release preview."""
+    return await shared.finish.selected(session, project_name, version_name)
diff --git a/atr/get/ignores.py b/atr/get/ignores.py
new file mode 100644
index 0000000..517509d
--- /dev/null
+++ b/atr/get/ignores.py
@@ -0,0 +1,132 @@
+# 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 typing import Final
+
+import markupsafe
+import werkzeug.wrappers.response as response
+import wtforms
+
+import atr.blueprints.get as get
+import atr.forms as forms
+import atr.htm as htm
+import atr.models.sql as sql
+import atr.post as post
+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
+
+# TODO: Port to TypeScript and move to static files
+_UPDATE_IGNORE_FORM: Final[str] = """
+document.querySelectorAll("table.page-details 
input.form-control").forEach(function (input) {
+    var row = input.closest("tr");
+    var updateBtn = row.querySelector("button.btn-primary");
+    function check() {
+        if (input.value !== input.dataset.value) {
+            updateBtn.classList.remove("disabled");
+        } else {
+            updateBtn.classList.add("disabled");
+        }
+    }
+    input.addEventListener("input", check);
+    check();
+});
+"""
+
+
[email protected]("/ignores/<committee_name>")
+async def ignores(session: web.Committer, committee_name: str) -> str | 
response.Response:
+    async with storage.read() as read:
+        ragp = read.as_general_public()
+        ignores = await ragp.checks.ignores(committee_name)
+
+    content = htm.div[
+        htm.h1["Ignored checks"],
+        htm.p[f"Manage ignored checks for committee {committee_name}."],
+        _add_ignore(committee_name),
+        _existing_ignores(ignores),
+        _script_dom_loaded(_UPDATE_IGNORE_FORM),
+    ]
+
+    return await template.blank("Ignored checks", content)
+
+
+def _check_result_ignore_card(cri: sql.CheckResultIgnore) -> htm.Element:
+    h3_id = cri.id or ""
+    h3_asf_uid = cri.asf_uid
+    h3_created = util.format_datetime(cri.created)
+    card_header_h3 = htm.h3(".mt-3.mb-0")[f"{h3_id} - {h3_asf_uid} - 
{h3_created}"]
+
+    form_update = shared.ignores.UpdateIgnoreForm(id=cri.id)
+
+    def set_field(field: wtforms.StringField | wtforms.SelectField, value: str 
| None) -> None:
+        if value is not None:
+            field.data = value
+
+    set_field(form_update.release_glob, cri.release_glob)
+    set_field(form_update.revision_number, cri.revision_number)
+    set_field(form_update.checker_glob, cri.checker_glob)
+    set_field(form_update.primary_rel_path_glob, cri.primary_rel_path_glob)
+    set_field(form_update.member_rel_path_glob, cri.member_rel_path_glob)
+    set_field(form_update.status, cri.status.to_form_field() if cri.status 
else "None")
+    set_field(form_update.message_glob, cri.message_glob)
+
+    form_path_update = util.as_url(post.ignores.ignores_committee_update, 
committee_name=cri.committee_name)
+    form_update_html = forms.render_table(form_update, form_path_update)
+
+    form_delete = shared.ignores.DeleteIgnoreForm(id=cri.id)
+    form_path_delete = util.as_url(post.ignores.ignores_committee_delete, 
committee_name=cri.committee_name)
+    form_delete_html = forms.render_simple(
+        form_delete,
+        form_path_delete,
+        form_classes=".mt-2.mb-0",
+        submit_classes="btn-danger",
+    )
+
+    card = htm.div(".card.mb-5")[
+        htm.div(".card-header.d-flex.justify-content-between")[card_header_h3, 
form_delete_html],
+        htm.div(".card-body")[form_update_html],
+    ]
+
+    return card
+
+
+def _add_ignore(committee_name: str) -> htm.Element:
+    form_path = util.as_url(post.ignores.ignores_committee_add, 
committee_name=committee_name)
+    return htm.div[
+        htm.h2["Add ignore"],
+        htm.p["Add a new ignore for a check result."],
+        forms.render_columns(shared.ignores.AddIgnoreForm(), form_path),
+    ]
+
+
+def _existing_ignores(ignores: list[sql.CheckResultIgnore]) -> htm.Element:
+    return htm.div[
+        htm.h2["Existing ignores"],
+        [_check_result_ignore_card(cri) for cri in ignores] or htm.p["No 
ignores found."],
+    ]
+
+
+def _script_dom_loaded(text: str) -> htm.Element:
+    script_text = markupsafe.Markup(f"""
+document.addEventListener("DOMContentLoaded", function () {{
+{text}
+}});
+""")
+    return htm.script[script_text]
diff --git a/atr/post/__init__.py b/atr/post/__init__.py
index 9a396cb..be0f6c2 100644
--- a/atr/post/__init__.py
+++ b/atr/post/__init__.py
@@ -21,8 +21,18 @@ 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.finish as finish
+import atr.post.ignores as ignores
 import atr.post.vote as vote
 
 ROUTES_MODULE: Final[Literal[True]] = True
 
-__all__ = ["announce", "candidate", "distribution", "draft", "vote"]
+__all__ = [
+    "announce",
+    "candidate",
+    "distribution",
+    "draft",
+    "finish",
+    "ignores",
+    "vote",
+]
diff --git a/atr/get/__init__.py b/atr/post/finish.py
similarity index 54%
copy from atr/get/__init__.py
copy to atr/post/finish.py
index d3b96cc..4ea8b0e 100644
--- a/atr/get/__init__.py
+++ b/atr/post/finish.py
@@ -15,28 +15,21 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from typing import Final, Literal
+from collections.abc import Awaitable, Callable
 
-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.docs as docs
-import atr.get.download as download
-import atr.get.draft as draft
-import atr.get.vote as vote
+import quart.wrappers.response as quart_response
+import werkzeug.wrappers.response as response
 
-ROUTES_MODULE: Final[Literal[True]] = True
+import atr.blueprints.post as post
+import atr.shared as shared
+import atr.web as web
 
-__all__ = [
-    "announce",
-    "candidate",
-    "committees",
-    "compose",
-    "distribution",
-    "docs",
-    "download",
-    "draft",
-    "vote",
-]
+type Respond = Callable[[int, str], Awaitable[tuple[quart_response.Response, 
int] | response.Response]]
+
+
[email protected]("/finish/<project_name>/<version_name>")
+async def selected(
+    session: web.Committer, project_name: str, version_name: str
+) -> tuple[quart_response.Response, int] | response.Response | str:
+    """Finish a release preview."""
+    return await shared.finish.selected(session, project_name, version_name)
diff --git a/atr/post/ignores.py b/atr/post/ignores.py
new file mode 100644
index 0000000..647da04
--- /dev/null
+++ b/atr/post/ignores.py
@@ -0,0 +1,121 @@
+# 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 quart
+import werkzeug.wrappers.response as response
+
+import atr.blueprints.post as post
+import atr.get as get
+import atr.models.sql as sql
+import atr.shared as shared
+import atr.storage as storage
+import atr.web as web
+
+
[email protected]("/ignores/<committee_name>/add")
+async def ignores_committee_add(session: web.Committer, committee_name: str) 
-> str | response.Response:
+    data = await quart.request.form
+    form = await shared.ignores.AddIgnoreForm.create_form(data=data)
+    if not (await form.validate_on_submit()):
+        return await session.redirect(get.ignores.ignores, error="Form 
validation errors")
+
+    status = sql.CheckResultStatusIgnore.from_form_field(form.status.data)
+
+    async with storage.write() as write:
+        wacm = write.as_committee_member(committee_name)
+        await wacm.checks.ignore_add(
+            release_glob=form.release_glob.data or None,
+            revision_number=form.revision_number.data or None,
+            checker_glob=form.checker_glob.data or None,
+            primary_rel_path_glob=form.primary_rel_path_glob.data or None,
+            member_rel_path_glob=form.member_rel_path_glob.data or None,
+            status=status,
+            message_glob=form.message_glob.data or None,
+        )
+
+    return await session.redirect(
+        get.ignores.ignores,
+        committee_name=committee_name,
+        success="Ignore added",
+    )
+
+
[email protected]("/ignores/<committee_name>/delete")
+async def ignores_committee_delete(session: web.Committer, committee_name: 
str) -> str | response.Response:
+    data = await quart.request.form
+    form = await shared.ignores.DeleteIgnoreForm.create_form(data=data)
+    if not (await form.validate_on_submit()):
+        return await session.redirect(
+            get.ignores.ignores,
+            committee_name=committee_name,
+            error="Form validation errors",
+        )
+
+    if not isinstance(form.id.data, str):
+        return await session.redirect(
+            get.ignores.ignores,
+            committee_name=committee_name,
+            error="Invalid ignore ID",
+        )
+
+    cri_id = int(form.id.data)
+    async with storage.write() as write:
+        wacm = write.as_committee_member(committee_name)
+        await wacm.checks.ignore_delete(id=cri_id)
+
+    return await session.redirect(
+        get.ignores.ignores,
+        committee_name=committee_name,
+        success="Ignore deleted",
+    )
+
+
[email protected]("/ignores/<committee_name>/update")
+async def ignores_committee_update(session: web.Committer, committee_name: 
str) -> str | response.Response:
+    data = await quart.request.form
+    form = await shared.ignores.UpdateIgnoreForm.create_form(data=data)
+    if not (await form.validate_on_submit()):
+        return await session.redirect(get.ignores.ignores, error="Form 
validation errors")
+
+    status = sql.CheckResultStatusIgnore.from_form_field(form.status.data)
+    if not isinstance(form.id.data, str):
+        return await session.redirect(
+            get.ignores.ignores,
+            committee_name=committee_name,
+            error="Invalid ignore ID",
+        )
+    cri_id = int(form.id.data)
+
+    async with storage.write() as write:
+        wacm = write.as_committee_member(committee_name)
+        await wacm.checks.ignore_update(
+            id=cri_id,
+            release_glob=form.release_glob.data or None,
+            revision_number=form.revision_number.data or None,
+            checker_glob=form.checker_glob.data or None,
+            primary_rel_path_glob=form.primary_rel_path_glob.data or None,
+            member_rel_path_glob=form.member_rel_path_glob.data or None,
+            status=status,
+            message_glob=form.message_glob.data or None,
+        )
+
+    return await session.redirect(
+        get.ignores.ignores,
+        committee_name=committee_name,
+        success="Ignore updated",
+    )
diff --git a/atr/routes/__init__.py b/atr/routes/__init__.py
index 4ab4b88..a3c6e18 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.file as file
-import atr.routes.finish as finish
-import atr.routes.ignores as ignores
 import atr.routes.keys as keys
 import atr.routes.preview as preview
 import atr.routes.projects as projects
@@ -36,9 +33,6 @@ import atr.routes.user as user
 import atr.routes.voting as voting
 
 __all__ = [
-    "file",
-    "finish",
-    "ignores",
     "keys",
     "preview",
     "projects",
diff --git a/atr/routes/ignores.py b/atr/routes/ignores.py
deleted file mode 100644
index 4240e92..0000000
--- a/atr/routes/ignores.py
+++ /dev/null
@@ -1,272 +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.
-
-from typing import Final
-
-import markupsafe
-import quart
-import werkzeug.wrappers.response as response
-import wtforms
-
-import atr.forms as forms
-import atr.htm as htm
-import atr.models.sql as sql
-import atr.route as route
-import atr.storage as storage
-import atr.template as template
-import atr.util as util
-
-# TODO: Port to TypeScript and move to static files
-_UPDATE_IGNORE_FORM: Final[str] = """
-document.querySelectorAll("table.page-details 
input.form-control").forEach(function (input) {
-    var row = input.closest("tr");
-    var updateBtn = row.querySelector("button.btn-primary");
-    function check() {
-        if (input.value !== input.dataset.value) {
-            updateBtn.classList.remove("disabled");
-        } else {
-            updateBtn.classList.add("disabled");
-        }
-    }
-    input.addEventListener("input", check);
-    check();
-});
-"""
-
-
-class AddIgnoreForm(forms.Typed):
-    # TODO: Validate that at least one field is set
-    release_glob = forms.optional("Release pattern")
-    revision_number = forms.optional("Revision number (literal)")
-    checker_glob = forms.optional("Checker pattern")
-    primary_rel_path_glob = forms.optional("Primary rel path pattern")
-    member_rel_path_glob = forms.optional("Member rel path pattern")
-    status = forms.select(
-        "Status",
-        optional=True,
-        choices=[
-            (None, "-"),
-            (sql.CheckResultStatusIgnore.EXCEPTION, "Exception"),
-            (sql.CheckResultStatusIgnore.FAILURE, "Failure"),
-            (sql.CheckResultStatusIgnore.WARNING, "Warning"),
-        ],
-    )
-    message_glob = forms.optional("Message pattern")
-    submit = forms.submit("Add ignore")
-
-
-class DeleteIgnoreForm(forms.Typed):
-    id = forms.hidden()
-    submit = forms.submit("Delete")
-
-
-class UpdateIgnoreForm(forms.Typed):
-    # TODO: Validate that at least one field is set
-    id = forms.hidden()
-    release_glob = forms.optional("Release pattern")
-    revision_number = forms.optional("Revision number (literal)")
-    checker_glob = forms.optional("Checker pattern")
-    primary_rel_path_glob = forms.optional("Primary rel path pattern")
-    member_rel_path_glob = forms.optional("Member rel path pattern")
-    status = forms.select(
-        "Status",
-        optional=True,
-        choices=[
-            (None, "-"),
-            (sql.CheckResultStatusIgnore.EXCEPTION, "Exception"),
-            (sql.CheckResultStatusIgnore.FAILURE, "Failure"),
-            (sql.CheckResultStatusIgnore.WARNING, "Warning"),
-        ],
-    )
-    message_glob = forms.optional("Message pattern")
-    submit = forms.submit("Update ignore")
-
-
[email protected]("/ignores/<committee_name>", methods=["GET", "POST"])
-async def ignores(session: route.CommitterSession, committee_name: str) -> str 
| response.Response:
-    async with storage.read() as read:
-        ragp = read.as_general_public()
-        ignores = await ragp.checks.ignores(committee_name)
-
-    content = htm.div[
-        htm.h1["Ignored checks"],
-        htm.p[f"Manage ignored checks for committee {committee_name}."],
-        _add_ignore(committee_name),
-        _existing_ignores(ignores),
-        _script_dom_loaded(_UPDATE_IGNORE_FORM),
-    ]
-
-    return await template.blank("Ignored checks", content)
-
-
[email protected]("/ignores/<committee_name>/add", methods=["POST"])
-async def ignores_committee_add(session: route.CommitterSession, 
committee_name: str) -> str | response.Response:
-    data = await quart.request.form
-    form = await AddIgnoreForm.create_form(data=data)
-    if not (await form.validate_on_submit()):
-        return await session.redirect(ignores, error="Form validation errors")
-
-    status = sql.CheckResultStatusIgnore.from_form_field(form.status.data)
-
-    async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
-        await wacm.checks.ignore_add(
-            release_glob=form.release_glob.data or None,
-            revision_number=form.revision_number.data or None,
-            checker_glob=form.checker_glob.data or None,
-            primary_rel_path_glob=form.primary_rel_path_glob.data or None,
-            member_rel_path_glob=form.member_rel_path_glob.data or None,
-            status=status,
-            message_glob=form.message_glob.data or None,
-        )
-
-    return await session.redirect(
-        ignores,
-        committee_name=committee_name,
-        success="Ignore added",
-    )
-
-
[email protected]("/ignores/<committee_name>/delete", methods=["POST"])
-async def ignores_committee_delete(session: route.CommitterSession, 
committee_name: str) -> str | response.Response:
-    data = await quart.request.form
-    form = await DeleteIgnoreForm.create_form(data=data)
-    if not (await form.validate_on_submit()):
-        return await session.redirect(
-            ignores,
-            committee_name=committee_name,
-            error="Form validation errors",
-        )
-
-    if not isinstance(form.id.data, str):
-        return await session.redirect(
-            ignores,
-            committee_name=committee_name,
-            error="Invalid ignore ID",
-        )
-
-    cri_id = int(form.id.data)
-    async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
-        await wacm.checks.ignore_delete(id=cri_id)
-
-    return await session.redirect(
-        ignores,
-        committee_name=committee_name,
-        success="Ignore deleted",
-    )
-
-
[email protected]("/ignores/<committee_name>/update", methods=["POST"])
-async def ignores_committee_update(session: route.CommitterSession, 
committee_name: str) -> str | response.Response:
-    data = await quart.request.form
-    form = await UpdateIgnoreForm.create_form(data=data)
-    if not (await form.validate_on_submit()):
-        return await session.redirect(ignores, error="Form validation errors")
-
-    status = sql.CheckResultStatusIgnore.from_form_field(form.status.data)
-    if not isinstance(form.id.data, str):
-        return await session.redirect(
-            ignores,
-            committee_name=committee_name,
-            error="Invalid ignore ID",
-        )
-    cri_id = int(form.id.data)
-
-    async with storage.write() as write:
-        wacm = write.as_committee_member(committee_name)
-        await wacm.checks.ignore_update(
-            id=cri_id,
-            release_glob=form.release_glob.data or None,
-            revision_number=form.revision_number.data or None,
-            checker_glob=form.checker_glob.data or None,
-            primary_rel_path_glob=form.primary_rel_path_glob.data or None,
-            member_rel_path_glob=form.member_rel_path_glob.data or None,
-            status=status,
-            message_glob=form.message_glob.data or None,
-        )
-
-    return await session.redirect(
-        ignores,
-        committee_name=committee_name,
-        success="Ignore updated",
-    )
-
-
-def _check_result_ignore_card(cri: sql.CheckResultIgnore) -> htm.Element:
-    h3_id = cri.id or ""
-    h3_asf_uid = cri.asf_uid
-    h3_created = util.format_datetime(cri.created)
-    card_header_h3 = htm.h3(".mt-3.mb-0")[f"{h3_id} - {h3_asf_uid} - 
{h3_created}"]
-
-    form_update = UpdateIgnoreForm(id=cri.id)
-
-    def set_field(field: wtforms.StringField | wtforms.SelectField, value: str 
| None) -> None:
-        if value is not None:
-            field.data = value
-
-    set_field(form_update.release_glob, cri.release_glob)
-    set_field(form_update.revision_number, cri.revision_number)
-    set_field(form_update.checker_glob, cri.checker_glob)
-    set_field(form_update.primary_rel_path_glob, cri.primary_rel_path_glob)
-    set_field(form_update.member_rel_path_glob, cri.member_rel_path_glob)
-    set_field(form_update.status, cri.status.to_form_field() if cri.status 
else "None")
-    set_field(form_update.message_glob, cri.message_glob)
-
-    form_path_update = util.as_url(ignores_committee_update, 
committee_name=cri.committee_name)
-    form_update_html = forms.render_table(form_update, form_path_update)
-
-    form_delete = DeleteIgnoreForm(id=cri.id)
-    form_path_delete = util.as_url(ignores_committee_delete, 
committee_name=cri.committee_name)
-    form_delete_html = forms.render_simple(
-        form_delete,
-        form_path_delete,
-        form_classes=".mt-2.mb-0",
-        submit_classes="btn-danger",
-    )
-
-    card = htm.div(".card.mb-5")[
-        htm.div(".card-header.d-flex.justify-content-between")[card_header_h3, 
form_delete_html],
-        htm.div(".card-body")[form_update_html],
-    ]
-
-    return card
-
-
-def _add_ignore(committee_name: str) -> htm.Element:
-    form_path = util.as_url(ignores_committee_add, 
committee_name=committee_name)
-    return htm.div[
-        htm.h2["Add ignore"],
-        htm.p["Add a new ignore for a check result."],
-        forms.render_columns(AddIgnoreForm(), form_path),
-    ]
-
-
-def _existing_ignores(ignores: list[sql.CheckResultIgnore]) -> htm.Element:
-    return htm.div[
-        htm.h2["Existing ignores"],
-        [_check_result_ignore_card(cri) for cri in ignores] or htm.p["No 
ignores found."],
-    ]
-
-
-def _script_dom_loaded(text: str) -> htm.Element:
-    script_text = markupsafe.Markup(f"""
-document.addEventListener("DOMContentLoaded", function () {{
-{text}
-}});
-""")
-    return htm.script[script_text]
diff --git a/atr/routes/mapping.py b/atr/routes/mapping.py
index 5adf040..930f52d 100644
--- a/atr/routes/mapping.py
+++ b/atr/routes/mapping.py
@@ -22,7 +22,6 @@ 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.finish as finish
 import atr.routes.release as routes_release
 import atr.util as util
 import atr.web as web
@@ -45,7 +44,7 @@ def release_as_route(release: sql.Release) -> Callable:
         case sql.ReleasePhase.RELEASE_CANDIDATE:
             return get.vote.selected
         case sql.ReleasePhase.RELEASE_PREVIEW:
-            return finish.selected
+            return get.finish.selected
         case sql.ReleasePhase.RELEASE:
             return routes_release.finished
 
diff --git a/atr/routes/resolve.py b/atr/routes/resolve.py
index dede4be..dd9a6e4 100644
--- a/atr/routes/resolve.py
+++ b/atr/routes/resolve.py
@@ -21,11 +21,9 @@ import quart
 import werkzeug.wrappers.response as response
 
 import atr.forms as forms
-import atr.get.compose as compose
-import atr.get.vote as vote
+import atr.get as get
 import atr.models.sql as sql
 import atr.route as route
-import atr.routes.finish as finish
 import atr.storage as storage
 import atr.tabulate as tabulate
 import atr.template as template
@@ -115,9 +113,9 @@ async def manual_selected_post(
     async with storage.write_as_project_committee_member(project_name) as wacm:
         success_message = await wacm.vote.resolve_manually(project_name, 
release, vote_result)
     if vote_result == "passed":
-        destination = finish.selected
+        destination = get.finish.selected
     else:
-        destination = compose.selected
+        destination = get.compose.selected
 
     return await session.redirect(
         destination, project_name=project_name, version_name=version_name, 
success=success_message
@@ -135,7 +133,7 @@ async def submit_selected(
     if not (await resolve_form.validate_on_submit()):
         # TODO: Render the page again with errors
         return await session.redirect(
-            vote.selected,
+            get.vote.selected,
             project_name=project_name,
             version_name=version_name,
             error="Invalid form submission.",
@@ -155,11 +153,11 @@ async def submit_selected(
         await quart.flash(error_message, "error")
     if vote_result == "passed":
         if voting_round == 1:
-            destination = vote.selected
+            destination = get.vote.selected
         else:
-            destination = finish.selected
+            destination = get.finish.selected
     else:
-        destination = compose.selected
+        destination = get.compose.selected
 
     return await session.redirect(
         destination, project_name=project_name, version_name=version_name, 
success=success_message
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 352c86b..df2ffe0 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -28,6 +28,8 @@ import atr.models.sql as sql
 import atr.shared.announce as announce
 import atr.shared.distribution as distribution
 import atr.shared.draft as draft
+import atr.shared.finish as finish
+import atr.shared.ignores as ignores
 import atr.shared.vote as vote
 import atr.storage as storage
 import atr.template as template
@@ -177,5 +179,8 @@ __all__ = [
     "announce",
     "check",
     "distribution",
+    "draft",
+    "finish",
+    "ignores",
     "vote",
 ]
diff --git a/atr/shared/distribution.py b/atr/shared/distribution.py
index a584195..67b32fd 100644
--- a/atr/shared/distribution.py
+++ b/atr/shared/distribution.py
@@ -29,7 +29,6 @@ import atr.get as get
 import atr.htm as htm
 import atr.models.distribution as distribution
 import atr.models.sql as sql
-import atr.routes.finish as finish
 import atr.storage as storage
 import atr.template as template
 import atr.util as util
@@ -124,7 +123,7 @@ def html_nav_phase(block: htm.Block, project: str, version: 
str, staging: bool)
     label: Phase
     route, label = (get.compose.selected, "COMPOSE")
     if not staging:
-        route, label = (finish.selected, "FINISH")
+        route, label = (get.finish.selected, "FINISH")
     html_nav(
         block,
         util.as_url(
diff --git a/atr/routes/finish.py b/atr/shared/finish.py
similarity index 98%
rename from atr/routes/finish.py
rename to atr/shared/finish.py
index 3f60489..9cc227f 100644
--- a/atr/routes/finish.py
+++ b/atr/shared/finish.py
@@ -34,12 +34,12 @@ import atr.db as db
 import atr.forms as forms
 import atr.log as log
 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.storage as storage
 import atr.template as template
 import atr.util as util
+import atr.web as web
 
 type Respond = Callable[[int, str], Awaitable[tuple[quart_response.Response, 
int] | response.Response]]
 
@@ -81,7 +81,7 @@ class RemoveRCTagsForm(forms.Typed):
 @dataclasses.dataclass
 class ProcessFormDataArgs:
     formdata: datastructures.MultiDict
-    session: route.CommitterSession
+    session: web.Committer
     project_name: str
     version_name: str
     move_form: MoveFileForm
@@ -99,9 +99,8 @@ class RCTagAnalysisResult:
     total_paths: int
 
 
[email protected]("/finish/<project_name>/<version_name>", methods=["GET", 
"POST"])
 async def selected(
-    session: route.CommitterSession, project_name: str, version_name: str
+    session: web.Committer, project_name: str, version_name: str
 ) -> tuple[quart_response.Response, int] | response.Response | str:
     """Finish a release preview."""
     await session.check_access(project_name)
@@ -244,7 +243,7 @@ async def _deletable_choices(latest_revision_dir: 
pathlib.Path, target_dirs: set
 
 async def _delete_empty_directory(
     dir_to_delete_rel: pathlib.Path,
-    session: route.CommitterSession,
+    session: web.Committer,
     project_name: str,
     version_name: str,
     respond: Respond,
@@ -265,7 +264,7 @@ async def _delete_empty_directory(
 async def _move_file_to_revision(
     source_files_rel: list[pathlib.Path],
     target_dir_rel: pathlib.Path,
-    session: route.CommitterSession,
+    session: web.Committer,
     project_name: str,
     version_name: str,
     respond: Respond,
@@ -306,7 +305,7 @@ async def _move_file_to_revision(
 
 
 async def _remove_rc_tags(
-    session: route.CommitterSession,
+    session: web.Committer,
     project_name: str,
     version_name: str,
     respond: Respond,
diff --git a/atr/shared/ignores.py b/atr/shared/ignores.py
new file mode 100644
index 0000000..c06632e
--- /dev/null
+++ b/atr/shared/ignores.py
@@ -0,0 +1,68 @@
+# 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.forms as forms
+import atr.models.sql as sql
+
+
+class AddIgnoreForm(forms.Typed):
+    # TODO: Validate that at least one field is set
+    release_glob = forms.optional("Release pattern")
+    revision_number = forms.optional("Revision number (literal)")
+    checker_glob = forms.optional("Checker pattern")
+    primary_rel_path_glob = forms.optional("Primary rel path pattern")
+    member_rel_path_glob = forms.optional("Member rel path pattern")
+    status = forms.select(
+        "Status",
+        optional=True,
+        choices=[
+            (None, "-"),
+            (sql.CheckResultStatusIgnore.EXCEPTION, "Exception"),
+            (sql.CheckResultStatusIgnore.FAILURE, "Failure"),
+            (sql.CheckResultStatusIgnore.WARNING, "Warning"),
+        ],
+    )
+    message_glob = forms.optional("Message pattern")
+    submit = forms.submit("Add ignore")
+
+
+class DeleteIgnoreForm(forms.Typed):
+    id = forms.hidden()
+    submit = forms.submit("Delete")
+
+
+class UpdateIgnoreForm(forms.Typed):
+    # TODO: Validate that at least one field is set
+    id = forms.hidden()
+    release_glob = forms.optional("Release pattern")
+    revision_number = forms.optional("Revision number (literal)")
+    checker_glob = forms.optional("Checker pattern")
+    primary_rel_path_glob = forms.optional("Primary rel path pattern")
+    member_rel_path_glob = forms.optional("Member rel path pattern")
+    status = forms.select(
+        "Status",
+        optional=True,
+        choices=[
+            (None, "-"),
+            (sql.CheckResultStatusIgnore.EXCEPTION, "Exception"),
+            (sql.CheckResultStatusIgnore.FAILURE, "Failure"),
+            (sql.CheckResultStatusIgnore.WARNING, "Warning"),
+        ],
+    )
+    message_glob = forms.optional("Message pattern")
+    submit = forms.submit("Update ignore")
diff --git a/atr/templates/announce-selected.html 
b/atr/templates/announce-selected.html
index d868edb..0627bb7 100644
--- a/atr/templates/announce-selected.html
+++ b/atr/templates/announce-selected.html
@@ -25,7 +25,7 @@
 
 {% block content %}
   <p class="d-flex justify-content-between align-items-center">
-    <a href="{{ as_url(routes.finish.selected, 
project_name=release.project.name, version_name=release.version) }}"
+    <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>
diff --git a/atr/templates/check-selected-path-table.html 
b/atr/templates/check-selected-path-table.html
index b67368b..b2375b7 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -44,7 +44,7 @@
             {% endif %}
           </td>
           <td class="py-2">
-            <a href="{{ as_url(routes.file.selected_path, 
project_name=project_name, version_name=version_name, file_path=path) }}"
+            <a href="{{ as_url(get.file.selected_path, 
project_name=project_name, version_name=version_name, file_path=path) }}"
                title="View file {{ path }}"
                class="text-decoration-none text-reset">
               {% if has_errors or has_warnings %}
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index 37567ee..d229cd0 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -162,7 +162,7 @@
       <p>No ignored checks found.</p>
     {% endif %}
     <p>
-      You can also <a href="{{ as_url(routes.ignores.ignores, 
committee_name=release.committee.name) }}">manage which check results are 
ignored</a>.
+      You can also <a href="{{ as_url(get.ignores.ignores, 
committee_name=release.committee.name) }}">manage which check results are 
ignored</a>.
     </p>
 
     <h3 id="debugging" class="mt-4">Debugging</h3>
diff --git a/atr/templates/phase-view.html b/atr/templates/phase-view.html
index 61ff04f..486bd9c 100644
--- a/atr/templates/phase-view.html
+++ b/atr/templates/phase-view.html
@@ -34,7 +34,7 @@
         <span class="atr-phase-symbol-other">③</span>
       </span>
     {% elif phase_key == "preview" %}
-      <a href="{{ as_url(routes.finish.selected, 
project_name=release.project.name, version_name=release.version) }}"
+      <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>
@@ -99,7 +99,7 @@
                   <td>
                     {% if stat.is_file %}
                       {% if phase_key == "draft" %}
-                        {% set file_url = as_url(routes.file.selected_path, 
project_name=release.project.name, version_name=release.version, 
file_path=stat.path) %}
+                        {% set file_url = as_url(get.file.selected_path, 
project_name=release.project.name, version_name=release.version, 
file_path=stat.path) %}
                       {% elif phase_key == "candidate" %}
                         {% set file_url = as_url(get.candidate.view_path, 
project_name=release.project.name, version_name=release.version, 
file_path=stat.path) %}
                       {% elif phase_key == "preview" %}
diff --git a/atr/templates/revisions-selected.html 
b/atr/templates/revisions-selected.html
index 0dddf84..8ba4049 100644
--- a/atr/templates/revisions-selected.html
+++ b/atr/templates/revisions-selected.html
@@ -22,7 +22,7 @@
         <span class="atr-phase-symbol-other">③</span>
       </span>
     {% elif phase_key == "preview" %}
-      <a href="{{ as_url(routes.finish.selected, 
project_name=release.project.name, version_name=release.version) }}"
+      <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>


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


Reply via email to