- @if (list.selected.size > 0) {
+ @if (list.hasSelection) {
None
@@ -127,11 +127,11 @@
Loading...
}
- @if (list.selected.size > 0) {
-
{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}
+ @if (list.hasSelection) {
+
{list.collectionSize, plural, =1 {Selected {{list.selectedCount}} of one document} other {Selected {{list.selectedCount}} of {{list.collectionSize || 0}} documents}}
}
@if (!list.isReloading) {
- @if (list.selected.size === 0) {
+ @if (!list.hasSelection) {
{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}
} @if (isFiltered) {
(filtered)
@@ -142,7 +142,7 @@
Reset filters
}
- @if (!list.isReloading && list.selected.size > 0) {
+ @if (!list.isReloading && list.hasSelection) {
Clear selection
diff --git a/src-ui/src/app/components/document-list/document-list.component.spec.ts b/src-ui/src/app/components/document-list/document-list.component.spec.ts
index 3ea39ccb0..9ea7f27de 100644
--- a/src-ui/src/app/components/document-list/document-list.component.spec.ts
+++ b/src-ui/src/app/components/document-list/document-list.component.spec.ts
@@ -388,8 +388,8 @@ describe('DocumentListComponent', () => {
it('should support select all, none, page & range', () => {
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
jest
- .spyOn(documentService, 'listAllFilteredIds')
- .mockReturnValue(of(docs.map((d) => d.id)))
+ .spyOn(documentListService, 'collectionSize', 'get')
+ .mockReturnValue(docs.length)
fixture.detectChanges()
expect(documentListService.selected.size).toEqual(0)
const docCards = fixture.debugElement.queryAll(
@@ -403,7 +403,8 @@ describe('DocumentListComponent', () => {
displayModeButtons[2].triggerEventHandler('click')
expect(selectAllSpy).toHaveBeenCalled()
fixture.detectChanges()
- expect(documentListService.selected.size).toEqual(3)
+ expect(documentListService.allSelected).toBeTruthy()
+ expect(documentListService.selectedCount).toEqual(3)
docCards.forEach((card) => {
expect(card.context.selected).toBeTruthy()
})
diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts
index 2cd2ccaf3..eb453d4dc 100644
--- a/src-ui/src/app/components/document-list/document-list.component.ts
+++ b/src-ui/src/app/components/document-list/document-list.component.ts
@@ -240,7 +240,7 @@ export class DocumentListComponent
}
get isBulkEditing(): boolean {
- return this.list.selected.size > 0
+ return this.list.hasSelection
}
toggleDisplayField(field: DisplayField) {
@@ -327,7 +327,7 @@ export class DocumentListComponent
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
- if (this.list.selected.size > 0) {
+ if (this.list.hasSelection) {
this.list.selectNone()
} else if (this.isFiltered) {
this.resetFilters()
@@ -356,7 +356,7 @@ export class DocumentListComponent
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.list.documents.length > 0) {
- if (this.list.selected.size > 0) {
+ if (this.list.hasSelection) {
this.openDocumentDetail(Array.from(this.list.selected)[0])
} else {
this.openDocumentDetail(this.list.documents[0])
diff --git a/src-ui/src/app/services/document-list-view.service.spec.ts b/src-ui/src/app/services/document-list-view.service.spec.ts
index 2b36fa95f..30cb8c701 100644
--- a/src-ui/src/app/services/document-list-view.service.spec.ts
+++ b/src-ui/src/app/services/document-list-view.service.spec.ts
@@ -534,12 +534,16 @@ describe('DocumentListViewService', () => {
})
it('should support select all', () => {
- documentListViewService.selectAll()
- const req = httpTestingController.expectOne(
- `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ documentListViewService.reload()
+ const reloadReq = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
- expect(req.request.method).toEqual('GET')
- req.flush(full_results)
+ expect(reloadReq.request.method).toEqual('GET')
+ reloadReq.flush(full_results)
+
+ documentListViewService.selectAll()
+ expect(documentListViewService.allSelected).toBeTruthy()
+ expect(documentListViewService.selectedCount).toEqual(documents.length)
expect(documentListViewService.selected.size).toEqual(documents.length)
expect(documentListViewService.isSelected(documents[0])).toBeTruthy()
documentListViewService.selectNone()
@@ -575,26 +579,62 @@ describe('DocumentListViewService', () => {
expect(documentListViewService.isSelected(documents[3])).toBeTruthy()
})
- it('should support selection range reduction', () => {
+ it('should clear all-selected mode when toggling a single document', () => {
+ documentListViewService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
+ )
+ req.flush(full_results)
+
documentListViewService.selectAll()
+ expect(documentListViewService.allSelected).toBeTruthy()
+
+ documentListViewService.toggleSelected(documents[0])
+
+ expect(documentListViewService.allSelected).toBeFalsy()
+ expect(documentListViewService.isSelected(documents[0])).toBeFalsy()
+ })
+
+ it('should clear all-selected mode when selecting a range', () => {
+ documentListViewService.reload()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
+ )
+ req.flush(full_results)
+
+ documentListViewService.selectAll()
+ documentListViewService.toggleSelected(documents[1])
+ documentListViewService.selectAll()
+ expect(documentListViewService.allSelected).toBeTruthy()
+
+ documentListViewService.selectRangeTo(documents[3])
+
+ expect(documentListViewService.allSelected).toBeFalsy()
+ expect(documentListViewService.isSelected(documents[1])).toBeTruthy()
+ expect(documentListViewService.isSelected(documents[2])).toBeTruthy()
+ expect(documentListViewService.isSelected(documents[3])).toBeTruthy()
+ })
+
+ it('should support selection range reduction', () => {
+ documentListViewService.reload()
let req = httpTestingController.expectOne(
- `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
+ `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true`
)
expect(req.request.method).toEqual('GET')
req.flush(full_results)
+
+ documentListViewService.selectAll()
expect(documentListViewService.selected.size).toEqual(6)
documentListViewService.setFilterRules(filterRules)
- httpTestingController.expectOne(
+ req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true&tags__id__all=9`
)
- const reqs = httpTestingController.match(
- `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id&tags__id__all=9`
- )
- reqs[0].flush({
+ req.flush({
count: 3,
results: documents.slice(0, 3),
})
+ expect(documentListViewService.allSelected).toBeTruthy()
expect(documentListViewService.selected.size).toEqual(3)
})
diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts
index 86888a088..1d37000bf 100644
--- a/src-ui/src/app/services/document-list-view.service.ts
+++ b/src-ui/src/app/services/document-list-view.service.ts
@@ -80,6 +80,11 @@ export interface ListViewState {
*/
selected?: Set
+ /**
+ * True if the full filtered result set is selected.
+ */
+ allSelected?: boolean
+
/**
* The page size of the list view.
*/
@@ -199,6 +204,20 @@ export class DocumentListViewService {
sortReverse: true,
filterRules: [],
selected: new Set(),
+ allSelected: false,
+ }
+ }
+
+ private syncSelectedToCurrentPage() {
+ if (!this.allSelected) {
+ return
+ }
+
+ this.selected.clear()
+ this.documents?.forEach((doc) => this.selected.add(doc.id))
+
+ if (!this.collectionSize) {
+ this.selectNone()
}
}
@@ -305,6 +324,7 @@ export class DocumentListViewService {
activeListViewState.collectionSize = result.count
activeListViewState.documents = result.results
this.selectionData = resultWithSelectionData.selection_data ?? null
+ this.syncSelectedToCurrentPage()
if (updateQueryParams && !this._activeSavedViewId) {
let base = ['/documents']
@@ -437,6 +457,20 @@ export class DocumentListViewService {
return this.activeListViewState.selected
}
+ get allSelected(): boolean {
+ return this.activeListViewState.allSelected ?? false
+ }
+
+ get selectedCount(): number {
+ return this.allSelected
+ ? (this.collectionSize ?? this.selected.size)
+ : this.selected.size
+ }
+
+ get hasSelection(): boolean {
+ return this.allSelected || this.selected.size > 0
+ }
+
setSort(field: string, reverse: boolean) {
this.activeListViewState.sortField = field
this.activeListViewState.sortReverse = reverse
@@ -591,11 +625,16 @@ export class DocumentListViewService {
}
selectNone() {
+ this.activeListViewState.allSelected = false
this.selected.clear()
this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null
}
reduceSelectionToFilter() {
+ if (this.allSelected) {
+ return
+ }
+
if (this.selected.size > 0) {
this.documentService
.listAllFilteredIds(this.filterRules)
@@ -610,12 +649,12 @@ export class DocumentListViewService {
}
selectAll() {
- this.documentService
- .listAllFilteredIds(this.filterRules)
- .subscribe((ids) => ids.forEach((id) => this.selected.add(id)))
+ this.activeListViewState.allSelected = true
+ this.syncSelectedToCurrentPage()
}
selectPage() {
+ this.activeListViewState.allSelected = false
this.selected.clear()
this.documents.forEach((doc) => {
this.selected.add(doc.id)
@@ -623,10 +662,13 @@ export class DocumentListViewService {
}
isSelected(d: Document) {
- return this.selected.has(d.id)
+ return this.allSelected || this.selected.has(d.id)
}
toggleSelected(d: Document): void {
+ if (this.allSelected) {
+ this.activeListViewState.allSelected = false
+ }
if (this.selected.has(d.id)) this.selected.delete(d.id)
else this.selected.add(d.id)
this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id)
@@ -634,6 +676,10 @@ export class DocumentListViewService {
}
selectRangeTo(d: Document) {
+ if (this.allSelected) {
+ this.activeListViewState.allSelected = false
+ }
+
if (this.rangeSelectionAnchorIndex !== null) {
const documentToIndex = this.documentIndexInCurrentView(d.id)
const fromIndex = Math.min(
diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts
index b3a9757ff..711aab743 100644
--- a/src-ui/src/app/services/rest/document.service.spec.ts
+++ b/src-ui/src/app/services/rest/document.service.spec.ts
@@ -198,7 +198,7 @@ describe(`DocumentService`, () => {
const content = 'both'
const useFilenameFormatting = false
subscription = service
- .bulkDownload(ids, content, useFilenameFormatting)
+ .bulkDownload({ documents: ids }, content, useFilenameFormatting)
.subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/bulk_download/`
@@ -218,7 +218,9 @@ describe(`DocumentService`, () => {
add_tags: [15],
remove_tags: [6],
}
- subscription = service.bulkEdit(ids, method, parameters).subscribe()
+ subscription = service
+ .bulkEdit({ documents: ids }, method, parameters)
+ .subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/bulk_edit/`
)
@@ -230,9 +232,32 @@ describe(`DocumentService`, () => {
})
})
+ it('should call appropriate api endpoint for bulk edit with all and filters', () => {
+ const method = 'modify_tags'
+ const parameters = {
+ add_tags: [15],
+ remove_tags: [6],
+ }
+ const selection = {
+ all: true,
+ filters: { title__icontains: 'apple' },
+ }
+ subscription = service.bulkEdit(selection, method, parameters).subscribe()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}${endpoint}/bulk_edit/`
+ )
+ expect(req.request.method).toEqual('POST')
+ expect(req.request.body).toEqual({
+ all: true,
+ filters: { title__icontains: 'apple' },
+ method,
+ parameters,
+ })
+ })
+
it('should call appropriate api endpoint for delete documents', () => {
const ids = [1, 2, 3]
- subscription = service.deleteDocuments(ids).subscribe()
+ subscription = service.deleteDocuments({ documents: ids }).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/delete/`
)
@@ -244,7 +269,7 @@ describe(`DocumentService`, () => {
it('should call appropriate api endpoint for reprocess documents', () => {
const ids = [1, 2, 3]
- subscription = service.reprocessDocuments(ids).subscribe()
+ subscription = service.reprocessDocuments({ documents: ids }).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/reprocess/`
)
@@ -256,7 +281,7 @@ describe(`DocumentService`, () => {
it('should call appropriate api endpoint for rotate documents', () => {
const ids = [1, 2, 3]
- subscription = service.rotateDocuments(ids, 90).subscribe()
+ subscription = service.rotateDocuments({ documents: ids }, 90).subscribe()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}${endpoint}/rotate/`
)
diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts
index 203b35341..cfee4c405 100644
--- a/src-ui/src/app/services/rest/document.service.ts
+++ b/src-ui/src/app/services/rest/document.service.ts
@@ -68,6 +68,12 @@ export interface RemovePasswordDocumentsRequest {
source_mode?: BulkEditSourceMode
}
+export interface DocumentSelectionQuery {
+ documents?: number[]
+ all?: boolean
+ filters?: { [key: string]: any }
+}
+
@Injectable({
providedIn: 'root',
})
@@ -325,33 +331,37 @@ export class DocumentService extends AbstractPaperlessService {
return this.http.get(url.toString())
}
- bulkEdit(ids: number[], method: DocumentBulkEditMethod, args: any) {
+ bulkEdit(
+ selection: DocumentSelectionQuery,
+ method: DocumentBulkEditMethod,
+ args: any
+ ) {
return this.http.post(this.getResourceUrl(null, 'bulk_edit'), {
- documents: ids,
+ ...selection,
method: method,
parameters: args,
})
}
- deleteDocuments(ids: number[]) {
+ deleteDocuments(selection: DocumentSelectionQuery) {
return this.http.post(this.getResourceUrl(null, 'delete'), {
- documents: ids,
+ ...selection,
})
}
- reprocessDocuments(ids: number[]) {
+ reprocessDocuments(selection: DocumentSelectionQuery) {
return this.http.post(this.getResourceUrl(null, 'reprocess'), {
- documents: ids,
+ ...selection,
})
}
rotateDocuments(
- ids: number[],
+ selection: DocumentSelectionQuery,
degrees: number,
sourceMode: BulkEditSourceMode = BulkEditSourceMode.LATEST_VERSION
) {
return this.http.post(this.getResourceUrl(null, 'rotate'), {
- documents: ids,
+ ...selection,
degrees,
source_mode: sourceMode,
})
@@ -399,14 +409,14 @@ export class DocumentService extends AbstractPaperlessService {
}
bulkDownload(
- ids: number[],
+ selection: DocumentSelectionQuery,
content = 'both',
useFilenameFormatting: boolean = false
) {
return this.http.post(
this.getResourceUrl(null, 'bulk_download'),
{
- documents: ids,
+ ...selection,
content: content,
follow_formatting: useFilenameFormatting,
},
diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py
index 8f96f638d..a8beb70c0 100644
--- a/src/documents/serialisers.py
+++ b/src/documents/serialisers.py
@@ -1558,6 +1558,41 @@ class DocumentListSerializer(serializers.Serializer):
return documents
+class DocumentSelectionSerializer(DocumentListSerializer):
+ documents = serializers.ListField(
+ required=False,
+ label="Documents",
+ write_only=True,
+ child=serializers.IntegerField(),
+ )
+
+ all = serializers.BooleanField(
+ default=False,
+ required=False,
+ write_only=True,
+ )
+
+ filters = serializers.DictField(
+ required=False,
+ allow_empty=True,
+ write_only=True,
+ )
+
+ def validate(self, attrs):
+ if attrs.get("all", False):
+ attrs.setdefault("documents", [])
+ return attrs
+
+ if "documents" not in attrs:
+ raise serializers.ValidationError(
+ "documents is required unless all is true.",
+ )
+
+ documents = attrs["documents"]
+ self._validate_document_id_list(documents)
+ return attrs
+
+
class SourceModeValidationMixin:
def validate_source_mode(self, source_mode: str) -> str:
if source_mode not in bulk_edit.SourceModeChoices.__dict__.values():
@@ -1565,7 +1600,7 @@ class SourceModeValidationMixin:
return source_mode
-class RotateDocumentsSerializer(DocumentListSerializer, SourceModeValidationMixin):
+class RotateDocumentsSerializer(DocumentSelectionSerializer, SourceModeValidationMixin):
degrees = serializers.IntegerField(required=True)
source_mode = serializers.CharField(
required=False,
@@ -1648,17 +1683,17 @@ class RemovePasswordDocumentsSerializer(
)
-class DeleteDocumentsSerializer(DocumentListSerializer):
+class DeleteDocumentsSerializer(DocumentSelectionSerializer):
pass
-class ReprocessDocumentsSerializer(DocumentListSerializer):
+class ReprocessDocumentsSerializer(DocumentSelectionSerializer):
pass
class BulkEditSerializer(
SerializerWithPerms,
- DocumentListSerializer,
+ DocumentSelectionSerializer,
SetPermissionsMixin,
SourceModeValidationMixin,
):
@@ -1986,6 +2021,19 @@ class BulkEditSerializer(
raise serializers.ValidationError("password must be a string")
def validate(self, attrs):
+ attrs = super().validate(attrs)
+
+ if attrs.get("all", False) and attrs["method"] in [
+ bulk_edit.merge,
+ bulk_edit.split,
+ bulk_edit.delete_pages,
+ bulk_edit.edit_pdf,
+ bulk_edit.remove_password,
+ ]:
+ raise serializers.ValidationError(
+ "This method does not support all=true.",
+ )
+
method = attrs["method"]
parameters = attrs["parameters"]
@@ -2243,7 +2291,7 @@ class DocumentVersionLabelSerializer(serializers.Serializer):
return normalized or None
-class BulkDownloadSerializer(DocumentListSerializer):
+class BulkDownloadSerializer(DocumentSelectionSerializer):
content = serializers.ChoiceField(
choices=["archive", "originals", "both"],
default="archive",
diff --git a/src/documents/tests/test_api_bulk_edit.py b/src/documents/tests/test_api_bulk_edit.py
index 86ef8bb44..ff780dccd 100644
--- a/src/documents/tests/test_api_bulk_edit.py
+++ b/src/documents/tests/test_api_bulk_edit.py
@@ -614,6 +614,63 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Document.objects.count(), 5)
+ def test_api_requires_documents_unless_all_is_true(self) -> None:
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "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"documents is required unless 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,
+ m,
+ ) -> None:
+ self.setup_mock(m, "set_storage_path")
+
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "all": True,
+ "filters": {"title__icontains": "B"},
+ "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.assertEqual(args[0], [self.doc2.id])
+ self.assertEqual(kwargs["storage_path"], self.sp1.id)
+
+ def test_api_bulk_edit_with_all_true_rejects_unsupported_methods(self) -> None:
+ response = self.client.post(
+ "/api/documents/bulk_edit/",
+ json.dumps(
+ {
+ "all": True,
+ "method": "merge",
+ "parameters": {"metadata_document_id": self.doc2.id},
+ },
+ ),
+ content_type="application/json",
+ )
+
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertIn(b"This method does not support all=true", response.content)
+
def test_api_invalid_method(self) -> None:
self.assertEqual(Document.objects.count(), 5)
response = self.client.post(
diff --git a/src/documents/views.py b/src/documents/views.py
index 3bcf77430..244e81161 100644
--- a/src/documents/views.py
+++ b/src/documents/views.py
@@ -2241,7 +2241,36 @@ class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet):
ordering_fields = ("name",)
-class DocumentOperationPermissionMixin(PassUserMixin):
+class DocumentSelectionMixin:
+ def _resolve_document_ids(
+ self,
+ *,
+ user: User,
+ validated_data: dict[str, Any],
+ permission_codename: str = "view_document",
+ ) -> list[int]:
+ if not validated_data.get("all", False):
+ # if all is not true, just pass through the provided document ids
+ return validated_data["documents"]
+
+ # otherwise, reconstruct the document list based on the provided filters
+ filters = validated_data.get("filters") or {}
+ permitted_documents = get_objects_for_user_owner_aware(
+ user,
+ permission_codename,
+ Document,
+ )
+ return list(
+ DocumentFilterSet(
+ data=filters,
+ queryset=permitted_documents,
+ )
+ .qs.distinct()
+ .values_list("pk", flat=True),
+ )
+
+
+class DocumentOperationPermissionMixin(PassUserMixin, DocumentSelectionMixin):
permission_classes = (IsAuthenticated,)
parser_classes = (parsers.JSONParser,)
METHOD_NAMES_REQUIRING_USER = {
@@ -2335,8 +2364,15 @@ class DocumentOperationPermissionMixin(PassUserMixin):
validated_data: dict[str, Any],
operation_label: str,
):
- documents = validated_data["documents"]
- parameters = {k: v for k, v in validated_data.items() if k != "documents"}
+ documents = self._resolve_document_ids(
+ user=self.request.user,
+ validated_data=validated_data,
+ )
+ parameters = {
+ k: v
+ for k, v in validated_data.items()
+ if k not in {"documents", "all", "filters"}
+ }
user = self.request.user
if method.__name__ in self.METHOD_NAMES_REQUIRING_USER:
@@ -2424,7 +2460,10 @@ class BulkEditView(DocumentOperationPermissionMixin):
user = self.request.user
method = serializer.validated_data.get("method")
parameters = serializer.validated_data.get("parameters")
- documents = serializer.validated_data.get("documents")
+ documents = self._resolve_document_ids(
+ user=user,
+ validated_data=serializer.validated_data,
+ )
if method.__name__ in self.METHOD_NAMES_REQUIRING_USER:
parameters["user"] = user
if not self._has_document_permissions(
@@ -3276,7 +3315,7 @@ class StatisticsView(GenericAPIView):
)
-class BulkDownloadView(GenericAPIView):
+class BulkDownloadView(DocumentSelectionMixin, GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = BulkDownloadSerializer
parser_classes = (parsers.JSONParser,)
@@ -3285,7 +3324,10 @@ class BulkDownloadView(GenericAPIView):
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,
+ )
documents = Document.objects.filter(pk__in=ids)
compression = serializer.validated_data.get("compression")
content = serializer.validated_data.get("content")