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-release.git


The following commit(s) were added to refs/heads/main by this push:
     new 2abe180  Improve form validation in candidate release creation
2abe180 is described below

commit 2abe180c0fce9a5867088fba2a93c3bce8d358e5
Author: Sean B. Palmer <[email protected]>
AuthorDate: Fri Mar 14 21:02:35 2025 +0200

    Improve form validation in candidate release creation
---
 atr/routes/candidate.py             | 49 ++++++++++++++++++++------
 atr/routes/vote_policy.py           | 11 +++---
 atr/static/css/atr.css              | 29 ++++++++-------
 atr/static/css/bootstrap.custom.css | 10 ++++++
 atr/templates/candidate-create.html | 70 ++++++++++++++++++-------------------
 bootstrap/custom.scss               | 10 ++++++
 6 files changed, 114 insertions(+), 65 deletions(-)

diff --git a/atr/routes/candidate.py b/atr/routes/candidate.py
index d426f47..c5ee23e 100644
--- a/atr/routes/candidate.py
+++ b/atr/routes/candidate.py
@@ -22,6 +22,7 @@ import secrets
 
 import quart
 import werkzeug.wrappers.response as response
+import wtforms
 
 import asfquart
 import asfquart.auth as auth
@@ -30,11 +31,23 @@ import asfquart.session as session
 import atr.db as db
 import atr.db.models as models
 import atr.routes as routes
+import atr.util as util
 
 if asfquart.APP is ...:
     raise RuntimeError("APP is not set")
 
 
+class ReleaseAddForm(util.QuartFormTyped):
+    committee_name = wtforms.StringField(
+        "Committee", validators=[wtforms.validators.InputRequired("Committee 
name is required")]
+    )
+    version = wtforms.StringField("Version", 
validators=[wtforms.validators.InputRequired("Version is required")])
+    project_name = wtforms.StringField(
+        "Project name", validators=[wtforms.validators.InputRequired("Project 
name is required")]
+    )
+    submit = wtforms.SubmitField("Create release candidate")
+
+
 def format_artifact_name(project_name: str, version: str, is_podling: bool = 
False) -> str:
     """Format an artifact name according to Apache naming conventions.
 
@@ -51,21 +64,33 @@ def format_artifact_name(project_name: str, version: str, 
is_podling: bool = Fal
 # Release functions
 
 
-async def release_add_post(session: session.ClientSession, request: 
quart.Request) -> response.Response:
+async def release_add_post(session: session.ClientSession, request: 
quart.Request) -> str | response.Response:
     """Handle POST request for creating a new release."""
-    form = await routes.get_form(request)
 
-    committee_name = form.get("committee_name")
-    if not committee_name:
-        raise base.ASFQuartException("Committee name is required", 
errorcode=400)
+    def not_none(value: str | None) -> str:
+        if value is None:
+            raise ValueError("This field is required")
+        return value
 
-    version = form.get("version")
-    if not version:
-        raise base.ASFQuartException("Version is required", errorcode=400)
+    form = await ReleaseAddForm.create_form(data=await request.form)
+
+    if not await form.validate():
+        # Get PMC objects for all projects the user is a member of
+        async with db.session() as data:
+            project_list = session.committees + session.projects
+            user_committees = await data.committee(name_in=project_list).all()
+
+        # Return the form with validation errors
+        return await quart.render_template(
+            "candidate-create.html",
+            asf_id=session.uid,
+            user_committees=user_committees,
+            form=form,
+        )
 
-    project_name = form.get("project_name")
-    if not project_name:
-        raise base.ASFQuartException("Project name is required", errorcode=400)
+    committee_name = form.committee_name.data
+    version = not_none(form.version.data)
+    project_name = not_none(form.project_name.data)
 
     # TODO: Forbid creating a release with an existing project and version
     # Create the release record in the database
@@ -134,10 +159,12 @@ async def root_candidate_create() -> response.Response | 
str:
         user_committees = await data.committee(name_in=project_list).all()
 
     # For GET requests, show the form
+    form = await ReleaseAddForm.create_form()
     return await quart.render_template(
         "candidate-create.html",
         asf_id=web_session.uid,
         user_committees=user_committees,
+        form=form,
     )
 
 
diff --git a/atr/routes/vote_policy.py b/atr/routes/vote_policy.py
index 889f8c9..f447235 100644
--- a/atr/routes/vote_policy.py
+++ b/atr/routes/vote_policy.py
@@ -18,18 +18,17 @@
 """vote_policy.py"""
 
 import quart
-import quart_wtf
 import werkzeug.wrappers.response as response
 import wtforms
 
+import asfquart.base as base
 import asfquart.session as session
 import atr.db as db
 import atr.routes as routes
-from asfquart import base
-from asfquart.base import ASFQuartException
+import atr.util as util
 
 
-class VotePolicyForm(quart_wtf.QuartForm):
+class VotePolicyForm(util.QuartFormTyped):
     project_name = wtforms.HiddenField("project_name")
     mailto_addresses = wtforms.StringField(
         "Email",
@@ -56,7 +55,7 @@ async def root_vote_policy_edit(vote_policy_id: str) -> 
response.Response | str:
 
     async with db.session() as data:
         vote_policy = await data.vote_policy(id=int(vote_policy_id)).demand(
-            ASFQuartException("Vote policy not found", 404)
+            base.ASFQuartException("Vote policy not found", 404)
         )
 
     form = await VotePolicyForm.create_form()
@@ -65,8 +64,8 @@ async def root_vote_policy_edit(vote_policy_id: str) -> 
response.Response | str:
         form.process(obj=vote_policy)
 
     if await form.validate_on_submit():
+        # return await add_voting_policy(web_session, form)
         return ""
-        # return await add_voting_policy(web_session, form)  # pyright: ignore 
[reportArgumentType]
 
     # For GET requests, show the form
     return await quart.render_template(
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 59a2373..6288cb3 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -28,8 +28,6 @@ body {
 }
 
 input, textarea, select, option {
-    border-width: 2px !important;
-    border-color: #cccccc !important;
     font-size: 17px !important;
     font-weight: 425 !important;
 }
@@ -49,10 +47,26 @@ label[for] {
     cursor: pointer;
 }
 
+input,
+textarea {
+    font-family: monospace;
+    padding: 0.5rem;
+}
+
+textarea {
+    width: 100%;
+    min-height: 200px;
+}
+
 select, input[type="file"] {
     padding: 6px 12px;
 }
 
+input:not([type="submit"]), textarea, select, option {
+    border-width: 2px !important;
+    border-color: #cccccc !important;
+}
+
 a {
     font-weight: 450;
 }
@@ -153,17 +167,6 @@ footer p {
     margin-bottom: 0;
 }
 
-input,
-textarea {
-    font-family: monospace;
-    padding: 0.5rem;
-}
-
-textarea {
-    width: 100%;
-    min-height: 200px;
-}
-
 summary {
     cursor: pointer;
 }
diff --git a/atr/static/css/bootstrap.custom.css 
b/atr/static/css/bootstrap.custom.css
index 295092b..9a0aada 100644
--- a/atr/static/css/bootstrap.custom.css
+++ b/atr/static/css/bootstrap.custom.css
@@ -11455,6 +11455,16 @@ th {
   font-weight: 475;
 }
 
+.btn:disabled {
+  background-color: #cccccc;
+  border-color: #cccccc;
+}
+
+.btn-primary:disabled {
+  background-color: #004477;
+  border-color: #004477;
+}
+
 .btn-primary {
   background-color: #004477;
   border-color: #004477;
diff --git a/atr/templates/candidate-create.html 
b/atr/templates/candidate-create.html
index 106d30c..bcfa835 100644
--- a/atr/templates/candidate-create.html
+++ b/atr/templates/candidate-create.html
@@ -23,11 +23,14 @@
   <form method="post"
         enctype="multipart/form-data"
         class="striking py-4 px-5">
+    <input type="hidden" name="form_type" value="single" />
+    {{ form.hidden_tag() }}
     <div class="mb-3 pb-3 row border-bottom">
-      <label for="committee_name" class="col-sm-3 col-form-label 
text-sm-end">Committee:</label>
+      <label for="{{ form.committee_name.id }}"
+             class="col-sm-3 col-form-label text-sm-end">{{ 
form.committee_name.label.text }}:</label>
       <div class="col-sm-8">
-        <select id="committee_name"
-                name="committee_name"
+        <select id="{{ form.committee_name.id }}"
+                name="{{ form.committee_name.name }}"
                 class="mb-2 form-select"
                 required>
           <option value="">Select a committee...</option>
@@ -35,39 +38,36 @@
             <option value="{{ committee.name }}">{{ committee.display_name 
}}</option>
           {% endfor %}
         </select>
-        {% if not user_committees %}
-          <p class="text-danger">You must be a (P)PMC member or committer to 
submit a release candidate.</p>
-        {% endif %}
+        {% if form.committee_name.errors -%}<span class="error-message">{{ 
form.committee_name.errors[0] }}</span>{%- endif %}
+          {% if not user_committees %}
+            <p class="text-danger">You must be a (P)PMC member or committer to 
submit a release candidate.</p>
+          {% endif %}
+        </div>
       </div>
-    </div>
 
-    <div class="mb-3 pb-3 row border-bottom">
-      <label for="version" class="col-sm-3 col-form-label 
text-sm-end">Version:</label>
-      <div class="col-sm-8">
-        <input type="text" id="version" name="version" class="form-control" 
required />
-      </div>
-    </div>
+      <div class="mb-3 pb-3 row border-bottom">
+        <label for="{{ form.version.id }}"
+               class="col-sm-3 col-form-label text-sm-end">{{ 
form.version.label.text }}:</label>
+        <div class="col-sm-8">
+          {{ form.version(class_="form-control") }}
+          {% if form.version.errors -%}<span class="error-message">{{ 
form.version.errors[0] }}</span>{%- endif %}
+          </div>
+        </div>
 
-    <div class="mb-3 pb-3 row border-bottom">
-      <label for="project_name" class="col-sm-3 col-form-label 
text-sm-end">Project name:</label>
-      <div class="col-sm-8">
-        <!-- TODO: Add a dropdown for the project name, plus "add new project" 
-->
-        <input type="text"
-               id="project_name"
-               name="project_name"
-               class="form-control"
-               required />
-        <!-- TODO: Add a subproject checkbox -->
-        <small class="text-muted">This can be the same as the committee name, 
but may be different if e.g. this is a subproject.</small>
-      </div>
-    </div>
+        <div class="mb-3 pb-3 row border-bottom">
+          <label for="{{ form.project_name.id }}"
+                 class="col-sm-3 col-form-label text-sm-end">{{ 
form.project_name.label.text }}:</label>
+          <div class="col-sm-8">
+            <!-- TODO: Add a dropdown for the project name, plus "add new 
project" -->
+            {{ form.project_name(class_="form-control") }}
+            {% if form.project_name.errors -%}<span class="error-message">{{ 
form.project_name.errors[0] }}</span>{%- endif %}
+              <!-- TODO: Add a subproject checkbox -->
+              <small class="text-muted">This can be the same as the committee 
name, but may be different if e.g. this is a subproject.</small>
+            </div>
+          </div>
 
-    <div class="row">
-      <div class="col-sm-9 offset-sm-3">
-        <button type="submit"
-                class="btn btn-primary mt-2"
-                {% if not user_committees %}disabled{% endif %}>Create 
release</button>
-      </div>
-    </div>
-  </form>
-{% endblock content %}
+          <div class="row">
+            <div class="col-sm-9 offset-sm-3">{{ form.submit(class_="btn 
btn-primary mt-3") }}</div>
+          </div>
+        </form>
+      {% endblock content %}
diff --git a/bootstrap/custom.scss b/bootstrap/custom.scss
index 5c076f4..84e00fe 100644
--- a/bootstrap/custom.scss
+++ b/bootstrap/custom.scss
@@ -76,6 +76,16 @@ th {
   font-weight: 475;
 }
 
+.btn:disabled {
+  background-color: #cccccc;
+  border-color: #cccccc;
+}
+
+.btn-primary:disabled {
+  background-color: #004477;
+  border-color: #004477;
+}
+
 .btn-primary {
   background-color: #004477;
   border-color: #004477;


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

Reply via email to