Enhancement (QoL): support deselecting single items from "select all" (#14117)

This commit is contained in:
shamoon
2026-09-18 01:28:55 +00:00
committed by GitHub
parent d80997a070
commit f4ff87e69b
9 changed files with 350 additions and 33 deletions
+12
View File
@@ -1647,11 +1647,23 @@ class DocumentSelectionSerializer(DocumentListSerializer):
write_only=True,
)
excluded_documents = serializers.ListField(
required=False,
default=list,
write_only=True,
child=serializers.IntegerField(),
)
def validate(self, attrs):
if attrs.get("all", False):
attrs.setdefault("documents", [])
return attrs
if attrs["excluded_documents"]:
raise serializers.ValidationError(
"excluded_documents is only supported when all is true.",
)
if "documents" not in attrs:
raise serializers.ValidationError(
"documents is required unless all is true.",
+94
View File
@@ -520,6 +520,30 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(args[0], [self.doc1.id])
self.assertEqual(len(kwargs), 0)
@mock.patch("documents.views.bulk_edit.delete")
def test_delete_documents_endpoint_with_excluded_documents(self, m) -> None:
self.setup_mock(m, "delete")
response = self.client.post(
"/api/documents/delete/",
json.dumps(
{
"all": True,
"excluded_documents": [
self.doc2.id,
self.doc3.id,
self.doc4.id,
self.doc5.id,
],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
m.assert_called_once()
args, kwargs = m.call_args
self.assertEqual(args[0], [self.doc1.id])
self.assertEqual(len(kwargs), 0)
@mock.patch("documents.views.bulk_edit.reprocess")
def test_reprocess_documents_endpoint(self, m) -> None:
self.setup_mock(m, "reprocess")
@@ -691,6 +715,26 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"documents is required unless all is true", response.content)
def test_api_rejects_excluded_documents_unless_all_is_true(self) -> None:
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"excluded_documents": [self.doc2.id],
"method": "set_storage_path",
"parameters": {"storage_path": self.sp1.id},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(
b"excluded_documents is only supported when all is true",
response.content,
)
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
def test_api_bulk_edit_with_all_true_resolves_documents_from_filters(
self,
@@ -717,6 +761,29 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(args[0], [self.doc2.id])
self.assertEqual(kwargs["storage_path"], self.sp1.id)
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
def test_api_bulk_edit_with_all_true_excludes_documents(self, m) -> None:
self.setup_mock(m, "set_storage_path")
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"all": True,
"excluded_documents": [self.doc2.id, self.doc4.id],
"method": "set_storage_path",
"parameters": {"storage_path": self.sp1.id},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
m.assert_called_once()
args, kwargs = m.call_args
self.assertCountEqual(args[0], [self.doc1.id, self.doc3.id, self.doc5.id])
self.assertEqual(kwargs["storage_path"], self.sp1.id)
@mock.patch("documents.serialisers.bulk_edit.set_storage_path")
def test_api_bulk_edit_with_all_true_resolves_owned_duplicates(self, m) -> None:
self.setup_mock(m, "set_storage_path")
@@ -1077,6 +1144,33 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
],
)
def test_api_selection_data_with_excluded_documents(self) -> None:
response = self.client.post(
"/api/documents/selection_data/",
json.dumps(
{
"all": True,
"excluded_documents": [self.doc2.id],
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertCountEqual(
response.data["selected_correspondents"],
[
{"id": self.c1.id, "document_count": 0},
{"id": self.c2.id, "document_count": 1},
],
)
self.assertCountEqual(
response.data["selected_tags"],
[
{"id": self.t1.id, "document_count": 1},
{"id": self.t2.id, "document_count": 2},
],
)
def test_api_selection_data_requires_view_permission(self) -> None:
self.doc2.owner = self.user
self.doc2.save()
+19 -5
View File
@@ -194,7 +194,7 @@ from documents.serialisers import BulkEditSerializer
from documents.serialisers import CorrespondentSerializer
from documents.serialisers import CustomFieldSerializer
from documents.serialisers import DeleteDocumentsSerializer
from documents.serialisers import DocumentListSerializer
from documents.serialisers import DocumentSelectionSerializer
from documents.serialisers import DocumentSerializer
from documents.serialisers import DocumentTypeSerializer
from documents.serialisers import DocumentVersionLabelSerializer
@@ -2933,6 +2933,10 @@ class DocumentSelectionMixin:
)
if search_filtered_ids is not None:
filtered_documents = filtered_documents.filter(pk__in=search_filtered_ids)
if validated_data.get("excluded_documents"):
filtered_documents = filtered_documents.exclude(
pk__in=validated_data["excluded_documents"],
)
return list(filtered_documents.values_list("pk", flat=True))
@@ -3056,7 +3060,14 @@ class DocumentOperationPermissionMixin(PassUserMixin, DocumentSelectionMixin):
parameters = {
k: v
for k, v in validated_data.items()
if k not in {"documents", "all", "filters", "from_webui"}
if k
not in {
"documents",
"all",
"filters",
"excluded_documents",
"from_webui",
}
}
user = self.request.user
from_webui = validated_data.get("from_webui", False)
@@ -3557,16 +3568,19 @@ class PostDocumentView(GenericAPIView[Any]):
},
),
)
class SelectionDataView(GenericAPIView[Any]):
class SelectionDataView(DocumentSelectionMixin, GenericAPIView[Any]):
permission_classes = (IsAuthenticated, ViewDocumentsPermissions)
serializer_class = DocumentListSerializer
serializer_class = DocumentSelectionSerializer
parser_classes = (parsers.MultiPartParser, parsers.JSONParser)
def post(self, request, format=None):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
ids = serializer.validated_data.get("documents")
ids = self._resolve_document_ids(
user=request.user,
validated_data=serializer.validated_data,
)
permitted_documents = Document.objects.filter(
id__in=permitted_document_ids(request.user),
)