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]