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 c74256d Add a form to delete empty directories
c74256d is described below
commit c74256d8e1a652e9c99a20c0e82cbab3b2bb954b
Author: Sean B. Palmer <[email protected]>
AuthorDate: Wed May 28 15:09:38 2025 +0100
Add a form to delete empty directories
---
atr/routes/finish.py | 80 +++++++++++++++++++++++++++++++++++++-
atr/templates/finish-selected.html | 23 ++++++-----
2 files changed, 93 insertions(+), 10 deletions(-)
diff --git a/atr/routes/finish.py b/atr/routes/finish.py
index 5c9455e..889e515 100644
--- a/atr/routes/finish.py
+++ b/atr/routes/finish.py
@@ -40,6 +40,15 @@ SPECIAL_SUFFIXES: Final[frozenset[str]] = frozenset({".asc",
".sha256", ".sha512
_LOGGER: Final = logging.getLogger(__name__)
+class DeleteEmptyDirectoryForm(util.QuartFormTyped):
+ """Form for deleting an empty directory within a preview revision."""
+
+ directory_to_delete = wtforms.SelectField(
+ "Directory to delete", choices=[],
validators=[wtforms.validators.DataRequired()]
+ )
+ submit = wtforms.SubmitField("Delete directory")
+
+
class MoveFileForm(util.QuartFormTyped):
"""Form for moving one or more files within a preview revision."""
@@ -96,14 +105,30 @@ async def selected(
move_form = await MoveFileForm.create_form(
data=formdata if (formdata and formdata.get("form_action") !=
"create_dir") else None
)
+ delete_dir_form = await DeleteEmptyDirectoryForm.create_form(
+ data=formdata if (formdata and formdata.get("form_action") ==
"delete_empty_dir") else None
+ )
# Populate choices dynamically for both GET and POST
move_form.source_files.choices = sorted([(str(p), str(p)) for p in
source_files_rel])
move_form.target_directory.choices = sorted([(str(d), str(d)) for d in
target_dirs])
can_move = (len(target_dirs) > 1) and (len(source_files_rel) > 0)
+ empty_deletable_dirs: list[pathlib.Path] = []
+ if latest_revision_dir.exists():
+ for d_rel in target_dirs:
+ if d_rel == pathlib.Path("."):
+ # Disallow deletion of the root directory
+ continue
+ d_full = latest_revision_dir / d_rel
+ if await aiofiles.os.path.isdir(d_full) and not await
aiofiles.os.listdir(d_full):
+ empty_deletable_dirs.append(d_rel)
+ delete_dir_form.directory_to_delete.choices = sorted([(str(p), str(p)) for
p in empty_deletable_dirs])
+
if formdata:
- result = await _process_formdata(formdata, session, project_name,
version_name, move_form, can_move)
+ result = await _process_formdata(
+ formdata, session, project_name, version_name, move_form,
delete_dir_form, can_move
+ )
if result is not None:
return result
@@ -119,6 +144,7 @@ async def selected(
release=release,
source_files=sorted(source_files_rel),
form=move_form,
+ delete_dir_form=delete_dir_form,
can_move=can_move,
user_ssh_keys=user_ssh_keys,
target_dirs=sorted(list(target_dirs)),
@@ -132,6 +158,7 @@ async def _process_formdata(
project_name: str,
version_name: str,
move_form: MoveFileForm,
+ delete_dir_form: DeleteEmptyDirectoryForm,
can_move: bool,
) -> tuple[quart_response.Response, int] | response.Response | str | None:
form_action = formdata.get("form_action")
@@ -168,6 +195,24 @@ async def _process_formdata(
source_files_rel, target_dir_rel, session, project_name,
version_name, wants_json
)
+ elif form_action == "delete_empty_dir":
+ wants_json =
quart.request.accept_mimetypes.best_match(["application/json", "text/html"]) ==
"application/json"
+ if await delete_dir_form.validate_on_submit():
+ dir_to_delete_str = delete_dir_form.directory_to_delete.data
+ return await _delete_empty_dir_action(
+ pathlib.Path(dir_to_delete_str), session, project_name,
version_name, wants_json
+ )
+ elif wants_json:
+ error_messages = []
+ for field_name_str, error_list in delete_dir_form.errors.items():
+ field_obj = getattr(delete_dir_form, field_name_str, None)
+ label_text = field_name_str.replace("_", " ").title()
+ if field_obj and hasattr(field_obj, "label") and
field_obj.label:
+ label_text = field_obj.label.text
+ error_messages.append(f"{label_text}: {', '.join(error_list)}")
+ error_msg = "; ".join(error_messages)
+ return await _respond(session, project_name, version_name, True,
False, error_msg or "Invalid input.", 400)
+
elif ((form_action != "create_dir") or (form_action is None)) and can_move:
return await _move_file(move_form, session, project_name, version_name)
return None
@@ -420,3 +465,36 @@ async def _sources_and_targets(latest_revision_dir:
pathlib.Path) -> tuple[list[
target_dirs.add(item_rel_path)
return source_items_rel, target_dirs
+
+
+async def _delete_empty_dir_action(
+ dir_to_delete_rel: pathlib.Path,
+ session: routes.CommitterSession,
+ project_name: str,
+ version_name: str,
+ wants_json: bool,
+) -> tuple[quart_response.Response, int] | response.Response:
+ try:
+ description = f"Delete empty directory {dir_to_delete_rel} via web
interface"
+ async with revision.create_and_manage(
+ project_name, version_name, session.uid, description=description
+ ) as creating:
+ path_to_remove = creating.interim_path / dir_to_delete_rel
+
path_to_remove.resolve().relative_to(creating.interim_path.resolve())
+ if not await aiofiles.os.path.isdir(path_to_remove):
+ raise revision.FailedError(f"Path '{dir_to_delete_rel}' is not
a directory.")
+ if await aiofiles.os.listdir(path_to_remove):
+ raise revision.FailedError(f"Directory '{dir_to_delete_rel}'
is not empty.")
+ await aiofiles.os.rmdir(path_to_remove)
+
+ except Exception:
+ _LOGGER.exception(f"Unexpected error deleting directory
{dir_to_delete_rel} for {project_name}/{version_name}")
+ return await _respond(
+ session, project_name, version_name, wants_json, False, "An
unexpected error occurred.", 500
+ )
+
+ if creating.failed is not None:
+ return await _respond(session, project_name, version_name, wants_json,
False, str(creating.failed), 400)
+ return await _respond(
+ session, project_name, version_name, wants_json, True, f"Deleted empty
directory '{dir_to_delete_rel}'.", 200
+ )
diff --git a/atr/templates/finish-selected.html
b/atr/templates/finish-selected.html
index 5d4335b..aadabe0 100644
--- a/atr/templates/finish-selected.html
+++ b/atr/templates/finish-selected.html
@@ -176,15 +176,20 @@
File moving is disabled as all files are currently in the same directory
or the revision is empty.
</div>
{% endif %}
- <!--
- TODO: Add a form to delete a directory.
- <h3>Delete a directory</h3>
- <p>Delete a directory to remove it from the release.</p>
- <form>
- <input type="text" class="form-control" placeholder="Directory name" />
- <button type="submit" class="btn btn-danger">Delete directory</button>
- </form>
--->
+ {% if delete_dir_form.directory_to_delete.choices %}
+ <h2>Delete an empty directory</h2>
+ <form method="post" class="mb-4">
+ {{ delete_dir_form.hidden_tag() }}
+ <div class="input-group">
+ {{ delete_dir_form.directory_to_delete(class="form-select") }}
+ <button type="submit"
+ class="btn btn-danger"
+ name="form_action"
+ value="delete_empty_dir">Delete directory</button>
+ </div>
+ {{ forms.errors(delete_dir_form.directory_to_delete,
classes="text-danger small mt-1") }}
+ </form>
+ {% endif %}
{% endblock content %}
{% block javascripts %}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]