From 84c3e7893e50775ef4c767828fa1ba4565eb28cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 May 2024 14:47:55 -0700 Subject: [PATCH 001/112] Changelog v2.8.3 - GHA (#6664) Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- docs/changelog.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index 57ed6430f..26f05b6c1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,36 @@ # Changelog +## paperless-ngx 2.8.3 + +### Bug Fixes + +- Fix: respect superuser for document history [@shamoon](https://github.com/shamoon) ([#6661](https://github.com/paperless-ngx/paperless-ngx/pull/6661)) +- Fix: allow 0 in monetary field [@shamoon](https://github.com/shamoon) ([#6658](https://github.com/paperless-ngx/paperless-ngx/pull/6658)) +- Fix: custom field removal doesnt always trigger change detection [@shamoon](https://github.com/shamoon) ([#6653](https://github.com/paperless-ngx/paperless-ngx/pull/6653)) +- Fix: Downgrade and lock lxml [@stumpylog](https://github.com/stumpylog) ([#6655](https://github.com/paperless-ngx/paperless-ngx/pull/6655)) +- Fix: correctly handle global search esc key when open and button foucsed [@shamoon](https://github.com/shamoon) ([#6644](https://github.com/paperless-ngx/paperless-ngx/pull/6644)) +- Fix: consistent monetary field display in list and cards [@shamoon](https://github.com/shamoon) ([#6645](https://github.com/paperless-ngx/paperless-ngx/pull/6645)) +- Fix: doc links and more illegible in light mode [@shamoon](https://github.com/shamoon) ([#6643](https://github.com/paperless-ngx/paperless-ngx/pull/6643)) +- Fix: Allow auditlog to be disabled [@stumpylog](https://github.com/stumpylog) ([#6638](https://github.com/paperless-ngx/paperless-ngx/pull/6638)) + +### Documentation + +- Chore(docs): Update the sample Compose file to latest database [@stumpylog](https://github.com/stumpylog) ([#6639](https://github.com/paperless-ngx/paperless-ngx/pull/6639)) + +### All App Changes + +
+7 changes + +- Fix: respect superuser for document history [@shamoon](https://github.com/shamoon) ([#6661](https://github.com/paperless-ngx/paperless-ngx/pull/6661)) +- Fix: allow 0 in monetary field [@shamoon](https://github.com/shamoon) ([#6658](https://github.com/paperless-ngx/paperless-ngx/pull/6658)) +- Fix: custom field removal doesnt always trigger change detection [@shamoon](https://github.com/shamoon) ([#6653](https://github.com/paperless-ngx/paperless-ngx/pull/6653)) +- Fix: correctly handle global search esc key when open and button foucsed [@shamoon](https://github.com/shamoon) ([#6644](https://github.com/paperless-ngx/paperless-ngx/pull/6644)) +- Fix: consistent monetary field display in list and cards [@shamoon](https://github.com/shamoon) ([#6645](https://github.com/paperless-ngx/paperless-ngx/pull/6645)) +- Fix: doc links and more illegible in light mode [@shamoon](https://github.com/shamoon) ([#6643](https://github.com/paperless-ngx/paperless-ngx/pull/6643)) +- Fix: Allow auditlog to be disabled [@stumpylog](https://github.com/stumpylog) ([#6638](https://github.com/paperless-ngx/paperless-ngx/pull/6638)) +
+ ## paperless-ngx 2.8.2 ### Bug Fixes From 79834874306a18785210a364ae3ba089611b79db Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 13 May 2024 08:44:35 -0700 Subject: [PATCH 002/112] Security: Correctly disable eval in pdfjs (#6702) --- .../app/components/common/pdf-viewer/pdf-viewer.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts b/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts index 4fc55429a..0c84521c6 100644 --- a/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts +++ b/src-ui/src/app/components/common/pdf-viewer/pdf-viewer.component.ts @@ -35,7 +35,6 @@ import type { import { PDFSinglePageViewer } from 'pdfjs-dist/web/pdf_viewer' PDFJS['verbosity'] = PDFJS.VerbosityLevel.ERRORS -PDFJS['isEvalSupported'] = false export enum RenderTextMode { DISABLED, @@ -440,6 +439,7 @@ export class PdfViewerComponent cMapPacked: true, enableXfa: true, } + params.isEvalSupported = false if (srcType === 'string') { params.url = this.src From a1e4365ff2df1d679c9f3cab3bb9c407643b7d39 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 13 May 2024 09:12:02 -0700 Subject: [PATCH 003/112] Enhancement: global search tweaks (#6674) --- src-ui/messages.xlf | 50 ++++++------ .../global-search.component.html | 16 ++-- .../global-search.component.spec.ts | 76 ++++++++++++++----- .../global-search/global-search.component.ts | 67 ++++++++-------- src-ui/src/styles.scss | 3 +- 5 files changed, 130 insertions(+), 82 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 4be198d69..f3e43d396 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -1013,7 +1013,7 @@ src/app/components/app-frame/global-search/global-search.component.ts - 92 + 93 @@ -2113,11 +2113,11 @@ src/app/components/app-frame/global-search/global-search.component.html - 51 + 55 src/app/components/app-frame/global-search/global-search.component.html - 68 + 72 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -2676,7 +2676,7 @@ Advanced search src/app/components/app-frame/global-search/global-search.component.html - 19 + 23 src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -2687,25 +2687,25 @@ Open src/app/components/app-frame/global-search/global-search.component.html - 45 + 49 src/app/components/app-frame/global-search/global-search.component.html - 48 + 52 Filter documents src/app/components/app-frame/global-search/global-search.component.html - 54 + 58 Download src/app/components/app-frame/global-search/global-search.component.html - 65 + 69 src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -2732,113 +2732,113 @@ No results src/app/components/app-frame/global-search/global-search.component.html - 79 + 83 Documents src/app/components/app-frame/global-search/global-search.component.html - 82 + 86 Saved Views src/app/components/app-frame/global-search/global-search.component.html - 88 + 92 Tags src/app/components/app-frame/global-search/global-search.component.html - 95 + 99 Correspondents src/app/components/app-frame/global-search/global-search.component.html - 102 + 106 Document types src/app/components/app-frame/global-search/global-search.component.html - 109 + 113 Storage paths src/app/components/app-frame/global-search/global-search.component.html - 116 + 120 Users src/app/components/app-frame/global-search/global-search.component.html - 123 + 127 Groups src/app/components/app-frame/global-search/global-search.component.html - 130 + 134 Custom fields src/app/components/app-frame/global-search/global-search.component.html - 137 + 141 Mail accounts src/app/components/app-frame/global-search/global-search.component.html - 144 + 148 Mail rules src/app/components/app-frame/global-search/global-search.component.html - 151 + 155 Workflows src/app/components/app-frame/global-search/global-search.component.html - 158 + 162 Successfully updated object. src/app/components/app-frame/global-search/global-search.component.ts - 168 + 175 src/app/components/app-frame/global-search/global-search.component.ts - 206 + 213 Error occurred saving object. src/app/components/app-frame/global-search/global-search.component.ts - 171 + 178 src/app/components/app-frame/global-search/global-search.component.ts - 209 + 216 diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.html b/src-ui/src/app/components/app-frame/global-search/global-search.component.html index 317303de7..eeb118967 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.html +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.html @@ -6,15 +6,19 @@
+ autocomplete="off" + spellcheck="false" + [(ngModel)]="query" + (ngModelChange)="this.queryDebounce.next($event)" + (keydown)="searchInputKeyDown($event)" + ngbDropdownAnchor>
@if (loading) {
}
- @if (query && (searchResults?.documents.length === searchService.searchResultObjectLimit || searchService.searchDbOnly)) { + @if (query) { @if (type !== DataType.SavedView && type !== DataType.Workflow && type !== DataType.CustomField && type !== DataType.Group && type !== DataType.User && type !== DataType.MailAccount && type !== DataType.MailRule) { + + + + + +
+ +
+
diff --git a/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.scss b/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.scss new file mode 100644 index 000000000..f74de973d --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.scss @@ -0,0 +1,28 @@ +.pdf-viewer-container { + background-color: gray; + height: 350px; + + pdf-viewer { + width: 100%; + height: 100%; + } +} + +.mw-60 { + max-width: 60px; +} + +div.position-absolute:has(.form-check-input:checked) { + background-color: rgba(var(--bs-dark-rgb), 0.4); +} + +.form-check-input { + &:checked { + background-color: var(--bs-danger); + border-color: var(--bs-danger); + } + &:focus { + box-shadow: 0 0 0 0.25rem rgba(var(--bs-danger-rgb), var(--pngx-focus-alpha)); + border-color: var(--bs-danger); + } +} diff --git a/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.spec.ts b/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.spec.ts new file mode 100644 index 000000000..78fab8c8d --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { DeletePagesConfirmDialogComponent } from './delete-pages-confirm-dialog.component' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { PdfViewerComponent } from 'ng2-pdf-viewer' + +describe('DeletePagesConfirmDialogComponent', () => { + let component: DeletePagesConfirmDialogComponent + let fixture: ComponentFixture + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DeletePagesConfirmDialogComponent, PdfViewerComponent], + providers: [NgbActiveModal, SafeHtmlPipe], + imports: [ + HttpClientTestingModule, + NgxBootstrapIconsModule.pick(allIcons), + FormsModule, + ReactiveFormsModule, + ], + }).compileComponents() + fixture = TestBed.createComponent(DeletePagesConfirmDialogComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should return a string with comma-separated pages', () => { + component.pages = [1, 2, 3, 4] + expect(component.pagesString).toEqual('1, 2, 3, 4') + }) + + it('should update totalPages when pdf is loaded', () => { + component.pdfPreviewLoaded({ numPages: 5 } as any) + expect(component.totalPages).toEqual(5) + }) + + it('should update checks when page is rendered', () => { + const event = { + target: document.createElement('div'), + detail: { pageNumber: 1 }, + } as any + component.pageRendered(event) + expect(component['checks'].length).toEqual(1) + }) + + it('should update pages when page check is changed', () => { + component.pageCheckChanged(1) + expect(component.pages).toEqual([1]) + component.pageCheckChanged(1) + expect(component.pages).toEqual([]) + }) +}) diff --git a/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.ts new file mode 100644 index 000000000..c47dea0ed --- /dev/null +++ b/src-ui/src/app/components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component.ts @@ -0,0 +1,64 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core' +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' +import { DocumentService } from 'src/app/services/rest/document.service' +import { ConfirmDialogComponent } from '../confirm-dialog.component' +import { PDFDocumentProxy, PdfViewerComponent } from 'ng2-pdf-viewer' + +@Component({ + selector: 'pngx-delete-pages-confirm-dialog', + templateUrl: './delete-pages-confirm-dialog.component.html', + styleUrl: './delete-pages-confirm-dialog.component.scss', +}) +export class DeletePagesConfirmDialogComponent extends ConfirmDialogComponent { + public documentID: number + public pages: number[] = [] + public currentPage: number = 1 + public totalPages: number + + @ViewChild('pdfViewer') pdfViewer: PdfViewerComponent + @ViewChild('pageCheckOverlay') pageCheckOverlay!: TemplateRef + private checks: HTMLElement[] = [] + + public get pagesString(): string { + return this.pages.join(', ') + } + + public get pdfSrc(): string { + return this.documentService.getPreviewUrl(this.documentID) + } + + constructor( + activeModal: NgbActiveModal, + private documentService: DocumentService + ) { + super(activeModal) + } + + public pdfPreviewLoaded(pdf: PDFDocumentProxy) { + this.totalPages = pdf.numPages + } + + pageRendered(event: CustomEvent) { + const pageDiv = event.target as HTMLDivElement + const check = this.pageCheckOverlay.createEmbeddedView({ + page: event.detail.pageNumber, + }) + this.checks[event.detail.pageNumber - 1] = check.rootNodes[0] + pageDiv?.insertBefore(check.rootNodes[0], pageDiv.firstChild) + this.updateChecks() + } + + pageCheckChanged(pageNumber: number) { + if (!this.pages.includes(pageNumber)) this.pages.push(pageNumber) + else if (this.pages.includes(pageNumber)) + this.pages.splice(this.pages.indexOf(pageNumber), 1) + this.updateChecks() + } + + private updateChecks() { + this.checks.forEach((check, i) => { + const input = check.getElementsByTagName('input')[0] + input.checked = this.pages.includes(i + 1) + }) + } +} diff --git a/src-ui/src/app/components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component.html b/src-ui/src/app/components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component.html index 00e8996d0..e996ecb44 100644 --- a/src-ui/src/app/components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component.html +++ b/src-ui/src/app/components/common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component.html @@ -21,21 +21,19 @@ -
-
- @if (messageBold) { -

{{messageBold}}

- } - @if (message) { -

- } -
-
@if (showPDFNote) {

Note that only PDFs will be rotated.

} - } @else { - @switch (contentRenderType) { + @switch (archiveContentRenderType) { @case (ContentRenderType.PDF) { @if (!useNativePdfViewer) {
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 d27c13ef1..b8a6389f2 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 @@ -81,6 +81,7 @@ import { environment } from 'src/environments/environment' import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' import { PdfViewerModule } from 'ng2-pdf-viewer' +import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component' const doc: Document = { id: 3, @@ -178,6 +179,7 @@ describe('DocumentDetailComponent', () => { CustomFieldsDropdownComponent, SplitConfirmDialogComponent, RotateConfirmDialogComponent, + DeletePagesConfirmDialogComponent, ], providers: [ DocumentTitlePipe, @@ -1035,7 +1037,9 @@ describe('DocumentDetailComponent', () => { component.metadata = { has_archive_version: true } initNormally() fixture.detectChanges() - expect(component.contentRenderType).toEqual(component.ContentRenderType.PDF) + expect(component.archiveContentRenderType).toEqual( + component.ContentRenderType.PDF + ) expect( fixture.debugElement.query(By.css('pdf-viewer-container')) ).not.toBeUndefined() @@ -1045,7 +1049,7 @@ describe('DocumentDetailComponent', () => { original_mime_type: 'text/plain', } fixture.detectChanges() - expect(component.contentRenderType).toEqual( + expect(component.archiveContentRenderType).toEqual( component.ContentRenderType.Text ) expect( @@ -1057,7 +1061,7 @@ describe('DocumentDetailComponent', () => { original_mime_type: 'image/jpg', } fixture.detectChanges() - expect(component.contentRenderType).toEqual( + expect(component.archiveContentRenderType).toEqual( component.ContentRenderType.Image ) expect( @@ -1070,7 +1074,7 @@ describe('DocumentDetailComponent', () => { 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', } fixture.detectChanges() - expect(component.contentRenderType).toEqual( + expect(component.archiveContentRenderType).toEqual( component.ContentRenderType.Other ) expect( @@ -1130,6 +1134,31 @@ describe('DocumentDetailComponent', () => { req.flush(true) }) + it('should support delete pages', () => { + let modal: NgbModalRef + modalService.activeInstances.subscribe((m) => (modal = m[0])) + initNormally() + component.deletePages() + expect(modal).not.toBeUndefined() + modal.componentInstance.documentID = doc.id + modal.componentInstance.pages = [1, 2] + modal.componentInstance.confirm() + let req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/bulk_edit/` + ) + expect(req.request.body).toEqual({ + documents: [doc.id], + method: 'delete_pages', + parameters: { pages: [1, 2] }, + }) + req.error(new ProgressEvent('failed')) + modal.componentInstance.confirm() + req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/bulk_edit/` + ) + req.flush(true) + }) + it('should support keyboard shortcuts', () => { initNormally() 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 820d7fbd5..23753f55b 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 @@ -68,6 +68,7 @@ import { CustomFieldInstance } from 'src/app/data/custom-field-instance' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' +import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component' import { HotKeyService } from 'src/app/services/hot-key.service' import { PDFDocumentProxy } from 'ng2-pdf-viewer' @@ -216,19 +217,27 @@ export class DocumentDetailComponent return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER) } - get contentRenderType(): ContentRenderType { - if (!this.metadata) return ContentRenderType.Unknown - const contentType = this.metadata?.has_archive_version - ? 'application/pdf' - : this.metadata?.original_mime_type + get archiveContentRenderType(): ContentRenderType { + return this.getRenderType( + this.metadata?.has_archive_version + ? 'application/pdf' + : this.metadata?.original_mime_type + ) + } - if (contentType === 'application/pdf') { + get originalContentRenderType(): ContentRenderType { + return this.getRenderType(this.metadata?.original_mime_type) + } + + private getRenderType(mimeType: string): ContentRenderType { + if (!mimeType) return ContentRenderType.Unknown + if (mimeType === 'application/pdf') { return ContentRenderType.PDF } else if ( - ['text/plain', 'application/csv', 'text/csv'].includes(contentType) + ['text/plain', 'application/csv', 'text/csv'].includes(mimeType) ) { return ContentRenderType.Text - } else if (contentType?.indexOf('image/') === 0) { + } else if (mimeType?.indexOf('image/') === 0) { return ContentRenderType.Image } return ContentRenderType.Other @@ -1138,7 +1147,6 @@ export class DocumentDetailComponent }) modal.componentInstance.title = $localize`Rotate confirm` modal.componentInstance.messageBold = $localize`This operation will permanently rotate the original version of the current document.` - modal.componentInstance.message = $localize`This will alter the original copy.` modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.documentID = this.document.id modal.componentInstance.showPDFNote = false @@ -1173,4 +1181,41 @@ export class DocumentDetailComponent }) }) } + + deletePages() { + let modal = this.modalService.open(DeletePagesConfirmDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.title = $localize`Delete pages confirm` + modal.componentInstance.messageBold = $localize`This operation will permanently delete the selected pages from the original document.` + modal.componentInstance.btnCaption = $localize`Proceed` + modal.componentInstance.documentID = this.document.id + modal.componentInstance.confirmClicked + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + modal.componentInstance.buttonsEnabled = false + this.documentsService + .bulkEdit([this.document.id], 'delete_pages', { + pages: modal.componentInstance.pages, + }) + .pipe(first(), takeUntil(this.unsubscribeNotifier)) + .subscribe({ + next: () => { + this.toastService.showInfo( + $localize`Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.` + ) + modal.close() + }, + error: (error) => { + if (modal) { + modal.componentInstance.buttonsEnabled = true + } + this.toastService.showError( + $localize`Error executing delete pages operation`, + error + ) + }, + }) + }) + } } diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index f59ef1af3..8dbdbc2dd 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -325,3 +325,29 @@ def split(doc_ids: list[int], pages: list[list[int]]): logger.exception(f"Error splitting document {doc.id}: {e}") return "OK" + + +def delete_pages(doc_ids: list[int], pages: list[int]): + logger.info( + f"Attempting to delete pages {pages} from {len(doc_ids)} documents", + ) + doc = Document.objects.get(id=doc_ids[0]) + pages = sorted(pages) # sort pages to avoid index issues + import pikepdf + + try: + with pikepdf.open(doc.source_path, allow_overwriting_input=True) as pdf: + offset = 1 # pages are 1-indexed + for page_num in pages: + pdf.pages.remove(pdf.pages[page_num - offset]) + offset += 1 # remove() changes the index of the pages + pdf.remove_unreferenced_resources() + pdf.save() + doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest() + doc.save() + update_document_archive_file.delay(document_id=doc.id) + logger.info(f"Deleted pages {pages} from document {doc.id}") + except Exception as e: + logger.exception(f"Error deleting pages from document {doc.id}: {e}") + + return "OK" diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 9d722ca5d..c92765e69 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -944,6 +944,7 @@ class BulkEditSerializer( "rotate", "merge", "split", + "delete_pages", ], label="Method", write_only=True, @@ -1000,6 +1001,8 @@ class BulkEditSerializer( return bulk_edit.merge elif method == "split": return bulk_edit.split + elif method == "delete_pages": + return bulk_edit.delete_pages else: raise serializers.ValidationError("Unsupported method.") @@ -1128,6 +1131,14 @@ class BulkEditSerializer( except ValueError: raise serializers.ValidationError("invalid pages specified") + def _validate_parameters_delete_pages(self, parameters): + if "pages" not in parameters: + raise serializers.ValidationError("pages not specified") + if not isinstance(parameters["pages"], list): + raise serializers.ValidationError("pages must be a list") + if not all(isinstance(i, int) for i in parameters["pages"]): + raise serializers.ValidationError("pages must be a list of integers") + def validate(self, attrs): method = attrs["method"] parameters = attrs["parameters"] @@ -1154,6 +1165,12 @@ class BulkEditSerializer( "Split method only supports one document", ) self._validate_parameters_split(parameters) + elif method == bulk_edit.delete_pages: + if len(attrs["documents"]) > 1: + raise serializers.ValidationError( + "Delete pages method only supports one document", + ) + self._validate_parameters_delete_pages(parameters) return attrs diff --git a/src/documents/tests/test_api_bulk_edit.py b/src/documents/tests/test_api_bulk_edit.py index c38ed8cfd..7078aca12 100644 --- a/src/documents/tests/test_api_bulk_edit.py +++ b/src/documents/tests/test_api_bulk_edit.py @@ -1065,3 +1065,95 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertIn(b"Split method only supports one document", response.content) + + @mock.patch("documents.serialisers.bulk_edit.delete_pages") + def test_delete_pages(self, m): + m.return_value = "OK" + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "delete_pages", + "parameters": {"pages": [1, 2, 3, 4]}, + }, + ), + 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.doc2.id]) + self.assertEqual(kwargs["pages"], [1, 2, 3, 4]) + + def test_delete_pages_invalid_params(self): + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [ + self.doc1.id, + self.doc2.id, + ], # only one document supported + "method": "delete_pages", + "parameters": { + "pages": [1, 2, 3, 4], + }, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn( + b"Delete pages method only supports one document", + response.content, + ) + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "delete_pages", + "parameters": {}, # pages not specified + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn(b"pages not specified", response.content) + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "delete_pages", + "parameters": {"pages": "1-3"}, # not a list + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn(b"pages must be a list", response.content) + + response = self.client.post( + "/api/documents/bulk_edit/", + json.dumps( + { + "documents": [self.doc2.id], + "method": "delete_pages", + "parameters": {"pages": ["1-3"]}, # not ints + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn(b"pages must be a list of integers", response.content) diff --git a/src/documents/tests/test_bulk_edit.py b/src/documents/tests/test_bulk_edit.py index 831fa9461..16579c887 100644 --- a/src/documents/tests/test_bulk_edit.py +++ b/src/documents/tests/test_bulk_edit.py @@ -585,3 +585,46 @@ class TestPDFActions(DirectoriesMixin, TestCase): mock_update_documents.assert_called_once() mock_chord.assert_called_once() self.assertEqual(result, "OK") + + @mock.patch("documents.tasks.update_document_archive_file.delay") + @mock.patch("pikepdf.Pdf.save") + def test_delete_pages(self, mock_pdf_save, mock_update_archive_file): + """ + GIVEN: + - Existing documents + WHEN: + - Delete pages action is called with 1 document and 2 pages + THEN: + - Save should be called once + - Archive file should be updated once + """ + doc_ids = [self.doc2.id] + pages = [1, 3] + result = bulk_edit.delete_pages(doc_ids, pages) + mock_pdf_save.assert_called_once() + mock_update_archive_file.assert_called_once() + self.assertEqual(result, "OK") + + @mock.patch("documents.tasks.update_document_archive_file.delay") + @mock.patch("pikepdf.Pdf.save") + def test_delete_pages_with_error(self, mock_pdf_save, mock_update_archive_file): + """ + GIVEN: + - Existing documents + WHEN: + - Delete pages action is called with 1 document and 2 pages + - PikePDF raises an error + THEN: + - Save should be called once + - Archive file should not be updated + """ + mock_pdf_save.side_effect = Exception("Error saving PDF") + doc_ids = [self.doc2.id] + pages = [1, 3] + + with self.assertLogs("paperless.bulk_edit", level="ERROR") as cm: + bulk_edit.delete_pages(doc_ids, pages) + error_str = cm.output[0] + expected_str = "Error deleting pages from document" + self.assertIn(expected_str, error_str) + mock_update_archive_file.assert_not_called() From c0c44b512c8cd39cd9b431d06d9812ab9506f24e Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 22 May 2024 16:08:25 -0700 Subject: [PATCH 040/112] Enhancement: accessibility improvements for tags, doc links, dashboard views (#6786) --- src-ui/messages.xlf | 128 ++++++++++++------ .../document-link.component.html | 4 +- .../common/input/tags/tags.component.html | 6 +- .../common/input/tags/tags.component.scss | 4 - .../saved-view-widget.component.html | 14 +- 5 files changed, 95 insertions(+), 61 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 9af66a45d..5edab40e9 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -253,6 +253,18 @@ src/app/app.component.ts 85 + + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html + 37 + + + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html + 40 + + + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html + 43 + src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html 73 @@ -2123,10 +2135,6 @@ src/app/components/common/input/permissions/permissions-form/permissions-form.component.html 53 - - src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html - 43 - src/app/components/document-list/document-card-large/document-card-large.component.html 57 @@ -4454,6 +4462,24 @@ 158 + + Remove link + + src/app/components/common/input/document-link/document-link.component.html + 30 + + + + Open link + + src/app/components/common/input/document-link/document-link.component.html + 31 + + + src/app/components/common/input/url/url.component.html + 14 + + No documents found @@ -4551,6 +4577,13 @@ 15 + + Remove tag + + src/app/components/common/input/tags/tags.component.html + 20 + + Filter documents with these Tags @@ -4558,13 +4591,6 @@ 41 - - Open link - - src/app/components/common/input/url/url.component.html - 14 - - What's this? @@ -5159,6 +5185,51 @@ 39 + + Filter by correspondent + + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html + 47 + + + src/app/components/document-list/document-card-large/document-card-large.component.html + 20 + + + src/app/components/document-list/document-list.component.html + 275 + + + + Filter by document type + + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html + 57 + + + src/app/components/document-list/document-card-large/document-card-large.component.html + 79 + + + src/app/components/document-list/document-list.component.html + 310 + + + + Filter by storage path + + src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html + 62 + + + src/app/components/document-list/document-card-large/document-card-large.component.html + 85 + + + src/app/components/document-list/document-list.component.html + 317 + + View Preview @@ -6348,17 +6419,6 @@ 819 - - Filter by correspondent - - src/app/components/document-list/document-card-large/document-card-large.component.html - 20 - - - src/app/components/document-list/document-list.component.html - 275 - - Filter by tag @@ -6384,30 +6444,8 @@ 75 - - Filter by document type - - src/app/components/document-list/document-card-large/document-card-large.component.html - 79 - - - src/app/components/document-list/document-list.component.html - 310 - - - - Filter by storage path - - src/app/components/document-list/document-card-large/document-card-large.component.html - 85 - - - src/app/components/document-list/document-list.component.html - 317 - - - Created: + Created: src/app/components/document-list/document-card-large/document-card-large.component.html 98,99 diff --git a/src-ui/src/app/components/common/input/document-link/document-link.component.html b/src-ui/src/app/components/common/input/document-link/document-link.component.html index fc4726a55..a8ecce4e6 100644 --- a/src-ui/src/app/components/common/input/document-link/document-link.component.html +++ b/src-ui/src/app/components/common/input/document-link/document-link.component.html @@ -27,8 +27,8 @@ (change)="onChange(selectedDocuments)"> diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index cb8d616b3..8384840a3 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -17,12 +17,12 @@ (change)="onChange(value)"> - - +
diff --git a/src-ui/src/app/components/common/input/tags/tags.component.scss b/src-ui/src/app/components/common/input/tags/tags.component.scss index a1d88a3a9..65b8603e0 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.scss +++ b/src-ui/src/app/components/common/input/tags/tags.component.scss @@ -7,10 +7,6 @@ font-size: 1rem; } -.tag-wrap-delete { - cursor: pointer; -} - .paperless-input-select.disabled { .input-group { cursor: not-allowed; diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index 4ea602098..3591f817f 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -34,32 +34,32 @@ @switch (field) { @case (DisplayField.ADDED) { - {{doc.added | customDate}} + {{doc.added | customDate}} } @case (DisplayField.CREATED) { - {{doc.created_date | customDate}} + {{doc.created_date | customDate}} } @case (DisplayField.TITLE) { - {{doc.title | documentTitle}} + {{doc.title | documentTitle}} } @case (DisplayField.CORRESPONDENT) { @if (doc.correspondent) { - {{(doc.correspondent$ | async)?.name}} + {{(doc.correspondent$ | async)?.name}} } } @case (DisplayField.TAGS) { @for (t of doc.tags$ | async; track t) { - + } } @case (DisplayField.DOCUMENT_TYPE) { @if (doc.document_type) { - {{(doc.document_type$ | async)?.name}} + {{(doc.document_type$ | async)?.name}} } } @case (DisplayField.STORAGE_PATH) { @if (doc.storage_path) { - {{(doc.storage_path$ | async)?.name}} + {{(doc.storage_path$ | async)?.name}} } } } From 8abb0cd75dd558eb3fe4a465d4d71367bca5011a Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 22 May 2024 16:15:58 -0700 Subject: [PATCH 041/112] Enhancement: only include correspondent 'last_correspondence' if requested (#6792) --- .../document-detail.component.html | 8 +-- .../document-detail.component.spec.ts | 12 ++--- .../document-detail.component.ts | 51 +++++++++--------- .../correspondent-list.component.ts | 21 ++++++++ .../management-list.component.ts | 6 +-- .../rest/abstract-name-filter-service.ts | 5 +- src/documents/serialisers.py | 2 +- src/documents/tests/test_api_objects.py | 53 +++++++++++++++++++ src/documents/views.py | 22 +++++--- 9 files changed, 131 insertions(+), 49 deletions(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 86e6398ec..35d252b5b 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -109,13 +109,13 @@ - - - - + @for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) { 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 b8a6389f2..7dcf4e9f7 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 @@ -80,8 +80,9 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { environment } from 'src/environments/environment' import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { SplitConfirmDialogComponent } from '../common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' -import { PdfViewerModule } from 'ng2-pdf-viewer' import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component' +import { PdfViewerModule } from 'ng2-pdf-viewer' +import { DataType } from 'src/app/data/datatype' const doc: Document = { id: 3, @@ -783,10 +784,9 @@ describe('DocumentDetailComponent', () => { const object = { id: 22, name: 'Correspondent22', - last_correspondence: new Date().toISOString(), } as Correspondent const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - component.filterDocuments([object]) + component.filterDocuments([object], DataType.Correspondent) expect(qfSpy).toHaveBeenCalledWith([ { rule_type: FILTER_CORRESPONDENT, @@ -799,7 +799,7 @@ describe('DocumentDetailComponent', () => { initNormally() const object = { id: 22, name: 'DocumentType22' } as DocumentType const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - component.filterDocuments([object]) + component.filterDocuments([object], DataType.DocumentType) expect(qfSpy).toHaveBeenCalledWith([ { rule_type: FILTER_DOCUMENT_TYPE, @@ -816,7 +816,7 @@ describe('DocumentDetailComponent', () => { path: '/foo/bar/', } as StoragePath const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - component.filterDocuments([object]) + component.filterDocuments([object], DataType.StoragePath) expect(qfSpy).toHaveBeenCalledWith([ { rule_type: FILTER_STORAGE_PATH, @@ -842,7 +842,7 @@ describe('DocumentDetailComponent', () => { text_color: '#000000', } as Tag const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - component.filterDocuments([object1, object2]) + component.filterDocuments([object1, object2], DataType.Tag) expect(qfSpy).toHaveBeenCalledWith([ { rule_type: FILTER_HAS_TAGS_ALL, 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 23753f55b..a80e401e2 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 @@ -71,6 +71,7 @@ import { RotateConfirmDialogComponent } from '../common/confirm-dialog/rotate-co import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component' import { HotKeyService } from 'src/app/services/hot-key.service' import { PDFDocumentProxy } from 'ng2-pdf-viewer' +import { DataType } from 'src/app/data/datatype' enum DocumentDetailNavIDs { Details = 1, @@ -171,6 +172,8 @@ export class DocumentDetailComponent public readonly ContentRenderType = ContentRenderType + public readonly DataType = DataType + @ViewChild('nav') nav: NgbNav @ViewChild('pdfPreview') set pdfPreview(element) { // this gets called when component added or removed from DOM @@ -998,7 +1001,7 @@ export class DocumentDetailComponent ) } - filterDocuments(items: ObjectWithId[] | NgbDateStruct[]) { + filterDocuments(items: ObjectWithId[] | NgbDateStruct[], type?: DataType) { const filterRules: FilterRule[] = items.flatMap((i) => { if (i.hasOwnProperty('year')) { const isoDateAdapter = new ISODateAdapter() @@ -1017,30 +1020,28 @@ export class DocumentDetailComponent value: dateBefore.toISOString().substring(0, 10), }, ] - } else if (i.hasOwnProperty('last_correspondence')) { - // Correspondent - return { - rule_type: FILTER_CORRESPONDENT, - value: (i as Correspondent).id.toString(), - } - } else if (i.hasOwnProperty('path')) { - // Storage Path - return { - rule_type: FILTER_STORAGE_PATH, - value: (i as StoragePath).id.toString(), - } - } else if (i.hasOwnProperty('is_inbox_tag')) { - // Tag - return { - rule_type: FILTER_HAS_TAGS_ALL, - value: (i as Tag).id.toString(), - } - } else { - // Document Type, has no specific props - return { - rule_type: FILTER_DOCUMENT_TYPE, - value: (i as DocumentType).id.toString(), - } + } + switch (type) { + case DataType.Correspondent: + return { + rule_type: FILTER_CORRESPONDENT, + value: (i as Correspondent).id.toString(), + } + case DataType.DocumentType: + return { + rule_type: FILTER_DOCUMENT_TYPE, + value: (i as DocumentType).id.toString(), + } + case DataType.StoragePath: + return { + rule_type: FILTER_STORAGE_PATH, + value: (i as StoragePath).id.toString(), + } + case DataType.Tag: + return { + rule_type: FILTER_HAS_TAGS_ALL, + value: (i as Tag).id.toString(), + } } }) diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index 2d02ba983..c0053353b 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -12,6 +12,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic import { ToastService } from 'src/app/services/toast.service' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' import { ManagementListComponent } from '../management-list/management-list.component' +import { takeUntil } from 'rxjs' @Component({ selector: 'pngx-correspondent-list', @@ -63,6 +64,26 @@ export class CorrespondentListComponent extends ManagementListComponent { + this.data = c.results + this.collectionSize = c.count + this.isLoading = false + }) + } + getDeleteMessage(object: Correspondent) { return $localize`Do you really want to delete the correspondent "${object.name}"?` } diff --git a/src-ui/src/app/components/manage/management-list/management-list.component.ts b/src-ui/src/app/components/manage/management-list/management-list.component.ts index 3fbf18e09..9453affd5 100644 --- a/src-ui/src/app/components/manage/management-list/management-list.component.ts +++ b/src-ui/src/app/components/manage/management-list/management-list.component.ts @@ -52,7 +52,7 @@ export abstract class ManagementListComponent implements OnInit, OnDestroy { constructor( - private service: AbstractNameFilterService, + protected service: AbstractNameFilterService, private modalService: NgbModal, private editDialogComponent: any, private toastService: ToastService, @@ -81,8 +81,8 @@ export abstract class ManagementListComponent public isLoading: boolean = false private nameFilterDebounce: Subject - private unsubscribeNotifier: Subject = new Subject() - private _nameFilter: string + protected unsubscribeNotifier: Subject = new Subject() + protected _nameFilter: string public selectedObjects: Set = new Set() public togggleAll: boolean = false diff --git a/src-ui/src/app/services/rest/abstract-name-filter-service.ts b/src-ui/src/app/services/rest/abstract-name-filter-service.ts index 1018f0fa2..03c7e5470 100644 --- a/src-ui/src/app/services/rest/abstract-name-filter-service.ts +++ b/src-ui/src/app/services/rest/abstract-name-filter-service.ts @@ -17,9 +17,10 @@ export abstract class AbstractNameFilterService< sortField?: string, sortReverse?: boolean, nameFilter?: string, - fullPerms?: boolean + fullPerms?: boolean, + extraParams?: { [key: string]: any } ) { - let params = {} + let params = extraParams ?? {} if (nameFilter) { params['name__icontains'] = nameFilter } diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index c92765e69..d7a06e181 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -291,7 +291,7 @@ class OwnedObjectSerializer( class CorrespondentSerializer(MatchingModelSerializer, OwnedObjectSerializer): - last_correspondence = serializers.DateTimeField(read_only=True) + last_correspondence = serializers.DateTimeField(read_only=True, required=False) class Meta: model = Correspondent diff --git a/src/documents/tests/test_api_objects.py b/src/documents/tests/test_api_objects.py index 65f379261..1a55a936c 100644 --- a/src/documents/tests/test_api_objects.py +++ b/src/documents/tests/test_api_objects.py @@ -1,8 +1,10 @@ +import datetime import json from unittest import mock from django.contrib.auth.models import Permission from django.contrib.auth.models import User +from django.utils import timezone from rest_framework import status from rest_framework.test import APITestCase @@ -89,6 +91,57 @@ class TestApiObjects(DirectoriesMixin, APITestCase): results = response.data["results"] self.assertEqual(len(results), 2) + def test_correspondent_last_correspondence(self): + """ + GIVEN: + - Correspondent with documents + WHEN: + - API is called + THEN: + - Last correspondence date is returned only if requested for list, and for detail + """ + + Document.objects.create( + mime_type="application/pdf", + correspondent=self.c1, + created=timezone.make_aware(datetime.datetime(2022, 1, 1)), + checksum="123", + ) + Document.objects.create( + mime_type="application/pdf", + correspondent=self.c1, + created=timezone.make_aware(datetime.datetime(2022, 1, 2)), + checksum="456", + ) + + # Only if requested for list + response = self.client.get( + "/api/correspondents/", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertNotIn("last_correspondence", results[0]) + + response = self.client.get( + "/api/correspondents/?last_correspondence=true", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + results = response.data["results"] + self.assertIn( + "2022-01-02", + results[0]["last_correspondence"], + ) + + # Included in detail by default + response = self.client.get( + f"/api/correspondents/{self.c1.id}/", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn( + "2022-01-02", + response.data["last_correspondence"], + ) + class TestApiStoragePaths(DirectoriesMixin, APITestCase): ENDPOINT = "/api/storage_paths/" diff --git a/src/documents/views.py b/src/documents/views.py index 8b3486f76..91b99b610 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -253,14 +253,7 @@ class PermissionsAwareDocumentCountMixin(PassUserMixin): class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): model = Correspondent - queryset = ( - Correspondent.objects.prefetch_related("documents") - .annotate( - last_correspondence=Max("documents__created"), - ) - .select_related("owner") - .order_by(Lower("name")) - ) + queryset = Correspondent.objects.select_related("owner").order_by(Lower("name")) serializer_class = CorrespondentSerializer pagination_class = StandardPagination @@ -279,6 +272,19 @@ class CorrespondentViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): "last_correspondence", ) + def list(self, request, *args, **kwargs): + if request.query_params.get("last_correspondence", None): + self.queryset = self.queryset.annotate( + last_correspondence=Max("documents__created"), + ) + return super().list(request, *args, **kwargs) + + def retrieve(self, request, *args, **kwargs): + self.queryset = self.queryset.annotate( + last_correspondence=Max("documents__created"), + ) + return super().retrieve(request, *args, **kwargs) + class TagViewSet(ModelViewSet, PermissionsAwareDocumentCountMixin): model = Tag From 719f76060b04d746aed806f26bf881bbd18e0ba9 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 22 May 2024 16:19:46 -0700 Subject: [PATCH 042/112] Enhancement: default to title/content search, allow choosing full search link from global search (#6805) --- src-ui/messages.xlf | 186 ++++++++++-------- .../admin/settings/settings.component.html | 18 +- .../admin/settings/settings.component.spec.ts | 2 +- .../admin/settings/settings.component.ts | 10 +- .../global-search.component.html | 8 +- .../global-search.component.spec.ts | 35 +++- .../global-search/global-search.component.ts | 22 ++- src-ui/src/app/data/ui-settings.ts | 11 ++ 8 files changed, 191 insertions(+), 101 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 5edab40e9..37fecf495 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -350,7 +350,7 @@ src/app/components/admin/settings/settings.component.html - 323 + 339 src/app/components/app-frame/app-frame.component.html @@ -563,7 +563,7 @@ src/app/components/admin/settings/settings.component.html - 403 + 419 src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -705,7 +705,7 @@ src/app/components/admin/settings/settings.component.html - 391 + 407 src/app/components/admin/tasks/tasks.component.html @@ -1025,21 +1025,50 @@ src/app/components/app-frame/global-search/global-search.component.ts - 93 + 104 - - Search database only (do not include advanced search results) + + Do not include advanced search results src/app/components/admin/settings/settings.component.html 204 + + Full search links to + + src/app/components/admin/settings/settings.component.html + 212 + + + + Title and content search + + src/app/components/admin/settings/settings.component.html + 216 + + + + Advanced search + + src/app/components/admin/settings/settings.component.html + 217 + + + src/app/components/app-frame/global-search/global-search.component.html + 24 + + + src/app/components/document-list/filter-editor/filter-editor.component.ts + 143 + + Notes src/app/components/admin/settings/settings.component.html - 208 + 224 src/app/components/document-list/document-list.component.html @@ -1058,14 +1087,14 @@ Enable notes src/app/components/admin/settings/settings.component.html - 212 + 228 Permissions src/app/components/admin/settings/settings.component.html - 220 + 236 src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html @@ -1120,28 +1149,28 @@ Default Permissions src/app/components/admin/settings/settings.component.html - 223 + 239 Settings apply to this user account for objects (Tags, Mail Rules, etc.) created via the web UI src/app/components/admin/settings/settings.component.html - 227,229 + 243,245 Default Owner src/app/components/admin/settings/settings.component.html - 234 + 250 Objects without an owner can be viewed and edited by all users src/app/components/admin/settings/settings.component.html - 238 + 254 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -1152,18 +1181,18 @@ Default View Permissions src/app/components/admin/settings/settings.component.html - 243 + 259 Users: src/app/components/admin/settings/settings.component.html - 248 + 264 src/app/components/admin/settings/settings.component.html - 275 + 291 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1194,11 +1223,11 @@ Groups: src/app/components/admin/settings/settings.component.html - 258 + 274 src/app/components/admin/settings/settings.component.html - 285 + 301 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1229,14 +1258,14 @@ Default Edit Permissions src/app/components/admin/settings/settings.component.html - 270 + 286 Edit permissions also grant viewing permissions src/app/components/admin/settings/settings.component.html - 294 + 310 src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html @@ -1255,56 +1284,56 @@ Notifications src/app/components/admin/settings/settings.component.html - 302 + 318 Document processing src/app/components/admin/settings/settings.component.html - 305 + 321 Show notifications when new documents are detected src/app/components/admin/settings/settings.component.html - 309 + 325 Show notifications when document processing completes successfully src/app/components/admin/settings/settings.component.html - 310 + 326 Show notifications when document processing fails src/app/components/admin/settings/settings.component.html - 311 + 327 Suppress notifications on dashboard src/app/components/admin/settings/settings.component.html - 312 + 328 This will suppress all messages about document processing status on the dashboard. src/app/components/admin/settings/settings.component.html - 312 + 328 Saved views src/app/components/admin/settings/settings.component.html - 320 + 336 src/app/components/app-frame/app-frame.component.html @@ -1315,14 +1344,14 @@ Show warning when closing saved views with unsaved changes src/app/components/admin/settings/settings.component.html - 326 + 342 Views src/app/components/admin/settings/settings.component.html - 330 + 346 src/app/components/document-list/document-list.component.html @@ -1333,7 +1362,7 @@ Show on dashboard src/app/components/admin/settings/settings.component.html - 343 + 359 src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html @@ -1344,7 +1373,7 @@ Show in sidebar src/app/components/admin/settings/settings.component.html - 347 + 363 src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html @@ -1355,7 +1384,7 @@ Actions src/app/components/admin/settings/settings.component.html - 351 + 367 src/app/components/admin/tasks/tasks.component.html @@ -1418,7 +1447,7 @@ Delete src/app/components/admin/settings/settings.component.html - 353 + 369 src/app/components/admin/users-groups/users-groups.component.html @@ -1529,42 +1558,42 @@ Documents page size src/app/components/admin/settings/settings.component.html - 364 + 380 Display as src/app/components/admin/settings/settings.component.html - 367 + 383 Table src/app/components/admin/settings/settings.component.html - 369 + 385 Small Cards src/app/components/admin/settings/settings.component.html - 370 + 386 Large Cards src/app/components/admin/settings/settings.component.html - 371 + 387 Show src/app/components/admin/settings/settings.component.html - 375 + 391 src/app/components/document-list/document-list.component.html @@ -1575,7 +1604,7 @@ Default src/app/components/admin/settings/settings.component.html - 375 + 391 src/app/components/document-detail/document-detail.component.html @@ -1586,14 +1615,14 @@ No saved views defined. src/app/components/admin/settings/settings.component.html - 384 + 400 Cancel src/app/components/admin/settings/settings.component.html - 404 + 420 src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -1678,7 +1707,7 @@ Error retrieving users src/app/components/admin/settings/settings.component.ts - 189 + 192 src/app/components/admin/users-groups/users-groups.component.ts @@ -1689,7 +1718,7 @@ Error retrieving groups src/app/components/admin/settings/settings.component.ts - 208 + 211 src/app/components/admin/users-groups/users-groups.component.ts @@ -1700,35 +1729,35 @@ Saved view "" deleted. src/app/components/admin/settings/settings.component.ts - 423 + 427 Settings were saved successfully. src/app/components/admin/settings/settings.component.ts - 553 + 561 Settings were saved successfully. Reload is required to apply some changes. src/app/components/admin/settings/settings.component.ts - 557 + 565 Reload now src/app/components/admin/settings/settings.component.ts - 558 + 566 An error occurred while saving settings. src/app/components/admin/settings/settings.component.ts - 568 + 576 src/app/components/app-frame/app-frame.component.ts @@ -1739,7 +1768,7 @@ Error while storing settings on server. src/app/components/admin/settings/settings.component.ts - 602 + 610 @@ -2125,11 +2154,11 @@ src/app/components/app-frame/global-search/global-search.component.html - 55 + 59 src/app/components/app-frame/global-search/global-search.component.html - 72 + 76 src/app/components/common/input/permissions/permissions-form/permissions-form.component.html @@ -2683,41 +2712,34 @@ src/app/components/app-frame/global-search/global-search.component.html 8 - - - Advanced search src/app/components/app-frame/global-search/global-search.component.html - 23 - - - src/app/components/document-list/filter-editor/filter-editor.component.ts - 143 + 26 Open src/app/components/app-frame/global-search/global-search.component.html - 49 + 53 src/app/components/app-frame/global-search/global-search.component.html - 52 + 56 Filter documents src/app/components/app-frame/global-search/global-search.component.html - 58 + 62 Download src/app/components/app-frame/global-search/global-search.component.html - 69 + 73 src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -2744,113 +2766,113 @@ No results src/app/components/app-frame/global-search/global-search.component.html - 83 + 87 Documents src/app/components/app-frame/global-search/global-search.component.html - 86 + 90 Saved Views src/app/components/app-frame/global-search/global-search.component.html - 92 + 96 Tags src/app/components/app-frame/global-search/global-search.component.html - 99 + 103 Correspondents src/app/components/app-frame/global-search/global-search.component.html - 106 + 110 Document types src/app/components/app-frame/global-search/global-search.component.html - 113 + 117 Storage paths src/app/components/app-frame/global-search/global-search.component.html - 120 + 124 Users src/app/components/app-frame/global-search/global-search.component.html - 127 + 131 Groups src/app/components/app-frame/global-search/global-search.component.html - 134 + 138 Custom fields src/app/components/app-frame/global-search/global-search.component.html - 141 + 145 Mail accounts src/app/components/app-frame/global-search/global-search.component.html - 148 + 152 Mail rules src/app/components/app-frame/global-search/global-search.component.html - 155 + 159 Workflows src/app/components/app-frame/global-search/global-search.component.html - 162 + 166 Successfully updated object. src/app/components/app-frame/global-search/global-search.component.ts - 182 + 193 src/app/components/app-frame/global-search/global-search.component.ts - 220 + 231 Error occurred saving object. src/app/components/app-frame/global-search/global-search.component.ts - 185 + 196 src/app/components/app-frame/global-search/global-search.component.ts - 223 + 234 diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html index 87d7ba68a..bcab7de33 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.html +++ b/src-ui/src/app/components/admin/settings/settings.component.html @@ -201,7 +201,23 @@
- + +
+
+ +
+
+
+
+ Full search links to +
+
+ +
+
diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index 71778d394..47581ddba 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -309,7 +309,7 @@ describe('SettingsComponent', () => { expect(toastErrorSpy).toHaveBeenCalled() expect(storeSpy).toHaveBeenCalled() expect(appearanceSettingsSpy).not.toHaveBeenCalled() - expect(setSpy).toHaveBeenCalledTimes(26) + expect(setSpy).toHaveBeenCalledTimes(27) // succeed storeSpy.mockReturnValueOnce(of(true)) diff --git a/src-ui/src/app/components/admin/settings/settings.component.ts b/src-ui/src/app/components/admin/settings/settings.component.ts index 036f27f48..fcb7d7c65 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.ts @@ -27,7 +27,7 @@ import { } from 'rxjs' import { Group } from 'src/app/data/group' import { SavedView } from 'src/app/data/saved-view' -import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings' import { User } from 'src/app/data/user' import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { @@ -101,6 +101,7 @@ export class SettingsComponent defaultPermsEditGroups: new FormControl(null), documentEditingRemoveInboxTags: new FormControl(null), searchDbOnly: new FormControl(null), + searchLink: new FormControl(null), notificationsConsumerNewDocument: new FormControl(null), notificationsConsumerSuccess: new FormControl(null), @@ -129,6 +130,8 @@ export class SettingsComponent public systemStatus: SystemStatus + public readonly GlobalSearchType = GlobalSearchType + get systemStatusHasErrors(): boolean { return ( this.systemStatus.database.status === SystemStatusItemStatus.ERROR || @@ -306,6 +309,7 @@ export class SettingsComponent SETTINGS_KEYS.DOCUMENT_EDITING_REMOVE_INBOX_TAGS ), searchDbOnly: this.settings.get(SETTINGS_KEYS.SEARCH_DB_ONLY), + searchLink: this.settings.get(SETTINGS_KEYS.SEARCH_FULL_TYPE), savedViews: {}, } } @@ -539,6 +543,10 @@ export class SettingsComponent SETTINGS_KEYS.SEARCH_DB_ONLY, this.settingsForm.value.searchDbOnly ) + this.settings.set( + SETTINGS_KEYS.SEARCH_FULL_TYPE, + this.settingsForm.value.searchLink + ) this.settings.setLanguage(this.settingsForm.value.displayLanguage) this.settings .storeSettings() diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.html b/src-ui/src/app/components/app-frame/global-search/global-search.component.html index eeb118967..1e51dc1b6 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.html +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.html @@ -19,8 +19,12 @@
@if (query) { - } diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts index 076fa95d1..f2a1ac3a6 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.spec.ts @@ -25,6 +25,7 @@ import { FILTER_HAS_DOCUMENT_TYPE_ANY, FILTER_HAS_STORAGE_PATH_ANY, FILTER_HAS_TAGS_ALL, + FILTER_TITLE_CONTENT, } from 'src/app/data/filter-rule-type' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { DocumentService } from 'src/app/services/rest/document.service' @@ -37,6 +38,8 @@ import { ElementRef } from '@angular/core' import { ToastService } from 'src/app/services/toast.service' import { DataType } from 'src/app/data/datatype' import { queryParamsFromFilterRules } from 'src/app/utils/query-params' +import { SettingsService } from 'src/app/services/settings.service' +import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings' const searchResults = { total: 11, @@ -130,6 +133,7 @@ describe('GlobalSearchComponent', () => { let documentService: DocumentService let documentListViewService: DocumentListViewService let toastService: ToastService + let settingsService: SettingsService beforeEach(async () => { await TestBed.configureTestingModule({ @@ -150,6 +154,7 @@ describe('GlobalSearchComponent', () => { documentService = TestBed.inject(DocumentService) documentListViewService = TestBed.inject(DocumentListViewService) toastService = TestBed.inject(ToastService) + settingsService = TestBed.inject(SettingsService) fixture = TestBed.createComponent(GlobalSearchComponent) component = fixture.componentInstance @@ -262,7 +267,7 @@ describe('GlobalSearchComponent', () => { component.searchResults = searchResults as any component.resultsDropdown.open() component.query = 'test' - const advancedSearchSpy = jest.spyOn(component, 'runAdvanedSearch') + const advancedSearchSpy = jest.spyOn(component, 'runFullSearch') component.searchInputKeyDown(new KeyboardEvent('keydown', { key: 'Enter' })) expect(advancedSearchSpy).toHaveBeenCalled() }) @@ -499,15 +504,6 @@ describe('GlobalSearchComponent', () => { expect(focusSpy).toHaveBeenCalled() }) - it('should support explicit advanced search', () => { - const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') - component.query = 'test' - component.runAdvanedSearch() - expect(qfSpy).toHaveBeenCalledWith([ - { rule_type: FILTER_FULLTEXT_QUERY, value: 'test' }, - ]) - }) - it('should support open in new window', () => { const openSpy = jest.spyOn(window, 'open') const event = new Event('click') @@ -528,4 +524,23 @@ describe('GlobalSearchComponent', () => { button.dispatchEvent(keyboardEvent) expect(dispatchSpy).toHaveBeenCalledTimes(2) // once for keydown, second for click }) + + it('should support title content search and advanced search', () => { + const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') + component.query = 'test' + component.runFullSearch() + expect(qfSpy).toHaveBeenCalledWith([ + { rule_type: FILTER_TITLE_CONTENT, value: 'test' }, + ]) + + settingsService.set( + SETTINGS_KEYS.SEARCH_FULL_TYPE, + GlobalSearchType.ADVANCED + ) + component.query = 'test' + component.runFullSearch() + expect(qfSpy).toHaveBeenCalledWith([ + { rule_type: FILTER_FULLTEXT_QUERY, value: 'test' }, + ]) + }) }) diff --git a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts index 2742ff59a..dda1bc5f3 100644 --- a/src-ui/src/app/components/app-frame/global-search/global-search.component.ts +++ b/src-ui/src/app/components/app-frame/global-search/global-search.component.ts @@ -15,6 +15,7 @@ import { FILTER_HAS_DOCUMENT_TYPE_ANY, FILTER_HAS_STORAGE_PATH_ANY, FILTER_HAS_TAGS_ALL, + FILTER_TITLE_CONTENT, } from 'src/app/data/filter-rule-type' import { DataType } from 'src/app/data/datatype' import { ObjectWithId } from 'src/app/data/object-with-id' @@ -42,6 +43,8 @@ import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dial import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component' import { HotKeyService } from 'src/app/services/hot-key.service' import { paramsFromViewState } from 'src/app/utils/query-params' +import { SettingsService } from 'src/app/services/settings.service' +import { GlobalSearchType, SETTINGS_KEYS } from 'src/app/data/ui-settings' @Component({ selector: 'pngx-global-search', @@ -63,6 +66,13 @@ export class GlobalSearchComponent implements OnInit { @ViewChildren('primaryButton') primaryButtons: QueryList @ViewChildren('secondaryButton') secondaryButtons: QueryList + get useAdvancedForFullSearch(): boolean { + return ( + this.settingsService.get(SETTINGS_KEYS.SEARCH_FULL_TYPE) === + GlobalSearchType.ADVANCED + ) + } + constructor( public searchService: SearchService, private router: Router, @@ -71,7 +81,8 @@ export class GlobalSearchComponent implements OnInit { private documentListViewService: DocumentListViewService, private permissionsService: PermissionsService, private toastService: ToastService, - private hotkeyService: HotKeyService + private hotkeyService: HotKeyService, + private settingsService: SettingsService ) { this.queryDebounce = new Subject() @@ -282,7 +293,7 @@ export class GlobalSearchComponent implements OnInit { this.primaryButtons.first.nativeElement.click() this.searchInput.nativeElement.blur() } else if (this.query?.length) { - this.runAdvanedSearch() + this.runFullSearch() this.reset(true) } } else if (event.key === 'Escape' && !this.resultsDropdown.isOpen()) { @@ -378,9 +389,12 @@ export class GlobalSearchComponent implements OnInit { ) } - public runAdvanedSearch() { + public runFullSearch() { + const ruleType = this.useAdvancedForFullSearch + ? FILTER_FULLTEXT_QUERY + : FILTER_TITLE_CONTENT this.documentListViewService.quickFilter([ - { rule_type: FILTER_FULLTEXT_QUERY, value: this.query }, + { rule_type: ruleType, value: this.query }, ]) this.reset(true) } diff --git a/src-ui/src/app/data/ui-settings.ts b/src-ui/src/app/data/ui-settings.ts index 6f8f246ff..29ea08786 100644 --- a/src-ui/src/app/data/ui-settings.ts +++ b/src-ui/src/app/data/ui-settings.ts @@ -12,6 +12,11 @@ export interface UiSetting { default: any } +export enum GlobalSearchType { + ADVANCED = 'advanced', + TITLE_CONTENT = 'title-content', +} + export const SETTINGS_KEYS = { LANGUAGE: 'language', APP_LOGO: 'app_logo', @@ -57,6 +62,7 @@ export const SETTINGS_KEYS = { DOCUMENT_EDITING_REMOVE_INBOX_TAGS: 'general-settings:document-editing:remove-inbox-tags', SEARCH_DB_ONLY: 'general-settings:search:db-only', + SEARCH_FULL_TYPE: 'general-settings:search:more-link', } export const SETTINGS: UiSetting[] = [ @@ -225,4 +231,9 @@ export const SETTINGS: UiSetting[] = [ type: 'boolean', default: false, }, + { + key: SETTINGS_KEYS.SEARCH_FULL_TYPE, + type: 'string', + default: GlobalSearchType.TITLE_CONTENT, + }, ] From 02ebcd29eeec6c898e99b67fc574ff9010197c74 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 22 May 2024 23:12:54 -0700 Subject: [PATCH 043/112] Bump @angular/cdk to 17.3.10 Fixes https://github.com/angular/components/issues/29053 --- src-ui/package-lock.json | 8 ++++---- src-ui/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index ecc954721..b60babda8 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@angular/cdk": "^17.3.9", + "@angular/cdk": "^17.3.10", "@angular/common": "~17.3.9", "@angular/compiler": "~17.3.9", "@angular/core": "~17.3.9", @@ -1206,9 +1206,9 @@ } }, "node_modules/@angular/cdk": { - "version": "17.3.9", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.9.tgz", - "integrity": "sha512-N/7Is+FkIIql5UEL/I+PV6THw+yXNCCGGpwimf/yaNgT9y1fHAmBWhDY0oQqFjCuD+kXl9gQL0ONfsl5Nlnk+w==", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", + "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", "dependencies": { "tslib": "^2.3.0" }, diff --git a/src-ui/package.json b/src-ui/package.json index 101e6700c..28a4a35fc 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -11,7 +11,7 @@ }, "private": true, "dependencies": { - "@angular/cdk": "^17.3.9", + "@angular/cdk": "^17.3.10", "@angular/common": "~17.3.9", "@angular/compiler": "~17.3.9", "@angular/core": "~17.3.9", From e6b856e13f1de2daaf7887a8457044a78bacead5 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 23 May 2024 12:52:40 -0700 Subject: [PATCH 044/112] Documentation: Add PAPERLESS_OCR_LANGUAGE config note (#6821) --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index bbe46f4de..254508f31 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -616,6 +616,8 @@ parsing documents. Keep in mind that Tesseract uses much more CPU time with multiple languages enabled. + If you are including languages that are not installed by default, you will need to also set [`PAPERLESS_OCR_LANGUAGES`](configuration.md#PAPERLESS_OCR_LANGUAGES) for docker deployments or install the tesseract language packages manually for bare metal installations. + Defaults to "eng". !!! note From 2b85e812f8cee84b68807aea548fd26030a7bbd8 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Thu, 23 May 2024 13:09:05 -0700 Subject: [PATCH 045/112] Chore(deps): Use psycopg as recommended (#6811) --- Dockerfile | 10 +++++++-- Pipfile | 2 +- Pipfile.lock | 43 ++++++++++++++++++++------------------- src/paperless/settings.py | 2 +- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index d13f1b9ac..d8ff8c8b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -83,7 +83,6 @@ ARG RUNTIME_PACKAGES="\ icc-profiles-free \ imagemagick \ # PostgreSQL - libpq5 \ postgresql-client \ # MySQL / MariaDB mariadb-client \ @@ -223,7 +222,13 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \ && apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \ && python3 -m pip install --no-cache-dir --upgrade wheel \ && echo "Installing Python requirements" \ - && python3 -m pip install --default-timeout=1000 --requirement requirements.txt \ + && curl --fail --silent --show-error --location \ + --output psycopg_c-3.1.19-cp311-cp311-linux_x86_64.whl \ + https://github.com/paperless-ngx/builder/releases/download/psycopg-3.1.19/psycopg_c-3.1.19-cp311-cp311-linux_x86_64.whl \ + && curl --fail --silent --show-error --location \ + --output psycopg_c-3.1.19-cp311-cp311-linux_aarch64.whl \ + https://github.com/paperless-ngx/builder/releases/download/psycopg-3.1.19/psycopg_c-3.1.19-cp311-cp311-linux_aarch64.whl \ + && python3 -m pip install --default-timeout=1000 --find-links . --requirement requirements.txt \ && echo "Patching whitenoise for compression speedup" \ && curl --fail --silent --show-error --location --output 484.patch https://github.com/evansd/whitenoise/pull/484.patch \ && patch -d /usr/local/lib/python3.11/site-packages --verbose -p2 < 484.patch \ @@ -236,6 +241,7 @@ RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \ && apt-get --yes purge ${BUILD_PACKAGES} \ && apt-get --yes autoremove --purge \ && apt-get clean --yes \ + && rm --recursive --force --verbose *.whl \ && rm --recursive --force --verbose /var/lib/apt/lists/* \ && rm --recursive --force --verbose /tmp/* \ && rm --recursive --force --verbose /var/tmp/* \ diff --git a/Pipfile b/Pipfile index aee4c5ba4..7a01efca1 100644 --- a/Pipfile +++ b/Pipfile @@ -37,7 +37,7 @@ nltk = "*" ocrmypdf = "~=15.4" pathvalidate = "*" pdf2image = "*" -psycopg2 = "*" +psycopg = {version = "*", extras = ["c"]} python-dateutil = "*" python-dotenv = "*" python-gnupg = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 0ab8c2316..8bc530b33 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4bb46e902c6ceb6e3647772809b7941e318f1408472c4eaf0a1d2f0360ddac3a" + "sha256": "01fe0c491cf8d9429174033c720df637a4a1a4c168e79ea2c7c2d482d5f94f07" }, "pipfile-spec": 6, "requires": {}, @@ -900,6 +900,7 @@ "sha256:19b4e485cd07b7d83e3fe3b72132e7df70bfac22b14fe4bf7a23822c3a35bff5", "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab", "sha256:1a7aca7964ac4bb07680d5c9d63b9d7028cace3e2d43175cb50bba8c5ad33316", + "sha256:1b590b39ef90c6b22ec0be925b211298e810b4856909c8ca60d27ffbca6c12e6", "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df", "sha256:1e275ea572389e41e8b039ac076a46cb87ee6b8542df3fff26f5baab43713bca", "sha256:2304d3c93f2258ccf2cf7a6ba8c761d76ef84948d87bf9664e14d203da2cd264", @@ -955,6 +956,7 @@ "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa", "sha256:7ce7ad8abebe737ad6143d9d3bf94b88b93365ea30a5b81f6877ec9c0dee0a48", "sha256:7ed07b3062b055d7a7f9d6557a251cc655eed0b3152b76de619516621c56f5d3", + "sha256:7ff762670cada8e05b32bf1e4dc50b140790909caa8303cfddc4d702b71ea184", "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67", "sha256:875a3f90d7eb5c5d77e529080d95140eacb3c6d13ad5b616ee8095447b1d22e7", "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34", @@ -972,6 +974,7 @@ "sha256:a233bb68625a85126ac9f1fc66d24337d6e8a0f9207b688eec2e7c880f012ec0", "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b", "sha256:a6d17e0370d2516d5bb9062c7b4cb731cff921fc875644c3d751ad857ba9c5b1", + "sha256:a6d2092797b388342c1bc932077ad232f914351932353e2e8706851c870bca1f", "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf", "sha256:ac6540c9fff6e3813d29d0403ee7a81897f1d8ecc09a8ff84d2eea70ede1cdbf", "sha256:ae4073a60ab98529ab8a72ebf429f2a8cc612619a8c04e08bed27450d52103c0", @@ -990,6 +993,7 @@ "sha256:bcc98f911f10278d1daf14b87d65325851a1d29153caaf146877ec37031d5f36", "sha256:be49ad33819d7dcc28a309b86d4ed98e1a65f3075c6acd3cd4fe32103235222b", "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07", + "sha256:c2faf60c583af0d135e853c86ac2735ce178f0e338a3c7f9ae8f622fd2eb788c", "sha256:c689d0d5381f56de7bd6966a4541bff6e08bf8d3871bbd89a0c6ab18aa699573", "sha256:c7079d5eb1c1315a858bbf180000757db8ad904a89476653232db835c3114001", "sha256:cb3942960f0beb9f46e2a71a3aca220d1ca32feb5a398656be934320804c0df9", @@ -1374,25 +1378,22 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.0.43" }, - "psycopg2": { - "hashes": [ - "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981", - "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516", - "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3", - "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa", - "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a", - "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693", - "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372", - "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e", - "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59", - "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156", - "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024", - "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913", - "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c" + "psycopg": { + "extras": [ + "c" + ], + "hashes": [ + "sha256:92d7b78ad82426cdcf1a0440678209faa890c6e1721361c2f8901f0dccd62961", + "sha256:dca5e5521c859f6606686432ae1c94e8766d29cc91f2ee595378c510cc5b0731" ], - "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.9.9" + "version": "==3.1.19" + }, + "psycopg-c": { + "hashes": [ + "sha256:8e90f53c430e7d661cb3a9298e2761847212ead1b24c5fb058fc9d0fd9616017" + ], + "version": "==3.1.19" }, "pycparser": { "hashes": [ @@ -4267,12 +4268,12 @@ }, "types-setuptools": { "hashes": [ - "sha256:3a8ccea3e3f1f639856a1dd622be282f74e94e00fdc364630240f999cc9594fc", - "sha256:bd3964c08cffd5a057d9cabe61641c86a41a1b5dd2b652b8d371eed64d89d726" + "sha256:275fb72048b0203d3fbef268298ea78a0913cd114a74872d93f8638ccc5b7c63", + "sha256:52b264eff8913b5d85848d83bd98efea935fc6129d681d370eb957783880b720" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==69.5.0.20240513" + "version": "==69.5.0.20240519" }, "types-tqdm": { "hashes": [ diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 5c7aa6c19..831e4e953 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -613,7 +613,7 @@ def _parse_db_settings() -> dict: } else: # Default to PostgresDB - engine = "django.db.backends.postgresql_psycopg2" + engine = "django.db.backends.postgresql" options = { "sslmode": os.getenv("PAPERLESS_DBSSLMODE", "prefer"), "sslrootcert": os.getenv("PAPERLESS_DBSSLROOTCERT", None), From 3d56a56eb875decc68367ef69ff3870fd8ca5770 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 25 May 2024 09:59:28 -0500 Subject: [PATCH 046/112] Fix: check original render type for split button --- .../components/document-detail/document-detail.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 35d252b5b..0119955d9 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -53,7 +53,7 @@  More like this - From 180b32651d11a49a89c281582dcc75e5ec049029 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 27 May 2024 15:23:40 -0700 Subject: [PATCH 047/112] Chore: add stale to any-of-labels --- .github/workflows/repo-maintenance.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml index c545eee17..c9c134242 100644 --- a/.github/workflows/repo-maintenance.yml +++ b/.github/workflows/repo-maintenance.yml @@ -22,7 +22,7 @@ jobs: with: days-before-stale: 7 days-before-close: 14 - any-of-labels: 'cant-reproduce,not a bug' + any-of-labels: 'stale,cant-reproduce,not a bug' stale-issue-label: stale stale-pr-label: stale stale-issue-message: > From 6d4897a1b81d262f371aa9cb8b3400f716fe9226 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Tue, 28 May 2024 12:56:40 -0700 Subject: [PATCH 048/112] Refresh the document instance before doing workflow work, in case some other process has updated it (#6849) --- src/documents/signals/handlers.py | 414 +++++++++++++++--------------- 1 file changed, 209 insertions(+), 205 deletions(-) diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index cdfedcb4c..68f366f44 100644 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -363,24 +363,22 @@ class CannotMoveFilesException(Exception): pass -def validate_move(instance, old_path, new_path): - if not os.path.isfile(old_path): - # Can't do anything if the old file does not exist anymore. - logger.fatal(f"Document {instance!s}: File {old_path} has gone.") - raise CannotMoveFilesException - - if os.path.isfile(new_path): - # Can't do anything if the new file already exists. Skip updating file. - logger.warning( - f"Document {instance!s}: Cannot rename file " - f"since target path {new_path} already exists.", - ) - raise CannotMoveFilesException - - @receiver(models.signals.m2m_changed, sender=Document.tags.through) @receiver(models.signals.post_save, sender=Document) def update_filename_and_move_files(sender, instance: Document, **kwargs): + def validate_move(instance, old_path, new_path): + if not os.path.isfile(old_path): + # Can't do anything if the old file does not exist anymore. + msg = f"Document {instance!s}: File {old_path} doesn't exist." + logger.fatal(msg) + raise CannotMoveFilesException(msg) + + if os.path.isfile(new_path): + # Can't do anything if the new file already exists. Skip updating file. + msg = f"Document {instance!s}: Cannot rename file since target path {new_path} already exists." + logger.warning(msg) + raise CannotMoveFilesException(msg) + if not instance.filename: # Can't update the filename if there is no filename to begin with # This happens when the consumer creates a new document. @@ -532,6 +530,196 @@ def run_workflow( document: Document, logging_group=None, ): + def assignment_action(): + if action.assign_tags.all().count() > 0: + document.tags.add(*action.assign_tags.all()) + + if action.assign_correspondent is not None: + document.correspondent = action.assign_correspondent + + if action.assign_document_type is not None: + document.document_type = action.assign_document_type + + if action.assign_storage_path is not None: + document.storage_path = action.assign_storage_path + + if action.assign_owner is not None: + document.owner = action.assign_owner + + if action.assign_title is not None: + try: + document.title = parse_doc_title_w_placeholders( + action.assign_title, + ( + document.correspondent.name + if document.correspondent is not None + else "" + ), + ( + document.document_type.name + if document.document_type is not None + else "" + ), + (document.owner.username if document.owner is not None else ""), + timezone.localtime(document.added), + ( + document.original_filename + if document.original_filename is not None + else "" + ), + timezone.localtime(document.created), + ) + except Exception: + logger.exception( + f"Error occurred parsing title assignment '{action.assign_title}', falling back to original", + extra={"group": logging_group}, + ) + + if ( + ( + action.assign_view_users is not None + and action.assign_view_users.count() > 0 + ) + or ( + action.assign_view_groups is not None + and action.assign_view_groups.count() > 0 + ) + or ( + action.assign_change_users is not None + and action.assign_change_users.count() > 0 + ) + or ( + action.assign_change_groups is not None + and action.assign_change_groups.count() > 0 + ) + ): + permissions = { + "view": { + "users": action.assign_view_users.all().values_list( + "id", + ) + or [], + "groups": action.assign_view_groups.all().values_list( + "id", + ) + or [], + }, + "change": { + "users": action.assign_change_users.all().values_list( + "id", + ) + or [], + "groups": action.assign_change_groups.all().values_list( + "id", + ) + or [], + }, + } + set_permissions_for_object( + permissions=permissions, + object=document, + merge=True, + ) + + if action.assign_custom_fields is not None: + for field in action.assign_custom_fields.all(): + if ( + CustomFieldInstance.objects.filter( + field=field, + document=document, + ).count() + == 0 + ): + # can be triggered on existing docs, so only add the field if it doesn't already exist + CustomFieldInstance.objects.create( + field=field, + document=document, + ) + + def removal_action(): + if action.remove_all_tags: + document.tags.clear() + else: + for tag in action.remove_tags.filter( + pk__in=list(document.tags.values_list("pk", flat=True)), + ).all(): + document.tags.remove(tag.pk) + + if action.remove_all_correspondents or ( + document.correspondent + and ( + action.remove_correspondents.filter( + pk=document.correspondent.pk, + ).exists() + ) + ): + document.correspondent = None + + if action.remove_all_document_types or ( + document.document_type + and ( + action.remove_document_types.filter( + pk=document.document_type.pk, + ).exists() + ) + ): + document.document_type = None + + if action.remove_all_storage_paths or ( + document.storage_path + and ( + action.remove_storage_paths.filter( + pk=document.storage_path.pk, + ).exists() + ) + ): + document.storage_path = None + + if action.remove_all_owners or ( + document.owner + and (action.remove_owners.filter(pk=document.owner.pk).exists()) + ): + document.owner = None + + if action.remove_all_permissions: + permissions = { + "view": { + "users": [], + "groups": [], + }, + "change": { + "users": [], + "groups": [], + }, + } + set_permissions_for_object( + permissions=permissions, + object=document, + merge=False, + ) + elif ( + (action.remove_view_users.all().count() > 0) + or (action.remove_view_groups.all().count() > 0) + or (action.remove_change_users.all().count() > 0) + or (action.remove_change_groups.all().count() > 0) + ): + for user in action.remove_view_users.all(): + remove_perm("view_document", user, document) + for user in action.remove_change_users.all(): + remove_perm("change_document", user, document) + for group in action.remove_view_groups.all(): + remove_perm("view_document", group, document) + for group in action.remove_change_groups.all(): + remove_perm("change_document", group, document) + + if action.remove_all_custom_fields: + CustomFieldInstance.objects.filter(document=document).delete() + elif action.remove_custom_fields.all().count() > 0: + CustomFieldInstance.objects.filter( + field__in=action.remove_custom_fields.all(), + document=document, + ).delete() + for workflow in ( Workflow.objects.filter( enabled=True, @@ -552,6 +740,10 @@ def run_workflow( .prefetch_related("triggers") .order_by("order") ): + # This can be called from bulk_update_documents, which may be running multiple times + # Refresh this so the matching data is fresh and instance fields are re-freshed + # Otherwise, this instance might be behind and overwrite the work another process did + document.refresh_from_db() if matching.document_matches_workflow( document, workflow, @@ -565,198 +757,10 @@ def run_workflow( ) if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT: - if action.assign_tags.all().count() > 0: - document.tags.add(*action.assign_tags.all()) - - if action.assign_correspondent is not None: - document.correspondent = action.assign_correspondent - - if action.assign_document_type is not None: - document.document_type = action.assign_document_type - - if action.assign_storage_path is not None: - document.storage_path = action.assign_storage_path - - if action.assign_owner is not None: - document.owner = action.assign_owner - - if action.assign_title is not None: - try: - document.title = parse_doc_title_w_placeholders( - action.assign_title, - ( - document.correspondent.name - if document.correspondent is not None - else "" - ), - ( - document.document_type.name - if document.document_type is not None - else "" - ), - ( - document.owner.username - if document.owner is not None - else "" - ), - timezone.localtime(document.added), - ( - document.original_filename - if document.original_filename is not None - else "" - ), - timezone.localtime(document.created), - ) - except Exception: - logger.exception( - f"Error occurred parsing title assignment '{action.assign_title}', falling back to original", - extra={"group": logging_group}, - ) - - if ( - ( - action.assign_view_users is not None - and action.assign_view_users.count() > 0 - ) - or ( - action.assign_view_groups is not None - and action.assign_view_groups.count() > 0 - ) - or ( - action.assign_change_users is not None - and action.assign_change_users.count() > 0 - ) - or ( - action.assign_change_groups is not None - and action.assign_change_groups.count() > 0 - ) - ): - permissions = { - "view": { - "users": action.assign_view_users.all().values_list( - "id", - ) - or [], - "groups": action.assign_view_groups.all().values_list( - "id", - ) - or [], - }, - "change": { - "users": action.assign_change_users.all().values_list( - "id", - ) - or [], - "groups": action.assign_change_groups.all().values_list( - "id", - ) - or [], - }, - } - set_permissions_for_object( - permissions=permissions, - object=document, - merge=True, - ) - - if action.assign_custom_fields is not None: - for field in action.assign_custom_fields.all(): - if ( - CustomFieldInstance.objects.filter( - field=field, - document=document, - ).count() - == 0 - ): - # can be triggered on existing docs, so only add the field if it doesn't already exist - CustomFieldInstance.objects.create( - field=field, - document=document, - ) + assignment_action() elif action.type == WorkflowAction.WorkflowActionType.REMOVAL: - if action.remove_all_tags: - document.tags.clear() - else: - for tag in action.remove_tags.filter( - pk__in=list(document.tags.values_list("pk", flat=True)), - ).all(): - document.tags.remove(tag.pk) - - if action.remove_all_correspondents or ( - document.correspondent - and ( - action.remove_correspondents.filter( - pk=document.correspondent.pk, - ).exists() - ) - ): - document.correspondent = None - - if action.remove_all_document_types or ( - document.document_type - and ( - action.remove_document_types.filter( - pk=document.document_type.pk, - ).exists() - ) - ): - document.document_type = None - - if action.remove_all_storage_paths or ( - document.storage_path - and ( - action.remove_storage_paths.filter( - pk=document.storage_path.pk, - ).exists() - ) - ): - document.storage_path = None - - if action.remove_all_owners or ( - document.owner - and (action.remove_owners.filter(pk=document.owner.pk).exists()) - ): - document.owner = None - - if action.remove_all_permissions: - permissions = { - "view": { - "users": [], - "groups": [], - }, - "change": { - "users": [], - "groups": [], - }, - } - set_permissions_for_object( - permissions=permissions, - object=document, - merge=False, - ) - elif ( - (action.remove_view_users.all().count() > 0) - or (action.remove_view_groups.all().count() > 0) - or (action.remove_change_users.all().count() > 0) - or (action.remove_change_groups.all().count() > 0) - ): - for user in action.remove_view_users.all(): - remove_perm("view_document", user, document) - for user in action.remove_change_users.all(): - remove_perm("change_document", user, document) - for group in action.remove_view_groups.all(): - remove_perm("view_document", group, document) - for group in action.remove_change_groups.all(): - remove_perm("change_document", group, document) - - if action.remove_all_custom_fields: - CustomFieldInstance.objects.filter(document=document).delete() - elif action.remove_custom_fields.all().count() > 0: - CustomFieldInstance.objects.filter( - field__in=action.remove_custom_fields.all(), - document=document, - ).delete() + removal_action() document.save() From b9960220037852e7c7b4d044cf68c3a026bc95d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 May 2024 13:22:44 -0700 Subject: [PATCH 049/112] Chore(deps): Bump the small-changes group with 3 updates (#6843) Bumps the small-changes group with 3 updates: [rapidfuzz](https://github.com/rapidfuzz/RapidFuzz), [scikit-learn](https://github.com/scikit-learn/scikit-learn) and [watchdog](https://github.com/gorakhargosh/watchdog). Updates `rapidfuzz` from 3.9.0 to 3.9.1 - [Release notes](https://github.com/rapidfuzz/RapidFuzz/releases) - [Changelog](https://github.com/rapidfuzz/RapidFuzz/blob/main/CHANGELOG.rst) - [Commits](https://github.com/rapidfuzz/RapidFuzz/compare/v3.9.0...v3.9.1) Updates `scikit-learn` from 1.4.2 to 1.5.0 - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.4.2...1.5.0) Updates `watchdog` from 4.0.0 to 4.0.1 - [Release notes](https://github.com/gorakhargosh/watchdog/releases) - [Changelog](https://github.com/gorakhargosh/watchdog/blob/master/changelog.rst) - [Commits](https://github.com/gorakhargosh/watchdog/compare/v4.0.0...v4.0.1) --- updated-dependencies: - dependency-name: rapidfuzz dependency-type: direct:production update-type: version-update:semver-patch dependency-group: small-changes - dependency-name: scikit-learn dependency-type: direct:production update-type: version-update:semver-minor dependency-group: small-changes - dependency-name: watchdog dependency-type: direct:production update-type: version-update:semver-patch dependency-group: small-changes ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- Pipfile.lock | 346 ++++++++++++++++++++++++++------------------------- 2 files changed, 177 insertions(+), 171 deletions(-) diff --git a/Pipfile b/Pipfile index 7a01efca1..da26987cf 100644 --- a/Pipfile +++ b/Pipfile @@ -46,7 +46,7 @@ python-magic = "*" pyzbar = "*" rapidfuzz = "*" redis = {extras = ["hiredis"], version = "*"} -scikit-learn = "~=1.4" +scikit-learn = "~=1.5" setproctitle = "*" tika-client = "*" tqdm = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 8bc530b33..c99c57fae 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "01fe0c491cf8d9429174033c720df637a4a1a4c168e79ea2c7c2d482d5f94f07" + "sha256": "1bf32173eb08a61ff9ce2f3fd4cb968930da3b480583c218d1c439560839c363" }, "pipfile-spec": 6, "requires": {}, @@ -1539,100 +1539,103 @@ }, "rapidfuzz": { "hashes": [ - "sha256:014ac55b03f4074f903248ded181f3000f4cdbd134e6155cbf643f0eceb4f70f", - "sha256:08d8b49b3a4fb8572e480e73fcddc750da9cbb8696752ee12cca4bf8c8220d52", - "sha256:0bb28ab5300cf974c7eb68ea21125c493e74b35b1129e629533468b2064ae0a2", - "sha256:0c5b8f9a7b177af6ce7c6ad5b95588b4b73e37917711aafa33b2e79ee80fe709", - "sha256:0ca799f882364e69d0872619afb19efa3652b7133c18352e4a3d86a324fb2bb1", - "sha256:0dcb95fde22f98e6d0480db8d6038c45fe2d18a338690e6f9bba9b82323f3469", - "sha256:0e2e106cc66453bb80d2ad9c0044f8287415676df5c8036d737d05d4b9cdbf8e", - "sha256:0e86e39c1c1a0816ceda836e6f7bd3743b930cbc51a43a81bb433b552f203f25", - "sha256:1179dcd3d150a67b8a678cd9c84f3baff7413ff13c9e8fe85e52a16c97e24c9b", - "sha256:11a7ec4676242c8a430509cff42ce98bca2fbe30188a63d0f60fdcbfd7e84970", - "sha256:134b7098ac109834eeea81424b6822f33c4c52bf80b81508295611e7a21be12a", - "sha256:13857f9070600ea1f940749f123b02d0b027afbaa45e72186df0f278915761d0", - "sha256:170822a1b1719f02b58e3dce194c8ad7d4c5b39be38c0fdec603bd19c6f9cf81", - "sha256:182b4e11de928fb4834e8f8b5ecd971b5b10a86fabe8636ab65d3a9b7e0e9ca7", - "sha256:1b1f74997b6d94d66375479fa55f70b1c18e4d865d7afcd13f0785bfd40a9d3c", - "sha256:1d536f8beb8dd82d6efb20fe9f82c2cfab9ffa0384b5d184327e393a4edde91d", - "sha256:2003071aa633477a01509890c895f9ef56cf3f2eaa72c7ec0b567f743c1abcba", - "sha256:2444d8155d9846f206e2079bb355b85f365d9457480b0d71677a112d0a7f7128", - "sha256:28da953eb2ef9ad527e536022da7afff6ace7126cdd6f3e21ac20f8762e76d2c", - "sha256:2a96209f046fe328be30fc43f06e3d4b91f0d5b74e9dcd627dbfd65890fa4a5e", - "sha256:2bc5559b9b94326922c096b30ae2d8e5b40b2e9c2c100c2cc396ad91bcb84d30", - "sha256:2d267d4c982ab7d177e994ab1f31b98ff3814f6791b90d35dda38307b9e7c989", - "sha256:3083512e9bf6ed2bb3d25883922974f55e21ae7f8e9f4e298634691ae1aee583", - "sha256:30f7609da871510583f87484a10820b26555a473a90ab356cdda2f3b4456256c", - "sha256:33cfabcb7fd994938a6a08e641613ce5fe46757832edc789c6a5602e7933d6fa", - "sha256:36bf35df2d6c7d5820da20a6720aee34f67c15cd2daf8cf92e8141995c640c25", - "sha256:3733aede16ea112728ffeafeb29ccc62e095ed8ec816822fa2a82e92e2c08696", - "sha256:3904d0084ab51f82e9f353031554965524f535522a48ec75c30b223eb5a0a488", - "sha256:3a16c48c6df8fb633efbbdea744361025d01d79bca988f884a620e63e782fe5b", - "sha256:3c42a238bf9dd48f4ccec4c6934ac718225b00bb3a438a008c219e7ccb3894c7", - "sha256:3f4a2468432a1db491af6f547fad8f6d55fa03e57265c2f20e5eaceb68c7907e", - "sha256:413ac49bae291d7e226a5c9be65c71b2630b3346bce39268d02cb3290232e4b7", - "sha256:4514980a5d204c076dd5b756960f6b1b7598f030009456e6109d76c4c331d03c", - "sha256:47b7c0840afa724db3b1a070bc6ed5beab73b4e659b1d395023617fc51bf68a2", - "sha256:47d97e28c42f1efb7781993b67c749223f198f6653ef177a0c8f2b1c516efcaf", - "sha256:48105991ff6e4a51c7f754df500baa070270ed3d41784ee0d097549bc9fcb16d", - "sha256:488f74126904db6b1bea545c2f3567ea882099f4c13f46012fe8f4b990c683df", - "sha256:491274080742110427f38a6085bb12dffcaff1eef12dccf9e8758398c7e3957e", - "sha256:4eea3bf72c4fe68e957526ffd6bcbb403a21baa6b3344aaae2d3252313df6199", - "sha256:544b0bf9d17170720809918e9ccd0d482d4a3a6eca35630d8e1459f737f71755", - "sha256:55e2c5076f38fc1dbaacb95fa026a3e409eee6ea5ac4016d44fb30e4cad42b20", - "sha256:5c396562d304e974b4b0d5cd3afc4f92c113ea46a36e6bc62e45333d6aa8837e", - "sha256:623883fb78e692d54ed7c43b09beec52c6685f10a45a7518128e25746667403b", - "sha256:633b9d03fc04abc585c197104b1d0af04b1f1db1abc99f674d871224cd15557a", - "sha256:65d9250a4b0bf86320097306084bc3ca479c8f5491927c170d018787793ebe95", - "sha256:68da1b70458fea5290ec9a169fcffe0c17ff7e5bb3c3257e63d7021a50601a8e", - "sha256:6993d361f28b9ef5f0fa4e79b8541c2f3507be7471b9f9cb403a255e123b31e1", - "sha256:6a7f273906b3c7cc6d63a76e088200805947aa0bc1ada42c6a0e582e19c390d7", - "sha256:6a83128d505cac76ea560bb9afcb3f6986e14e50a6f467db9a31faef4bd9b347", - "sha256:6c1ed63345d1581c39d4446b1a8c8f550709656ce2a3c88c47850b258167f3c2", - "sha256:731269812ea837e0b93d913648e404736407408e33a00b75741e8f27c590caa2", - "sha256:7988363b3a415c5194ce1a68d380629247f8713e669ad81db7548eb156c4f365", - "sha256:7c09f4e87e82a164c9db769474bc61f8c8b677f2aeb0234b8abac73d2ecf9799", - "sha256:849160dc0f128acb343af514ca827278005c1d00148d025e4035e034fc2d8c7f", - "sha256:8e5ff882d3a3d081157ceba7e0ebc7fac775f95b08cbb143accd4cece6043819", - "sha256:905b01a9b633394ff6bb5ebb1c5fd660e0e180c03fcf9d90199cc6ed74b87cf7", - "sha256:9a06a99f1335fe43464d7121bc6540de7cd9c9475ac2025babb373fe7f27846b", - "sha256:9bc0f7e6256a9c668482c41c8a3de5d0aa12e8ca346dcc427b97c7edb82cba48", - "sha256:9d6478957fb35c7844ad08f2442b62ba76c1857a56370781a707eefa4f4981e1", - "sha256:a2de844e0e971d7bd8aa41284627dbeacc90e750b90acfb016836553c7a63192", - "sha256:a365886c42177b2beab475a50ba311b59b04f233ceaebc4c341f6f91a86a78e2", - "sha256:ab872cb57ae97c54ba7c71a9e3c9552beb57cb907c789b726895576d1ea9af6f", - "sha256:ae643220584518cbff8bf2974a0494d3e250763af816b73326a512c86ae782ce", - "sha256:b11e602987bcb4ea22b44178851f27406fca59b0836298d0beb009b504dba266", - "sha256:b1256915f7e7a5cf2c151c9ac44834b37f9bd1c97e8dec6f936884f01b9dfc7d", - "sha256:b182f0fb61f6ac435e416eb7ab330d62efdbf9b63cf0c7fa12d1f57c2eaaf6f3", - "sha256:b33c6d4b3a1190bc0b6c158c3981535f9434e8ed9ffa40cf5586d66c1819fb4b", - "sha256:b9bf90b3d96925cbf8ef44e5ee3cf39ef0c422f12d40f7a497e91febec546650", - "sha256:bd375c4830fee11d502dd93ecadef63c137ae88e1aaa29cc15031fa66d1e0abb", - "sha256:bdd8c15c3a14e409507fdf0c0434ec481d85c6cbeec8bdcd342a8cd1eda03825", - "sha256:c4ef34b2ddbf448f1d644b4ec6475df8bbe5b9d0fee173ff2e87322a151663bd", - "sha256:c56d2efdfaa1c642029f3a7a5bb76085c5531f7a530777be98232d2ce142553c", - "sha256:c74f2da334ce597f31670db574766ddeaee5d9430c2c00e28d0fbb7f76172036", - "sha256:cd2e6e97daf17ebb3254285cf8dd86c60d56d6cf35c67f0f9a557ef26bd66290", - "sha256:ce897b5dafb7fb7587a95fe4d449c1ea0b6d9ac4462fbafefdbbeef6eee4cf6a", - "sha256:d20ab9abc7e19767f1951772a6ab14cb4eddd886493c2da5ee12014596ad253f", - "sha256:d5d5684f54d82d9b0cf0b2701e55a630527a9c3dd5ddcf7a2e726a475ac238f2", - "sha256:dcb523243e988c849cf81220164ec3bbed378a699e595a8914fffe80596dc49f", - "sha256:e2218d62ab63f3c5ad48eced898854d0c2c327a48f0fb02e2288d7e5332a22c8", - "sha256:e33362e98c7899b5f60dcb06ada00acd8673ce0d59aefe9a542701251fd00423", - "sha256:e3f2d1ea7cd57dfcd34821e38b4924c80a31bcf8067201b1ab07386996a9faee", - "sha256:e65b8f7921bf60cbb207c132842a6b45eefef48c4c3b510eb16087d6c08c70af", - "sha256:e721842e6b601ebbeb8cc5e12c75bbdd1d9e9561ea932f2f844c418c31256e82", - "sha256:f81fe99a69ac8ee3fd905e70c62f3af033901aeb60b69317d1d43d547b46e510", - "sha256:f83bd3d01f04061c3660742dc85143a89d49fd23eb31eccbf60ad56c4b955617", - "sha256:fb67cf43ad83cb886cbbbff4df7dcaad7aedf94d64fca31aea0da7d26684283c", - "sha256:fc02157f521af15143fae88f92ef3ddcc4e0cff05c40153a9549dc0fbdb9adb3", - "sha256:fc4e26f592b51f97acf0a3f8dfed95e4d830c6a8fbf359361035df836381ab81", - "sha256:ff08081c49b18ba253a99e6a47f492e6ee8019e19bbb6ddc3ed360cd3ecb2f62", - "sha256:ff8982fc3bd49d55a91569fc8a3feba0de4cef0b391ff9091be546e9df075b81" + "sha256:02ed579f35ddd3552c7f74bc0c10800b432d9b09a4cebb19fd7a10b3b4759cc0", + "sha256:03a0a4bc8d4bd3e6f882b4c2ac183825a9b6dabe7e5a97bb6a1075e4635c944d", + "sha256:06879b598e798a4d33a283c2b4fa0d555d7706b6531e3321b161d62e986f7f57", + "sha256:07decc6b058f935d2219423a50aac426027928cc734809f793bc250de4a3756e", + "sha256:08dcd347d408912b6da778a73a0d7a2adad7fe238a44263e5e3789f2a8d84669", + "sha256:08f85d6674d804a493c3e9ec10a807f9bd8f482781487eda064913b537f99d7f", + "sha256:0c568b89a5016e76f0b3f85e9379036da99c5e7ec26b33935453d353a1938b74", + "sha256:0ceb0d7bdec910d93793d32633ba0cb644356cf6778f9d91b727da0075beaec1", + "sha256:0e931539edeb9158ef83537cd571051f8a9608737642c20b088a37bd5d76c5c9", + "sha256:0edc950c6a04c19db10670cd04e33403b3eb0f175deb620f9668595d378b1005", + "sha256:0f906ab6220778404498e0ce255c4cc89f98ea5e656e54cc59c5813c877eb86b", + "sha256:1539e7439b68013c5d2ab7ed9d3d221480a15595207764145ae177077d28016d", + "sha256:177bddf50577db59bcb00b6f7a5c2b70f2ec5a2aba40c8add7a6f7fd8609224e", + "sha256:1e872627d5359c840f3e431b0beb263518048917c3e076f624870552d84e7dc6", + "sha256:25ea055ae40fb60f503f02b44b3ac35a39a9108be33f89e05b81bc4e3c849ec8", + "sha256:2ef42c43c94139c890aeec40bc442c4bf8d48e15b456a88ce0f4cc5cfcad1896", + "sha256:2f9044a4470343087cde10beaa36266519d5da110a9a4597b43e6aa35fa928d3", + "sha256:32718fa69306df969bf4fca1719f8900b83df315a2a8153942d5b8906f4fd1d6", + "sha256:349e9c15092d20a1f6ff1795e068f39a9ee5e84c54b3addbc66d0ac469c4ef43", + "sha256:3ab6ad7e70469aed24e24378b19a9e47fc757c847399b22c612a0fccacc795cb", + "sha256:414644a2fc8a3e5fafda95b430214ed892faa4d0a07401d33892bc9ca5c84974", + "sha256:493354f50b9855271ac846b213e394e08446e70cef5cc033e5302a2220f3ae7b", + "sha256:4a231e8f3bae82f10e7188965b37c91d8bfb80136595c860c8a08eb0dd07764d", + "sha256:50c2f7ad132dfeb6247c90b41431662af939a820f761cf930708d55912377ed8", + "sha256:53477e1e6d85d603c9a319cfd00ab9f0a57b6d68bcdb268d6b15a79e64d693d0", + "sha256:5713c56b30ed75ada3a8f008cf8e8e6323386ce48fac2bf2d07285fe6c91f5a4", + "sha256:5d8eb7fe39e81dc1530a3ec81a35e69770839c76607c461eb9d0902427fab3e1", + "sha256:5dbcc4add07bd60ea73b94392fed28f83dba0fe796097da47627fd539bd6daca", + "sha256:5f897a9bff517d5c6af6a90131796b4298b547b9a9a4df3cf285006be33aae5b", + "sha256:61b343c581f4926260248069d8fdbbbf293c19c12ef440ad5ced15bcff277a84", + "sha256:64058f4a3698c6c8464df47a3b7da303db2477b2447142da3e67fc091f4c366a", + "sha256:678fcaa5117ddb6263160a7c5f33cc9ea3df335465f5d53715707fad103e1d09", + "sha256:7149afaf0294882b6b15bb6fa9fc38ff1d761e50117460ee3561181c1c4e2230", + "sha256:76a55bcc3abc9f8e38a1218cb5a09719126cfc4cba23ebd8caa27dfdc69cedd8", + "sha256:77767b119ac05662d216a8cc4092ac28dbc015d9caabebdbefe371b0dd82a38e", + "sha256:7f006c3af82c478df09a790fb4846b5acd00a187d75715674d71f5dc0ac982ce", + "sha256:829fbad93266fffa0f9d722a94cbb1b95b53e3c04be4e872193496a0cfbd66f0", + "sha256:83c570ce23b447625929c0e7c4f2eab6d90f5a576db2b26a5aa0594a53d560ea", + "sha256:874317057a58a9c6ddf59fe1491e478217daa9fdb043a00358a15de4f62f9a2d", + "sha256:8781e45c56f7f3a64940f4d594a4ffd69360147925a706569b2b0c57347b2225", + "sha256:8e11405d81e8baea4999a5757a982009566cff8f6a121d5ccf042aab81ae0230", + "sha256:8e70605350cb6ec5091e06de62d3dcb058f694b059b4e1a9d85bfbf892f70030", + "sha256:97ab8f153984a5d827ebb5a5b80ee59563efcf2fa3e569dcd46ea7e7c9845e93", + "sha256:998977df2ae01ff8b7bc3b29a860b4a863005e0533e323df3fd555a31ef33f0e", + "sha256:9bfa8c0a8ce09b4bcd36322f8f375750dca160fbdbeb2e763a695cef3ae9133e", + "sha256:9f66f9d5f14141b4b017e76118ec4bda29266f6b281989026e3a9ba1a2aaf032", + "sha256:9f74d93148081049ccc86f276d54cd7c8c0692250245660b4fcd904ed1db1e01", + "sha256:9f8615a2a67a1f80b3aa7a3d7fbe6a2ed062a54c98988e3f9b664b49a3bc115e", + "sha256:9f9dfdcd75e16e5874efee233b28aec1322623b0f1f20641452d06ea2d8ba5ef", + "sha256:a079164675d24eb715230bf9dd252683ae3c9c0c0a236f0b8098630268b899e9", + "sha256:a3a48fc6dc274b803a366a4baec99e212792ae1b1e73d42235b2042cd3ade7c1", + "sha256:a42eb645241f39a59c45a7fc15e3faf61886bff3a4a22263fd0f7cfb90e91b7f", + "sha256:a47550eabf235e5d50e7d448c18f77f6e8082aa3571e9df511c8388525ea9372", + "sha256:a645f362dafc103dbe7f43a2ad34f76284773cd7d1b00514d1c591848a1c817f", + "sha256:a89b219c1a7933a0673b2dbb1ffe701057d82e5cb843552be4f55b61b557031e", + "sha256:aa44aef769e5834fef4fde091fd646cc1c52a2813b3aa241ae54b3028960abaa", + "sha256:aaba665f92c011c6f284e933ab02b5dc129a6d3f48fce913ec4a214bd530135e", + "sha256:b26cc9459e096959fab3a4a8a17b96a6c7c961f9db5c37c1c3c7a06789316cf7", + "sha256:b71e7f99ed048a338e4a1ac34f56b3b3933a3ba2dfbb04450c786a8ddd97f4db", + "sha256:b8a6d5a8edc452920efdf1b499a2a47bb8a28440f7ab3fe28bb7d6636ccf71c3", + "sha256:bc4503841cd3cbe22b5ac44f15bc834ec97d811a3c3943f73f5643266c8674e1", + "sha256:bc68fb8f2a8b5b3a4526b7a65e7d5c7f821882f56d9dcbcce4c6859a9e5bdcd7", + "sha256:bca2b93c75f87cd85832cdd5bb06b4b5642e2a05c8e3550841ddf5d564ce4abb", + "sha256:bcc0ffcaeb1e499e708f32ec30177ed690b3f25455c91ad8c2240986c69f9ebe", + "sha256:bf5184b17e26a82b00c7ee05d9ec5d826113df55830bbc447bf6d6e7469c70fb", + "sha256:c0899de4fc1a7a36f14be556a0dacf40ea5c0fe22c6b45b2ea2674e1ac47e269", + "sha256:c125095d1828fa10ac79077594dd2d8829167d9e184e20baa97620fc52ebdcc9", + "sha256:c6437cba4b9460d5ee0bafd796e13ef9307091b81685bbe745b0f1619fb887ca", + "sha256:c6b11a38b61cc2462a113b123f5e932cda0e525f816d6fe4b68516f97d7f9d49", + "sha256:c883d2d2e31c759af1f3fdeb67ec151cf94e307f745b3d02ab3a2ef6595485f2", + "sha256:c8b0e6640421e55d69e186ce7fb9e6c723cfd3b6f91beaeb28705c2a46c8a194", + "sha256:c99d001c45c31c2cd2f7361bc2036d3062b21db5f43beea8bc5109d43fe9f283", + "sha256:ce3335324198e1388a1c4e50d40f45107367010afe9fa09fd46278160f0ab591", + "sha256:d2ff268a8bf57a76512804d5ca2097afaf98e64e8947d514cde7e2e8446aa5f7", + "sha256:d3397630f22e6c60dda8be3e9dbcf6a341695d487df8a6c92f4a2f7ebcdaecf7", + "sha256:d3da444890c9559fd15717d97f8373b1cd14007f68c9b037aa93ef7ca969b559", + "sha256:da3f495cf4f7a443b34a6d3c6805265595fcd13641b3253a8e2034289d828dd9", + "sha256:dc4b5de5d6f9347d836d849b56bca630169353cbe5c10fa7fe93bb1677b49770", + "sha256:dec2792f864be731c8339cad99001caa6540aa909e6fd8bc688bb0419c501f44", + "sha256:df7bba8d4a8fb8e7559a9e83dfc5385dc6fe89efd73e32d253667242faf1883c", + "sha256:e18f0e9351f7e5d5387774ff4d5cabd824341e16b866eb1c8d3f557111b447ef", + "sha256:e4c6b6455ee8404a663e15477a8bfe98b1afb329ff224bcf6d15f623a3761b95", + "sha256:e58489934d0147f1edda693cb983bea40f2b45ae6756fd47c1005b538f817a2f", + "sha256:e5d7b3154f6df9e05c2016de5e95f8cba4fe636a4e5520ebcd89bc6c54b8e4ed", + "sha256:e9f075366cee63a6b06bd7f9285eb9f1785382a6493afcb7054202e20508bf94", + "sha256:ea62c82eb2c65bd49651e95f4f46874483ae4da1c3b57997e58f1b4fb2de6c05", + "sha256:eadf8c4b24b63aef8810ed585c24ac1fc022ee771211772a6e9f78c63aa949ff", + "sha256:eb023adfefa62410fff877f7cc70cd4758cbfbad963e87d146cf71b022dce197", + "sha256:eb3f1af099cd1d98001691fbdadd422f088f21eadcacf5698b393b7569e24dc4", + "sha256:f6f4e8235d0acf1972f5eb4091c4a0473e5670a754f166c0c718ce21e945f879", + "sha256:f8869dcf072227a40a6f9e87b3fc4eb020055a08ad12b63d751c354e3a973ccb", + "sha256:fb592bad9d58b47c6681f0c180767d2c98775a35f7267131d33723139c3d6c2e", + "sha256:fbce66cb2e331b0888c79b594eab76e2c609c2637050085daadff5325d471dc2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.9.0" + "version": "==3.9.1" }, "redis": { "extras": [ @@ -1764,62 +1767,62 @@ }, "scikit-learn": { "hashes": [ - "sha256:1d0b25d9c651fd050555aadd57431b53d4cf664e749069da77f3d52c5ad14b3b", - "sha256:36f0ea5d0f693cb247a073d21a4123bdf4172e470e6d163c12b74cbb1536cf38", - "sha256:426d258fddac674fdf33f3cb2d54d26f49406e2599dbf9a32b4d1696091d4256", - "sha256:44c62f2b124848a28fd695db5bc4da019287abf390bfce602ddc8aa1ec186aae", - "sha256:45dee87ac5309bb82e3ea633955030df9bbcb8d2cdb30383c6cd483691c546cc", - "sha256:49d64ef6cb8c093d883e5a36c4766548d974898d378e395ba41a806d0e824db8", - "sha256:5460a1a5b043ae5ae4596b3126a4ec33ccba1b51e7ca2c5d36dac2169f62ab1d", - "sha256:5cd7b524115499b18b63f0c96f4224eb885564937a0b3477531b2b63ce331904", - "sha256:671e2f0c3f2c15409dae4f282a3a619601fa824d2c820e5b608d9d775f91780c", - "sha256:68b8404841f944a4a1459b07198fa2edd41a82f189b44f3e1d55c104dbc2e40c", - "sha256:81bf5d8bbe87643103334032dd82f7419bc8c8d02a763643a6b9a5c7288c5054", - "sha256:8539a41b3d6d1af82eb629f9c57f37428ff1481c1e34dddb3b9d7af8ede67ac5", - "sha256:87440e2e188c87db80ea4023440923dccbd56fbc2d557b18ced00fef79da0727", - "sha256:90378e1747949f90c8f385898fff35d73193dfcaec3dd75d6b542f90c4e89755", - "sha256:b0203c368058ab92efc6168a1507d388d41469c873e96ec220ca8e74079bf62e", - "sha256:c97a50b05c194be9146d61fe87dbf8eac62b203d9e87a3ccc6ae9aed2dfaf361", - "sha256:d36d0bc983336bbc1be22f9b686b50c964f593c8a9a913a792442af9bf4f5e68", - "sha256:d762070980c17ba3e9a4a1e043ba0518ce4c55152032f1af0ca6f39b376b5928", - "sha256:d9993d5e78a8148b1d0fdf5b15ed92452af5581734129998c26f481c46586d68", - "sha256:daa1c471d95bad080c6e44b4946c9390a4842adc3082572c20e4f8884e39e959", - "sha256:ff4effe5a1d4e8fed260a83a163f7dbf4f6087b54528d8880bab1d1377bd78be" + "sha256:057b991ac64b3e75c9c04b5f9395eaf19a6179244c089afdebaad98264bff37c", + "sha256:118a8d229a41158c9f90093e46b3737120a165181a1b58c03461447aa4657415", + "sha256:12e40ac48555e6b551f0a0a5743cc94cc5a765c9513fe708e01f0aa001da2801", + "sha256:174beb56e3e881c90424e21f576fa69c4ffcf5174632a79ab4461c4c960315ac", + "sha256:1b94d6440603752b27842eda97f6395f570941857456c606eb1d638efdb38184", + "sha256:1f77547165c00625551e5c250cefa3f03f2fc92c5e18668abd90bfc4be2e0bff", + "sha256:261fe334ca48f09ed64b8fae13f9b46cc43ac5f580c4a605cbb0a517456c8f71", + "sha256:2a65af2d8a6cce4e163a7951a4cfbfa7fceb2d5c013a4b593686c7f16445cf9d", + "sha256:2c75ea812cd83b1385bbfa94ae971f0d80adb338a9523f6bbcb5e0b0381151d4", + "sha256:40fb7d4a9a2db07e6e0cae4dc7bdbb8fada17043bac24104d8165e10e4cff1a2", + "sha256:460806030c666addee1f074788b3978329a5bfdc9b7d63e7aad3f6d45c67a210", + "sha256:47132440050b1c5beb95f8ba0b2402bbd9057ce96ec0ba86f2f445dd4f34df67", + "sha256:4c0c56c3005f2ec1db3787aeaabefa96256580678cec783986836fc64f8ff622", + "sha256:789e3db01c750ed6d496fa2db7d50637857b451e57bcae863bff707c1247bef7", + "sha256:855fc5fa8ed9e4f08291203af3d3e5fbdc4737bd617a371559aaa2088166046e", + "sha256:a03b09f9f7f09ffe8c5efffe2e9de1196c696d811be6798ad5eddf323c6f4d40", + "sha256:a3a10e1d9e834e84d05e468ec501a356226338778769317ee0b84043c0d8fb06", + "sha256:a90c5da84829a0b9b4bf00daf62754b2be741e66b5946911f5bdfaa869fcedd6", + "sha256:d82c2e573f0f2f2f0be897e7a31fcf4e73869247738ab8c3ce7245549af58ab8", + "sha256:df8ccabbf583315f13160a4bb06037bde99ea7d8211a69787a6b7c5d4ebb6fc3", + "sha256:f405c4dae288f5f6553b10c4ac9ea7754d5180ec11e296464adb5d6ac68b6ef5" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==1.4.2" + "version": "==1.5.0" }, "scipy": { "hashes": [ - "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922", - "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5", - "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa", - "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820", - "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd", - "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42", - "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e", - "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d", - "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86", - "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e", - "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c", - "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602", - "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e", - "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5", - "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a", - "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21", - "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d", - "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6", - "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78", - "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551", - "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7", - "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4", - "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d", - "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b", - "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9" + "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d", + "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c", + "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca", + "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9", + "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54", + "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16", + "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2", + "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5", + "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59", + "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326", + "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b", + "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1", + "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d", + "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24", + "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627", + "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c", + "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa", + "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949", + "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989", + "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004", + "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f", + "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884", + "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299", + "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94", + "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f" ], "markers": "python_version >= '3.9'", - "version": "==1.13.0" + "version": "==1.13.1" }, "setproctitle": { "hashes": [ @@ -2072,39 +2075,42 @@ }, "watchdog": { "hashes": [ - "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257", - "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca", - "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b", - "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85", - "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b", - "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19", - "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50", - "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92", - "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269", - "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f", - "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c", - "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b", - "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87", - "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b", - "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b", - "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8", - "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c", - "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3", - "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7", - "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605", - "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935", - "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b", - "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927", - "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101", - "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07", - "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec", - "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4", - "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245", - "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d" + "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7", + "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", + "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175", + "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", + "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", + "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", + "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", + "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", + "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7", + "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28", + "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235", + "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", + "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", + "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5", + "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709", + "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee", + "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", + "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd", + "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba", + "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db", + "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682", + "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35", + "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", + "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645", + "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", + "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193", + "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b", + "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", + "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b", + "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625", + "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", + "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "watchfiles": { "hashes": [ From fdf873ad6a13c3b36ced04a9f2f9a2d34d0dec68 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 28 May 2024 23:52:45 -0700 Subject: [PATCH 050/112] Fix: enforce dropdown input min width --- src-ui/src/styles.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 0e084e838..412324142 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -259,6 +259,10 @@ a.btn-link:focus-visible, .ng-select-container .ng-value-container .ng-input { top: 7px; + + input { + min-width: 10px; + } } .ng-dropdown-panel .ng-dropdown-panel-items .ng-option.ng-option-marked { From 235b0a4c33c70f4209c562f3689307ba6da03efa Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 28 May 2024 23:54:51 -0700 Subject: [PATCH 051/112] Update messages.xlf --- src-ui/messages.xlf | 94 ++++++++++++++++++++++----------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 37fecf495..36563485c 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -2286,7 +2286,7 @@
src/app/components/document-detail/document-detail.component.ts - 818 + 821 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -2329,19 +2329,19 @@ src/app/components/document-detail/document-detail.component.ts - 820 + 823 src/app/components/document-detail/document-detail.component.ts - 1113 + 1114 src/app/components/document-detail/document-detail.component.ts - 1150 + 1151 src/app/components/document-detail/document-detail.component.ts - 1191 + 1192 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -5510,7 +5510,7 @@ src/app/components/document-detail/document-detail.component.ts - 1168 + 1169 src/app/guards/dirty-saved-view.guard.ts @@ -5786,56 +5786,56 @@ An error occurred loading content: src/app/components/document-detail/document-detail.component.ts - 339,341 + 342,344
Document changes detected src/app/components/document-detail/document-detail.component.ts - 362 + 365 The version of this document in your browser session appears older than the existing version. src/app/components/document-detail/document-detail.component.ts - 363 + 366 Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document. src/app/components/document-detail/document-detail.component.ts - 364 + 367 Ok src/app/components/document-detail/document-detail.component.ts - 366 + 369 Next document src/app/components/document-detail/document-detail.component.ts - 473 + 476 Previous document src/app/components/document-detail/document-detail.component.ts - 483 + 486 Close document src/app/components/document-detail/document-detail.component.ts - 491 + 494 src/app/services/open-documents.service.ts @@ -5846,50 +5846,50 @@ Save document src/app/components/document-detail/document-detail.component.ts - 498 + 501 Error retrieving metadata src/app/components/document-detail/document-detail.component.ts - 540 + 543 Error retrieving suggestions. src/app/components/document-detail/document-detail.component.ts - 565 + 568 Document saved successfully. src/app/components/document-detail/document-detail.component.ts - 687 + 690 src/app/components/document-detail/document-detail.component.ts - 701 + 704 Error saving document src/app/components/document-detail/document-detail.component.ts - 705 + 708 src/app/components/document-detail/document-detail.component.ts - 746 + 749 Confirm delete src/app/components/document-detail/document-detail.component.ts - 773 + 776 src/app/components/manage/management-list/management-list.component.ts @@ -5904,35 +5904,35 @@ Do you really want to delete document ""? src/app/components/document-detail/document-detail.component.ts - 774 + 777 The files for this document will be deleted permanently. This operation cannot be undone. src/app/components/document-detail/document-detail.component.ts - 775 + 778 Delete document src/app/components/document-detail/document-detail.component.ts - 777 + 780 Error deleting document src/app/components/document-detail/document-detail.component.ts - 796 + 799 Redo OCR confirm src/app/components/document-detail/document-detail.component.ts - 816 + 819 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -5943,63 +5943,63 @@ This operation will permanently redo OCR for this document. src/app/components/document-detail/document-detail.component.ts - 817 + 820 Redo OCR operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content. src/app/components/document-detail/document-detail.component.ts - 828 + 831 Error executing operation src/app/components/document-detail/document-detail.component.ts - 839 + 842 Page Fit src/app/components/document-detail/document-detail.component.ts - 908 + 911 Split confirm src/app/components/document-detail/document-detail.component.ts - 1111 + 1112 This operation will split the selected document(s) into new documents. src/app/components/document-detail/document-detail.component.ts - 1112 + 1113 Split operation will begin in the background. src/app/components/document-detail/document-detail.component.ts - 1127 + 1128 Error executing split operation src/app/components/document-detail/document-detail.component.ts - 1136 + 1137 Rotate confirm src/app/components/document-detail/document-detail.component.ts - 1148 + 1149 src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -6010,49 +6010,49 @@ This operation will permanently rotate the original version of the current document. src/app/components/document-detail/document-detail.component.ts - 1149 + 1150 Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes. src/app/components/document-detail/document-detail.component.ts - 1165 + 1166 Error executing rotate operation src/app/components/document-detail/document-detail.component.ts - 1177 + 1178 Delete pages confirm src/app/components/document-detail/document-detail.component.ts - 1189 + 1190 This operation will permanently delete the selected pages from the original document. src/app/components/document-detail/document-detail.component.ts - 1190 + 1191 Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes. src/app/components/document-detail/document-detail.component.ts - 1205 + 1206 Error executing delete pages operation src/app/components/document-detail/document-detail.component.ts - 1214 + 1215 @@ -7070,28 +7070,28 @@ correspondent src/app/components/manage/correspondent-list/correspondent-list.component.ts - 39 + 40 correspondents src/app/components/manage/correspondent-list/correspondent-list.component.ts - 40 + 41 Last used src/app/components/manage/correspondent-list/correspondent-list.component.ts - 45 + 46 Do you really want to delete the correspondent ""? src/app/components/manage/correspondent-list/correspondent-list.component.ts - 67 + 88 From ce3d5b006574d8a0cbd451df5882e19602f603a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 07:04:01 +0000 Subject: [PATCH 052/112] Chore(deps-dev): Bump the development group across 1 directory with 2 updates (#6851) * Chore(deps-dev): Bump the development group across 1 directory with 2 updates Bumps the development group with 2 updates in the / directory: [ruff](https://github.com/astral-sh/ruff) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material). Updates `ruff` from 0.4.4 to 0.4.6 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.4.4...v0.4.6) Updates `mkdocs-material` from 9.5.24 to 9.5.25 - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.24...9.5.25) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch dependency-group: development - dependency-name: mkdocs-material dependency-type: direct:development update-type: version-update:semver-patch dependency-group: development ... Signed-off-by: dependabot[bot] * Updates hook versions to match * New codespell fixes * Remove unneeded i18n --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- .codespellrc | 2 +- .pre-commit-config.yaml | 4 +- CODE_OF_CONDUCT.md | 2 +- Pipfile.lock | 117 +++++++++--------- src-ui/messages.xlf | 50 ++++---- .../admin/config/config.component.html | 2 +- src/paperless_tesseract/parsers.py | 2 +- 7 files changed, 89 insertions(+), 90 deletions(-) diff --git a/.codespellrc b/.codespellrc index 5e7cb2b2e..9b2cf624d 100644 --- a/.codespellrc +++ b/.codespellrc @@ -1,3 +1,3 @@ [codespell] write-changes = True -ignore-words-list = criterias,afterall,valeu,ureue,equest,ure +ignore-words-list = criterias,afterall,valeu,ureue,equest,ure,assertIn diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 87780b056..026344a2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: - id: check-case-conflict - id: detect-private-key - repo: https://github.com/codespell-project/codespell - rev: v2.2.6 + rev: v2.3.0 hooks: - id: codespell exclude: "(^src-ui/src/locale/)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)" @@ -47,7 +47,7 @@ repos: exclude: "(^Pipfile\\.lock$)" # Python hooks - repo: https://github.com/astral-sh/ruff-pre-commit - rev: 'v0.4.4' + rev: 'v0.4.6' hooks: - id: ruff - id: ruff-format diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index a0c3bb8ee..5debd43b3 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -5,7 +5,7 @@ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, +identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. diff --git a/Pipfile.lock b/Pipfile.lock index c99c57fae..6232585de 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -3064,12 +3064,12 @@ }, "mkdocs-material": { "hashes": [ - "sha256:02d5aaba0ee755e707c3ef6e748f9acb7b3011187c0ea766db31af8905078a34", - "sha256:e12cd75954c535b61e716f359cf2a5056bf4514889d17161fdebd5df4b0153c6" + "sha256:68fdab047a0b9bfbefe79ce267e8a7daaf5128bcf7867065fcd201ee335fece1", + "sha256:d0662561efb725b712207e0ee01f035ca15633f29a64628e24f01ec99d7078f4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==9.5.24" + "version": "==9.5.25" }, "mkdocs-material-extensions": { "hashes": [ @@ -3568,35 +3568,35 @@ }, "requests": { "hashes": [ - "sha256:21ac9465cdf8c1650fe1ecde8a71669a93d4e6f147550483a2967d08396a56a5", - "sha256:eb97e87e64c79e64e5b8ac75cee9dd1f97f49e289b083ee6be96268930725685" + "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", + "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" ], "markers": "python_version >= '3.8'", - "version": "==2.32.1" + "version": "==2.32.2" }, "ruff": { "hashes": [ - "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768", - "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6", - "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6", - "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae", - "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15", - "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab", - "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef", - "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95", - "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e", - "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595", - "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36", - "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85", - "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd", - "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891", - "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9", - "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876", - "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af" + "sha256:04a80acfc862e0e1630c8b738e70dcca03f350bad9e106968a8108379e12b31f", + "sha256:0cf5cc02d3ae52dfb0c8a946eb7a1d6ffe4d91846ffc8ce388baa8f627e3bd50", + "sha256:1fa8561489fadf483ffbb091ea94b9c39a00ed63efacd426aae2f197a45e67fc", + "sha256:1ff930d6e05f444090a0139e4e13e1e2e1f02bd51bb4547734823c760c621e79", + "sha256:3a6a0a4f4b5f54fff7c860010ab3dd81425445e37d35701a965c0248819dde7a", + "sha256:3f9ced5cbb7510fd7525448eeb204e0a22cabb6e99a3cb160272262817d49786", + "sha256:4d5b914818d8047270308fe3e85d9d7f4a31ec86c6475c9f418fbd1624d198e0", + "sha256:4f02284335c766678778475e7698b7ab83abaf2f9ff0554a07b6f28df3b5c259", + "sha256:602ebd7ad909eab6e7da65d3c091547781bb06f5f826974a53dbe563d357e53c", + "sha256:735a16407a1a8f58e4c5b913ad6102722e80b562dd17acb88887685ff6f20cf6", + "sha256:9018bf59b3aa8ad4fba2b1dc0299a6e4e60a4c3bc62bbeaea222679865453062", + "sha256:a769ae07ac74ff1a019d6bd529426427c3e30d75bdf1e08bb3d46ac8f417326a", + "sha256:a797a87da50603f71e6d0765282098245aca6e3b94b7c17473115167d8dfb0b7", + "sha256:be47700ecb004dfa3fd4dcdddf7322d4e632de3c06cd05329d69c45c0280e618", + "sha256:ea3424793c29906407e3cf417f28fc33f689dacbbadfb52b7e9a809dd535dcef", + "sha256:ef995583a038cd4a7edf1422c9e19118e2511b8ba0b015861b4abd26ec5367c5", + "sha256:f13410aabd3b5776f9c5699f42b37a3a348d65498c4310589bc6e5c548dc8a2f" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.4.4" + "version": "==0.4.6" }, "scipy": { "hashes": [ @@ -3721,46 +3721,49 @@ }, "watchdog": { "hashes": [ - "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257", - "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca", - "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b", - "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85", - "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b", - "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19", - "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50", - "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92", - "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269", - "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f", - "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c", - "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b", - "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87", - "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b", - "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b", - "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8", - "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c", - "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3", - "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7", - "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605", - "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935", - "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b", - "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927", - "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101", - "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07", - "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec", - "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4", - "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245", - "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d" + "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7", + "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", + "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175", + "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", + "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", + "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", + "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", + "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", + "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7", + "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28", + "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235", + "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", + "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", + "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5", + "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709", + "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee", + "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", + "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd", + "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba", + "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db", + "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682", + "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35", + "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", + "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645", + "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", + "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193", + "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b", + "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", + "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b", + "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625", + "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", + "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5" ], "markers": "python_version >= '3.8'", - "version": "==4.0.0" + "version": "==4.0.1" }, "zipp": { "hashes": [ - "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059", - "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e" + "sha256:952df858fb3164426c976d9338d3961e8e8b3758e2e059e0f754b8c4262625ee", + "sha256:96dc6ad62f1441bcaccef23b274ec471518daf4fbbc580341204936a5a3dddec" ], "markers": "python_version >= '3.8'", - "version": "==3.18.2" + "version": "==3.19.0" }, "zope-interface": { "hashes": [ diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 36563485c..edec5cef0 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -503,33 +503,6 @@ 4 - - - - src/app/components/admin/config/config.component.html - 14,15 - - - src/app/components/common/custom-field-display/custom-field-display.component.html - 32 - - - src/app/components/common/input/drag-drop-select/drag-drop-select.component.html - 12 - - - src/app/components/common/input/tags/tags.component.html - 4 - - - src/app/components/common/permissions-select/permissions-select.component.html - 22 - - - src/app/components/document-history/document-history.component.html - 35 - - Read the documentation about this setting @@ -3045,6 +3018,29 @@ 62 + + + + src/app/components/common/custom-field-display/custom-field-display.component.html + 32 + + + src/app/components/common/input/drag-drop-select/drag-drop-select.component.html + 12 + + + src/app/components/common/input/tags/tags.component.html + 4 + + + src/app/components/common/permissions-select/permissions-select.component.html + 22 + + + src/app/components/document-history/document-history.component.html + 35 + + Search fields diff --git a/src-ui/src/app/components/admin/config/config.component.html b/src-ui/src/app/components/admin/config/config.component.html index 03ca04b7b..0f74339fb 100644 --- a/src-ui/src/app/components/admin/config/config.component.html +++ b/src-ui/src/app/components/admin/config/config.component.html @@ -11,7 +11,7 @@
+
+ + +
} @else { - + } } @case (ContentRenderType.Text) { From fa7a5451db669768f5bb9721d35747864186ec27 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:37:42 -0700 Subject: [PATCH 086/112] Fix: use local pdf worker js (#6990) --- src-ui/src/app/app.component.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index 7e8abdf34..c6de83cd1 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -35,6 +35,8 @@ export class AppComponent implements OnInit, OnDestroy { private permissionsService: PermissionsService, private hotKeyService: HotKeyService ) { + let anyWindow = window as any + anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js' this.settings.updateAppearanceSettings() } From 61485b0f1d86280ab3c72c5949a843295edab66b Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Wed, 12 Jun 2024 16:23:47 -0700 Subject: [PATCH 087/112] Fix: Document history could include extra fields (#6989) * Fixes creation of a custom field being included in a document's history even if not attached * Show custom field creation in UI --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- .../document-history.component.html | 50 ++++++++++--------- src/documents/tests/test_api_documents.py | 21 ++++++-- src/documents/views.py | 9 ++-- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/src-ui/src/app/components/document-history/document-history.component.html b/src-ui/src/app/components/document-history/document-history.component.html index ea4a3c9bb..c63a1223c 100644 --- a/src-ui/src/app/components/document-history/document-history.component.html +++ b/src-ui/src/app/components/document-history/document-history.component.html @@ -27,31 +27,33 @@ } {{ entry.action | titlecase }} - @if (entry.action === AuditLogAction.Update) { -
    - @for (change of entry.changes | keyvalue; track change.key) { - @if (change.value["type"] === 'm2m') { -
  • - {{ change.value["operation"] | titlecase }}  - {{ change.key | titlecase }}:  - {{ change.value["objects"].join(', ') }} -
  • - } - @else if (change.value["type"] === 'custom_field') { -
  • - {{ change.value["field"] }}:  - {{ change.value["value"] }} -
  • - } - @else { -
  • - {{ change.key | titlecase }}:  - {{ change.value[1] }} -
  • - } +
      + @for (change of entry.changes | keyvalue; track change.key) { + @if (change.value["type"] === 'm2m') { +
    • + {{ change.value["operation"] | titlecase }}  + {{ change.key | titlecase }}:  + {{ change.value["objects"].join(', ') }} +
    • } -
    - } + @else if (change.value["type"] === 'custom_field') { +
  • + {{ change.value["field"] }}:  + {{ change.value["value"] }} +
  • + } + @else { +
  • + {{ change.key | titlecase }}:  + @if (change.key === 'content') { + {{ change.value[1]?.substring(0,100) }}... + } @else { + {{ change.value[1] }} + } +
  • + } + } +
} } diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 7e69cb024..4ad6cb828 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -366,6 +366,16 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): data_type=CustomField.FieldDataType.STRING, ) self.client.force_login(user=self.user) + + # Initial response should include only document's creation + response = self.client.get(f"/api/documents/{doc.pk}/history/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + + self.assertIsNone(response.data[0]["actor"]) + self.assertEqual(response.data[0]["action"], "create") + self.client.patch( f"/api/documents/{doc.pk}/", data={ @@ -379,12 +389,15 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): format="json", ) + # Second response should include custom field addition response = self.client.get(f"/api/documents/{doc.pk}/history/") + self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data[1]["actor"]["id"], self.user.id) - self.assertEqual(response.data[1]["action"], "create") + self.assertEqual(len(response.data), 2) + self.assertEqual(response.data[0]["actor"]["id"], self.user.id) + self.assertEqual(response.data[0]["action"], "create") self.assertEqual( - response.data[1]["changes"], + response.data[0]["changes"], { "custom_fields": { "type": "custom_field", @@ -393,6 +406,8 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): }, }, ) + self.assertIsNone(response.data[1]["actor"]) + self.assertEqual(response.data[1]["action"], "create") @override_settings(AUDIT_LOG_ENABLED=False) def test_document_history_action_disabled(self): diff --git a/src/documents/views.py b/src/documents/views.py index 68addd0f4..9e8e7301d 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -19,7 +19,6 @@ from django.apps import apps from django.conf import settings from django.contrib.auth.models import Group from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType from django.db import connections from django.db.migrations.loader import MigrationLoader from django.db.migrations.recorder import MigrationRecorder @@ -106,7 +105,6 @@ from documents.matching import match_storage_paths from documents.matching import match_tags from documents.models import Correspondent from documents.models import CustomField -from documents.models import CustomFieldInstance from documents.models import Document from documents.models import DocumentType from documents.models import Note @@ -799,15 +797,14 @@ class DocumentViewSet( else None ), } - for entry in LogEntry.objects.filter(object_pk=doc.pk).select_related( + for entry in LogEntry.objects.get_for_object(doc).select_related( "actor", ) ] # custom fields - for entry in LogEntry.objects.filter( - object_pk__in=list(doc.custom_fields.values_list("id", flat=True)), - content_type=ContentType.objects.get_for_model(CustomFieldInstance), + for entry in LogEntry.objects.get_for_objects( + doc.custom_fields.all(), ).select_related("actor"): entries.append( { From 22a6360edf610b8fdf3672084f5dcb8ab46f750c Mon Sep 17 00:00:00 2001 From: "martin f. krafft" Date: Thu, 13 Jun 2024 16:46:18 +0200 Subject: [PATCH 088/112] Fix: default order of documents gets lost in QuerySet pipeline (#6982) * Send ordered document list to Django REST pagination Currently, when pages of documents are requested from the API, the webserver logs a warning: ``` gunicorn[1550]: /home/madduck/code/paperless-ngx/.direnv/python-3.11.2/lib/python3.11/site-packages/rest_framework/pagination.py:200: UnorderedObjectListWarning: Pagination may yield inconsistent results with an unordered object_list: QuerySet. ``` This can yield unexpected and problematic results, including duplicate and missing IDs in the enumeration, as demonstrated in https://github.com/paperless-ngx/paperless-ngx/discussions/6859 The patch is simple: turn the unordered Documents QuerySet into one that's ordered by reverse creation date, which is the default ordering for `Document`. Note that the default ordering for `Document` means that `QuerySet.ordered` is actually `True` following the call to `distinct()`, but after `annotate()`, the flag changes to `False`, unless `order_by()` is used explicitly, as per this patch. Closes: https://github.com/paperless-ngx/paperless-ngx/discussions/6859 Signed-off-by: martin f. krafft * Ensure order of documents in permissions test The patch for #6982 changes the ordering of documents returned by the API, which was previously implicit, and is now explicit. Therefore, this patch masssages the API result to ensure the previous order. Signed-off-by: martin f. krafft --------- Signed-off-by: martin f. krafft --- src/documents/tests/test_api_permissions.py | 28 +++++++++++++-------- src/documents/views.py | 1 + 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py index d7131b834..7708b8541 100644 --- a/src/documents/tests/test_api_permissions.py +++ b/src/documents/tests/test_api_permissions.py @@ -432,13 +432,18 @@ class TestApiAuth(DirectoriesMixin, APITestCase): resp_data = response.json() - self.assertNotIn("permissions", resp_data["results"][0]) - self.assertIn("user_can_change", resp_data["results"][0]) - self.assertTrue(resp_data["results"][0]["user_can_change"]) # doc1 - self.assertFalse(resp_data["results"][0]["is_shared_by_requester"]) # doc1 - self.assertFalse(resp_data["results"][1]["user_can_change"]) # doc2 - self.assertTrue(resp_data["results"][2]["user_can_change"]) # doc3 - self.assertTrue(resp_data["results"][3]["is_shared_by_requester"]) # doc4 + # The response will contain the documents in reversed order of creation + # due to #6982, but previously this code relied on implicit ordering + # so let's ensure the order is as expected: + results = resp_data["results"][::-1] + + self.assertNotIn("permissions", results[0]) + self.assertIn("user_can_change", results[0]) + self.assertTrue(results[0]["user_can_change"]) # doc1 + self.assertFalse(results[0]["is_shared_by_requester"]) # doc1 + self.assertFalse(results[1]["user_can_change"]) # doc2 + self.assertTrue(results[2]["user_can_change"]) # doc3 + self.assertTrue(results[3]["is_shared_by_requester"]) # doc4 response = self.client.get( "/api/documents/?full_perms=true", @@ -449,9 +454,12 @@ class TestApiAuth(DirectoriesMixin, APITestCase): resp_data = response.json() - self.assertIn("permissions", resp_data["results"][0]) - self.assertNotIn("user_can_change", resp_data["results"][0]) - self.assertNotIn("is_shared_by_requester", resp_data["results"][0]) + # See above about response ordering + results = resp_data["results"][::-1] + + self.assertIn("permissions", results[0]) + self.assertNotIn("user_can_change", results[0]) + self.assertNotIn("is_shared_by_requester", results[0]) class TestApiUser(DirectoriesMixin, APITestCase): diff --git a/src/documents/views.py b/src/documents/views.py index 9e8e7301d..72414d4f0 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -362,6 +362,7 @@ class DocumentViewSet( def get_queryset(self): return ( Document.objects.distinct() + .order_by("-created") .annotate(num_notes=Count("notes")) .select_related("correspondent", "storage_path", "document_type", "owner") .prefetch_related("tags", "custom_fields", "notes") From 28db7e84e695a23513655ba3a3bfa0a1f91b9b3d Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:53:34 -0700 Subject: [PATCH 089/112] Documentation: Corrections and clarifications for Python support (#6995) * Clarifies Python version support and a rough policy of what versions are supported --- CONTRIBUTING.md | 2 +- docs/development.md | 4 ---- docs/setup.md | 16 +++++++++++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4136b547e..a7375000b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ If you want to implement something big: ## Python -Paperless supports python 3.9 - 3.11. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/). +Paperless supports python 3.9 - 3.11 at this time. We format Python code with [ruff](https://docs.astral.sh/ruff/formatter/). ## Branches diff --git a/docs/development.md b/docs/development.md index 969c293b1..bc9ef4c2b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -81,10 +81,6 @@ first-time setup. !!! note Using a virtual environment is highly recommended. You can spawn one via `pipenv shell`. - Make sure you're using Python 3.10.x or lower. Otherwise you might - get issues with building dependencies. You can use - [pyenv](https://github.com/pyenv/pyenv) to install a specific - Python version. 5. Install pre-commit hooks: diff --git a/docs/setup.md b/docs/setup.md index 19811e7c6..b0a0a5fed 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -250,9 +250,14 @@ a minimal installation of Debian/Buster, which is the current stable release at the time of writing. Windows is not and will never be supported. +Paperless requires Python 3. At this time, 3.9 - 3.11 are tested versions. +Newer versions may work, but some dependencies may not fully support newer versions. +Support for older Python versions may be dropped as they reach end of life or as newer versions +are released, dependency support is confirmed, etc. + 1. Install dependencies. Paperless requires the following packages. - - `python3` - 3.9 - 3.11 are supported + - `python3` - `python3-pip` - `python3-dev` - `default-libmysqlclient-dev` for MariaDB @@ -410,8 +415,7 @@ supported. sudo chown paperless:paperless /opt/paperless/consume ``` -8. Install python requirements from the `requirements.txt` file. It is - up to you if you wish to use a virtual environment or not. First you should update your pip, so it gets the actual packages. +8. Install python requirements from the `requirements.txt` file. ```shell-session sudo -Hu paperless pip3 install -r requirements.txt @@ -420,6 +424,12 @@ supported. This will install all python dependencies in the home directory of the new paperless user. + !!! tip + + It is up to you if you wish to use a virtual environment or not for the Python + dependencies. This is an alternative to the above and may require adjusting + the example scripts to utilize the virtual environment paths + 9. Go to `/opt/paperless/src`, and execute the following commands: ```bash From 9d4e2d4652450dc2a06f3b96a46516c4ceeec14d Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 15 Jun 2024 09:00:18 -0700 Subject: [PATCH 090/112] Enhancement: better boolean custom field display (#7001) --- .../custom-field-display.component.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html index 07347f8e0..812c283e2 100644 --- a/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html +++ b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html @@ -24,6 +24,12 @@ } } + @case (CustomFieldDataType.Boolean) { +
+ {{field.name}}: + +
+ } @default { {{value}} } From a796e58a94abf1f619b8e04fc33e1464594c50b5 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Mon, 17 Jun 2024 08:07:08 -0700 Subject: [PATCH 091/112] Feature: documents trash aka soft delete (#6944) --- Pipfile | 1 + Pipfile.lock | 9 + docs/configuration.md | 24 +- docs/usage.md | 9 + paperless.conf.example | 2 +- src-ui/messages.xlf | 405 ++++++++++++------ src-ui/src/app/app-routing.module.ts | 9 + src-ui/src/app/app.module.ts | 2 + .../admin/settings/settings.component.html | 2 +- .../admin/trash/trash.component.html | 98 +++++ .../admin/trash/trash.component.scss | 0 .../admin/trash/trash.component.spec.ts | 163 +++++++ .../components/admin/trash/trash.component.ts | 137 ++++++ .../app-frame/app-frame.component.html | 7 + .../confirm-dialog.component.spec.ts | 10 - .../confirm-dialog.component.ts | 20 - .../document-detail.component.ts | 8 +- .../bulk-editor/bulk-editor.component.spec.ts | 9 +- .../bulk-editor/bulk-editor.component.ts | 31 +- src-ui/src/app/data/document.ts | 2 + src-ui/src/app/data/ui-settings.ts | 6 + src-ui/src/app/services/trash.service.spec.ts | 59 +++ src-ui/src/app/services/trash.service.ts | 37 ++ ...ocument_deleted_at_document_restored_at.py | 23 + src/documents/models.py | 4 +- src/documents/serialisers.py | 24 ++ src/documents/signals/handlers.py | 8 +- src/documents/tasks.py | 30 ++ src/documents/tests/test_api_trash.py | 155 +++++++ src/documents/tests/test_api_uisettings.py | 1 + src/documents/tests/test_document_model.py | 30 ++ src/documents/tests/test_file_handling.py | 25 +- src/documents/tests/test_tasks.py | 34 ++ src/documents/views.py | 42 ++ src/paperless/checks.py | 2 +- src/paperless/settings.py | 23 +- src/paperless/tests/test_settings.py | 17 + src/paperless/urls.py | 6 + 38 files changed, 1283 insertions(+), 191 deletions(-) create mode 100644 src-ui/src/app/components/admin/trash/trash.component.html create mode 100644 src-ui/src/app/components/admin/trash/trash.component.scss create mode 100644 src-ui/src/app/components/admin/trash/trash.component.spec.ts create mode 100644 src-ui/src/app/components/admin/trash/trash.component.ts create mode 100644 src-ui/src/app/services/trash.service.spec.ts create mode 100644 src-ui/src/app/services/trash.service.ts create mode 100644 src/documents/migrations/1049_document_deleted_at_document_restored_at.py create mode 100644 src/documents/tests/test_api_trash.py diff --git a/Pipfile b/Pipfile index da26987cf..948e22273 100644 --- a/Pipfile +++ b/Pipfile @@ -17,6 +17,7 @@ django-extensions = "*" django-filter = "~=24.2" django-guardian = "*" django-multiselectfield = "*" +django-soft-delete = "*" djangorestframework = "==3.14.0" djangorestframework-guardian = "*" drf-writable-nested = "*" diff --git a/Pipfile.lock b/Pipfile.lock index f3753a0cd..e011e1c60 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -540,6 +540,15 @@ "index": "pypi", "version": "==0.1.12" }, + "django-soft-delete": { + "hashes": [ + "sha256:443c00a54c06d236ff8806c3260243d775cc536581d7377c2785080b1041ce1d", + "sha256:7cb4524231763a70ad79cfccd49d001b7e5fa666ec897cc044d897dd73e0146e" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==1.0.13" + }, "djangorestframework": { "hashes": [ "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8", diff --git a/docs/configuration.md b/docs/configuration.md index f4c271ce1..0c3345145 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -219,10 +219,10 @@ database, classification model, etc). Defaults to "../data/", relative to the "src" directory. -#### [`PAPERLESS_TRASH_DIR=`](#PAPERLESS_TRASH_DIR) {#PAPERLESS_TRASH_DIR} +#### [`PAPERLESS_EMPTY_TRASH_DIR=`](#PAPERLESS_EMPTY_TRASH_DIR) {#PAPERLESS_EMPTY_TRASH_DIR} -: Instead of removing deleted documents, they are moved to this -directory. +: When documents are deleted (e.g. after emptying the trash) the original files will be moved here +instead of being removed from the filesystem. Only the original version is kept. This must be writeable by the user running paperless. When running inside docker, ensure that this path is within a permanent volume @@ -230,7 +230,9 @@ directory. Note that the directory must exist prior to using this setting. - Defaults to empty (i.e. really delete documents). + Defaults to empty (i.e. really delete files). + + This setting was previously named PAPERLESS_TRASH_DIR. #### [`PAPERLESS_MEDIA_ROOT=`](#PAPERLESS_MEDIA_ROOT) {#PAPERLESS_MEDIA_ROOT} @@ -1362,6 +1364,20 @@ processing. This only has an effect if Defaults to false. +## Trash + +#### [`EMPTY_TRASH_DELAY=`](#EMPTY_TRASH_DELAY) {#EMPTY_TRASH_DELAY} + +: Sets how long in days documents remain in the 'trash' before they are permanently deleted. + + Defaults to 30 days, minimum of 1 day. + +#### [`PAPERLESS_EMPTY_TRASH_TASK_CRON=`](#PAPERLESS_EMPTY_TRASH_TASK_CRON) {#PAPERLESS_EMPTY_TRASH_TASK_CRON} + +: Configures the schedule to empty the trash of expired deleted documents. + + Defaults to `0 1 * * *`, once per day. + ## Binaries There are a few external software packages that Paperless expects to diff --git a/docs/usage.md b/docs/usage.md index 5705be3da..034447d6e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -478,6 +478,15 @@ As of version 2.7, Paperless-ngx automatically records all changes to a document Changes to documents are visible under the "History" tab. Note that certain changes such as those made by workflows, record the 'actor' as "System". +## Document Trash + +When you first delete a document it is moved to the 'trash' until either it is explicitly deleted or it is automatically removed after a set amount of time has passed. +You can set how long documents remain in the trash before being automatically deleted with [`EMPTY_TRASH_DELAY`](configuration.md#EMPTY_TRASH_DELAY), which defaults +to 30 days. Until the file is actually deleted (e.g. the trash is emptied), all files and database content remains intact and can be restored at any point up until that time. + +Additionally you may configure a directory where deleted files are moved to when they the trash is emptied with [`PAPERLESS_EMPTY_TRASH_DIR`](configuration.md#PAPERLESS_EMPTY_TRASH_DIR). +Note that the empty trash directory only stores the original file, the archive file and all database information is permanently removed once a document is fully deleted. + ## Best practices {#basic-searching} Paperless offers a couple tools that help you organize your document diff --git a/paperless.conf.example b/paperless.conf.example index db557a7b6..63ee7be22 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -19,7 +19,7 @@ #PAPERLESS_CONSUMPTION_DIR=../consume #PAPERLESS_DATA_DIR=../data -#PAPERLESS_TRASH_DIR= +#PAPERLESS_EMPTY_TRASH_DIR= #PAPERLESS_MEDIA_ROOT=../media #PAPERLESS_STATICDIR=../static #PAPERLESS_FILENAME_FORMAT= diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 56cfa9ae4..492c160c9 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -240,18 +240,18 @@ Document was added to Paperless-ngx. src/app/app.component.ts - 83 + 85 src/app/app.component.ts - 92 + 94
Open document src/app/app.component.ts - 85 + 87 src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -274,21 +274,21 @@ Could not add : src/app/app.component.ts - 107 + 109 Document is being processed by Paperless-ngx. src/app/app.component.ts - 122 + 124 Dashboard src/app/app.component.ts - 129 + 131 src/app/components/app-frame/app-frame.component.html @@ -307,7 +307,7 @@ Documents src/app/app.component.ts - 140 + 142 src/app/components/app-frame/app-frame.component.html @@ -342,7 +342,7 @@ Settings src/app/app.component.ts - 152 + 154 src/app/components/admin/settings/settings.component.html @@ -369,14 +369,14 @@ Prev src/app/app.component.ts - 158 + 160 Next src/app/app.component.ts - 159 + 161 src/app/components/document-detail/document-detail.component.html @@ -387,56 +387,56 @@ End src/app/app.component.ts - 160 + 162 The dashboard can be used to show saved views, such as an 'Inbox'. Those settings are found under Settings > Saved Views once you have created some. src/app/app.component.ts - 166 + 168 Drag-and-drop documents here to start uploading or place them in the consume folder. You can also drag-and-drop documents anywhere on all other pages of the web app. Once you do, Paperless-ngx will start training its machine learning algorithms. src/app/app.component.ts - 173 + 175 The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar. src/app/app.component.ts - 178 + 180 The filtering tools allow you to quickly find documents using various searches, dates, tags, etc. src/app/app.component.ts - 185 + 187 Any combination of filters can be saved as a 'view' which can then be displayed on the dashboard and / or sidebar. src/app/app.component.ts - 191 + 193 Tags, correspondents, document types and storage paths can all be managed using these pages. They can also be created from the document edit view. src/app/app.component.ts - 196 + 198 Manage e-mail accounts and rules for automatically importing documents. src/app/app.component.ts - 204 + 206 src/app/components/manage/mail/mail.component.html @@ -447,14 +447,14 @@ Workflows give you more control over the document pipeline. src/app/app.component.ts - 212 + 214 File Tasks shows you documents that have been consumed, are waiting to be, or may have failed during the process. src/app/app.component.ts - 220 + 222 src/app/components/admin/tasks/tasks.component.html @@ -465,28 +465,28 @@ Check out the settings for various tweaks to the web app and toggle settings for saved views. src/app/app.component.ts - 228 + 230 Thank you! 🙏 src/app/app.component.ts - 236 + 238 There are <em>tons</em> more features and info we didn't cover here, but this should get you started. Check out the documentation or visit the project on GitHub to learn more or to report issues. src/app/app.component.ts - 238 + 240 Lastly, on behalf of every contributor to this community-supported project, thank you for using Paperless-ngx! src/app/app.component.ts - 240 + 242 @@ -684,6 +684,10 @@ src/app/components/admin/tasks/tasks.component.html 23 + + src/app/components/admin/trash/trash.component.html + 45 + src/app/components/admin/users-groups/users-groups.component.html 92 @@ -976,13 +980,6 @@ 195 - - Deleting documents will always ask for confirmation. - - src/app/components/admin/settings/settings.component.html - 195 - - Apply on close @@ -1363,6 +1360,10 @@ src/app/components/admin/tasks/tasks.component.html 42 + + src/app/components/admin/trash/trash.component.html + 37 + src/app/components/admin/users-groups/users-groups.component.html 23 @@ -1422,6 +1423,22 @@ src/app/components/admin/settings/settings.component.html 369 + + src/app/components/admin/trash/trash.component.html + 67 + + + src/app/components/admin/trash/trash.component.html + 76 + + + src/app/components/admin/trash/trash.component.ts + 57 + + + src/app/components/admin/trash/trash.component.ts + 80 + src/app/components/admin/users-groups/users-groups.component.html 38 @@ -1765,6 +1782,10 @@ src/app/components/admin/tasks/tasks.component.html 9 + + src/app/components/admin/trash/trash.component.html + 8 + src/app/components/manage/management-list/management-list.component.html 3 @@ -1788,6 +1809,10 @@ src/app/components/admin/tasks/tasks.component.html 36 + + src/app/components/admin/trash/trash.component.html + 35 + src/app/components/admin/users-groups/users-groups.component.html 21 @@ -2045,6 +2070,188 @@ 141 + + Trash + + src/app/components/admin/trash/trash.component.html + 2 + + + src/app/components/app-frame/app-frame.component.html + 271 + + + src/app/components/app-frame/app-frame.component.html + 274 + + + + Manage trashed documents that are pending deletion. + + src/app/components/admin/trash/trash.component.html + 4 + + + + Restore selected + + src/app/components/admin/trash/trash.component.html + 11 + + + + Delete selected + + src/app/components/admin/trash/trash.component.html + 14 + + + + Empty trash + + src/app/components/admin/trash/trash.component.html + 17 + + + + Remaining + + src/app/components/admin/trash/trash.component.html + 36 + + + + days + + src/app/components/admin/trash/trash.component.html + 58 + + + + Restore + + src/app/components/admin/trash/trash.component.html + 66 + + + src/app/components/admin/trash/trash.component.html + 73 + + + + {VAR_PLURAL, plural, =1 {One document in trash} other { total documents in trash}} + + src/app/components/admin/trash/trash.component.html + 89 + + + + Confirm delete + + src/app/components/admin/trash/trash.component.ts + 53 + + + src/app/components/admin/trash/trash.component.ts + 74 + + + src/app/components/manage/management-list/management-list.component.ts + 203 + + + src/app/components/manage/management-list/management-list.component.ts + 320 + + + + This operation will permanently delete this document. + + src/app/components/admin/trash/trash.component.ts + 54 + + + + This operation cannot be undone. + + src/app/components/admin/trash/trash.component.ts + 55 + + + src/app/components/admin/trash/trash.component.ts + 78 + + + src/app/components/admin/users-groups/users-groups.component.ts + 116 + + + src/app/components/admin/users-groups/users-groups.component.ts + 166 + + + src/app/components/manage/custom-fields/custom-fields.component.ts + 73 + + + src/app/components/manage/mail/mail.component.ts + 114 + + + src/app/components/manage/mail/mail.component.ts + 173 + + + src/app/components/manage/management-list/management-list.component.ts + 322 + + + src/app/components/manage/workflows/workflows.component.ts + 97 + + + + Document deleted + + src/app/components/admin/trash/trash.component.ts + 63 + + + + This operation will permanently delete the selected documents. + + src/app/components/admin/trash/trash.component.ts + 76 + + + + This operation will permanently delete all documents in the trash. + + src/app/components/admin/trash/trash.component.ts + 77 + + + + Document(s) deleted + + src/app/components/admin/trash/trash.component.ts + 87 + + + + Document restored + + src/app/components/admin/trash/trash.component.ts + 97 + + + + Document(s) restored + + src/app/components/admin/trash/trash.component.ts + 106 + + Users & Groups @@ -2247,41 +2454,6 @@ 115 - - This operation cannot be undone. - - src/app/components/admin/users-groups/users-groups.component.ts - 116 - - - src/app/components/admin/users-groups/users-groups.component.ts - 166 - - - src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 714 - - - src/app/components/manage/custom-fields/custom-fields.component.ts - 73 - - - src/app/components/manage/mail/mail.component.ts - 114 - - - src/app/components/manage/mail/mail.component.ts - 173 - - - src/app/components/manage/management-list/management-list.component.ts - 322 - - - src/app/components/manage/workflows/workflows.component.ts - 97 - - Proceed @@ -2310,15 +2482,15 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 755 + 758 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 788 + 791 src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 807 + 810 src/app/components/manage/custom-fields/custom-fields.component.ts @@ -2433,11 +2605,11 @@ src/app/components/app-frame/app-frame.component.html - 272 + 279 src/app/components/app-frame/app-frame.component.html - 275 + 282 @@ -2612,42 +2784,42 @@ GitHub src/app/components/app-frame/app-frame.component.html - 282 + 289 is available. src/app/components/app-frame/app-frame.component.html - 291,292 + 298,299 Click to view. src/app/components/app-frame/app-frame.component.html - 292 + 299 Paperless-ngx can automatically check for updates src/app/components/app-frame/app-frame.component.html - 296 + 303 How does this work? src/app/components/app-frame/app-frame.component.html - 303,305 + 310,312 Update available src/app/components/app-frame/app-frame.component.html - 316 + 323 @@ -2887,6 +3059,10 @@ src/app/components/common/permissions-dialog/permissions-dialog.component.html 26 + + src/app/components/document-detail/document-detail.component.ts + 776 + src/app/components/document-list/bulk-editor/bulk-editor.component.ts 401 @@ -2907,6 +3083,10 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts 579 + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 712 + Page @@ -5864,41 +6044,34 @@ 749 - - Confirm delete - - src/app/components/document-detail/document-detail.component.ts - 776 - - - src/app/components/manage/management-list/management-list.component.ts - 203 - - - src/app/components/manage/management-list/management-list.component.ts - 320 - - - - Do you really want to delete document ""? + + Do you really want to move the document "" to the trash? src/app/components/document-detail/document-detail.component.ts 777 - - The files for this document will be deleted permanently. This operation cannot be undone. + + Documents can be restored prior to permanent deletion. src/app/components/document-detail/document-detail.component.ts 778 + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 714 + - - Delete document + + Move to trash src/app/components/document-detail/document-detail.component.ts 780 + + src/app/components/document-list/bulk-editor/bulk-editor.component.ts + 716 + Error deleting document @@ -5915,7 +6088,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 751 + 754 @@ -5989,7 +6162,7 @@ src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 784 + 787 @@ -6364,74 +6537,60 @@ 571,575 - - Delete confirm - - src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 712 - - - - This operation will permanently delete selected document(s). + + Move selected document(s) to the trash? src/app/components/document-list/bulk-editor/bulk-editor.component.ts 713 - - Delete document(s) - - src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 716 - - This operation will permanently recreate the archive files for selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 752 + 755 The archive files will be re-generated with the current settings. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 753 + 756 This operation will permanently rotate the original version of document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 785 + 788 This will alter the original copy. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 786 + 789 Merge confirm src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 805 + 808 This operation will merge selected documents into a new document. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 806 + 809 Merged document will be queued for consumption. src/app/components/document-list/bulk-editor/bulk-editor.component.ts - 822 + 825 diff --git a/src-ui/src/app/app-routing.module.ts b/src-ui/src/app/app-routing.module.ts index 12b412f67..bbeba9e8a 100644 --- a/src-ui/src/app/app-routing.module.ts +++ b/src-ui/src/app/app-routing.module.ts @@ -26,6 +26,7 @@ import { MailComponent } from './components/manage/mail/mail.component' import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component' import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component' import { ConfigComponent } from './components/admin/config/config.component' +import { TrashComponent } from './components/admin/trash/trash.component' export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, @@ -144,6 +145,14 @@ export const routes: Routes = [ requireAdmin: true, }, }, + { + path: 'trash', + component: TrashComponent, + canActivate: [PermissionsGuard], + data: { + requireAdmin: true, + }, + }, // redirect old paths { path: 'settings/mail', diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index f9e04b069..2232dad19 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -125,6 +125,7 @@ import { CustomFieldDisplayComponent } from './components/common/custom-field-di import { GlobalSearchComponent } from './components/app-frame/global-search/global-search.component' import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component' import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component' +import { TrashComponent } from './components/admin/trash/trash.component' import { airplane, archive, @@ -497,6 +498,7 @@ function initializeApp(settings: SettingsService) { GlobalSearchComponent, HotkeyDialogComponent, DeletePagesConfirmDialogComponent, + TrashComponent, ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/admin/settings/settings.component.html b/src-ui/src/app/components/admin/settings/settings.component.html index bcab7de33..285599639 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.html +++ b/src-ui/src/app/components/admin/settings/settings.component.html @@ -192,7 +192,7 @@
- +
diff --git a/src-ui/src/app/components/admin/trash/trash.component.html b/src-ui/src/app/components/admin/trash/trash.component.html new file mode 100644 index 000000000..1c66bdd44 --- /dev/null +++ b/src-ui/src/app/components/admin/trash/trash.component.html @@ -0,0 +1,98 @@ + + + + + + + +
+ +
+ +
+ + + + + + + + + + + @if (isLoading) { + + + + } + @for (document of documentsInTrash; track document.id) { + + + + + + + } + +
+
+ + +
+
NameRemainingActions
+
+ Loading... +
+
+ + +
+
{{ document.title }}{{ getDaysRemaining(document) }} days +
+
+ +
+ + +
+
+
+
+ + +
+
+
+ +@if (!isLoading) { +
+
+ {totalDocuments, plural, =1 {One document in trash} other {{{totalDocuments || 0}} total documents in trash}} + @if (selectedDocuments.size > 0) { +  ({{selectedDocuments.size}} selected) + } +
+ @if (documentsInTrash.length > 20) { + + } +
+} diff --git a/src-ui/src/app/components/admin/trash/trash.component.scss b/src-ui/src/app/components/admin/trash/trash.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/admin/trash/trash.component.spec.ts b/src-ui/src/app/components/admin/trash/trash.component.spec.ts new file mode 100644 index 000000000..063d4bb8f --- /dev/null +++ b/src-ui/src/app/components/admin/trash/trash.component.spec.ts @@ -0,0 +1,163 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { TrashComponent } from './trash.component' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { PageHeaderComponent } from '../../common/page-header/page-header.component' +import { + NgbModal, + NgbPaginationModule, + NgbPopoverModule, +} from '@ng-bootstrap/ng-bootstrap' +import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { TrashService } from 'src/app/services/trash.service' +import { of } from 'rxjs' +import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' +import { By } from '@angular/platform-browser' + +const documentsInTrash = [ + { + id: 1, + name: 'test1', + created: new Date('2023-03-01T10:26:03.093116Z'), + deleted_at: new Date('2023-03-01T10:26:03.093116Z'), + }, + { + id: 2, + name: 'test2', + created: new Date('2023-03-01T10:26:03.093116Z'), + deleted_at: new Date('2023-03-01T10:26:03.093116Z'), + }, +] + +describe('TrashComponent', () => { + let component: TrashComponent + let fixture: ComponentFixture + let trashService: TrashService + let modalService: NgbModal + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + TrashComponent, + PageHeaderComponent, + ConfirmDialogComponent, + ], + imports: [ + HttpClientTestingModule, + FormsModule, + ReactiveFormsModule, + NgbPopoverModule, + NgbPaginationModule, + NgxBootstrapIconsModule.pick(allIcons), + ], + }).compileComponents() + + fixture = TestBed.createComponent(TrashComponent) + trashService = TestBed.inject(TrashService) + modalService = TestBed.inject(NgbModal) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should call correct service method on reload', () => { + const trashSpy = jest.spyOn(trashService, 'getTrash') + trashSpy.mockReturnValue( + of({ + count: 2, + all: documentsInTrash.map((d) => d.id), + results: documentsInTrash, + }) + ) + component.reload() + expect(trashSpy).toHaveBeenCalled() + expect(component.documentsInTrash).toEqual(documentsInTrash) + }) + + it('should support delete document', () => { + const trashSpy = jest.spyOn(trashService, 'emptyTrash') + let modal + modalService.activeInstances.subscribe((instances) => { + modal = instances[0] + }) + trashSpy.mockReturnValue(of('OK')) + component.delete(documentsInTrash[0]) + expect(modal).toBeDefined() + modal.componentInstance.confirmClicked.next() + expect(trashSpy).toHaveBeenCalled() + }) + + it('should support empty trash', () => { + const trashSpy = jest.spyOn(trashService, 'emptyTrash') + let modal + modalService.activeInstances.subscribe((instances) => { + modal = instances[instances.length - 1] + }) + trashSpy.mockReturnValue(of('OK')) + component.emptyTrash() + expect(modal).toBeDefined() + modal.componentInstance.confirmClicked.next() + expect(trashSpy).toHaveBeenCalled() + modal.close() + component.emptyTrash(new Set([1, 2])) + modal.componentInstance.confirmClicked.next() + expect(trashSpy).toHaveBeenCalledWith([1, 2]) + }) + + it('should support restore document', () => { + const restoreSpy = jest.spyOn(trashService, 'restoreDocuments') + const reloadSpy = jest.spyOn(component, 'reload') + restoreSpy.mockReturnValue(of('OK')) + component.restore(documentsInTrash[0]) + expect(restoreSpy).toHaveBeenCalledWith([documentsInTrash[0].id]) + expect(reloadSpy).toHaveBeenCalled() + }) + + it('should support restore all documents', () => { + const restoreSpy = jest.spyOn(trashService, 'restoreDocuments') + const reloadSpy = jest.spyOn(component, 'reload') + restoreSpy.mockReturnValue(of('OK')) + component.restoreAll() + expect(restoreSpy).toHaveBeenCalled() + expect(reloadSpy).toHaveBeenCalled() + component.restoreAll(new Set([1, 2])) + expect(restoreSpy).toHaveBeenCalledWith([1, 2]) + }) + + it('should support toggle all items in view', () => { + component.documentsInTrash = documentsInTrash + expect(component.selectedDocuments.size).toEqual(0) + const toggleAllSpy = jest.spyOn(component, 'toggleAll') + const checkButton = fixture.debugElement.queryAll( + By.css('input.form-check-input') + )[0] + checkButton.nativeElement.dispatchEvent(new Event('click')) + checkButton.nativeElement.checked = true + checkButton.nativeElement.dispatchEvent(new Event('click')) + expect(toggleAllSpy).toHaveBeenCalled() + expect(component.selectedDocuments.size).toEqual(documentsInTrash.length) + }) + + it('should support toggle item', () => { + component.selectedDocuments = new Set([1]) + component.toggleSelected(documentsInTrash[0]) + expect(component.selectedDocuments.size).toEqual(0) + component.toggleSelected(documentsInTrash[0]) + expect(component.selectedDocuments.size).toEqual(1) + }) + + it('should support clear selection', () => { + component.selectedDocuments = new Set([1]) + component.clearSelection() + expect(component.selectedDocuments.size).toEqual(0) + }) + + it('should correctly display days remaining', () => { + expect(component.getDaysRemaining(documentsInTrash[0])).toBeLessThan(0) + const tenDaysAgo = new Date() + tenDaysAgo.setDate(tenDaysAgo.getDate() - 10) + expect( + component.getDaysRemaining({ deleted_at: tenDaysAgo }) + ).toBeGreaterThan(0) // 10 days ago but depends on month + }) +}) diff --git a/src-ui/src/app/components/admin/trash/trash.component.ts b/src-ui/src/app/components/admin/trash/trash.component.ts new file mode 100644 index 000000000..b867f1706 --- /dev/null +++ b/src-ui/src/app/components/admin/trash/trash.component.ts @@ -0,0 +1,137 @@ +import { Component, OnDestroy } from '@angular/core' +import { NgbModal } from '@ng-bootstrap/ng-bootstrap' +import { Document } from 'src/app/data/document' +import { ToastService } from 'src/app/services/toast.service' +import { TrashService } from 'src/app/services/trash.service' +import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' +import { Subject, takeUntil } from 'rxjs' +import { SettingsService } from 'src/app/services/settings.service' +import { SETTINGS_KEYS } from 'src/app/data/ui-settings' + +@Component({ + selector: 'pngx-trash', + templateUrl: './trash.component.html', + styleUrl: './trash.component.scss', +}) +export class TrashComponent implements OnDestroy { + public documentsInTrash: Document[] = [] + public selectedDocuments: Set = new Set() + public allToggled: boolean = false + public page: number = 1 + public totalDocuments: number + public isLoading: boolean = false + unsubscribeNotifier: Subject = new Subject() + + constructor( + private trashService: TrashService, + private toastService: ToastService, + private modalService: NgbModal, + private settingsService: SettingsService + ) { + this.reload() + } + + ngOnDestroy() { + this.unsubscribeNotifier.next() + this.unsubscribeNotifier.complete() + } + + reload() { + this.isLoading = true + this.trashService.getTrash(this.page).subscribe((r) => { + this.documentsInTrash = r.results + this.totalDocuments = r.count + this.isLoading = false + this.selectedDocuments.clear() + }) + } + + delete(document: Document) { + let modal = this.modalService.open(ConfirmDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.title = $localize`Confirm delete` + modal.componentInstance.messageBold = $localize`This operation will permanently delete this document.` + modal.componentInstance.message = $localize`This operation cannot be undone.` + modal.componentInstance.btnClass = 'btn-danger' + modal.componentInstance.btnCaption = $localize`Delete` + modal.componentInstance.confirmClicked + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + modal.componentInstance.buttonsEnabled = false + this.trashService.emptyTrash([document.id]).subscribe(() => { + this.toastService.showInfo($localize`Document deleted`) + modal.close() + this.reload() + }) + }) + } + + emptyTrash(documents?: Set) { + let modal = this.modalService.open(ConfirmDialogComponent, { + backdrop: 'static', + }) + modal.componentInstance.title = $localize`Confirm delete` + modal.componentInstance.messageBold = documents + ? $localize`This operation will permanently delete the selected documents.` + : $localize`This operation will permanently delete all documents in the trash.` + modal.componentInstance.message = $localize`This operation cannot be undone.` + modal.componentInstance.btnClass = 'btn-danger' + modal.componentInstance.btnCaption = $localize`Delete` + modal.componentInstance.confirmClicked + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe(() => { + this.trashService + .emptyTrash(documents ? Array.from(documents) : null) + .subscribe(() => { + this.toastService.showInfo($localize`Document(s) deleted`) + this.allToggled = false + modal.close() + this.reload() + }) + }) + } + + restore(document: Document) { + this.trashService.restoreDocuments([document.id]).subscribe(() => { + this.toastService.showInfo($localize`Document restored`) + this.reload() + }) + } + + restoreAll(documents: Set = null) { + this.trashService + .restoreDocuments(documents ? Array.from(documents) : null) + .subscribe(() => { + this.toastService.showInfo($localize`Document(s) restored`) + this.allToggled = false + this.reload() + }) + } + + toggleAll(event: PointerEvent) { + if ((event.target as HTMLInputElement).checked) { + this.selectedDocuments = new Set(this.documentsInTrash.map((t) => t.id)) + } else { + this.clearSelection() + } + } + + toggleSelected(object: Document) { + this.selectedDocuments.has(object.id) + ? this.selectedDocuments.delete(object.id) + : this.selectedDocuments.add(object.id) + } + + clearSelection() { + this.allToggled = false + this.selectedDocuments.clear() + } + + getDaysRemaining(document: Document): number { + const delay = this.settingsService.get(SETTINGS_KEYS.EMPTY_TRASH_DELAY) + const diff = new Date().getTime() - new Date(document.deleted_at).getTime() + const days = Math.ceil(diff / (1000 * 3600 * 24)) + return delay - days + } +} diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index ab5759ec0..20c90b402 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -267,6 +267,13 @@ } +