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 20c5be1  Make the project forms more type safe
20c5be1 is described below

commit 20c5be1bfe4da202b11f02b3007e4e1253263b9a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Dec 9 15:11:59 2025 +0000

    Make the project forms more type safe
---
 atr/forms.py                                   | 507 -------------------------
 atr/get/projects.py                            |  41 +-
 atr/get/report.py                              |   2 -
 atr/post/projects.py                           |  13 +-
 atr/post/tokens.py                             |   4 +-
 atr/shared/__init__.py                         |   5 +-
 atr/shared/projects.py                         |   4 +
 atr/templates/check-selected-path-table.html   |   2 +-
 atr/templates/check-selected-release-info.html |   2 +-
 atr/templates/check-selected.html              |   2 -
 atr/templates/macros/dialog.html               |  80 ----
 atr/templates/projects.html                    |  18 +-
 atr/util.py                                    |  17 -
 13 files changed, 39 insertions(+), 658 deletions(-)

diff --git a/atr/forms.py b/atr/forms.py
deleted file mode 100644
index a58daa0..0000000
--- a/atr/forms.py
+++ /dev/null
@@ -1,507 +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 __future__ import annotations
-
-import dataclasses
-import enum
-from typing import TYPE_CHECKING, Any, Final, Literal, TypeVar
-
-import markupsafe
-import quart_wtf
-import quart_wtf.typing
-import wtforms
-
-import atr.htm as htm
-
-if TYPE_CHECKING:
-    from collections.abc import Callable
-
-EMAIL: Final = wtforms.validators.Email()
-REQUIRED: Final = wtforms.validators.InputRequired()
-REQUIRED_DATA: Final = wtforms.validators.DataRequired()
-OPTIONAL: Final = wtforms.validators.Optional()
-
-# Match _Choice in the wtforms.fields.choices stub
-# typeshed-fallback/stubs/WTForms/wtforms/fields/choices.pyi
-type Choice = tuple[Any, str] | tuple[Any, str, dict[str, Any]]
-type Choices = list[Choice]
-
-
-E = TypeVar("E", bound=enum.Enum)
-
-
-class Typed(quart_wtf.QuartForm):
-    """Quart form with type annotations."""
-
-    csrf_token = wtforms.HiddenField()
-
-    @classmethod
-    async def create_form(
-        cls: type[F],
-        formdata: object | quart_wtf.typing.FormData = quart_wtf.form._Auto,
-        obj: Any | None = None,
-        prefix: str = "",
-        data: dict | None = None,
-        meta: dict | None = None,
-        **kwargs: dict[str, Any],
-    ) -> F:
-        """Create a form instance with typing."""
-        form = await super().create_form(formdata, obj, prefix, data, meta, 
**kwargs)
-        if not isinstance(form, cls):
-            raise TypeError(f"Form is not of type {cls.__name__}")
-        return form
-
-
-F = TypeVar("F", bound=Typed)
-
-
[email protected]
-class Elements:
-    hidden: list[markupsafe.Markup]
-    fields: list[tuple[markupsafe.Markup, markupsafe.Markup]]
-    submit: markupsafe.Markup | None
-
-
-class Empty(Typed):
-    pass
-
-
-class Hidden(Typed):
-    hidden_field = wtforms.HiddenField()
-    submit = wtforms.SubmitField()
-
-
-class Submit(Typed):
-    submit = wtforms.SubmitField()
-
-
-class Value(Typed):
-    value = wtforms.StringField(validators=[REQUIRED])
-    submit = wtforms.SubmitField()
-
-
-def boolean(
-    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
-) -> wtforms.BooleanField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED_DATA)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.BooleanField(label, **kwargs)
-
-
-def checkbox(
-    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
-) -> wtforms.BooleanField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED_DATA)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.BooleanField(label, **kwargs)
-
-
-def checkboxes(
-    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
-) -> wtforms.SelectMultipleField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.SelectMultipleField(
-        label,
-        validators=validators,
-        coerce=str,
-        option_widget=wtforms.widgets.CheckboxInput(),
-        widget=wtforms.widgets.ListWidget(prefix_label=False),
-        **kwargs,
-    )
-
-
-def choices(
-    field: wtforms.RadioField | wtforms.SelectMultipleField, choices: Choices, 
default: str | None = None
-) -> None:
-    field.choices = choices
-    # Form construction calls Field.process
-    # This sets data = self.default() or self.default
-    # Then self.object_data = data
-    # Then calls self.process_data(data) which sets self.data = data
-    # And SelectField.iter_choices reads self.data for the default
-    if isinstance(field, wtforms.RadioField):
-        if default is not None:
-            field.data = default
-
-
-def constant(value: str) -> list[wtforms.validators.InputRequired | 
wtforms.validators.Regexp]:
-    return [REQUIRED, wtforms.validators.Regexp(value, message=f"You must 
enter {value!r} in this field")]
-
-
-def enumeration[E: enum.Enum](enum_cls: type[E]) -> list[tuple[str, str]]:
-    return [(member.name, member.value.name) for member in enum_cls]
-
-
-def enumeration_coerce[E: enum.Enum](enum_cls: type[E]) -> Callable[[Any], E | 
None]:
-    def coerce(value: Any) -> E | None:
-        if isinstance(value, enum_cls):
-            return value
-        if value in (None, ""):
-            return None
-        try:
-            return enum_cls[value]
-        except KeyError as exc:
-            raise ValueError(value) from exc
-
-    return coerce
-
-
-def error(field: wtforms.Field, message: str) -> Literal[False]:
-    if not isinstance(field.errors, list):
-        try:
-            field.errors = list(field.errors)
-        except Exception:
-            field.errors = []
-    field.errors.append(message)
-    return False
-
-
-def clear_errors(field: wtforms.Field) -> None:
-    if not isinstance(field.errors, list):
-        try:
-            field.errors = list(field.errors)
-        except Exception:
-            field.errors = []
-    field.errors[:] = []
-    entries = getattr(field, "entries", None)
-    if isinstance(entries, list):
-        for entry in entries:
-            entry_errors = getattr(entry, "errors", None)
-            if isinstance(entry_errors, list):
-                entry_errors[:] = []
-            else:
-                setattr(entry, "errors", [])
-
-
-def file(label: str, optional: bool = False, validators: list[Any] | None = 
None, **kwargs: Any) -> wtforms.FileField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.FileField(label, validators=validators, **kwargs)
-
-
-def files(
-    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
-) -> wtforms.MultipleFileField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.MultipleFileField(label, validators=validators, **kwargs)
-
-
-def hidden(optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any) -> wtforms.HiddenField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.HiddenField(validators=validators, **kwargs)
-
-
-def integer(
-    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
-) -> wtforms.IntegerField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.IntegerField(label, validators=validators, **kwargs)
-
-
-def length(min: int | None = None, max: int | None = None) -> 
list[wtforms.validators.Length]:
-    validators = []
-    if min is not None:
-        validators.append(wtforms.validators.Length(min=min))
-    if max is not None and max > 0:
-        validators.append(wtforms.validators.Length(max=max))
-    return validators
-
-
-# TODO: Do we need this?
-def multiple(label: str, validators: list[Any] | None = None, **kwargs: Any) 
-> wtforms.SelectMultipleField:
-    if validators is None:
-        validators = [REQUIRED]
-    return wtforms.SelectMultipleField(label, validators=validators, **kwargs)
-
-
-def optional(label: str, **kwargs: Any) -> wtforms.StringField:
-    return string(label, optional=True, **kwargs)
-
-
-def radio(label: str, optional: bool = False, validators: list[Any] | None = 
None, **kwargs: Any) -> wtforms.RadioField:
-    # Choices and default must be set at runtime
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    return wtforms.RadioField(label, validators=validators, **kwargs)
-
-
-def render_columns(
-    form: Typed,
-    action: str,
-    form_classes: str = ".atr-canary",
-    submit_classes: str = "btn-primary",
-    descriptions: bool = False,
-) -> htm.Element:
-    label_classes = "col-sm-3 col-form-label text-sm-end"
-    elements = _render_elements(
-        form,
-        label_classes=label_classes,
-        submit_classes=submit_classes,
-        descriptions=descriptions,
-    )
-
-    field_rows: list[htm.Element] = []
-    for label, widget in elements.fields:
-        row_div = htm.div(".mb-3.row")
-        widget_div = htm.div(".col-sm-8")
-        field_rows.append(row_div[label, widget_div[widget]])
-
-    form_children: list[htm.Element | markupsafe.Markup] = elements.hidden + 
field_rows
-
-    if elements.submit is not None:
-        submit_div = htm.div(".col-sm-9.offset-sm-3")
-        submit_row = htm.div(".row")[submit_div[elements.submit]]
-        form_children.append(submit_row)
-
-    return htm.form(form_classes, action=action, method="post")[form_children]
-
-
-def render_simple(
-    form: Typed,
-    action: str,
-    form_classes: str = "",
-    submit_classes: str = "btn-primary",
-    descriptions: bool = False,
-) -> htm.Element:
-    elements = _render_elements(form, submit_classes=submit_classes, 
descriptions=descriptions)
-
-    field_rows: list[htm.Element] = []
-    for label, widget in elements.fields:
-        row_div = htm.div[label, widget]
-        field_rows.append(row_div)
-
-    form_children: list[htm.Element | markupsafe.Markup] = []
-    form_children.extend(elements.hidden)
-    form_children.append(htm.div[field_rows])
-
-    if elements.submit is not None:
-        submit_row = htm.p[elements.submit]
-        form_children.append(submit_row)
-
-    return htm.form(form_classes, action=action, method="post")[form_children]
-
-
-def render_table(
-    form: Typed,
-    action: str,
-    form_classes: str = "",
-    table_classes: str = ".table.table-striped.table-bordered",
-    submit_classes: str = "btn-primary",
-    descriptions: bool = False,
-) -> htm.Element:
-    # Small elements in Bootstrap
-    elements = _render_elements(form, submit_classes=submit_classes, 
small=True, descriptions=descriptions)
-
-    field_rows: list[htm.Element] = []
-    for label, widget in elements.fields:
-        row_tr = htm.tr[htm.th[label], htm.td[widget]]
-        field_rows.append(row_tr)
-
-    form_children: list[htm.Element | markupsafe.Markup] = []
-    form_children.extend(elements.hidden)
-    form_children.append(htm.table(table_classes)[htm.tbody[field_rows]])
-
-    if elements.submit is not None:
-        submit_row = htm.p[elements.submit]
-        form_children.append(submit_row)
-
-    return htm.form(form_classes, action=action, method="post")[form_children]
-
-
-def select(
-    label: str, optional: bool = False, validators: list[Any] | None = None, 
**kwargs: Any
-) -> wtforms.SelectField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    if "choices" in kwargs:
-        # https://github.com/pallets-eco/wtforms/issues/338
-        if isinstance(kwargs["choices"], type) and 
issubclass(kwargs["choices"], enum.Enum):
-            enum_cls = kwargs["choices"]
-            kwargs["choices"] = enumeration(enum_cls)
-            kwargs["coerce"] = enumeration_coerce(enum_cls)
-    return wtforms.SelectField(label, validators=validators, **kwargs)
-
-
-# TODO: No shared class for Validators?
-def string(
-    label: str,
-    optional: bool = False,
-    validators: list[Any] | None = None,
-    placeholder: str | None = None,
-    **kwargs: Any,
-) -> wtforms.StringField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    if placeholder is not None:
-        if "render_kw" not in kwargs:
-            kwargs["render_kw"] = {}
-        kwargs["render_kw"]["placeholder"] = placeholder
-    return wtforms.StringField(label, validators=validators, **kwargs)
-
-
-def submit(label: str = "Submit", **kwargs: Any) -> wtforms.SubmitField:
-    return wtforms.SubmitField(label, **kwargs)
-
-
-def textarea(
-    label: str,
-    optional: bool = False,
-    validators: list[Any] | None = None,
-    placeholder: str | None = None,
-    rows: int | None = None,
-    **kwargs: Any,
-) -> wtforms.TextAreaField:
-    if validators is None:
-        validators = []
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    if placeholder is not None:
-        if "render_kw" not in kwargs:
-            kwargs["render_kw"] = {}
-        kwargs["render_kw"]["placeholder"] = placeholder
-    if rows is not None:
-        if "render_kw" not in kwargs:
-            kwargs["render_kw"] = {}
-        kwargs["render_kw"]["rows"] = rows
-    return wtforms.TextAreaField(label, validators=validators, **kwargs)
-
-
-def url(
-    label: str,
-    optional: bool = False,
-    validators: list[Any] | None = None,
-    placeholder: str | None = None,
-    **kwargs: Any,
-) -> wtforms.URLField:
-    if validators is None:
-        validators = [wtforms.validators.URL()]
-    if optional is False:
-        validators.append(REQUIRED)
-    else:
-        validators.append(OPTIONAL)
-    if placeholder is not None:
-        if "render_kw" not in kwargs:
-            kwargs["render_kw"] = {}
-        kwargs["render_kw"]["placeholder"] = placeholder
-    return wtforms.URLField(label, validators=validators, **kwargs)
-
-
-def _render_elements(
-    form: Typed,
-    label_classes: str = "col-sm-3 col-form-label text-sm-end",
-    submit_classes: str = "btn-primary",
-    small: bool = False,
-    descriptions: bool = False,
-) -> Elements:
-    hidden_elements: list[markupsafe.Markup] = []
-    field_elements: list[tuple[markupsafe.Markup, markupsafe.Markup]] = []
-    submit_element: markupsafe.Markup | None = None
-
-    for field in form:
-        if isinstance(field, wtforms.HiddenField):
-            hidden_elements.append(markupsafe.Markup(str(field)))
-            continue
-
-        if isinstance(field, wtforms.StringField) or isinstance(field, 
wtforms.SelectField):
-            label = markupsafe.Markup(str(field.label(class_=label_classes)))
-
-            widget_class = "form-control" if isinstance(field, 
wtforms.StringField) else "form-select"
-            widget_classes = widget_class if (small is False) else 
f"{widget_class}-sm"
-            if field.errors:
-                widget_classes += " is-invalid"
-            widget = markupsafe.Markup(str(field(class_=widget_classes)))
-
-            if field.errors:
-                joined_errors = " ".join(field.errors)
-                div = htm.div(".invalid-feedback.d-block")[joined_errors]
-                widget += markupsafe.Markup(str(div))
-
-            if descriptions is True and field.description:
-                desc = htm.div(".form-text.text-muted")[str(field.description)]
-                widget += markupsafe.Markup(str(desc))
-
-            field_elements.append((label, widget))
-            continue
-
-        # wtforms.SubmitField is a subclass of wtforms.BooleanField
-        # So we need to check for it before BooleanField
-        if isinstance(field, wtforms.SubmitField):
-            button_class = "btn " + submit_classes
-            submit_element = markupsafe.Markup(str(field(class_=button_class)))
-            continue
-
-        if isinstance(field, wtforms.BooleanField):
-            # Replacing col-form-label with form-check-label moves the label up
-            # This aligns it properly with the checkbox
-            # TODO: Move the widget down instead of the label up
-            classes = label_classes.replace("col-form-label", 
"form-check-label")
-            label = markupsafe.Markup(str(field.label(class_=classes)))
-            widget = markupsafe.Markup(str(field(class_="form-check-input")))
-            field_elements.append((label, widget))
-            # TODO: Errors and description
-            continue
-
-        raise TypeError(f"Unsupported field type: {type(field).__name__}")
-
-    return Elements(hidden_elements, field_elements, submit_element)
diff --git a/atr/get/projects.py b/atr/get/projects.py
index f18207b..307d5c7 100644
--- a/atr/get/projects.py
+++ b/atr/get/projects.py
@@ -27,7 +27,6 @@ import atr.construct as construct
 import atr.db as db
 import atr.db.interaction as interaction
 import atr.form as form
-import atr.forms as forms
 import atr.get.committees as committees
 import atr.get.file as file
 import atr.get.start as start
@@ -98,7 +97,21 @@ async def projects(session: web.Committer | None) -> str:
     """Main project directory page."""
     async with db.session() as data:
         projects = await 
data.project(_committee=True).order_by(sql.Project.full_name).all()
-        return await template.render("projects.html", projects=projects, 
empty_form=await forms.Empty.create_form())
+
+    delete_forms: dict[str, htm.Element] = {}
+    for project in projects:
+        delete_forms[project.name] = form.render(
+            model_cls=shared.projects.DeleteSelectedProject,
+            action=util.as_url(post.projects.delete),
+            form_classes=".d-inline-block.m-0",
+            submit_classes="btn-sm btn-outline-danger",
+            submit_label="Delete project",
+            empty=True,
+            defaults={"project_name": project.name},
+            confirm="Are you sure you want to delete this project? This cannot 
be undone.",
+        )
+
+    return await template.render("projects.html", projects=projects, 
delete_forms=delete_forms)
 
 
 @get.committer("/project/select")
@@ -286,22 +299,16 @@ def _render_delete_section(project: sql.Project) -> 
htm.Element:
     section = htm.Block(htm.div)
     section.h2["Actions"]
 
-    delete_form = htm.form(
-        ".d-inline-block.m-0",
-        method="post",
+    delete_form = form.render(
+        shared.projects.DeleteProjectForm,
         action=util.as_url(post.projects.view, name=project.name),
-        onsubmit=(
-            f"return confirm('Are you sure you want to delete the project "
-            f"\\'{project.display_name}\\'? This cannot be undone.');"
-        ),
-    )[
-        form.csrf_input(),
-        htpy.input(type="hidden", name="project_name", value=project.name),
-        htpy.input(type="hidden", name="variant", value="delete_project"),
-        htpy.button(".btn.btn-sm.btn-outline-danger", type="submit", 
title=f"Delete {project.display_name}")[
-            htpy.i(".bi.bi-trash"), " Delete project"
-        ],
-    ]
+        form_classes="",
+        submit_classes="btn-sm btn-outline-danger",
+        submit_label="Delete project",
+        defaults={"project_name": project.name},
+        confirm="Are you sure you want to delete this project? This cannot be 
undone.",
+        empty=True,
+    )
 
     section.div(".my-3")[delete_form]
     return section.collect()
diff --git a/atr/get/report.py b/atr/get/report.py
index 76a88e4..4d4a68e 100644
--- a/atr/get/report.py
+++ b/atr/get/report.py
@@ -22,7 +22,6 @@ import aiofiles.os
 import asfquart.base as base
 
 import atr.blueprints.get as get
-import atr.forms as forms
 import atr.models.sql as sql
 import atr.storage as storage
 import atr.template as template
@@ -80,5 +79,4 @@ async def selected_path(session: web.Committer, project_name: 
str, version_name:
         member_results=check_results.member_results_list,
         ignored_results=check_results.ignored_checks,
         format_file_size=util.format_file_size,
-        empty_form=await forms.Empty.create_form(),
     )
diff --git a/atr/post/projects.py b/atr/post/projects.py
index 608b4b0..2997b67 100644
--- a/atr/post/projects.py
+++ b/atr/post/projects.py
@@ -26,7 +26,6 @@ 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
 
 
@@ -53,14 +52,12 @@ async def add_project(
 
 
 @post.committer("/project/delete")
-async def delete(session: web.Committer) -> web.WerkzeugResponse:
[email protected](shared.projects.DeleteSelectedProject)
+async def delete(
+    session: web.Committer, delete_selected_project_form: 
shared.projects.DeleteSelectedProject
+) -> web.WerkzeugResponse:
     """Delete a project created by the user."""
-    # TODO: This is not truly empty, so make a form object for this
-    await util.validate_empty_form()
-    form_data = await quart.request.form
-    project_name = form_data.get("project_name")
-    if not project_name:
-        return await session.redirect(get.projects.projects, error="Missing 
project name for deletion.")
+    project_name = delete_selected_project_form.project_name
 
     async with storage.write(session) as write:
         wacm = await write.as_project_committee_member(project_name)
diff --git a/atr/post/tokens.py b/atr/post/tokens.py
index 4d30d70..2e43ef2 100644
--- a/atr/post/tokens.py
+++ b/atr/post/tokens.py
@@ -28,16 +28,14 @@ import atr.htm as htm
 import atr.jwtoken as jwtoken
 import atr.shared as shared
 import atr.storage as storage
-import atr.util as util
 import atr.web as web
 
 _EXPIRY_DAYS: Final[int] = 180
 
 
 @post.committer("/tokens/jwt")
[email protected]()
 async def jwt_post(session: web.Committer) -> web.QuartResponse:
-    await util.validate_empty_form()
-
     jwt_token = jwtoken.issue(session.uid)
     return web.TextResponse(jwt_token)
 
diff --git a/atr/shared/__init__.py b/atr/shared/__init__.py
index 3535a0b..ded07fa 100644
--- a/atr/shared/__init__.py
+++ b/atr/shared/__init__.py
@@ -17,8 +17,6 @@
 
 from typing import TYPE_CHECKING, Final
 
-import wtforms
-
 import atr.db as db
 import atr.db.interaction as interaction
 import atr.form as form
@@ -85,7 +83,7 @@ async def check(
     release: sql.Release,
     task_mid: str | None = None,
     vote_form: htm.Element | None = None,
-    resolve_form: wtforms.Form | None = None,
+    resolve_form: htm.Element | None = None,
     archive_url: str | None = None,
     vote_task: sql.Task | None = None,
     can_vote: bool = False,
@@ -193,6 +191,7 @@ async def check(
         archive_url=archive_url,
         vote_task_warnings=vote_task_warnings,
         empty_form=empty_form,
+        csrf_input=str(form.csrf_input()),
         resolve_form=resolve_form,
         has_files=has_files,
         strict_checking_errors=strict_checking_errors,
diff --git a/atr/shared/projects.py b/atr/shared/projects.py
index a663c16..fe5e969 100644
--- a/atr/shared/projects.py
+++ b/atr/shared/projects.py
@@ -268,6 +268,10 @@ class DeleteProjectForm(form.Form):
     project_name: str = form.label("Project name", widget=form.Widget.HIDDEN)
 
 
+class DeleteSelectedProject(form.Form):
+    project_name: str = form.label("Project name", widget=form.Widget.HIDDEN)
+
+
 type ProjectViewForm = Annotated[
     ComposePolicyForm
     | VotePolicyForm
diff --git a/atr/templates/check-selected-path-table.html 
b/atr/templates/check-selected-path-table.html
index d23291a..afa82f2 100644
--- a/atr/templates/check-selected-path-table.html
+++ b/atr/templates/check-selected-path-table.html
@@ -60,7 +60,7 @@
                 <form method="post"
                       action="{{ as_url(post.keys.import_selected_revision, 
project_name=project_name, version_name=version_name) }}"
                       class="d-inline mb-0">
-                  {{ empty_form.hidden_tag() }}
+                  {{ csrf_input|safe }}
 
                   <button type="submit" class="btn btn-sm 
btn-outline-primary">Import keys</button>
                 </form>
diff --git a/atr/templates/check-selected-release-info.html 
b/atr/templates/check-selected-release-info.html
index 8830992..e218912 100644
--- a/atr/templates/check-selected-release-info.html
+++ b/atr/templates/check-selected-release-info.html
@@ -109,7 +109,7 @@
           <form action="{{ as_url(post.resolve.selected, 
project_name=release.project.name, version_name=release.version) }}"
                 method="post"
                 class="mb-0">
-            {{ resolve_form.hidden_tag() }}
+            {{ csrf_input|safe }}
             <input type="hidden" name="variant" value="tabulate" />
             <button type="submit" class="btn btn-success">
               <i class="bi bi-clipboard-check me-1"></i> Resolve vote
diff --git a/atr/templates/check-selected.html 
b/atr/templates/check-selected.html
index 8ae75d3..c798f3e 100644
--- a/atr/templates/check-selected.html
+++ b/atr/templates/check-selected.html
@@ -4,8 +4,6 @@
 
 {%- block description -%}Review page for the {{ release.project.display_name 
}} {{ version_name }} candidate{%- endblock description -%}
 
-{% import 'macros/dialog.html' as dialog %}
-
 {% block stylesheets %}
   {{ super() }}
   <style>
diff --git a/atr/templates/macros/dialog.html b/atr/templates/macros/dialog.html
deleted file mode 100644
index db40510..0000000
--- a/atr/templates/macros/dialog.html
+++ /dev/null
@@ -1,80 +0,0 @@
-{% macro delete_modal_with_confirm(id, title, item, action, form, field_name) 
%}
-  {# TODO: Make ESC close this modal #}
-  {% set element_id = id|slugify %}
-  <div class="modal modal-lg fade"
-       id="delete-{{ element_id }}"
-       data-bs-backdrop="static"
-       data-bs-keyboard="false"
-       tabindex="-1"
-       aria-labelledby="delete-{{ element_id }}-label"
-       aria-hidden="true">
-    <div class="modal-dialog border-primary">
-      <div class="modal-content">
-        <div class="modal-header bg-danger bg-opacity-10 text-danger">
-          <h1 class="modal-title fs-5" id="delete-{{ element_id }}-label">{{ 
title }}</h1>
-          <button type="button"
-                  class="btn-close"
-                  data-bs-dismiss="modal"
-                  aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          <p class="text-muted mb-3 atr-sans">
-            Warning: This action will permanently delete this {{ item }} and 
cannot be undone.
-          </p>
-          <form method="post" action="{{ action }}">
-            {{ form.hidden_tag() }}
-
-            <div class="mb-3">
-              <label for="confirm_delete_{{ element_id }}" class="form-label">
-                Type <strong>DELETE</strong> to confirm:
-              </label>
-              <input class="form-control mt-2"
-                     id="confirm_delete_{{ element_id }}"
-                     name="confirm_delete"
-                     placeholder="DELETE"
-                     required=""
-                     type="text"
-                     value=""
-                     onkeyup="updateDeleteButton(this, 'delete-button-{{ 
element_id }}')" />
-            </div>
-            {{ form.submit(class_="btn btn-danger", id_="delete-button-" + 
element_id, disabled=True) }}
-          </form>
-        </div>
-      </div>
-    </div>
-  </div>
-{% endmacro %}
-
-{% macro delete_modal(id, title, item, action, form, field_name) %}
-  {% set element_id = id|string|slugify %}
-  <div class="modal modal-lg fade"
-       id="delete-{{ element_id }}"
-       data-bs-backdrop="static"
-       data-bs-keyboard="false"
-       tabindex="-1"
-       aria-labelledby="delete-{{ element_id }}-label"
-       aria-hidden="true">
-    <div class="modal-dialog border-primary">
-      <div class="modal-content">
-        <div class="modal-header bg-danger bg-opacity-10 text-danger">
-          <h1 class="modal-title fs-5" id="delete-{{ element_id }}-label">{{ 
title }}</h1>
-          <button type="button"
-                  class="btn-close"
-                  data-bs-dismiss="modal"
-                  aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          <p class="text-muted mb-3 atr-sans">
-            Warning: This action will permanently delete this {{ item }} and 
cannot be undone.
-          </p>
-          <form method="post" action="{{ action }}">
-            {{ form.hidden_tag() }}
-            <input type="hidden" name="{{ field_name }}" value="{{ id }}" />
-
-            {{ form.submit(class_="btn btn-danger", id_="delete-button-" + 
element_id) }}
-          </form>
-        </div>
-      </div>
-    </div>
-  </div>
-{% endmacro %}
diff --git a/atr/templates/projects.html b/atr/templates/projects.html
index 442779e..f27b3cd 100644
--- a/atr/templates/projects.html
+++ b/atr/templates/projects.html
@@ -73,23 +73,7 @@
 
             {# TODO: Could add "or is_viewing_as_admin_fn(current_user.uid)" #}
             {# But then the page is noisy for admins #}
-          {% if project.created_by == current_user.uid %}
-            <div class="mt-3">
-              <form method="post"
-                    action="{{ as_url(post.projects.delete) }}"
-                    class="d-inline-block m-0"
-                    onsubmit="return confirm('Are you sure you want to delete 
the project \'{{ project.display_name }}\'? This cannot be undone.');">
-                {{ empty_form.hidden_tag() }}
-
-                <input type="hidden" name="project_name" value="{{ 
project.name }}" />
-                <button type="submit"
-                        class="btn btn-sm btn-outline-danger"
-                        title="Delete {{ project.display_name }}">
-                  <i class="bi bi-trash"></i> Delete project
-                </button>
-              </form>
-            </div>
-          {% endif %}
+          {% if project.created_by == current_user.uid %}<div class="mt-3">{{ 
delete_forms[project.name] }}</div>{% endif %}
 
         </div>
       </div>
diff --git a/atr/util.py b/atr/util.py
index 0745002..701560d 100644
--- a/atr/util.py
+++ b/atr/util.py
@@ -42,12 +42,10 @@ import asfquart.session as session
 import gitignore_parser
 import jinja2
 import quart
-import wtforms
 
 # NOTE: The atr.db module imports this module
 # Therefore, this module must not import atr.db
 import atr.config as config
-import atr.forms as forms
 import atr.ldap as ldap
 import atr.log as log
 import atr.models.sql as sql
@@ -988,21 +986,6 @@ def validate_as_type[T](value: Any, t: type[T]) -> T:
     return value
 
 
-async def validate_empty_form() -> None:
-    empty_form = await forms.Empty.create_form(data=await quart.request.form)
-    if not await empty_form.validate_on_submit():
-        raise base.ASFQuartException("Invalid form submission. Please check 
your input and try again.", errorcode=400)
-
-
-def validate_vote_duration(form: wtforms.Form, field: wtforms.IntegerField) -> 
None:
-    """Checks if the value is 0 or between 72 and 144."""
-    if field.data is None:
-        # TODO: Check that this is what we intend
-        return
-    if not ((field.data == 0) or (72 <= field.data <= 144)):
-        raise wtforms.validators.ValidationError("Minimum voting period must 
be 0 hours, or between 72 and 144 hours")
-
-
 def version_name_error(version_name: str) -> str | None:
     """Check if the given version name is valid."""
     if version_name == "":


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

Reply via email to