You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tooling.apache.org by ar...@apache.org on 2026/08/05 13:52:27 UTC

(tooling-trusted-releases) branch arm updated: Give mandatory form fields an asterisk and fix error with file form inputs validation

This is an automated email from the ASF dual-hosted git repository.

alitheg pushed a commit to branch arm
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/arm by this push:
     new 1b8611f0 Give mandatory form fields an asterisk and fix error with file form inputs validation
1b8611f0 is described below

commit 1b8611f024d9f32f94dea897181ca3130ac05772
Author: Alastair McFarlane <ar...@apache.org>
AuthorDate: Wed Aug 5 14:52:09 2026 +0100

    Give mandatory form fields an asterisk and fix error with file form inputs validation
---
 atr/form.py            | 31 +++++++++++++++++++++++++------
 atr/shared/keys.py     |  1 +
 atr/shared/projects.py |  3 +++
 3 files changed, 29 insertions(+), 6 deletions(-)

diff --git a/atr/form.py b/atr/form.py
index 6365b9a4..0b496f4e 100644
--- a/atr/form.py
+++ b/atr/form.py
@@ -160,11 +160,12 @@ def flash_error_summary(errors: list[pydantic_core.ErrorDetails], flash_data: di
 def json_suitable(field_value: Any) -> Any:
     if isinstance(field_value, datastructures.FileStorage):
         return field_value.filename
-    elif isinstance(field_value, list):
-        if all(isinstance(f, datastructures.FileStorage) for f in field_value):
-            return [f.filename for f in field_value]
-        else:
-            return field_value
+    # A model-level validation error carries the whole submitted mapping as its input, so a file
+    # nested inside a dict or list has to be reduced to its name too, not just one at the top level
+    if isinstance(field_value, dict):
+        return {key: json_suitable(value) for key, value in field_value.items()}
+    if isinstance(field_value, list):
+        return [json_suitable(item) for item in field_value]
     return field_value
 
 
@@ -176,6 +177,7 @@ def label(
     widget: Widget | None = None,
     max_length: int | None = None,
     readonly: bool = False,
+    required: bool = False,
     **kwargs,
 ) -> Any:
     extra: dict[str, Any] = {}
@@ -184,6 +186,10 @@ def label(
     if documentation is not None:
         extra["documentation"] = documentation
     extra["readonly"] = readonly
+    # A field with no default is already marked required from is_required. This flag is for the fields
+    # that carry an empty default - a list, an optional - but a validator still makes them mandatory
+    if required:
+        extra["required"] = True
     if len(kwargs) > 0:
         extra.update(kwargs)
     return pydantic.Field(default, description=description, json_schema_extra=extra, max_length=max_length)
@@ -877,6 +883,11 @@ def _render_field_value(
     return field_value
 
 
+def _label_marks_required(field_info: pydantic.fields.FieldInfo) -> bool:
+    extra = field_info.json_schema_extra
+    return isinstance(extra, dict) and bool(extra.get("required"))
+
+
 def _render_row(  # noqa: C901
     field_info: pydantic.fields.FieldInfo,
     field_name: str,
@@ -926,7 +937,15 @@ def _render_row(  # noqa: C901
 
     label_classes = f"{label_col_class} col-form-label text-sm-end"
     label_classes_with_error = f"{label_classes} text-danger" if has_flash_error else label_classes
-    label_elem = htpy.label(for_=field_name, class_=label_classes_with_error)[label_text]
+    label_content: list[Any] = [label_text]
+    # A field with no default is required, but a collection type carries an empty default even when a
+    # validator makes it mandatory, so `required=True` on the label covers what is_required can't see
+    if is_required or _label_marks_required(field_info):
+        # The asterisk is hidden from screen readers, which pick the requirement up from the
+        # visually-hidden note beside it instead
+        label_content.append(htm.span(".text-danger.ms-1", aria_hidden="true")["*"])
+        label_content.append(htm.span(".visually-hidden")[" (required)"])
+    label_elem = htpy.label(for_=field_name, class_=label_classes_with_error)[label_content]
 
     widget_elem = _render_widget(
         field_name=field_name,
diff --git a/atr/shared/keys.py b/atr/shared/keys.py
index 6b3aa337..e4a7b5ae 100644
--- a/atr/shared/keys.py
+++ b/atr/shared/keys.py
@@ -56,6 +56,7 @@ class AddOpenPGPKeyForm(form.Form):
         "Associating your key with a committee adds it to that committee's public KEYS file on the"
         " ASF distribution site, which ATR and downstream users rely on to verify the signatures on its"
         " releases. Associate a key with a committee when you sign releases for it.",
+        required=True,
     )
 
     @pydantic.model_validator(mode="after")
diff --git a/atr/shared/projects.py b/atr/shared/projects.py
index 23357f25..02cc29d4 100644
--- a/atr/shared/projects.py
+++ b/atr/shared/projects.py
@@ -301,6 +301,7 @@ class EditMetadataForm(form.Form):
     homepage: form.OptionalURL = form.label(
         "Homepage",
         "Project website URL.",
+        required=True,
     )
     lifecycle_page: form.OptionalURL = form.label(
         "Lifecycle page",
@@ -309,6 +310,7 @@ class EditMetadataForm(form.Form):
     download_page: form.OptionalURL = form.label(
         "Download page",
         "URL of the project's official download page.",
+        required=True,
     )
     bug_database: form.OptionalURL = form.label(
         "Bug database",
@@ -323,6 +325,7 @@ class EditMetadataForm(form.Form):
         "Repository URLs, one per line.",
         widget=form.Widget.TEXTAREA,
         rows=3,
+        required=True,
     )
     standards: form.URIList = form.label(
         "Standards",


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@tooling.apache.org
For additional commands, e-mail: commits-help@tooling.apache.org