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]

Reply via email to