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 7df8ddc Add a dedicated form for creating a new derived project
7df8ddc is described below
commit 7df8ddc810acbf6f213b38fce0f36809f8e9f4f6
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Apr 10 16:13:08 2025 +0100
Add a dedicated form for creating a new derived project
---
atr/routes/projects.py | 97 ++++++++++++++++++++++-
atr/static/css/atr.css | 2 +-
atr/templates/candidate-vote-project.html | 2 +-
atr/templates/draft-add-files.html | 2 +-
atr/templates/draft-add.html | 2 +-
atr/templates/keys-add.html | 2 +-
atr/templates/keys-ssh-add.html | 2 +-
atr/templates/keys-upload.html | 2 +-
atr/templates/project-add.html | 123 ++++++++++++++++++++++++++++++
atr/templates/project-view.html | 9 +++
atr/templates/vote-policy-form.html | 2 +-
11 files changed, 236 insertions(+), 9 deletions(-)
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 71db8e0..3f40f46 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -18,6 +18,8 @@
"""project.py"""
import http.client
+import re
+from typing import Protocol
import asfquart.base as base
import quart
@@ -31,9 +33,102 @@ import atr.user as user
import atr.util as util
+class AddFormProtocol(Protocol):
+ project_name: wtforms.SelectField
+ derived_project_name: wtforms.StringField
+ submit: wtforms.SubmitField
+
+
@routes.committer("/project/add", methods=["GET", "POST"])
async def add(session: routes.CommitterSession) -> response.Response | str:
- raise NotImplementedError("Not implemented")
+ def long_name(project: models.Project) -> str:
+ if project.full_name:
+ return project.full_name
+ return project.name
+
+ user_projects = await session.user_projects
+
+ class AddForm(util.QuartFormTyped):
+ project_name = wtforms.SelectField("Project", choices=[(p.name,
long_name(p)) for p in user_projects])
+ derived_project_name = wtforms.StringField(
+ "Derived project",
+ validators=[
+ wtforms.validators.InputRequired("Please provide a derived
project name."),
+ wtforms.validators.Length(min=1, max=100),
+ ],
+ )
+ submit = wtforms.SubmitField("Create derived project")
+
+ form = await AddForm.create_form()
+
+ if await form.validate_on_submit():
+ return await _add_project(form)
+
+ return await quart.render_template("project-add.html", form=form)
+
+
+async def _add_project(form: AddFormProtocol) -> response.Response:
+ base_project_name = str(form.project_name.data)
+ derived_project_name = str(form.derived_project_name.data).strip()
+
+ def _generate_label(text: str) -> str:
+ # TODO: We should probably add long name validation
+ text = text.lower()
+ text = re.sub(r" +", "-", text)
+ text = re.sub(r"[^a-z0-9-]", "", text)
+ return text
+
+ async with db.session() as data:
+ # Get the base project to derive from
+ base_project = await data.project(name=base_project_name).get()
+ if not base_project:
+ # This should not happen, assuming that the dropdown is populated
correctly
+ raise routes.FlashError(f"Base project {base_project_name} not
found")
+
+ # Construct the new label
+ derived_label = _generate_label(derived_project_name)
+ if not derived_label:
+ raise routes.FlashError("Derived project name must contain valid
characters for label generation")
+ new_project_label = f"{base_project.name}-{derived_label}"
+
+ # Construct the new full name
+ # We ensure that parenthesised suffixes like "(Incubating)" are
preserved
+ base_name = base_project.full_name or base_project.name
+ match = re.match(r"^(.*?) *(\(.*\))?$", base_name)
+ if match:
+ main_part = match.group(1).strip()
+ suffix_part = match.group(2)
+ else:
+ main_part = base_name.strip()
+ suffix_part = None
+
+ if suffix_part:
+ new_project_full_name = f"{main_part} {derived_project_name}
{suffix_part}"
+ else:
+ new_project_full_name = f"{main_part} {derived_project_name}"
+ new_project_full_name = re.sub(r" +", " ",
new_project_full_name).strip()
+
+ # Check whether the derived project already exists by its constructed
label
+ if await data.project(name=new_project_label).get():
+ raise routes.FlashError(f"Derived project {new_project_label}
already exists")
+
+ project = models.Project(
+ name=new_project_label,
+ full_name=new_project_full_name,
+ is_podling=base_project.is_podling,
+ is_retired=base_project.is_retired,
+ description=base_project.description,
+ category=base_project.category,
+ programming_languages=base_project.programming_languages,
+ committee_id=base_project.committee_id,
+ vote_policy_id=base_project.vote_policy_id,
+ # TODO: Add "created" and "created_by" to models.Project perhaps?
+ )
+
+ data.add(project)
+ await data.commit()
+
+ return quart.redirect(util.as_url(view, name=new_project_label))
@routes.public("/projects")
diff --git a/atr/static/css/atr.css b/atr/static/css/atr.css
index 1c195f3..30b6d4c 100644
--- a/atr/static/css/atr.css
+++ b/atr/static/css/atr.css
@@ -185,7 +185,7 @@ summary {
cursor: pointer;
}
-form.striking {
+form.atr-canary {
background-color: #ffffee;
border: 2px solid #ddddbb;
padding: 1rem;
diff --git a/atr/templates/candidate-vote-project.html
b/atr/templates/candidate-vote-project.html
index e51115e..46c2336 100644
--- a/atr/templates/candidate-vote-project.html
+++ b/atr/templates/candidate-vote-project.html
@@ -38,7 +38,7 @@
{% endif %}
<form method="post"
- class="striking py-4 px-5"
+ class="atr-canary py-4 px-5"
action="{{ as_url(routes.candidate.vote_project,
project_name=release.project.name, version=release.version) }}">
{{ form.hidden_tag() if form.hidden_tag }}
{{ form.release_name }}
diff --git a/atr/templates/draft-add-files.html
b/atr/templates/draft-add-files.html
index 7f54d86..9df7401 100644
--- a/atr/templates/draft-add-files.html
+++ b/atr/templates/draft-add-files.html
@@ -16,7 +16,7 @@
<form method="post"
enctype="multipart/form-data"
- class="striking py-4 px-5 needs-validation"
+ class="atr-canary py-4 px-5 needs-validation"
novalidate>
{{ form.csrf_token }}
<div class="mb-3 pb-3 row border-bottom">
diff --git a/atr/templates/draft-add.html b/atr/templates/draft-add.html
index b1923a0..cfa591d 100644
--- a/atr/templates/draft-add.html
+++ b/atr/templates/draft-add.html
@@ -23,7 +23,7 @@
<form method="post"
enctype="multipart/form-data"
- class="striking py-4 px-5">
+ class="atr-canary py-4 px-5">
<input type="hidden" name="form_type" value="single" />
{{ form.hidden_tag() }}
<div class="mb-3 pb-3 row border-bottom">
diff --git a/atr/templates/keys-add.html b/atr/templates/keys-add.html
index aa47e5c..398c11d 100644
--- a/atr/templates/keys-add.html
+++ b/atr/templates/keys-add.html
@@ -16,7 +16,7 @@
{% if form.errors %}<div class="alert alert-danger">Please correct the
errors below:</div>{% endif %}
<form method="post"
- class="striking py-4 px-5"
+ class="atr-canary py-4 px-5"
action="{{ as_url(routes.keys.add) }}"
novalidate>
{{ form.hidden_tag() if form.hidden_tag }}
diff --git a/atr/templates/keys-ssh-add.html b/atr/templates/keys-ssh-add.html
index 26db6ed..e04ba03 100644
--- a/atr/templates/keys-ssh-add.html
+++ b/atr/templates/keys-ssh-add.html
@@ -27,7 +27,7 @@
</div>
{% endif %}
- <form method="post" class="striking">
+ <form method="post" class="atr-canary">
{{ form.csrf_token }}
<div class="mb-4">
<div class="mb-3">
diff --git a/atr/templates/keys-upload.html b/atr/templates/keys-upload.html
index 218e92b..812e722 100644
--- a/atr/templates/keys-upload.html
+++ b/atr/templates/keys-upload.html
@@ -72,7 +72,7 @@
{% endfor %}
{% endif %}
- <form method="post" class="striking" enctype="multipart/form-data">
+ <form method="post" class="atr-canary" enctype="multipart/form-data">
{{ form.csrf_token }}
<div class="mb-4">
diff --git a/atr/templates/project-add.html b/atr/templates/project-add.html
new file mode 100644
index 0000000..0793c0b
--- /dev/null
+++ b/atr/templates/project-add.html
@@ -0,0 +1,123 @@
+{% extends "layouts/base.html" %}
+
+{% block title %}
+ Add project ~ ATR
+{% endblock title %}
+
+{% block description %}
+ Add a new project based on an existing one.
+{% endblock description %}
+
+{% block content %}
+ <h1>Add derived project</h1>
+ <p class="intro">Derive a new project from an existing one, by adding a
suffix.</p>
+
+ <form method="post" class="atr-canary py-4">
+ {{ form.hidden_tag() }}
+ <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">
+ {{ form.project_name(class_="form-select") }}
+ {% if form.project_name.errors -%}<span class="text-danger small">{{
form.project_name.errors[0] }}</span>{%- endif %}
+ <p class="text-muted small mt-1">Select the base ASF project.</p>
+ </div>
+ </div>
+
+ <div class="mb-3 pb-3 row border-bottom">
+ <label for="{{ form.derived_project_name.id }}"
+ class="col-sm-3 col-form-label text-sm-end">{{
form.derived_project_name.label.text }}:</label>
+ <div class="col-sm-8">
+ {{ form.derived_project_name(class_="form-control") }}
+ {% if form.derived_project_name.errors -%}
+ <span class="text-danger small">{{
form.derived_project_name.errors[0] }}</span>{%- endif %}
+ <p class="text-muted small mt-1">The desired suffix for the full
project name.</p>
+ </div>
+ </div>
+
+ <div class="mb-3 pb-3 row border-bottom">
+ <label id="new-project-name-label"
+ for="new-project-name-display"
+ class="col-sm-3 col-form-label text-sm-end">New project
name:</label>
+ <div class="col-sm-8">
+ <code id="new-project-name-display"
+ class="form-control-plaintext bg-light p-2 rounded
d-block"></code>
+ <p class="text-muted small mt-1">This will be the full display
name for the derived project.</p>
+ </div>
+ </div>
+
+ <div class="mb-3 pb-3 row border-bottom">
+ <label id="new-project-label-label"
+ for="new-project-label-display"
+ class="col-sm-3 col-form-label text-sm-end">New project
label:</label>
+ <div class="col-sm-8">
+ <code id="new-project-label-display"
+ class="form-control-plaintext bg-light p-2 rounded
d-block"></code>
+ <p class="text-muted small mt-1">This will be the short label used
in URLs and identifiers.</p>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-sm-9 offset-sm-3">{{ form.submit(class_="btn
btn-primary mt-3") }}</div>
+ </div>
+ </form>
+ {% endblock content %}
+
+ {% block javascripts %}
+ {{ super() }}
+ <script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const projectSelect = document.getElementById("{{
form.project_name.id }}");
+ const derivedNameInput = document.getElementById("{{
form.derived_project_name.id }}");
+ const newNameDisplay =
document.getElementById("new-project-name-display");
+ const newLabelDisplay =
document.getElementById("new-project-label-display");
+
+ if (!projectSelect || !derivedNameInput || !newNameDisplay ||
!newLabelDisplay) return;
+
+ function generateSlug(text) {
+ return text.toLowerCase().replace(/\s+/g,
"-").replace(/[^a-z0-9-]/g, "");
+ }
+
+ function updatePreview() {
+ const selectedOption =
projectSelect.options[projectSelect.selectedIndex];
+ const baseLabel = selectedOption ? selectedOption.value : "";
+ const baseFullName = selectedOption ? selectedOption.text :
"";
+ const derivedNameValue = derivedNameInput.value.trim();
+
+ let newFullName = baseFullName;
+ if (derivedNameValue) {
+ const match = baseFullName.match(/^(.*?)\s*(\(.*\))?$/);
+ let mainPart = baseFullName.trim();
+ let suffixPart = null;
+
+ if (match) {
+ mainPart = match[1] ? match[1].trim() : mainPart;
+ suffixPart = match[2];
+ }
+
+ if (suffixPart) {
+ newFullName = `${mainPart} ${derivedNameValue}
${suffixPart}`;
+ } else {
+ newFullName = `${mainPart} ${derivedNameValue}`;
+ }
+ newFullName = newFullName.replace(/\s{2,}/g, " ").trim();
+ }
+ newNameDisplay.textContent = newFullName || "(Select base
project)";
+
+ let newLabel = baseLabel;
+ if (derivedNameValue) {
+ const derivedSlug = generateSlug(derivedNameValue);
+ if (derivedSlug) {
+ newLabel = `${baseLabel}-${derivedSlug}`;
+ }
+ }
+ newLabelDisplay.textContent = newLabel || "(Enter derived
project name)";
+ }
+
+ projectSelect.addEventListener("change", updatePreview);
+ derivedNameInput.addEventListener("input", updatePreview);
+
+ updatePreview();
+ });
+ </script>
+ {% endblock javascripts %}
diff --git a/atr/templates/project-view.html b/atr/templates/project-view.html
index 2610f94..a630088 100644
--- a/atr/templates/project-view.html
+++ b/atr/templates/project-view.html
@@ -20,6 +20,15 @@
{% endif %}
</div>
+ <div class="card mb-4">
+ <div class="card-header bg-light">
+ <h3 class="mb-2">Project label</h3>
+ </div>
+ <div class="card-body">
+ <code class="fs-6">{{ project.name }}</code>
+ </div>
+ </div>
+
{% set is_admin = is_admin_fn(current_user.uid) %}
{% set is_committee_member = is_committee_member_fn(project.committee,
current_user.uid) %}
diff --git a/atr/templates/vote-policy-form.html
b/atr/templates/vote-policy-form.html
index 5302e5b..5d83ad3 100644
--- a/atr/templates/vote-policy-form.html
+++ b/atr/templates/vote-policy-form.html
@@ -1,7 +1,7 @@
<form method="post"
enctype="multipart/form-data"
- class="striking py-4 px-5 needs-validation"
+ class="atr-canary py-4 px-5 needs-validation"
novalidate>
<input type="hidden" name="form_type" value="single" />
{{ form.hidden_tag() }}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]