From d882c4bc0cffc6d9170e309d3e0e1bc3a1be2199 Mon Sep 17 00:00:00 2001 From: Nick Sweeting Date: Fri, 28 Aug 2026 14:35:40 -0700 Subject: [PATCH] feat: add output controls to snapshot admin --- archivebox/core/admin_archiveresults.py | 28 +++++-- archivebox/core/admin_snapshots.py | 7 +- archivebox/templates/admin/base.html | 1 + archivebox/templates/core/snapshot.html | 70 +---------------- .../includes/output_delete_controls.html | 78 +++++++++++++++++++ archivebox/tests/test_ui_admin_snapshot.py | 24 ++++++ 6 files changed, 134 insertions(+), 74 deletions(-) create mode 100644 archivebox/templates/includes/output_delete_controls.html diff --git a/archivebox/core/admin_archiveresults.py b/archivebox/core/admin_archiveresults.py index 24efc925..4a6e9831 100644 --- a/archivebox/core/admin_archiveresults.py +++ b/archivebox/core/admin_archiveresults.py @@ -24,6 +24,7 @@ from archivebox.core.models import ArchiveResult, Snapshot from archivebox.core.routes_util import build_snapshot_url from archivebox.core.widgets import InlineTagEditorWidget from archivebox.machine.env_util import env_to_shell_exports +from archivebox.misc.logging_util import printable_filesize from archivebox.misc.paginators import AcceleratedPaginator from archivebox.plugins.discovery import get_plugin_icon from archivebox.plugins.views import LIVE_PLUGIN_BASE_URL @@ -79,7 +80,7 @@ def get_process_link_label(process) -> str: return str(process.id)[-8:] -def render_archiveresults_list(archiveresults_qs, limit=50, config=None): +def render_archiveresults_list(archiveresults_qs, limit=50, config=None, can_delete=False): """Render a nice inline list view of archive results with status, plugin, output, and actions.""" results = list( @@ -112,6 +113,7 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): } rows = [] + delete_url = html.escape(reverse("admin:core_archiveresult_changelist"), quote=True) for idx, result in enumerate(results): status = result.status or "queued" color, bg = status_colors.get(status, ("#6b7280", "#f3f4f6")) @@ -126,6 +128,8 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): output_file_count = 0 else: output_file_count = 0 + output_size = int(result.output_size or 0) + output_size_display = html.escape(printable_filesize(output_size)) # Get plugin icon icon = get_plugin_icon(result.plugin) @@ -190,9 +194,16 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): # Unique ID for this row's expandable output row_id = f"output_{idx}_{str(result.id)[:8]}" + delete_button = "" + if can_delete: + delete_button = f''' + + ''' rows.append(f''' - + {output_file_count} + + {output_size_display} + {end_time} @@ -248,11 +262,12 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): ✏️ + {delete_button} - +
Details & Output @@ -289,8 +304,8 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): footer = "" if total_count > limit: footer = f""" - - + + Showing {limit} of {total_count} results   View all → @@ -309,6 +324,9 @@ def render_archiveresults_list(archiveresults_qs, limit=50, config=None): Plugin Output Files + + + Completed Process Machine diff --git a/archivebox/core/admin_snapshots.py b/archivebox/core/admin_snapshots.py index e0aaf4ae..45687c52 100644 --- a/archivebox/core/admin_snapshots.py +++ b/archivebox/core/admin_snapshots.py @@ -873,7 +873,12 @@ class SnapshotAdmin(SearchResultsAdminMixin, ConfigEditorMixin, BaseModelAdmin): @admin.display(description="Archive Results") def archiveresults_list(self, obj): request = self.request - return render_archiveresults_list(obj.archiveresult_set.all(), limit=8, config=request.archivebox_config) + return render_archiveresults_list( + obj.archiveresult_set.all(), + limit=8, + config=request.archivebox_config, + can_delete=request.user.is_superuser, + ) @admin.display( description="Title", diff --git a/archivebox/templates/admin/base.html b/archivebox/templates/admin/base.html index 707c639e..83c7dfb4 100644 --- a/archivebox/templates/admin/base.html +++ b/archivebox/templates/admin/base.html @@ -2556,5 +2556,6 @@ } + {% include "includes/output_delete_controls.html" %} diff --git a/archivebox/templates/core/snapshot.html b/archivebox/templates/core/snapshot.html index 80fdbedf..8e638d28 100644 --- a/archivebox/templates/core/snapshot.html +++ b/archivebox/templates/core/snapshot.html @@ -1712,7 +1712,7 @@ ⬇️ {% endif %} {% if can_delete_outputs and result.result %} - + {% endif %} {% if display_path %} @@ -1801,78 +1801,12 @@ {% if can_delete_outputs %}{% endif %} + {% if can_delete_outputs %}{% include "includes/output_delete_controls.html" %}{% endif %} diff --git a/archivebox/tests/test_ui_admin_snapshot.py b/archivebox/tests/test_ui_admin_snapshot.py index aa9d3e39..63a8dc98 100644 --- a/archivebox/tests/test_ui_admin_snapshot.py +++ b/archivebox/tests/test_ui_admin_snapshot.py @@ -831,6 +831,30 @@ class TestSnapshotOutputDeletion: assert "screenshot" not in {output["name"] for output in snapshot.discover_outputs()} assert '"plugin": "screenshot"' not in (Path(snapshot.output_dir) / "index.jsonl").read_text() + def test_admin_inline_shows_sortable_output_sizes_and_delete_controls(self, client, snapshot, admin_user): + first = self._create_output(snapshot, plugin="screenshot", size=11) + second = self._create_output(snapshot, plugin="pdf", hook_name="on_Snapshot__60_pdf.py", size=2048) + assert client.login(username=admin_user.username, password="testpassword") + + response = client.get( + reverse("admin:core_snapshot_change", args=[snapshot.pk]), + HTTP_HOST=ADMIN_TEST_HOST, + ) + html = response.content.decode() + + assert response.status_code == 200 + assert "data-output-size-sort" in html + assert 'data-output-size="11"' in html + assert 'data-output-size="2048"' in html + assert "11.0 Bytes" in html + assert "2.0 KB" in html + assert f'data-archive-result-ids="{first.id}"' in html + assert f'data-archive-result-ids="{second.id}"' in html + assert html.count('title="Delete this output"') == 2 + assert reverse("admin:core_archiveresult_changelist") in html + assert "const queuedOutputIds = new Set()" in html + assert "action: 'delete_selected'" in html + class TestAdminSnapshotListView: """Tests for the admin snapshot list view."""