diff --git a/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.spec.ts b/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.spec.ts index 434deb11d..0e9abafbb 100644 --- a/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.spec.ts +++ b/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.spec.ts @@ -3,6 +3,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing' import { ComponentFixture, TestBed } from '@angular/core/testing' import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { DocumentService } from 'src/app/services/rest/document.service' import { PDFEditorComponent } from './pdf-editor.component' describe('PDFEditorComponent', () => { @@ -139,4 +140,16 @@ describe('PDFEditorComponent', () => { expect(component.pages[1].page).toBe(2) expect(component.pages[2].page).toBe(3) }) + + it('should include selected version in preview source when provided', () => { + const documentService = TestBed.inject(DocumentService) + const previewSpy = jest + .spyOn(documentService, 'getPreviewUrl') + .mockReturnValue('preview-version') + component.documentID = 3 + component.versionID = 10 + + expect(component.pdfSrc).toBe('preview-version') + expect(previewSpy).toHaveBeenCalledWith(3, false, 10) + }) }) diff --git a/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.ts b/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.ts index c294516e0..5ea624aef 100644 --- a/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.ts +++ b/src-ui/src/app/components/common/pdf-editor/pdf-editor.component.ts @@ -46,6 +46,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent { activeModal: NgbActiveModal = inject(NgbActiveModal) documentID: number + versionID?: number pages: PageOperation[] = [] totalPages = 0 editMode: PdfEditorEditMode = this.settingsService.get( @@ -55,7 +56,11 @@ export class PDFEditorComponent extends ConfirmDialogComponent { includeMetadata: boolean = true get pdfSrc(): string { - return this.documentService.getPreviewUrl(this.documentID) + return this.documentService.getPreviewUrl( + this.documentID, + false, + this.versionID + ) } pdfLoaded(pdf: PngxPdfDocumentProxy) { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index e4cd1941f..990c6b4d0 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -1661,22 +1661,25 @@ describe('DocumentDetailComponent', () => { const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument') const errorSpy = jest.spyOn(toastService, 'showError') initNormally() + component.selectedVersionId = 10 component.editPdf() expect(modal).not.toBeUndefined() modal.componentInstance.documentID = doc.id + expect(modal.componentInstance.versionID).toBe(10) modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }] modal.componentInstance.confirm() let req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/bulk_edit/` ) expect(req.request.body).toEqual({ - documents: [doc.id], + documents: [10], method: 'edit_pdf', parameters: { operations: [{ page: 1, rotate: 0, doc: 0 }], delete_original: false, update_document: false, include_metadata: true, + source_mode: 'explicit_selection', }, }) req.error(new ErrorEvent('failed')) @@ -1698,6 +1701,7 @@ describe('DocumentDetailComponent', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[0])) initNormally() + component.selectedVersionId = 10 component.password = 'secret' component.removePassword() const dialog = @@ -1710,13 +1714,14 @@ describe('DocumentDetailComponent', () => { `${environment.apiBaseUrl}documents/bulk_edit/` ) expect(req.request.body).toEqual({ - documents: [doc.id], + documents: [10], method: 'remove_password', parameters: { password: 'secret', update_document: false, include_metadata: false, delete_original: true, + source_mode: 'explicit_selection', }, }) req.flush(true) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 942ed4742..a2c193770 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -74,7 +74,10 @@ import { import { CorrespondentService } from 'src/app/services/rest/correspondent.service' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { DocumentTypeService } from 'src/app/services/rest/document-type.service' -import { DocumentService } from 'src/app/services/rest/document.service' +import { + BulkEditSourceMode, + DocumentService, +} from 'src/app/services/rest/document.service' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' import { TagService } from 'src/app/services/rest/tag.service' @@ -1753,20 +1756,23 @@ export class DocumentDetailComponent size: 'xl', scrollable: true, }) + const sourceDocumentId = this.selectedVersionId ?? this.document.id modal.componentInstance.title = $localize`PDF Editor` modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.documentID = this.document.id + modal.componentInstance.versionID = sourceDocumentId modal.componentInstance.confirmClicked .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { modal.componentInstance.buttonsEnabled = false this.documentsService - .bulkEdit([this.document.id], 'edit_pdf', { + .bulkEdit([sourceDocumentId], 'edit_pdf', { operations: modal.componentInstance.getOperations(), delete_original: modal.componentInstance.deleteOriginal, update_document: modal.componentInstance.editMode == PdfEditorEditMode.Update, include_metadata: modal.componentInstance.includeMetadata, + source_mode: BulkEditSourceMode.EXPLICIT_SELECTION, }) .pipe(first(), takeUntil(this.unsubscribeNotifier)) .subscribe({ @@ -1812,16 +1818,18 @@ export class DocumentDetailComponent modal.componentInstance.confirmClicked .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe(() => { + const sourceDocumentId = this.selectedVersionId ?? this.document.id const dialog = modal.componentInstance as PasswordRemovalConfirmDialogComponent dialog.buttonsEnabled = false this.networkActive = true this.documentsService - .bulkEdit([this.document.id], 'remove_password', { + .bulkEdit([sourceDocumentId], 'remove_password', { password: this.password, update_document: dialog.updateDocument, include_metadata: dialog.includeMetadata, delete_original: dialog.deleteOriginal, + source_mode: BulkEditSourceMode.EXPLICIT_SELECTION, }) .pipe(first(), takeUntil(this.unsubscribeNotifier)) .subscribe({ diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 4b4ed7072..bc06d571c 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -37,6 +37,11 @@ export interface SelectionData { selected_custom_fields: SelectionDataItem[] } +export enum BulkEditSourceMode { + LATEST_VERSION = 'latest_version', + EXPLICIT_SELECTION = 'explicit_selection', +} + @Injectable({ providedIn: 'root', }) diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 457fb00ab..f5ef3db07 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -1517,6 +1517,11 @@ class DocumentListSerializer(serializers.Serializer): return documents +class SourceModeChoices: + LATEST_VERSION = "latest_version" + EXPLICIT_SELECTION = "explicit_selection" + + class BulkEditSerializer( SerializerWithPerms, DocumentListSerializer, @@ -1723,6 +1728,15 @@ class BulkEditSerializer( except ValueError: raise serializers.ValidationError("invalid rotation degrees") + def _validate_source_mode(self, parameters) -> None: + source_mode = parameters.get("source_mode", SourceModeChoices.LATEST_VERSION) + if source_mode not in { + SourceModeChoices.LATEST_VERSION, + SourceModeChoices.EXPLICIT_SELECTION, + }: + raise serializers.ValidationError("Invalid source_mode") + parameters["source_mode"] = source_mode + def _validate_parameters_split(self, parameters) -> None: if "pages" not in parameters: raise serializers.ValidationError("pages not specified") @@ -1823,6 +1837,9 @@ class BulkEditSerializer( method = attrs["method"] parameters = attrs["parameters"] + if "source_mode" in parameters: + self._validate_source_mode(parameters) + if method == bulk_edit.set_correspondent: self._validate_parameters_correspondent(parameters) elif method == bulk_edit.set_document_type: diff --git a/src/documents/tests/test_api_bulk_edit.py b/src/documents/tests/test_api_bulk_edit.py index 81d972bd4..6a465e19f 100644 --- a/src/documents/tests/test_api_bulk_edit.py +++ b/src/documents/tests/test_api_bulk_edit.py @@ -1395,7 +1395,10 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): { "documents": [self.doc2.id], "method": "edit_pdf", - "parameters": {"operations": [{"page": 1}]}, + "parameters": { + "operations": [{"page": 1}], + "source_mode": "explicit_selection", + }, }, ), content_type="application/json", @@ -1407,6 +1410,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): args, kwargs = m.call_args self.assertCountEqual(args[0], [self.doc2.id]) self.assertEqual(kwargs["operations"], [{"page": 1}]) + self.assertEqual(kwargs["source_mode"], "explicit_selection") self.assertEqual(kwargs["user"], self.user) def test_edit_pdf_invalid_params(self) -> None: @@ -1572,6 +1576,24 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): response.content, ) + # invalid source mode + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "edit_pdf", + "parameters": { + "operations": [{"page": 1}], + "source_mode": "not_a_mode", + }, + }, + ), + content_type="application/json", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn(b"Invalid source_mode", response.content) + @mock.patch("documents.serialisers.bulk_edit.edit_pdf") def test_edit_pdf_page_out_of_bounds(self, m) -> None: """