🐛(backend) find deleted root items when searching the trashbin

Search rebuilds the visible set from the user's accesses, skipping those
whose item was soft deleted. A root item holds its own access, so
deleting it made it unreachable from any location, trashbin included.
Hard deleted items remain excluded, and the scope filter still keeps
deleted items out of regular search results.
This commit is contained in:
Nicolas Clerc
2026-07-08 16:37:59 +02:00
parent ca6bbd50f7
commit 8e5fb9971e
3 changed files with 52 additions and 2 deletions
+1
View File
@@ -16,6 +16,7 @@ and this project adheres to
### Fixed
- 🐛(backend) find deleted root items when searching the trashbin
- 🐛(backend) exclude folders from file type search results
- 🐛(frontend) keep uploaded items usable while malware analysis runs
- 🐛(backend) stream export files from S3 without buffering
+5 -2
View File
@@ -1356,11 +1356,14 @@ class ItemViewSet(
workspace = filterset.form.cleaned_data.get("workspace")
# First look for all top level items user has access to
# First look for all top level items user has access to. Soft deleted items
# are kept: a deleted root item is its own access holder and would become
# unreachable, even from the trashbin. The scope filter excludes them from
# the results.
user = request.user
item_access_queryset = models.ItemAccess.objects.select_related("item").filter(
db.Q(user=user) | db.Q(team__in=user.teams),
item__deleted_at__isnull=True,
item__hard_deleted_at__isnull=True,
)
# Remove items with upload_state SUSPICIOUS for non-creators
@@ -825,3 +825,49 @@ def test_api_items_search_filter_category_excludes_folders():
assert response.status_code == 200
assert [item["id"] for item in response.json()["results"]] == [str(spreadsheet.id)]
def test_api_items_search_trashbin_finds_deleted_root_item():
"""A deleted root item should still be reachable from the trashbin location."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
# A root item carries its own access: deleting it removes the only access
# pointing at it.
item = factories.ItemFactory(
title="ywh_export_report.pdf",
filename="ywh_export_report.pdf",
creator=user,
users=[(user, models.RoleChoices.OWNER)],
type=models.ItemTypeChoices.FILE,
update_upload_state=models.ItemUploadStateChoices.READY,
)
item.soft_delete()
response = client.get("/api/v1.0/items/search/?location=trashbin&title=ywh_export")
assert response.status_code == 200
assert [result["id"] for result in response.json()["results"]] == [str(item.id)]
def test_api_items_search_excludes_deleted_root_item_by_default():
"""A deleted root item should stay out of the search results without a scope."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
item = factories.ItemFactory(
title="ywh_export_report.pdf",
filename="ywh_export_report.pdf",
creator=user,
users=[(user, models.RoleChoices.OWNER)],
type=models.ItemTypeChoices.FILE,
update_upload_state=models.ItemUploadStateChoices.READY,
)
item.soft_delete()
response = client.get("/api/v1.0/items/search/?title=ywh_export")
assert response.status_code == 200
assert response.json()["results"] == []