Skip to content

Commit e919ee6

Browse files
committed
Select the project for the user when deriving a sub-project
1 parent df8a5bd commit e919ee6

File tree

7 files changed

+160
-184
lines changed

7 files changed

+160
-184
lines changed

atr/routes/projects.py

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535

3636

3737
class AddFormProtocol(Protocol):
38-
project_name: wtforms.SelectField
38+
project_name: wtforms.HiddenField
3939
derived_project_name: wtforms.StringField
4040
submit: wtforms.SubmitField
4141

@@ -74,17 +74,16 @@ class VotePolicyForm(util.QuartFormTyped):
7474
submit = wtforms.SubmitField("Save")
7575

7676

77-
@routes.committer("/project/add", methods=["GET", "POST"])
78-
async def add(session: routes.CommitterSession) -> response.Response | str:
79-
def long_name(project: models.Project) -> str:
80-
if project.full_name:
81-
return project.full_name
82-
return project.name
83-
84-
user_projects = await session.user_projects
77+
@routes.committer("/project/add/<project_name>", methods=["GET", "POST"])
78+
async def add_project(session: routes.CommitterSession, project_name: str) -> response.Response | str:
79+
await session.check_access(project_name)
80+
async with db.session() as data:
81+
project = await data.project(name=project_name).demand(
82+
base.ASFQuartException(f"Project {project_name} not found", errorcode=404)
83+
)
8584

8685
class AddForm(util.QuartFormTyped):
87-
project_name = wtforms.SelectField("Project", choices=[(p.name, long_name(p)) for p in user_projects])
86+
project_name = wtforms.HiddenField("project_name")
8887
derived_project_name = wtforms.StringField(
8988
"Derived project",
9089
validators=[
@@ -94,12 +93,12 @@ class AddForm(util.QuartFormTyped):
9493
)
9594
submit = wtforms.SubmitField("Add project")
9695

97-
form = await AddForm.create_form()
96+
form = await AddForm.create_form(data={"project_name": project_name})
9897

9998
if await form.validate_on_submit():
10099
return await _add_project(form, session.uid)
101100

102-
return await quart.render_template("project-add.html", form=form)
101+
return await quart.render_template("project-add-project.html", form=form, project_name=project.display_name)
103102

104103

105104
@routes.committer("/project/delete", methods=["POST"])
@@ -277,13 +276,14 @@ def _generate_label(text: str) -> str:
277276
# Get the base project to derive from
278277
base_project = await data.project(name=base_project_name).get()
279278
if not base_project:
280-
# This should not happen, assuming that the dropdown is populated correctly
281-
raise routes.FlashError(f"Base project {base_project_name} not found")
279+
# This should not happen
280+
raise RuntimeError(f"Base project {base_project_name} not found")
282281

283282
# Construct the new label
284283
derived_label = _generate_label(derived_project_name)
285284
if not derived_label:
286-
raise routes.FlashError("Derived project name must contain valid characters for label generation")
285+
await quart.flash("Derived project name must contain valid characters for label generation", "error")
286+
return quart.redirect(util.as_url(add_project, project_name=base_project.name))
287287
new_project_label = f"{base_project.name}-{derived_label}"
288288

289289
# Construct the new full name
@@ -305,7 +305,8 @@ def _generate_label(text: str) -> str:
305305

306306
# Check whether the derived project already exists by its constructed label
307307
if await data.project(name=new_project_label).get():
308-
raise routes.FlashError(f"Derived project {new_project_label} already exists")
308+
await quart.flash(f"Derived project {new_project_label} already exists", "error")
309+
return quart.redirect(util.as_url(add_project, project_name=base_project.name))
309310

310311
project = models.Project(
311312
name=new_project_label,

atr/templates/draft-add.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ <h2>Create an empty candidate draft for an existing project</h2>
3535
{% if not user_projects %}
3636
<p class="text-danger">You must be a participant of a project to submit a release candidate.</p>
3737
{% endif %}
38-
<p class="text-muted">
39-
If your project is not listed above, you may need to <a href="{{ as_url(routes.projects.add) }}">add it first</a>.
40-
</p>
38+
<p class="text-muted">If your project is not listed above, you may need to add it first.</p>
4139
</div>
4240
</div>
4341

atr/templates/index-committer.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ <h2 class="border-bottom border-secondary pb-2 mb-3">
8585
<a href="{{ as_url(routes.projects.view, name=project.name) }}"
8686
class="text-decoration-none me-2">About this project</a>
8787
<span class="text-muted me-2">/</span>
88-
<a href="{{ as_url(routes.projects.add) }}"
88+
<a href="{{ as_url(routes.projects.add_project, project_name=project.name) }}"
8989
class="text-decoration-none me-2">Create a sub-project</a>
9090
{% if completed_releases %}
9191
<span class="text-muted me-2">/</span>
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
{% extends "layouts/base.html" %}
2+
3+
{% block title %}
4+
Add a new project ~ ATR
5+
{% endblock title %}
6+
7+
{% block description %}
8+
Add a new project based on an existing one.
9+
{% endblock description %}
10+
11+
{% block content %}
12+
<h1>
13+
Add a new <strong>{{ project_name.removesuffix(" (Incubating)") }}</strong> sub-project
14+
</h1>
15+
<p>New projects can only be derived from existing projects, by adding a suffix.</p>
16+
17+
<form method="post" class="atr-canary py-4">
18+
{{ form.hidden_tag() }}
19+
20+
<div class="mb-3 pb-3 row border-bottom">
21+
<label for="{{ form.derived_project_name.id }}"
22+
class="col-sm-3 col-form-label text-sm-end">{{ form.derived_project_name.label.text }}:</label>
23+
<div class="col-sm-8">
24+
{{ form.derived_project_name(class_="form-control") }}
25+
{% if form.derived_project_name.errors -%}
26+
<span class="text-danger small">{{ form.derived_project_name.errors[0] }}</span>{%- endif %}
27+
<p class="text-muted mt-1">The desired suffix for the full project name.</p>
28+
<p id="capitalisation-warning" class="text-danger small mt-1 d-none">
29+
<span class="fa-solid fa-triangle-exclamation"></span>
30+
Warning: Ensure all words in the derived name start with a capital for proper display.
31+
</p>
32+
</div>
33+
</div>
34+
35+
<div class="mb-3 pb-3 row border-bottom">
36+
<label id="new-project-name-label"
37+
for="new-project-name-display"
38+
class="col-sm-3 col-form-label text-sm-end">Project name preview:</label>
39+
<div class="col-sm-8">
40+
<code id="new-project-name-display"
41+
class="form-control-plaintext bg-light p-2 rounded d-block"></code>
42+
<p class="text-muted small mt-1">This will be the full display name for the derived project.</p>
43+
</div>
44+
</div>
45+
46+
<div class="mb-3 pb-3 row border-bottom">
47+
<label id="new-project-label-label"
48+
for="new-project-label-display"
49+
class="col-sm-3 col-form-label text-sm-end">Project label preview:</label>
50+
<div class="col-sm-8">
51+
<code id="new-project-label-display"
52+
class="form-control-plaintext bg-light p-2 rounded d-block"></code>
53+
<p class="text-muted small mt-1">This will be the short label used in URLs and identifiers.</p>
54+
</div>
55+
</div>
56+
57+
<div class="row">
58+
<div class="col-sm-9 offset-sm-3">{{ form.submit(class_="btn btn-primary mt-3") }}</div>
59+
</div>
60+
</form>
61+
{% endblock content %}
62+
63+
{% block javascripts %}
64+
{{ super() }}
65+
<script>
66+
document.addEventListener("DOMContentLoaded", () => {
67+
const projectLabel = document.getElementById("{{ form.project_name.id }}");
68+
const projectSelect = "{{ project_name }}";
69+
const derivedNameInput = document.getElementById("{{ form.derived_project_name.id }}");
70+
const newNameDisplay = document.getElementById("new-project-name-display");
71+
const newLabelDisplay = document.getElementById("new-project-label-display");
72+
const capitalisationWarning = document.getElementById("capitalisation-warning");
73+
74+
if (!projectSelect || !derivedNameInput || !newNameDisplay || !newLabelDisplay || !capitalisationWarning) return;
75+
76+
function generateSlug(text) {
77+
return text.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
78+
}
79+
80+
function updatePreview() {
81+
const selectedOption = projectSelect;
82+
const baseLabel = projectLabel.value;
83+
const baseFullName = selectedOption;
84+
const derivedNameValue = derivedNameInput.value.trim();
85+
86+
let hasCapitalisationIssue = false;
87+
if (derivedNameValue) {
88+
const words = derivedNameValue.split(/\s+/);
89+
for (const word of words) {
90+
if (word.length > 0 && !/^[A-Z]/.test(word)) {
91+
hasCapitalisationIssue = true;
92+
break;
93+
}
94+
}
95+
}
96+
97+
if (hasCapitalisationIssue) {
98+
capitalisationWarning.classList.remove("d-none");
99+
} else {
100+
capitalisationWarning.classList.add("d-none");
101+
}
102+
103+
let newFullName = baseFullName;
104+
if (derivedNameValue) {
105+
const match = baseFullName.match(/^(.*?)\s*(\(.*\))?$/);
106+
let mainPart = baseFullName.trim();
107+
let suffixPart = null;
108+
109+
if (match) {
110+
mainPart = match[1] ? match[1].trim() : mainPart;
111+
suffixPart = match[2];
112+
}
113+
114+
if (suffixPart) {
115+
newFullName = `${mainPart} ${derivedNameValue} ${suffixPart}`;
116+
} else {
117+
newFullName = `${mainPart} ${derivedNameValue}`;
118+
}
119+
newFullName = newFullName.replace(/\s{2,}/g, " ").trim();
120+
}
121+
newNameDisplay.textContent = newFullName || "(Select base project)";
122+
123+
let newLabel = baseLabel;
124+
if (derivedNameValue) {
125+
const derivedSlug = generateSlug(derivedNameValue);
126+
if (derivedSlug) {
127+
newLabel = `${baseLabel}-${derivedSlug}`;
128+
}
129+
}
130+
newLabelDisplay.textContent = newLabel || "(Enter derived project name)";
131+
}
132+
133+
derivedNameInput.addEventListener("input", updatePreview);
134+
135+
updatePreview();
136+
});
137+
</script>
138+
{% endblock javascripts %}

atr/templates/project-add.html

Lines changed: 0 additions & 145 deletions
This file was deleted.

0 commit comments

Comments
 (0)