From 7cc3c739942f8eaa6f6200f8588f2384a5afc523 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 14 Jan 2021 10:56:30 -0800 Subject: [PATCH 01/61] Convert selection functions on cards to toggling, let service handle setting selected Also because we need to capture mouseevents --- .../document-card-large.component.html | 4 ++-- .../document-card-large/document-card-large.component.ts | 9 ++------- .../document-card-small.component.html | 4 ++-- .../document-card-small/document-card-small.component.ts | 9 ++------- .../document-list/document-list.component.html | 6 +++--- .../components/document-list/document-list.component.ts | 4 ++++ src-ui/src/app/services/document-list-view.service.ts | 3 +++ 7 files changed, 18 insertions(+), 21 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index 6f2742a8d..b4783ff24 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -1,11 +1,11 @@
- +
- +
diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index e3bd4b7f7..f8bb9f518 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -15,16 +15,11 @@ export class DocumentCardLargeComponent implements OnInit { @Input() selected = false - setSelected(value: boolean) { - this.selected = value - this.selectedChange.emit(value) - } - @Output() - selectedChange = new EventEmitter() + toggleSelected = new EventEmitter() get selectable() { - return this.selectedChange.observers.length > 0 + return this.toggleSelected.observers.length > 0 } @Input() diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index c951bf281..192696a47 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,11 +1,11 @@
- +
- +
diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index ed69c5c50..5db0e30c0 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -14,14 +14,9 @@ export class DocumentCardSmallComponent implements OnInit { @Input() selected = false - - setSelected(value: boolean) { - this.selected = value - this.selectedChange.emit(value) - } - + @Output() - selectedChange = new EventEmitter() + toggleSelected = new EventEmitter() @Input() document: PaperlessDocument diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 612238215..86aacc031 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -90,7 +90,7 @@
- +
@@ -138,7 +138,7 @@
- +
@@ -170,5 +170,5 @@
- +
diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index fda99eb8d..1243e6e8a 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -160,6 +160,10 @@ export class DocumentListComponent implements OnInit { this.filterRulesModified = modified } + toggleSelected(document: PaperlessDocument, event: Event): void { + this.list.toggleSelected(document) + } + clickTag(tagID: number) { this.list.selectNone() setTimeout(() => { diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 1bbcca38e..a852eda55 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -255,6 +255,9 @@ export class DocumentListViewService { } else if (!value) { this.selected.delete(d.id) } + toggleSelected(d: PaperlessDocument): void { + if (this.selected.has(d.id)) this.selected.delete(d.id) + else this.selected.add(d.id) } constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) { From e6961d52875e14cf1231e32016f95ba8789af18b Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Thu, 14 Jan 2021 14:10:23 -0800 Subject: [PATCH 02/61] Allow range selection with shift --- .../document-list/document-list.component.ts | 4 +-- .../services/document-list-view.service.ts | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index 1243e6e8a..a3ec2d664 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -160,8 +160,8 @@ export class DocumentListComponent implements OnInit { this.filterRulesModified = modified } - toggleSelected(document: PaperlessDocument, event: Event): void { - this.list.toggleSelected(document) + toggleSelected(document: PaperlessDocument, event: MouseEvent): void { + this.list.toggleSelected(document, event.shiftKey) } clickTag(tagID: number) { diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index a852eda55..407c81572 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -27,6 +27,7 @@ export class DocumentListViewService { currentPage = 1 currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) collectionSize: number + lastSelectedDocumentIndex: number /** * This is the current config for the document list. The service will always remember the last settings used for the document list. @@ -108,6 +109,7 @@ export class DocumentListViewService { if (onFinish) { onFinish() } + this.lastSelectedDocumentIndex = null this.isReloading = false }, error => { @@ -218,6 +220,7 @@ export class DocumentListViewService { selectNone() { this.selected.clear() + this.lastSelectedDocumentIndex = null } reduceSelectionToFilter() { @@ -249,15 +252,25 @@ export class DocumentListViewService { return this.selected.has(d.id) } - setSelected(d: PaperlessDocument, value: boolean) { - if (value) { - this.selected.add(d.id) - } else if (!value) { - this.selected.delete(d.id) - } - toggleSelected(d: PaperlessDocument): void { - if (this.selected.has(d.id)) this.selected.delete(d.id) + toggleSelected(d: PaperlessDocument, includeRange: boolean): void { + if (this.selected.has(d.id) && !includeRange) this.selected.delete(d.id) else this.selected.add(d.id) + + if (includeRange && this.lastSelectedDocumentIndex !== null) { + const toIndex = this.documentIndexInCurrentView(d.id) + console.log('select from', this.lastSelectedDocumentIndex, 'to', toIndex); + this.documents.slice(Math.min(this.lastSelectedDocumentIndex, toIndex), Math.max(this.lastSelectedDocumentIndex, toIndex)).forEach(d => { + this.selected.add(d.id) + }) + } + + if (!includeRange || (includeRange && this.lastSelectedDocumentIndex == null)) { + this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) + } + } + + documentIndexInCurrentView(documentID: number): number { + return this.documents.map(d => d.id).indexOf(documentID) } constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) { From 4b42c97d0a1318ab349bc32545ec3bf16eb86fda Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 01:10:01 -0800 Subject: [PATCH 03/61] Allow select anywhere in tr or card --- .../document-card-large.component.html | 10 +++++----- .../document-card-small.component.html | 12 ++++++------ .../document-list/document-list.component.html | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index b4783ff24..c2ea9980b 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -1,11 +1,11 @@ -
+
- +
- +
@@ -17,11 +17,11 @@
- {{(document.correspondent$ | async)?.name}} + {{(document.correspondent$ | async)?.name}} {{(document.correspondent$ | async)?.name}}: {{document.title | documentTitle}} - +
#{{document.archive_serial_number}}
diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 192696a47..4f11b2a98 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,18 +1,18 @@
-
+
- +
- +
- +
+ {{moreTags}} @@ -23,7 +23,7 @@

- {{(document.correspondent$ | async)?.name}}: + {{(document.correspondent$ | async)?.name}}: {{document.title | documentTitle}} (#{{document.archive_serial_number}})

@@ -43,7 +43,7 @@ - + diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 86aacc031..18f39bfb5 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -135,7 +135,7 @@ i18n>Added -
+
@@ -147,7 +147,7 @@ - {{(d.correspondent$ | async)?.name}} + {{(d.correspondent$ | async)?.name}} @@ -156,7 +156,7 @@ - {{(d.document_type$ | async)?.name}} + {{(d.document_type$ | async)?.name}} From 6d786f89878f3d683778d71ce1a2e3582273f0b0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 01:11:06 -0800 Subject: [PATCH 04/61] remove log statement --- src-ui/src/app/services/document-list-view.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 407c81572..13c362918 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -258,7 +258,6 @@ export class DocumentListViewService { if (includeRange && this.lastSelectedDocumentIndex !== null) { const toIndex = this.documentIndexInCurrentView(d.id) - console.log('select from', this.lastSelectedDocumentIndex, 'to', toIndex); this.documents.slice(Math.min(this.lastSelectedDocumentIndex, toIndex), Math.max(this.lastSelectedDocumentIndex, toIndex)).forEach(d => { this.selected.add(d.id) }) From f94da1cf2730cb520770de650ae9c21eed6f5556 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 01:54:33 -0800 Subject: [PATCH 05/61] Allow reversing selection range --- .../services/document-list-view.service.ts | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 13c362918..bcfb5468f 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -28,6 +28,7 @@ export class DocumentListViewService { currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) collectionSize: number lastSelectedDocumentIndex: number + lastSelectedDocumentToIndex: number /** * This is the current config for the document list. The service will always remember the last settings used for the document list. @@ -109,7 +110,7 @@ export class DocumentListViewService { if (onFinish) { onFinish() } - this.lastSelectedDocumentIndex = null + this.lastSelectedDocumentIndex = this.lastSelectedDocumentToIndex = null this.isReloading = false }, error => { @@ -220,7 +221,7 @@ export class DocumentListViewService { selectNone() { this.selected.clear() - this.lastSelectedDocumentIndex = null + this.lastSelectedDocumentIndex = this.lastSelectedDocumentToIndex = null } reduceSelectionToFilter() { @@ -253,17 +254,30 @@ export class DocumentListViewService { } toggleSelected(d: PaperlessDocument, includeRange: boolean): void { - if (this.selected.has(d.id) && !includeRange) this.selected.delete(d.id) - else this.selected.add(d.id) + if (!includeRange) { + // regular i.e. no shift key toggle + if (this.selected.has(d.id)) this.selected.delete(d.id) + else this.selected.add(d.id) + } else if (includeRange && this.lastSelectedDocumentIndex !== null) { + const documentToIndex = this.documentIndexInCurrentView(d.id) + const fromIndex = Math.min(this.lastSelectedDocumentIndex, documentToIndex) + const toIndex = Math.max(this.lastSelectedDocumentIndex, documentToIndex) + + if ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex < this.lastSelectedDocumentIndex) || + (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex > this.lastSelectedDocumentIndex)) { + // invert last selected + this.documents.slice(Math.min(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex), Math.max(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex) + 1).forEach(d => { + this.selected.delete(d.id) + }) + } - if (includeRange && this.lastSelectedDocumentIndex !== null) { - const toIndex = this.documentIndexInCurrentView(d.id) - this.documents.slice(Math.min(this.lastSelectedDocumentIndex, toIndex), Math.max(this.lastSelectedDocumentIndex, toIndex)).forEach(d => { + this.documents.slice(fromIndex, toIndex + 1).forEach(d => { this.selected.add(d.id) }) + this.lastSelectedDocumentToIndex = documentToIndex } - if (!includeRange || (includeRange && this.lastSelectedDocumentIndex == null)) { + if (!includeRange || (includeRange && this.lastSelectedDocumentIndex == null)) { // e.g. shift key but first click this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) } } From 48220ceeb8cdba13c81f86f3556163a44c4c3642 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 01:59:29 -0800 Subject: [PATCH 06/61] Refactor selection functions to two separate ones for clarity --- .../document-list/document-list.component.ts | 3 ++- .../services/document-list-view.service.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index a3ec2d664..509c0a735 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -161,7 +161,8 @@ export class DocumentListComponent implements OnInit { } toggleSelected(document: PaperlessDocument, event: MouseEvent): void { - this.list.toggleSelected(document, event.shiftKey) + if (!event.shiftKey) this.list.toggleSelected(document) + else this.list.selectRangeTo(document) } clickTag(tagID: number) { diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index bcfb5468f..d2b9d75c2 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -253,19 +253,21 @@ export class DocumentListViewService { return this.selected.has(d.id) } - toggleSelected(d: PaperlessDocument, includeRange: boolean): void { - if (!includeRange) { - // regular i.e. no shift key toggle - if (this.selected.has(d.id)) this.selected.delete(d.id) - else this.selected.add(d.id) - } else if (includeRange && this.lastSelectedDocumentIndex !== null) { + toggleSelected(d: PaperlessDocument): void { + if (this.selected.has(d.id)) this.selected.delete(d.id) + else this.selected.add(d.id) + this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) + } + + selectRangeTo(d: PaperlessDocument) { + if (this.lastSelectedDocumentIndex !== null) { const documentToIndex = this.documentIndexInCurrentView(d.id) const fromIndex = Math.min(this.lastSelectedDocumentIndex, documentToIndex) const toIndex = Math.max(this.lastSelectedDocumentIndex, documentToIndex) if ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex < this.lastSelectedDocumentIndex) || (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex > this.lastSelectedDocumentIndex)) { - // invert last selected + // new click is "opposite side" of anchor so we invert the old selection this.documents.slice(Math.min(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex), Math.max(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex) + 1).forEach(d => { this.selected.delete(d.id) }) @@ -275,9 +277,7 @@ export class DocumentListViewService { this.selected.add(d.id) }) this.lastSelectedDocumentToIndex = documentToIndex - } - - if (!includeRange || (includeRange && this.lastSelectedDocumentIndex == null)) { // e.g. shift key but first click + } else { // e.g. shift key but was first click this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) } } From 86376c8c5f112ce7f014f4988e3a332c7a02f993 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 02:09:13 -0800 Subject: [PATCH 07/61] Fix repeat range selections were clashing --- .../app/services/document-list-view.service.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index d2b9d75c2..1ebf86d12 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -257,6 +257,7 @@ export class DocumentListViewService { if (this.selected.has(d.id)) this.selected.delete(d.id) else this.selected.add(d.id) this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) + this.lastSelectedDocumentToIndex = null } selectRangeTo(d: PaperlessDocument) { @@ -265,12 +266,15 @@ export class DocumentListViewService { const fromIndex = Math.min(this.lastSelectedDocumentIndex, documentToIndex) const toIndex = Math.max(this.lastSelectedDocumentIndex, documentToIndex) - if ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex < this.lastSelectedDocumentIndex) || - (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex > this.lastSelectedDocumentIndex)) { - // new click is "opposite side" of anchor so we invert the old selection - this.documents.slice(Math.min(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex), Math.max(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex) + 1).forEach(d => { - this.selected.delete(d.id) - }) + if (this.lastSelectedDocumentToIndex !== null && + ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex < this.lastSelectedDocumentIndex) || + (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex > this.lastSelectedDocumentIndex))) { + console.log('invert'); + + // new click is "opposite side" of anchor so we invert the old selection + this.documents.slice(Math.min(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex), Math.max(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex) + 1).forEach(d => { + this.selected.delete(d.id) + }) } this.documents.slice(fromIndex, toIndex + 1).forEach(d => { From 8d606b9f34ab13e38b3188e831c84c880c53fba7 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 02:13:21 -0800 Subject: [PATCH 08/61] Handle shift-clicking orignal item --- src-ui/src/app/services/document-list-view.service.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 1ebf86d12..c7ddbd44e 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -267,9 +267,8 @@ export class DocumentListViewService { const toIndex = Math.max(this.lastSelectedDocumentIndex, documentToIndex) if (this.lastSelectedDocumentToIndex !== null && - ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex < this.lastSelectedDocumentIndex) || - (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex > this.lastSelectedDocumentIndex))) { - console.log('invert'); + ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex <= this.lastSelectedDocumentIndex) || + (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex >= this.lastSelectedDocumentIndex))) { // new click is "opposite side" of anchor so we invert the old selection this.documents.slice(Math.min(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex), Math.max(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex) + 1).forEach(d => { @@ -282,7 +281,7 @@ export class DocumentListViewService { }) this.lastSelectedDocumentToIndex = documentToIndex } else { // e.g. shift key but was first click - this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) + this.toggleSelected(d) } } From 01cd4c7546dd645eda6b3090109a264580fcdde1 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 02:15:26 -0800 Subject: [PATCH 09/61] Refactor variable names for clarity --- .../services/document-list-view.service.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index c7ddbd44e..35b6296cc 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -27,8 +27,8 @@ export class DocumentListViewService { currentPage = 1 currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) collectionSize: number - lastSelectedDocumentIndex: number - lastSelectedDocumentToIndex: number + rangeSelectionAnchorIndex: number + lastRangeSelectionToIndex: number /** * This is the current config for the document list. The service will always remember the last settings used for the document list. @@ -110,7 +110,7 @@ export class DocumentListViewService { if (onFinish) { onFinish() } - this.lastSelectedDocumentIndex = this.lastSelectedDocumentToIndex = null + this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null this.isReloading = false }, error => { @@ -221,7 +221,7 @@ export class DocumentListViewService { selectNone() { this.selected.clear() - this.lastSelectedDocumentIndex = this.lastSelectedDocumentToIndex = null + this.rangeSelectionAnchorIndex = this.lastRangeSelectionToIndex = null } reduceSelectionToFilter() { @@ -256,22 +256,22 @@ export class DocumentListViewService { toggleSelected(d: PaperlessDocument): void { if (this.selected.has(d.id)) this.selected.delete(d.id) else this.selected.add(d.id) - this.lastSelectedDocumentIndex = this.documentIndexInCurrentView(d.id) - this.lastSelectedDocumentToIndex = null + this.rangeSelectionAnchorIndex = this.documentIndexInCurrentView(d.id) + this.lastRangeSelectionToIndex = null } selectRangeTo(d: PaperlessDocument) { - if (this.lastSelectedDocumentIndex !== null) { + if (this.rangeSelectionAnchorIndex !== null) { const documentToIndex = this.documentIndexInCurrentView(d.id) - const fromIndex = Math.min(this.lastSelectedDocumentIndex, documentToIndex) - const toIndex = Math.max(this.lastSelectedDocumentIndex, documentToIndex) + const fromIndex = Math.min(this.rangeSelectionAnchorIndex, documentToIndex) + const toIndex = Math.max(this.rangeSelectionAnchorIndex, documentToIndex) - if (this.lastSelectedDocumentToIndex !== null && - ((this.lastSelectedDocumentToIndex > this.lastSelectedDocumentIndex && documentToIndex <= this.lastSelectedDocumentIndex) || - (this.lastSelectedDocumentToIndex < this.lastSelectedDocumentIndex && documentToIndex >= this.lastSelectedDocumentIndex))) { + if (this.lastRangeSelectionToIndex !== null && + ((this.lastRangeSelectionToIndex > this.rangeSelectionAnchorIndex && documentToIndex <= this.rangeSelectionAnchorIndex) || + (this.lastRangeSelectionToIndex < this.rangeSelectionAnchorIndex && documentToIndex >= this.rangeSelectionAnchorIndex))) { // new click is "opposite side" of anchor so we invert the old selection - this.documents.slice(Math.min(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex), Math.max(this.lastSelectedDocumentIndex, this.lastSelectedDocumentToIndex) + 1).forEach(d => { + this.documents.slice(Math.min(this.rangeSelectionAnchorIndex, this.lastRangeSelectionToIndex), Math.max(this.rangeSelectionAnchorIndex, this.lastRangeSelectionToIndex) + 1).forEach(d => { this.selected.delete(d.id) }) } @@ -279,7 +279,7 @@ export class DocumentListViewService { this.documents.slice(fromIndex, toIndex + 1).forEach(d => { this.selected.add(d.id) }) - this.lastSelectedDocumentToIndex = documentToIndex + this.lastRangeSelectionToIndex = documentToIndex } else { // e.g. shift key but was first click this.toggleSelected(d) } From 59008ea765cbe8c69425a2add311a8a68f78b9a0 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 02:22:10 -0800 Subject: [PATCH 10/61] remove new line =/ --- src-ui/src/app/services/document-list-view.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 35b6296cc..1fc5e09c5 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -269,7 +269,6 @@ export class DocumentListViewService { if (this.lastRangeSelectionToIndex !== null && ((this.lastRangeSelectionToIndex > this.rangeSelectionAnchorIndex && documentToIndex <= this.rangeSelectionAnchorIndex) || (this.lastRangeSelectionToIndex < this.rangeSelectionAnchorIndex && documentToIndex >= this.rangeSelectionAnchorIndex))) { - // new click is "opposite side" of anchor so we invert the old selection this.documents.slice(Math.min(this.rangeSelectionAnchorIndex, this.lastRangeSelectionToIndex), Math.max(this.rangeSelectionAnchorIndex, this.lastRangeSelectionToIndex) + 1).forEach(d => { this.selected.delete(d.id) From 09262666634e92193ff84626cd79d7f1f59d57dc Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Fri, 15 Jan 2021 12:50:34 +0100 Subject: [PATCH 11/61] add a language switcher fixes #352 --- src-ui/messages.xlf | 87 +++++++++++++++---- .../manage/settings/settings.component.html | 15 ++++ .../manage/settings/settings.component.ts | 8 +- src-ui/src/app/services/settings.service.ts | 36 +++++++- 4 files changed, 125 insertions(+), 21 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 9b6324a76..a1f0e8b88 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -475,21 +475,21 @@ Saved view "" deleted. src/app/components/manage/settings/settings.component.ts - 55 + 56 Settings saved successfully. src/app/components/manage/settings/settings.component.ts - 68 + 70 Error while storing settings on server: src/app/components/manage/settings/settings.component.ts - 80 + 86 @@ -510,7 +510,7 @@ Saved views src/app/components/manage/settings/settings.component.html - 64 + 79 @@ -520,109 +520,123 @@ 13 + + Display language + + src/app/components/manage/settings/settings.component.html + 17 + + + + You need to reload the page after applying a new language. + + src/app/components/manage/settings/settings.component.html + 25 + + Items per page src/app/components/manage/settings/settings.component.html - 17 + 32 Document editor src/app/components/manage/settings/settings.component.html - 33 + 48 Use PDF viewer provided by the browser src/app/components/manage/settings/settings.component.html - 37 + 52 This is usually faster for displaying large PDF documents, but it might not work on some browsers. src/app/components/manage/settings/settings.component.html - 37 + 52 Dark mode src/app/components/manage/settings/settings.component.html - 44 + 59 Use system settings src/app/components/manage/settings/settings.component.html - 47 + 62 Enable dark mode src/app/components/manage/settings/settings.component.html - 48 + 63 Bulk editing src/app/components/manage/settings/settings.component.html - 52 + 67 Show confirmation dialogs src/app/components/manage/settings/settings.component.html - 56 + 71 Deleting documents will always ask for confirmation. src/app/components/manage/settings/settings.component.html - 56 + 71 Apply on close src/app/components/manage/settings/settings.component.html - 57 + 72 Appears on src/app/components/manage/settings/settings.component.html - 76 + 91 Show on dashboard src/app/components/manage/settings/settings.component.html - 79 + 94 Show in sidebar src/app/components/manage/settings/settings.component.html - 83 + 98 No saved views defined. src/app/components/manage/settings/settings.component.html - 93 + 108 @@ -1400,6 +1414,41 @@ 12 + + Use system language + + src/app/services/settings.service.ts + 64 + + + + English + + src/app/services/settings.service.ts + 65 + + + + German + + src/app/services/settings.service.ts + 66 + + + + Dutch + + src/app/services/settings.service.ts + 67 + + + + French + + src/app/services/settings.service.ts + 68 + + Error diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html index cc4de9bf0..e7fac646f 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.html +++ b/src-ui/src/app/components/manage/settings/settings.component.html @@ -12,6 +12,21 @@

Appearance

+
+
+ Display language +
+
+ + + + You need to reload the page after applying a new language. + +
+
+
Items per page diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 0e364e6c5..f0d83a15f 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -22,7 +22,8 @@ export class SettingsComponent implements OnInit { 'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)), 'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)), 'useNativePdfViewer': new FormControl(this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)), - 'savedViews': this.savedViewGroup + 'savedViews': this.savedViewGroup, + 'displayLanguage': new FormControl(this.settings.getLanguage()) }) savedViews: PaperlessSavedView[] @@ -63,11 +64,16 @@ export class SettingsComponent implements OnInit { this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem) this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString()) this.settings.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, this.settingsForm.value.useNativePdfViewer) + this.settings.setLanguage(this.settingsForm.value.displayLanguage) this.documentListViewService.updatePageSize() this.settings.updateDarkModeSettings() this.toastService.showInfo($localize`Settings saved successfully.`) } + get languages() { + return this.settings.getLanguageOptions() + } + saveSettings() { let x = [] for (let id in this.savedViewGroup.value) { diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index eb533e118..50adcf5a7 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -1,5 +1,7 @@ import { DOCUMENT } from '@angular/common'; import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; +import { Meta } from '@angular/platform-browser'; +import { CookieService } from 'ngx-cookie-service'; export interface PaperlessSettings { key: string @@ -34,7 +36,9 @@ export class SettingsService { constructor( private rendererFactory: RendererFactory2, - @Inject(DOCUMENT) private document + @Inject(DOCUMENT) private document, + private cookieService: CookieService, + private meta: Meta ) { this.renderer = rendererFactory.createRenderer(null, null); @@ -55,6 +59,36 @@ export class SettingsService { } + getLanguageOptions() { + return [ + {code: "", name: $localize`Use system language`}, + {code: "en-us", name: `${$localize`English`} (English)`}, + {code: "de", name: `${$localize`German`} (German)`}, + {code: "nl", name: `${$localize`Dutch`} (Dutch)`}, + {code: "fr", name: `${$localize`French`} (French)`} + ] + } + + private getLanguageCookieName() { + let prefix = "" + if (this.meta.getTag('name=cookie_prefix')) { + prefix = this.meta.getTag('name=cookie_prefix').content + } + return `${prefix || ''}django_language` + } + + getLanguage(): string { + return this.cookieService.get(this.getLanguageCookieName()) + } + + setLanguage(language: string) { + if (language) { + this.cookieService.set(this.getLanguageCookieName(), language) + } else { + this.cookieService.delete(this.getLanguageCookieName()) + } + } + get(key: string): any { let setting = SETTINGS.find(s => s.key == key) From cc06d528cb4c360f33882425e0fd17ae842f58d1 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 15 Jan 2021 11:52:06 +0000 Subject: [PATCH 12/61] Translate /src-ui/messages.xlf in de translation completed for the source file '/src-ui/messages.xlf' on the 'de' language. --- src-ui/src/locale/messages.de.xlf | 94 ++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 19 deletions(-) diff --git a/src-ui/src/locale/messages.de.xlf b/src-ui/src/locale/messages.de.xlf index 077f7c92e..030cfb28c 100644 --- a/src-ui/src/locale/messages.de.xlf +++ b/src-ui/src/locale/messages.de.xlf @@ -542,7 +542,7 @@ Gespeicherte Ansicht "" gelöscht. src/app/components/manage/settings/settings.component.ts - 55 + 56 @@ -550,7 +550,7 @@ Einstellungen erfolgreich gespeichert. src/app/components/manage/settings/settings.component.ts - 68 + 70 @@ -558,7 +558,7 @@ Fehler beim Speichern der Einstellungen auf dem Server: src/app/components/manage/settings/settings.component.ts - 80 + 86 @@ -582,7 +582,7 @@ Gespeicherte Ansichten src/app/components/manage/settings/settings.component.html - 64 + 79 @@ -593,12 +593,28 @@ 13 + + Display language + Anzeigesprache + + src/app/components/manage/settings/settings.component.html + 17 + + + + You need to reload the page after applying a new language. + Nachdem Sie eine neue Sprache ausgewählt haben, müssen Sie die Seite neu laden. + + src/app/components/manage/settings/settings.component.html + 25 + + Items per page Dokumente pro Seite src/app/components/manage/settings/settings.component.html - 17 + 32 @@ -606,7 +622,7 @@ Dokumenteditor src/app/components/manage/settings/settings.component.html - 33 + 48 @@ -614,7 +630,7 @@ Benutze PDF-Betrachter des Web Browsers src/app/components/manage/settings/settings.component.html - 37 + 52 @@ -622,7 +638,7 @@ Der integrierte PDF-Betrachter des Web-Browsers ist in der Regel schneller bei der Anzeige besonders großer Dokumente, funktioniert aber nicht in allen Browsern. src/app/components/manage/settings/settings.component.html - 37 + 52 @@ -630,7 +646,7 @@ Dunkler Modus src/app/components/manage/settings/settings.component.html - 44 + 59 @@ -638,7 +654,7 @@ Benutze Systemeinstellungen src/app/components/manage/settings/settings.component.html - 47 + 62 @@ -646,7 +662,7 @@ Dunklen Modus aktivieren src/app/components/manage/settings/settings.component.html - 48 + 63 @@ -654,7 +670,7 @@ Massenbearbeitung src/app/components/manage/settings/settings.component.html - 52 + 67 @@ -662,7 +678,7 @@ Bestätigungsdialoge anzeigen src/app/components/manage/settings/settings.component.html - 56 + 71 @@ -670,7 +686,7 @@ Beim Löschen von Dokumenten wird immer nach einer Bestätigung gefragt. src/app/components/manage/settings/settings.component.html - 56 + 71 @@ -678,7 +694,7 @@ Anwenden beim Schließen src/app/components/manage/settings/settings.component.html - 57 + 72 @@ -686,7 +702,7 @@ Erscheint auf src/app/components/manage/settings/settings.component.html - 76 + 91 @@ -694,7 +710,7 @@ Auf Startseite zeigen src/app/components/manage/settings/settings.component.html - 79 + 94 @@ -702,7 +718,7 @@ In Seitenleiste zeigen src/app/components/manage/settings/settings.component.html - 83 + 98 @@ -710,7 +726,7 @@ Keine gespeicherten Ansichten vorhanden. src/app/components/manage/settings/settings.component.html - 93 + 108 @@ -1598,6 +1614,46 @@ 12 + + Use system language + Benutze Systemsprache + + src/app/services/settings.service.ts + 64 + + + + English + Englisch + + src/app/services/settings.service.ts + 65 + + + + German + Deutsch + + src/app/services/settings.service.ts + 66 + + + + Dutch + Niederländisch + + src/app/services/settings.service.ts + 67 + + + + French + Französisch + + src/app/services/settings.service.ts + 68 + + Error Fehler From a3a9949ebc887b08a688c0a6e03103f19a6fcf3a Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 15 Jan 2021 12:42:26 +0000 Subject: [PATCH 13/61] Translate /src-ui/messages.xlf in fr translation completed for the source file '/src-ui/messages.xlf' on the 'fr' language. --- src-ui/src/locale/messages.fr.xlf | 186 +++++++++++++++++++++--------- 1 file changed, 133 insertions(+), 53 deletions(-) diff --git a/src-ui/src/locale/messages.fr.xlf b/src-ui/src/locale/messages.fr.xlf index 84c8295af..06ca713a4 100644 --- a/src-ui/src/locale/messages.fr.xlf +++ b/src-ui/src/locale/messages.fr.xlf @@ -38,7 +38,7 @@ Sélectionner aucun src/app/components/document-list/document-list.component.html - 11 + 10 @@ -46,7 +46,7 @@ Sélectionner la page src/app/components/document-list/document-list.component.html - 12 + 11 @@ -54,15 +54,15 @@ Sélectionner tout src/app/components/document-list/document-list.component.html - 13 + 12 - - Sort by - Trier par + + Sort + Trier src/app/components/document-list/document-list.component.html - 41 + 39 @@ -94,7 +94,7 @@ {VAR_PLURAL, plural, =1 { document sélectionné sur 1} other { documents sélectionnés sur }} src/app/components/document-list/document-list.component.html - 87 + 85 @@ -102,7 +102,7 @@ {VAR_PLURAL, plural, =1 {Un document} other { documents}} src/app/components/document-list/document-list.component.html - 88 + 86 @@ -110,7 +110,7 @@ (filtré) src/app/components/document-list/document-list.component.html - 88 + 86 @@ -118,7 +118,7 @@ NSA src/app/components/document-list/document-list.component.html - 107 + 105 @@ -126,7 +126,7 @@ Correspondant src/app/components/document-list/document-list.component.html - 113 + 111 @@ -134,7 +134,7 @@ Titre src/app/components/document-list/document-list.component.html - 119 + 117 @@ -142,7 +142,7 @@ Type de document src/app/components/document-list/document-list.component.html - 125 + 123 @@ -150,7 +150,7 @@ Date de création src/app/components/document-list/document-list.component.html - 131 + 129 @@ -158,7 +158,7 @@ Date d'ajout src/app/components/document-list/document-list.component.html - 137 + 135 @@ -166,7 +166,7 @@ Confirmer la suppression src/app/components/document-detail/document-detail.component.ts - 186 + 192 @@ -174,7 +174,7 @@ Voulez-vous vraiment supprimer le document "" ? src/app/components/document-detail/document-detail.component.ts - 187 + 193 @@ -182,7 +182,7 @@ Les fichiers liés à ce document seront supprimés définitivement. Cette action est irréversible. src/app/components/document-detail/document-detail.component.ts - 188 + 194 @@ -190,7 +190,7 @@ Supprimer le document src/app/components/document-detail/document-detail.component.ts - 190 + 196 @@ -198,7 +198,7 @@ Une erreur s'est produite lors de la suppression du document : src/app/components/document-detail/document-detail.component.ts - 197 + 203 @@ -542,7 +542,7 @@ Vue "" supprimée. src/app/components/manage/settings/settings.component.ts - 54 + 56 @@ -550,7 +550,7 @@ Paramètres enregistrés avec succès. src/app/components/manage/settings/settings.component.ts - 74 + 70 @@ -582,7 +582,7 @@ Vues enregistrées src/app/components/manage/settings/settings.component.html - 56 + 79 @@ -593,12 +593,52 @@ 13 + + Display language + Langue d'affichage + + src/app/components/manage/settings/settings.component.html + 17 + + + + You need to reload the page after applying a new language. + Vous devez recharger la page après avoir sélectionné une nouvelle langue. + + src/app/components/manage/settings/settings.component.html + 25 + + Items per page Éléments par page src/app/components/manage/settings/settings.component.html - 17 + 32 + + + + Document editor + Éditeur de documents + + src/app/components/manage/settings/settings.component.html + 48 + + + + Use PDF viewer provided by the browser + Utiliser la visionneuse PDF fournie par le navigateur + + src/app/components/manage/settings/settings.component.html + 52 + + + + This is usually faster for displaying large PDF documents, but it might not work on some browsers. + Cette option est généralement plus rapide pour l'affichage de grands documents PDF, mais elle peut ne pas fonctionner sur certains navigateurs. + + src/app/components/manage/settings/settings.component.html + 52 @@ -606,7 +646,7 @@ Mode sombre src/app/components/manage/settings/settings.component.html - 33 + 59 @@ -614,7 +654,15 @@ Utiliser les paramètres du système src/app/components/manage/settings/settings.component.html - 36 + 62 + + + + Enable dark mode + Activer le mode sombre + + src/app/components/manage/settings/settings.component.html + 63 @@ -622,7 +670,7 @@ Edition en masse src/app/components/manage/settings/settings.component.html - 44 + 67 @@ -630,7 +678,7 @@ Afficher les messages de confirmation src/app/components/manage/settings/settings.component.html - 48 + 71 @@ -638,7 +686,7 @@ La suppression de documents requiert toujours une confirmation. src/app/components/manage/settings/settings.component.html - 48 + 71 @@ -646,15 +694,7 @@ Appliquer lors de la fermeture src/app/components/manage/settings/settings.component.html - 49 - - - - Enable dark mode - Activer le mode sombre - - src/app/components/manage/settings/settings.component.html - 39 + 72 @@ -662,7 +702,7 @@ Apparaît sur src/app/components/manage/settings/settings.component.html - 68 + 91 @@ -670,7 +710,7 @@ Montrer sur le tableau de bord src/app/components/manage/settings/settings.component.html - 71 + 94 @@ -678,7 +718,7 @@ Montrer dans la barre latérale src/app/components/manage/settings/settings.component.html - 75 + 98 @@ -686,7 +726,7 @@ Aucune vue sauvegardée n'est définie. src/app/components/manage/settings/settings.component.html - 85 + 108 @@ -1039,7 +1079,7 @@ Filtrer les correspondants src/app/components/document-list/filter-editor/filter-editor.component.html - 19 + 20 @@ -1047,7 +1087,7 @@ Filtrer les types de documents src/app/components/document-list/filter-editor/filter-editor.component.html - 25 + 27 @@ -1055,7 +1095,7 @@ Réinitialiser les filtres src/app/components/document-list/filter-editor/filter-editor.component.html - 47 + 50 @@ -1063,7 +1103,7 @@ Non affecté src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts - 161 + 166 Filter drop down element to filter for documents with no correspondent/type/tag assigned @@ -1072,7 +1112,7 @@ Appliquer src/app/components/common/filterable-dropdown/filterable-dropdown.component.html - 28 + 26 @@ -1144,7 +1184,7 @@ Créé le : src/app/components/document-list/document-card-large/document-card-large.component.html - 65 + 67 @@ -1168,7 +1208,7 @@ Score : src/app/components/document-list/document-card-large/document-card-large.component.html - 61 + 62 @@ -1355,7 +1395,7 @@ Sélectionner : src/app/components/document-list/bulk-editor/bulk-editor.component.html - 11 + 10 @@ -1363,7 +1403,7 @@ Tout src/app/components/document-list/bulk-editor/bulk-editor.component.html - 21 + 20 @@ -1371,7 +1411,7 @@ Éditer : src/app/components/document-list/bulk-editor/bulk-editor.component.html - 28 + 27 @@ -1574,6 +1614,46 @@ 12 + + Use system language + Utiliser la langue du système + + src/app/services/settings.service.ts + 64 + + + + English + Anglais + + src/app/services/settings.service.ts + 65 + + + + German + Allemand + + src/app/services/settings.service.ts + 66 + + + + Dutch + Néerlandais + + src/app/services/settings.service.ts + 67 + + + + French + Français + + src/app/services/settings.service.ts + 68 + + Error Erreur From ef924e896b8b7740d166b4d0d27525a74e93b664 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 07:57:01 -0800 Subject: [PATCH 14/61] Fix reverting old selection in all cases by always doing it --- src-ui/src/app/services/document-list-view.service.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 1fc5e09c5..4eb0c276c 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -266,10 +266,8 @@ export class DocumentListViewService { const fromIndex = Math.min(this.rangeSelectionAnchorIndex, documentToIndex) const toIndex = Math.max(this.rangeSelectionAnchorIndex, documentToIndex) - if (this.lastRangeSelectionToIndex !== null && - ((this.lastRangeSelectionToIndex > this.rangeSelectionAnchorIndex && documentToIndex <= this.rangeSelectionAnchorIndex) || - (this.lastRangeSelectionToIndex < this.rangeSelectionAnchorIndex && documentToIndex >= this.rangeSelectionAnchorIndex))) { - // new click is "opposite side" of anchor so we invert the old selection + if (this.lastRangeSelectionToIndex !== null) { + // revert the old selection this.documents.slice(Math.min(this.rangeSelectionAnchorIndex, this.lastRangeSelectionToIndex), Math.max(this.rangeSelectionAnchorIndex, this.lastRangeSelectionToIndex) + 1).forEach(d => { this.selected.delete(d.id) }) From c31b6e63f5a3708bfc784f5217f3387feff48265 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Fri, 15 Jan 2021 16:13:22 -0800 Subject: [PATCH 15/61] Prevent text selection on table rows --- .../app/components/document-list/document-list.component.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index f90a94be4..28f765e29 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -1,5 +1,9 @@ @import "/src/theme"; +tr { + user-select: none; +} + .table-row-selected { background-color: $primaryFaded; } From 98f3e99e2e11a62dc04a115f80ea7a071a0f99f2 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 16 Jan 2021 14:09:23 -0800 Subject: [PATCH 16/61] Prevent close button 'stealing' focus from modal input fields --- .../common/edit-dialog/edit-dialog.component.ts | 7 +++++++ .../save-view-config-dialog.component.html | 2 +- .../save-view-config-dialog.component.ts | 8 +++++++- .../correspondent-edit-dialog.component.html | 3 +-- .../document-type-edit-dialog.component.html | 2 +- .../tag-edit-dialog/tag-edit-dialog.component.html | 2 +- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts index 46f486167..135b5a137 100644 --- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts +++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts @@ -27,6 +27,8 @@ export abstract class EditDialogComponent implements OnI networkActive = false + closeEnabled = false + error = null abstract getForm(): FormGroup @@ -37,6 +39,11 @@ export abstract class EditDialogComponent implements OnI if (this.object != null) { this.objectForm.patchValue(this.object) } + + // we wait to enable the close button so it doesnt pull browser focus since its the first clickable element in the DOM + setTimeout(() => { + this.closeEnabled = true + }); } getCreateTitle() { diff --git a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html index f2063df7e..7f1d39859 100644 --- a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html +++ b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html @@ -1,7 +1,7 @@
diff --git a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts index e930ffaed..57de978aa 100644 --- a/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts +++ b/src-ui/src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.ts @@ -20,6 +20,8 @@ export class SaveViewConfigDialogComponent implements OnInit { @Input() buttonsEnabled = true + closeEnabled = false + _defaultName = "" get defaultName() { @@ -31,7 +33,7 @@ export class SaveViewConfigDialogComponent implements OnInit { this._defaultName = value this.saveViewConfigForm.patchValue({name: value}) } - + saveViewConfigForm = new FormGroup({ name: new FormControl(''), showInSideBar: new FormControl(false), @@ -39,6 +41,10 @@ export class SaveViewConfigDialogComponent implements OnInit { }) ngOnInit(): void { + // wait to enable close button so it doesnt steal focus form input + setTimeout(() => { + this.closeEnabled = true + }); } save() { diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html index 3544aeed0..bbc2c8453 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html @@ -1,12 +1,11 @@ - Created: {{document.created | date}} + Created: {{document.created | customDate}}
diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index 4f11b2a98..a0d0299a5 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -50,7 +50,7 @@
- {{document.created | date}} + {{document.created | customDate:'shortDate'}}
diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index 18f39bfb5..a29e146bf 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -160,10 +160,10 @@ - {{d.created | date}} + {{d.created | customDate}} - {{d.added | date}} + {{d.added | customDate}} diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html index 08704190e..ffe260d73 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html @@ -21,7 +21,7 @@ {{ correspondent.name }} {{ getMatching(correspondent) }} {{ correspondent.document_count }} - {{ correspondent.last_correspondence | date }} + {{ correspondent.last_correspondence | customDate }}
+
+
+ Date display +
+
+ + + +
+
+ +
+
+ Date format +
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+
+
Items per page diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index f0d83a15f..8c1517800 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -3,7 +3,7 @@ import { FormControl, FormGroup } from '@angular/forms'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; -import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; +import { LanguageOption, SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; import { ToastService } from 'src/app/services/toast.service'; @Component({ @@ -23,7 +23,9 @@ export class SettingsComponent implements OnInit { 'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)), 'useNativePdfViewer': new FormControl(this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)), 'savedViews': this.savedViewGroup, - 'displayLanguage': new FormControl(this.settings.getLanguage()) + 'displayLanguage': new FormControl(this.settings.getLanguage()), + 'dateLocale': new FormControl(this.settings.get(SETTINGS_KEYS.DATE_LOCALE)), + 'dateFormat': new FormControl(this.settings.get(SETTINGS_KEYS.DATE_FORMAT)), }) savedViews: PaperlessSavedView[] @@ -64,14 +66,24 @@ export class SettingsComponent implements OnInit { this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem) this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString()) this.settings.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, this.settingsForm.value.useNativePdfViewer) + this.settings.set(SETTINGS_KEYS.DATE_LOCALE, this.settingsForm.value.dateLocale) + this.settings.set(SETTINGS_KEYS.DATE_FORMAT, this.settingsForm.value.dateFormat) this.settings.setLanguage(this.settingsForm.value.displayLanguage) this.documentListViewService.updatePageSize() this.settings.updateDarkModeSettings() this.toastService.showInfo($localize`Settings saved successfully.`) } - get languages() { - return this.settings.getLanguageOptions() + get displayLanguageOptions(): LanguageOption[] { + return [{code: "", name: $localize`Use system language`}].concat(this.settings.getLanguageOptions()) + } + + get dateLocaleOptions(): LanguageOption[] { + return [{code: "", name: $localize`Use same as display language`}].concat(this.settings.getLanguageOptions()) + } + + get today() { + return new Date() } saveSettings() { diff --git a/src-ui/src/app/pipes/custom-date.pipe.spec.ts b/src-ui/src/app/pipes/custom-date.pipe.spec.ts new file mode 100644 index 000000000..822966baf --- /dev/null +++ b/src-ui/src/app/pipes/custom-date.pipe.spec.ts @@ -0,0 +1,8 @@ +import { CustomDatePipe } from './custom-date.pipe'; + +describe('CustomDatePipe', () => { + it('create an instance', () => { + const pipe = new CustomDatePipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/pipes/custom-date.pipe.ts b/src-ui/src/app/pipes/custom-date.pipe.ts new file mode 100644 index 000000000..19989fbdd --- /dev/null +++ b/src-ui/src/app/pipes/custom-date.pipe.ts @@ -0,0 +1,19 @@ +import { DatePipe } from '@angular/common'; +import { Inject, LOCALE_ID, Pipe, PipeTransform } from '@angular/core'; +import { SettingsService, SETTINGS_KEYS } from '../services/settings.service'; + +@Pipe({ + name: 'customDate' +}) +export class CustomDatePipe extends DatePipe implements PipeTransform { + + constructor(@Inject(LOCALE_ID) locale: string, private settings: SettingsService) { + super(settings.get(SETTINGS_KEYS.DATE_LOCALE) || locale) + + } + + transform(value: any, format?: string, timezone?: string, locale?: string): string | null { + return super.transform(value, format || this.settings.get(SETTINGS_KEYS.DATE_FORMAT), timezone, locale) + } + +} diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 50adcf5a7..04918d835 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -9,13 +9,21 @@ export interface PaperlessSettings { default: any } +export interface LanguageOption { + code: string, + name: string, + englishName?: string +} + export const SETTINGS_KEYS = { BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs', BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system', DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled', - USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer' + USE_NATIVE_PDF_VIEWER: 'general-settings:document-details:native-pdf-viewer', + DATE_LOCALE: 'general-settings:date-display:date-locale', + DATE_FORMAT: 'general-settings:date-display:date-format' } const SETTINGS: PaperlessSettings[] = [ @@ -24,7 +32,9 @@ const SETTINGS: PaperlessSettings[] = [ {key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50}, {key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: "boolean", default: true}, {key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: "boolean", default: false}, - {key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: "boolean", default: false} + {key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: "boolean", default: false}, + {key: SETTINGS_KEYS.DATE_LOCALE, type: "string", default: "de"}, + {key: SETTINGS_KEYS.DATE_FORMAT, type: "string", default: "mediumDate"} ] @Injectable({ @@ -59,13 +69,12 @@ export class SettingsService { } - getLanguageOptions() { + getLanguageOptions(): LanguageOption[] { return [ - {code: "", name: $localize`Use system language`}, - {code: "en-us", name: `${$localize`English`} (English)`}, - {code: "de", name: `${$localize`German`} (German)`}, - {code: "nl", name: `${$localize`Dutch`} (Dutch)`}, - {code: "fr", name: `${$localize`French`} (French)`} + {code: "en-us", name: $localize`English`, englishName: "English"}, + {code: "de", name: $localize`German`, englishName: "German"}, + {code: "nl", name: $localize`Dutch`, englishName: "Dutch"}, + {code: "fr", name: $localize`French`, englishName: "French"} ] } From d5eff386dbfc5a976b58df5fe96f9eec05cba125 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Sun, 17 Jan 2021 00:25:40 +0100 Subject: [PATCH 19/61] update translation messages --- src-ui/messages.xlf | 106 ++++++++++++------ .../manage/settings/settings.component.ts | 2 +- 2 files changed, 75 insertions(+), 33 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index a1f0e8b88..a98c076ec 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -475,21 +475,35 @@ Saved view "" deleted. src/app/components/manage/settings/settings.component.ts - 56 + 58 Settings saved successfully. src/app/components/manage/settings/settings.component.ts - 70 + 74 + + + + Use system language + + src/app/components/manage/settings/settings.component.ts + 78 + + + + Use date format of display language + + src/app/components/manage/settings/settings.component.ts + 82 Error while storing settings on server: src/app/components/manage/settings/settings.component.ts - 86 + 98 @@ -510,7 +524,7 @@ Saved views src/app/components/manage/settings/settings.component.html - 79 + 114 @@ -534,109 +548,144 @@ 25 + + Date display + + src/app/components/manage/settings/settings.component.html + 32 + + + + Date format + + src/app/components/manage/settings/settings.component.html + 45 + + + + Short: + + src/app/components/manage/settings/settings.component.html + 51 + + + + Medium: + + src/app/components/manage/settings/settings.component.html + 55 + + + + Long: + + src/app/components/manage/settings/settings.component.html + 59 + + Items per page src/app/components/manage/settings/settings.component.html - 32 + 67 Document editor src/app/components/manage/settings/settings.component.html - 48 + 83 Use PDF viewer provided by the browser src/app/components/manage/settings/settings.component.html - 52 + 87 This is usually faster for displaying large PDF documents, but it might not work on some browsers. src/app/components/manage/settings/settings.component.html - 52 + 87 Dark mode src/app/components/manage/settings/settings.component.html - 59 + 94 Use system settings src/app/components/manage/settings/settings.component.html - 62 + 97 Enable dark mode src/app/components/manage/settings/settings.component.html - 63 + 98 Bulk editing src/app/components/manage/settings/settings.component.html - 67 + 102 Show confirmation dialogs src/app/components/manage/settings/settings.component.html - 71 + 106 Deleting documents will always ask for confirmation. src/app/components/manage/settings/settings.component.html - 71 + 106 Apply on close src/app/components/manage/settings/settings.component.html - 72 + 107 Appears on src/app/components/manage/settings/settings.component.html - 91 + 126 Show on dashboard src/app/components/manage/settings/settings.component.html - 94 + 129 Show in sidebar src/app/components/manage/settings/settings.component.html - 98 + 133 No saved views defined. src/app/components/manage/settings/settings.component.html - 108 + 143 @@ -1033,8 +1082,8 @@ 50 - - Created: + + Created: src/app/components/document-list/document-card-large/document-card-large.component.html 67 @@ -1414,39 +1463,32 @@ 12 - - Use system language - - src/app/services/settings.service.ts - 64 - - English src/app/services/settings.service.ts - 65 + 74 German src/app/services/settings.service.ts - 66 + 75 Dutch src/app/services/settings.service.ts - 67 + 76 French src/app/services/settings.service.ts - 68 + 77 diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 8c1517800..d10297f95 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -79,7 +79,7 @@ export class SettingsComponent implements OnInit { } get dateLocaleOptions(): LanguageOption[] { - return [{code: "", name: $localize`Use same as display language`}].concat(this.settings.getLanguageOptions()) + return [{code: "", name: $localize`Use date format of display language`}].concat(this.settings.getLanguageOptions()) } get today() { From fc5def157d6c47331d22ed9b45a238b195f69c89 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 16 Jan 2021 23:28:01 +0000 Subject: [PATCH 20/61] Translate /src-ui/messages.xlf in de translation completed for the source file '/src-ui/messages.xlf' on the 'de' language. --- src-ui/src/locale/messages.de.xlf | 116 +++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 34 deletions(-) diff --git a/src-ui/src/locale/messages.de.xlf b/src-ui/src/locale/messages.de.xlf index 030cfb28c..a5b28274f 100644 --- a/src-ui/src/locale/messages.de.xlf +++ b/src-ui/src/locale/messages.de.xlf @@ -542,7 +542,7 @@ Gespeicherte Ansicht "" gelöscht. src/app/components/manage/settings/settings.component.ts - 56 + 58 @@ -550,7 +550,23 @@ Einstellungen erfolgreich gespeichert. src/app/components/manage/settings/settings.component.ts - 70 + 74 + + + + Use system language + Benutze Systemsprache + + src/app/components/manage/settings/settings.component.ts + 78 + + + + Use date format of display language + Benutze Datumsformat der Anzeigesprache + + src/app/components/manage/settings/settings.component.ts + 82 @@ -558,7 +574,7 @@ Fehler beim Speichern der Einstellungen auf dem Server: src/app/components/manage/settings/settings.component.ts - 86 + 98 @@ -582,7 +598,7 @@ Gespeicherte Ansichten src/app/components/manage/settings/settings.component.html - 79 + 114 @@ -609,12 +625,52 @@ 25 + + Date display + Datumsanzeige + + src/app/components/manage/settings/settings.component.html + 32 + + + + Date format + Datumsformat + + src/app/components/manage/settings/settings.component.html + 45 + + + + Short: + Kurz: + + src/app/components/manage/settings/settings.component.html + 51 + + + + Medium: + Mittel: + + src/app/components/manage/settings/settings.component.html + 55 + + + + Long: + Lang: + + src/app/components/manage/settings/settings.component.html + 59 + + Items per page Dokumente pro Seite src/app/components/manage/settings/settings.component.html - 32 + 67 @@ -622,7 +678,7 @@ Dokumenteditor src/app/components/manage/settings/settings.component.html - 48 + 83 @@ -630,7 +686,7 @@ Benutze PDF-Betrachter des Web Browsers src/app/components/manage/settings/settings.component.html - 52 + 87 @@ -638,7 +694,7 @@ Der integrierte PDF-Betrachter des Web-Browsers ist in der Regel schneller bei der Anzeige besonders großer Dokumente, funktioniert aber nicht in allen Browsern. src/app/components/manage/settings/settings.component.html - 52 + 87 @@ -646,7 +702,7 @@ Dunkler Modus src/app/components/manage/settings/settings.component.html - 59 + 94 @@ -654,7 +710,7 @@ Benutze Systemeinstellungen src/app/components/manage/settings/settings.component.html - 62 + 97 @@ -662,7 +718,7 @@ Dunklen Modus aktivieren src/app/components/manage/settings/settings.component.html - 63 + 98 @@ -670,7 +726,7 @@ Massenbearbeitung src/app/components/manage/settings/settings.component.html - 67 + 102 @@ -678,7 +734,7 @@ Bestätigungsdialoge anzeigen src/app/components/manage/settings/settings.component.html - 71 + 106 @@ -686,7 +742,7 @@ Beim Löschen von Dokumenten wird immer nach einer Bestätigung gefragt. src/app/components/manage/settings/settings.component.html - 71 + 106 @@ -694,7 +750,7 @@ Anwenden beim Schließen src/app/components/manage/settings/settings.component.html - 72 + 107 @@ -702,7 +758,7 @@ Erscheint auf src/app/components/manage/settings/settings.component.html - 91 + 126 @@ -710,7 +766,7 @@ Auf Startseite zeigen src/app/components/manage/settings/settings.component.html - 94 + 129 @@ -718,7 +774,7 @@ In Seitenleiste zeigen src/app/components/manage/settings/settings.component.html - 98 + 133 @@ -726,7 +782,7 @@ Keine gespeicherten Ansichten vorhanden. src/app/components/manage/settings/settings.component.html - 108 + 143 @@ -1179,9 +1235,9 @@ 50 - - Created: - Erstellt: + + Created: + Erstellt: src/app/components/document-list/document-card-large/document-card-large.component.html 67 @@ -1614,20 +1670,12 @@ 12 - - Use system language - Benutze Systemsprache - - src/app/services/settings.service.ts - 64 - - English Englisch src/app/services/settings.service.ts - 65 + 74 @@ -1635,7 +1683,7 @@ Deutsch src/app/services/settings.service.ts - 66 + 75 @@ -1643,7 +1691,7 @@ Niederländisch src/app/services/settings.service.ts - 67 + 76 @@ -1651,7 +1699,7 @@ Französisch src/app/services/settings.service.ts - 68 + 77 From f919ec0d57ede446e95143cfc137b1b1702457d3 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Sun, 17 Jan 2021 00:52:54 +0100 Subject: [PATCH 21/61] version bump --- .github/workflows/ci.yml | 2 +- docs/changelog.rst | 25 ++++++++++++++++----- src-ui/src/environments/environment.prod.ts | 2 +- src/paperless/version.py | 2 +- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fadf260e..425243163 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -208,7 +208,7 @@ jobs: tag_name: ng-${{ steps.get_version.outputs.version }} release_name: Paperless-ng ${{ steps.get_version.outputs.version }} draft: false - prerelease: true + prerelease: false body: | For a complete list of changes, see the changelog at https://paperless-ng.readthedocs.io/en/latest/changelog.html. - diff --git a/docs/changelog.rst b/docs/changelog.rst index 498c53301..f5bf41b25 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,19 @@ Changelog ********* + +paperless-ng 1.0.0 +################## + +Nothing special about this release, but since there are relatively few bug reports coming in, I think that this is reasonably stable. + +* Changes and additions + + * Added a language selector to the settings. + * Added date format options to the settings. + * Range selection with shift clicking is now possible in the document list. + + paperless-ng 0.9.14 ################### @@ -116,7 +129,7 @@ paperless-ng 0.9.10 * There are some configuration options in the settings to alter the behavior. * Other changes and additions - + * Thanks to `zjean`_, paperless now publishes a webmanifest, which is useful for adding the application to home screens on mobile devices. * The Paperless-ng logo now navigates to the dashboard. * Filter for documents that don't have any correspondents, types or tags assigned. @@ -136,7 +149,7 @@ paperless-ng 0.9.10 The bulk delete operations did not update the search index. Therefore, documents that you deleted remained in the index and caused the search to return messages about missing documents when searching. Further bulk operations will properly update the index. - + However, this change is not retroactive: If you used the delete method of the bulk editor, you need to reindex your search index by :ref:`running the management command document_index with the argument reindex `. @@ -191,12 +204,12 @@ paperless-ng 0.9.7 * Thanks to the hard work of `Michael Shamoon`_, paperless now comes with a much more streamlined UI for filtering documents. - + * `Michael Shamoon`_ replaced the document preview with another component. This should fix compatibility with Safari browsers. * Added buttons to the management pages to quickly show all documents with one specific tag, correspondent, or title. - - * Paperless now stores your saved views on the server and associates them with your user account. + + * Paperless now stores your saved views on the server and associates them with your user account. This means that you can access your views on multiple devices and have separate views for different users. You will have to recreate your views. @@ -214,7 +227,7 @@ paperless-ng 0.9.7 This option enables you to be logged in into multiple instances by specifying different cookie names for each instance. * Fixes - + * Sometimes paperless would assign dates in the future to newly consumed documents. * The filename format fields ``{created_month}`` and ``{created_day}`` now use a leading zero for single digit values. * The filename format field ``{tags}`` can no longer be used without arguments. diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index c5ef431cd..fbd65d573 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -2,5 +2,5 @@ export const environment = { production: true, apiBaseUrl: "/api/", appTitle: "Paperless-ng", - version: "0.9.14" + version: "1.0.0" }; diff --git a/src/paperless/version.py b/src/paperless/version.py index ac1d6312c..8dcd08521 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1 +1 @@ -__version__ = (0, 9, 14) +__version__ = (1, 0, 0) From 31b74515b6fd8057d12ad5c9131973314a6ec102 Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 16 Jan 2021 20:49:12 -0800 Subject: [PATCH 22/61] Allow "live" changing of date format --- .../app/components/manage/settings/settings.component.html | 6 +++--- .../app/components/manage/settings/settings.component.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html index d5acfb149..9b35b5b3f 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.html +++ b/src-ui/src/app/components/manage/settings/settings.component.html @@ -48,15 +48,15 @@

Appearance

- +
- +
- +
diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index d10297f95..9a315c245 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -30,6 +30,10 @@ export class SettingsComponent implements OnInit { savedViews: PaperlessSavedView[] + get comptuedDateLocale(): string { + return this.settingsForm.value.dateLocale || this.settingsForm.value.displayLanguage + } + constructor( public savedViewService: SavedViewService, private documentListViewService: DocumentListViewService, From 741ccfa280be57e622c8a8253419229cda5d246f Mon Sep 17 00:00:00 2001 From: Michael Shamoon <4887959+nikonratm@users.noreply.github.com> Date: Sat, 16 Jan 2021 21:39:13 -0800 Subject: [PATCH 23/61] Fix svg attribute error Unrelated to the rest of this branch but not worth a separate PR --- src-ui/src/app/components/dashboard/dashboard.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/dashboard/dashboard.component.html b/src-ui/src/app/components/dashboard/dashboard.component.html index 47751fe8b..c10b8ea04 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.html +++ b/src-ui/src/app/components/dashboard/dashboard.component.html @@ -1,5 +1,5 @@ -

Appearance

- +
- +
- +
diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 9a315c245..3c4de59b6 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -30,7 +30,7 @@ export class SettingsComponent implements OnInit { savedViews: PaperlessSavedView[] - get comptuedDateLocale(): string { + get computedDateLocale(): string { return this.settingsForm.value.dateLocale || this.settingsForm.value.displayLanguage } diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 04918d835..d2a190c1f 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -33,7 +33,7 @@ const SETTINGS: PaperlessSettings[] = [ {key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: "boolean", default: true}, {key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: "boolean", default: false}, {key: SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, type: "boolean", default: false}, - {key: SETTINGS_KEYS.DATE_LOCALE, type: "string", default: "de"}, + {key: SETTINGS_KEYS.DATE_LOCALE, type: "string", default: ""}, {key: SETTINGS_KEYS.DATE_FORMAT, type: "string", default: "mediumDate"} ] From a68b858733327313408e898c8e307df2826ebf41 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 01:15:39 +0100 Subject: [PATCH 26/61] new exporter that updates the export in place, fixes #376 #343 #166 --- .../management/commands/document_exporter.py | 162 ++++++++++++++---- .../management/commands/document_importer.py | 6 +- .../samples/documents/originals/0000002.pdf | Bin 0 -> 25743 bytes .../samples/documents/originals/0000003.pdf | Bin 0 -> 12022 bytes .../samples/documents/thumbnails/0000002.png | Bin 0 -> 7913 bytes .../samples/documents/thumbnails/0000003.png | Bin 0 -> 7913 bytes .../tests/test_management_exporter.py | 152 ++++++++++++---- 7 files changed, 245 insertions(+), 75 deletions(-) create mode 100644 src/documents/tests/samples/documents/originals/0000002.pdf create mode 100644 src/documents/tests/samples/documents/originals/0000003.pdf create mode 100644 src/documents/tests/samples/documents/thumbnails/0000002.png create mode 100644 src/documents/tests/samples/documents/thumbnails/0000003.png diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index a7a17f124..e2313e86a 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -1,15 +1,21 @@ +import hashlib import json import os import shutil import time +import tqdm +from django.conf import settings from django.core import serializers from django.core.management.base import BaseCommand, CommandError +from django.db import transaction +from filelock import FileLock from documents.models import Document, Correspondent, Tag, DocumentType from documents.settings import EXPORTER_FILE_NAME, EXPORTER_THUMBNAIL_NAME, \ EXPORTER_ARCHIVE_NAME from paperless.db import GnuPG +from ...file_handling import generate_filename, delete_empty_directories from ...mixins import Renderable @@ -24,13 +30,36 @@ class Command(Renderable, BaseCommand): def add_arguments(self, parser): parser.add_argument("target") + parser.add_argument( + "--compare-checksums", + default=False, + action="store_true", + help="Compare file checksums when determining whether to export " + "a file or not. If not specified, file size and time " + "modified is used instead." + ) + + parser.add_argument( + "--use-filename-format", + default=False, + action="store_true", + help="Use PAPERLESS_FILENAME_FORMAT for storing files in the " + "export directory, if configured." + ) + def __init__(self, *args, **kwargs): BaseCommand.__init__(self, *args, **kwargs) self.target = None + self.files_in_export_dir = [] + self.exported_files = [] + self.compare_checksums = False + self.use_filename_format = False def handle(self, *args, **options): self.target = options["target"] + self.compare_checksums = options['compare_checksums'] + self.use_filename_format = options['use_filename_format'] if not os.path.exists(self.target): raise CommandError("That path doesn't exist") @@ -38,52 +67,75 @@ def handle(self, *args, **options): if not os.access(self.target, os.W_OK): raise CommandError("That path doesn't appear to be writable") - if os.listdir(self.target): - raise CommandError("That directory is not empty.") - - self.dump() + with FileLock(settings.MEDIA_LOCK): + self.dump() def dump(self): - - documents = Document.objects.all() - document_map = {d.pk: d for d in documents} - manifest = json.loads(serializers.serialize("json", documents)) - - for index, document_dict in enumerate(manifest): - - # Force output to unencrypted as that will be the current state. - # The importer will make the decision to encrypt or not. - manifest[index]["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED # NOQA: E501 + # 1. Take a snapshot of what files exist in the current export folder + for root, dirs, files in os.walk(self.target): + self.files_in_export_dir.extend( + map(lambda f: os.path.abspath(os.path.join(root, f)), files) + ) + + # 2. Create manifest, containing all correspondents, types, tags and + # documents + with transaction.atomic(): + manifest = json.loads( + serializers.serialize("json", Correspondent.objects.all())) + + manifest += json.loads(serializers.serialize( + "json", Tag.objects.all())) + + manifest += json.loads(serializers.serialize( + "json", DocumentType.objects.all())) + + documents = Document.objects.order_by("id") + document_map = {d.pk: d for d in documents} + document_manifest = json.loads( + serializers.serialize("json", documents)) + manifest += document_manifest + + # 3. Export files from each document + for index, document_dict in tqdm.tqdm(enumerate(document_manifest), + total=len(document_manifest)): + # 3.1. store files unencrypted + document_dict["fields"]["storage_type"] = Document.STORAGE_TYPE_UNENCRYPTED # NOQA: E501 document = document_map[document_dict["pk"]] - print(f"Exporting: {document}") - + # 3.2. generate a unique filename filename_counter = 0 while True: - original_name = document.get_public_filename( - counter=filename_counter) - original_target = os.path.join(self.target, original_name) + if self.use_filename_format: + base_name = generate_filename( + document, counter=filename_counter) + else: + base_name = document.get_public_filename( + counter=filename_counter) - if not os.path.exists(original_target): + if base_name not in self.exported_files: + self.exported_files.append(base_name) break else: filename_counter += 1 - thumbnail_name = original_name + "-thumbnail.png" - thumbnail_target = os.path.join(self.target, thumbnail_name) - + # 3.3. write filenames into manifest + original_name = base_name + original_target = os.path.join(self.target, original_name) document_dict[EXPORTER_FILE_NAME] = original_name + + thumbnail_name = base_name + "-thumbnail.png" + thumbnail_target = os.path.join(self.target, thumbnail_name) document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_name if os.path.exists(document.archive_path): - archive_name = document.get_public_filename( - archive=True, counter=filename_counter, suffix="_archive") + archive_name = base_name + "-archive.pdf" archive_target = os.path.join(self.target, archive_name) document_dict[EXPORTER_ARCHIVE_NAME] = archive_name else: archive_target = None + # 3.4. write files to target folder t = int(time.mktime(document.created.timetuple())) if document.storage_type == Document.STORAGE_TYPE_GPG: @@ -100,21 +152,57 @@ def dump(self): f.write(GnuPG.decrypted(document.archive_path)) os.utime(archive_target, times=(t, t)) else: + self.check_and_copy(document.source_path, + document.checksum, + original_target) - shutil.copy(document.source_path, original_target) - shutil.copy(document.thumbnail_path, thumbnail_target) + self.check_and_copy(document.thumbnail_path, + None, + thumbnail_target) if archive_target: - shutil.copy(document.archive_path, archive_target) + self.check_and_copy(document.archive_path, + document.archive_checksum, + archive_target) - manifest += json.loads( - serializers.serialize("json", Correspondent.objects.all())) + # 4. write manifest to target forlder + manifest_path = os.path.abspath( + os.path.join(self.target, "manifest.json")) - manifest += json.loads(serializers.serialize( - "json", Tag.objects.all())) - - manifest += json.loads(serializers.serialize( - "json", DocumentType.objects.all())) - - with open(os.path.join(self.target, "manifest.json"), "w") as f: + with open(manifest_path, "w") as f: json.dump(manifest, f, indent=2) + + if manifest_path in self.files_in_export_dir: + self.files_in_export_dir.remove(manifest_path) + + # 5. Remove files which we did not explicitly export in this run + for f in self.files_in_export_dir: + os.remove(f) + + delete_empty_directories(os.path.abspath(os.path.dirname(f)), + os.path.abspath(self.target)) + + def check_and_copy(self, source, source_checksum, target): + if os.path.abspath(target) in self.files_in_export_dir: + self.files_in_export_dir.remove(os.path.abspath(target)) + + perform_copy = False + + if os.path.exists(target): + source_stat = os.stat(source) + target_stat = os.stat(target) + if self.compare_checksums and source_checksum: + with open(target, "rb") as f: + target_checksum = hashlib.md5(f.read()).hexdigest() + perform_copy = target_checksum != source_checksum + elif source_stat.st_mtime != target_stat.st_mtime: + perform_copy = True + elif source_stat.st_size != target_stat.st_size: + perform_copy = True + else: + # Copy if it does not exist + perform_copy = True + + if perform_copy: + os.makedirs(os.path.dirname(target), exist_ok=True) + shutil.copy2(source, target) diff --git a/src/documents/management/commands/document_importer.py b/src/documents/management/commands/document_importer.py index 6df14a82c..a2e19e3cc 100644 --- a/src/documents/management/commands/document_importer.py +++ b/src/documents/management/commands/document_importer.py @@ -148,10 +148,10 @@ def _import_files_from_manifest(self): create_source_path_directory(document.source_path) - shutil.copy(document_path, document.source_path) - shutil.copy(thumbnail_path, document.thumbnail_path) + shutil.copy2(document_path, document.source_path) + shutil.copy2(thumbnail_path, document.thumbnail_path) if archive_path: create_source_path_directory(document.archive_path) - shutil.copy(archive_path, document.archive_path) + shutil.copy2(archive_path, document.archive_path) document.save() diff --git a/src/documents/tests/samples/documents/originals/0000002.pdf b/src/documents/tests/samples/documents/originals/0000002.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5e75266ca7e534ae87533e612c662a32320eb0c6 GIT binary patch literal 25743 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 literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/originals/0000003.pdf b/src/documents/tests/samples/documents/originals/0000003.pdf new file mode 100644 index 0000000000000000000000000000000000000000..afbeef5c8fc783d55b40d5a642a6d523413472d5 GIT binary patch literal 12022 zcma)i1z40#*S~<&5+YsVilX#(krL7k0!qh%)KW`>s0dP`w3MVMiU=Yg(kUq*f^@f( zl!3zkUVOsmdEf8*{x{b!bI+VPbLO1ioHKjfM?gnWNem{A1_?CfHB~p2H)Vq0U?|wt z+7Tor1<}A0?1|UF2%tv~qU7LA#Ct%LoUMp>MZArxEnY?jGg1TB(6CO-tE(>XX8lPi96kOCT%uYM!^4>k<85w(<--$ z3>ruIjZaHSWw-N?xzVIA_l!7CF(* zLa);1g&R`bbSfsloBe}HOwuzCveIs83BnU>|JXUuA}I?*(h;cvN8m`KM}3(8&?NSM z(BvPQhJj%S6bk*jUg!M-0!;=|roOEYbS61V7uvI`8tJAdPbRUruV;jm@RaI&3wbJ*%@{X(c9rvEGyOHhUU3uQ&f=<{2##fS^b$P`-T;zcsJk5`8pWT3snL#bNjS5e+fuS=5?Ii5Z6X zjZm%P;5$o`dNhWaJ0|d;P4-SnP%4uboKHQ!ac+W3-Z53tU+QE;#i{iGhS9brK1JTC zOkW2kgK(Fy#CA5d%W~OQ^$iLKmKg+;;)ksT4(m*}D4G?o&OeJtYF_pnY~eeL-ol>-YJaU4R*bnxc)V(r^#J#JJh7bVlk%v=bu#aM5>FlrkOXM@NND_vJ&~9X0#f5tFwOy%RamtubAT-%uh=`M> z=~u7){_KltLsw?+)6q7nEsU)gS4X@rZA@_}XPvmm@u2UZmdp5v4#PdFg$wkEpHMX4 zImyqAWFzl*Ek9llxpVLWQl7@zU|pWTYI?Wh!%3I#bot&3{F0fhaqB4sq6E9yuFN4F zf5kzOG!YB2fS;qSI#Pl>N)GVt(H*hn;%c)4dZkZS(#$Z|{IQt09tZoA1W;u( z{zif)`SCQ$@`V0<`G07{*@?8$ z#uCl$)raYLN4xOxR4Ml+(?&^aHqNB=nDlLh>#RFCvUfC_V+iecOFwVj2e+wv@ex*g zj(2}H{c4Lof6j{a(HmKoldKQC$eownqmsw()bjCc`(Y=I-9(8VcCUJxmZfZ1xVw$3 zxyChrNDsBNr*LeP($(%$+!E&uWg}}RPA1)zZ(mrGCf8q?%{kLN6&&)mW^8?M(f7xs z;`t?ZBBTG%8vD50r0T;RD+SK+v3Lj1yq*K8>1Q3}1F6k*rvxqF$cqchv^Q&UJf*y4tkd_f>t z7mu#3hB8k}GW-RdH?QT*iTZ(nVxE#rJ8e&$?~^QL*3`TbZ^}gDY0RX_CvP7+9mhW? zrE3<-oDHUOE{>qmf3np~nfn;VVW#_hq2V2$Fn9%pPQ0}dv zWP8sZx@JjFLQYKS=`V2-%-5Jxx<5fX*^&2lZ&M#P>g8+o!_@B%jgz0Qr=gaTj=k}~ zdnvnRaTSRpM;g8HM|=CD2@`{*rh4ZTn5yCv(H4DoAHRLLOvR$3llb&CU+X)n`?_~% z-rc$J*qu#=5?=svJtdKD-O}u0+Om5i!?seF!3y?~*CFm}^IoCWytTx0r|!LcrnI4+ zH-=S`Q}0aK=IP^b4weZz5n3+Yam5;ENbGujInEWm0RbCd!FDj>) zKLplXY-U|Z{RI4w1@OqMwS)MJf?A0!WXrPcwr+2_F48Ssl(M?d->LIew1VFK zDLo-!W(g&ynEW}^!@PxKlRH1WMEgd#KOYP-BU65?&7FbuW)S(Ka-AJj&J4k+HLe)l zxhVwq#AuM;(z=WEThwcb(Pb8DM^=aihZAea-A*+G2RqCB;626{o6$dWH~V7vM~K8%(s;a%O(TOvoZ z3T5$==;iAsH#??r@(ZofPa=)m&&jH3Y&FV1SCxEgm_6%(r1g>inQ@sk;2EIcscY@z@{A~ErzqX-KcYTgCkSy~@=444@|71q(r_B~(ZqTw`)#}EA zsOHA?ZKDU5qTDU=nxy^7a2;>wUt@H?QoPQMEA6u4i z#oPGy1u&R;mW^A z>pyP_PBqszOD$!!p41*~ecP~Ddf8xTF8Xrxi{J&8!MC+xi+3yMj@5NjE!iciI(cAS znoswRn4*=MMx41r8dl$8o#xUgxVQ6Ij$eqDoX%oV%3$)5@1?zdf$2hF1INZ~#|v&+ zy*R7Ltuy*)dC4s{0Rv5#PK{(yPpdGGXnY?n=QAgbmZ_AAixw4Koy(d~Z7YwDV#kJ8 zeo7{vdRlbd{}c6#IzJFq|2-v}&0ep~hgPfZb#gUhm%q`rL4uO_hRC&L_CL5w*+TQ! zT*g~!eCmygk2;Xyiz}B7bH&(&HWW=9yU)Ys*@dOHcf6dk$dU%d zHoN+7*)PL&*mDbVvN!jpseK;z3q(Z77M%`Wz1SJ%wpcx{d~?j*XM;lXXW6RA5Ly&t9p{ zYwNx$suf&M=qqu2bq94%{N`6?8ga5i=BcE5PxUMxTB7JpXF@C$q%7*HK`;)PgTmS@ zh#9~B-~~4M{Sy-OrziV%y1Km>ee~-#2B}S5>5aant&A>yFf1BXQw-P) zY(Ah`ONkm-MC<8UUv@&OJ$pZ$?zBz4;HX|~RsNkK)GnS3cV#^3*t38R+Qp2`r!3H$ zwpvf#ww}3=m(JuZpUje)uCsXin^v(t+zuX}`SwB0PAE)(q1M&L7iULOpnwB2`Z-)N0{ZNiWx*fB14{!GvX*$8~h9W-aFDGLQEfIRlUA zi^Fy4{(IppNz2QtUK+{a;Lc&;UW`(-SLmv@^?9)$jg2$bc-qdZh48yqLv~dP2Dxeb zTp5|RPm0hOUMrSQy!?va=vJC12tgvM~l)M{}(bdF8H|h7-$^URfY5eN5s8+SYsC!n0(Y6SSY$IX0Tv!U5 zf6m9{6zj@|_-F~USlO(8VN~c7>+FJ$^x}w3yP-s*%9gxse|Pca(x>>RH|?jTE z3b~*4w{m8w*y%_X3ixZn9BqXMndcXdsV`W+q{mu`QLk>Vnnn~vC?ydw`1>}McRv=z^C_&t4a(rbK5g@ zmvqX%K+915`^JkK7LtXpYIYLNPFNl|HAN_$XEPL*ZCfU$5#s^ zB0M#&i{z>2$UqhEK|Gy#wjL@*4rg6VJz(OlPqvJ!h(r?*&z}!TCk* z=_F+4JQXrjrv0`xYXCnR*7NDb&T!0&Z^9KC%MeK#A|ZXnx(_Y#DKGV|I@5mOo87Le zjf3w-8<}6FQppD>@1Ddc{~?*#@40F5-T#7 z7H8pBN(-*BbKP~j^vLU$c-0JJ<9WMdAMQ;`?3it?YaYrCY*{yb(40En4dd7@c|P;3 zsXVegbl1|Bs`yPLI_YFiu~nhdlwXOR;KDnv_)Ag3!eiWnCMNpJCq7)`s9an@vEkVc zZm65sy%b`lq-+(tNj@&`4ijeM$dxu9S_#FQKiHrw-G zKyFOZX_`OD4^)D*RWoIDz#nePchFUHtO#;BaMWzxqoWoYy=!i~Oerqey47`T)||#aTSPJ}R_)>AxI5L*$fj+)A5k(Dd{o>LY zI5QY9l2CkzI}h(GxiKm^p05t}xSpK_N&CTDu@!nMLb|ER%-^Lr`3QT1sQv0P8H?v1 zKbkJO)E4EJl~Ktp^#C{3ORYEP89r8Y3(7Y`s_TsYZNS>L>u>7r^+3MET;r@x8*2?&${7R? z<9s(8R|fN+X@uAtSzn!RM6!Gq~V1pMG2Ukvd6b~wcf#r6IbE}I; z(#ND9k?PmnPd$1-&y?rej&7f;#$Wl)?17Vm?g<))rX! z*pGj`Y1G}-?!4S}w)Mtt55#(7+3Pn?h<0b4n)TDUFiB2ZKJWF3Co+aj`44a+JCSLf zrU~A2?8>g!lagoFWBdD0@LFpy<*$^kq2881dhu;M;7hcH{$Prl#gF~d(ntf@VAzk( zU;2x7IC=~o<=S6gW%t6{$;8%02jI_sW7!Z*aJuV}+uk)*o}DNp40oiPSF4oGGIER> zScykuZu@}cy2)cxs=9h@!@k+|rpVcaMcHBHIw#K{#vH;}9~tV0Jr;yJhlyW}T8Z72 zay));(7DlWh?tV2)dqJ7CE~A4}=wMfrZ-2$(qCR`w z%$HZWk9pmou1_SWyB_m4lXWFfyu(esqG&w@UAgg^!k5~Ou@`qSOY_ux+Whzrh3yzu zL180N@I>`l-3jHku2@d>?ZOvCMkd+My&l_XxyqRb6Zx&Fk$wXVUKW9${jYkT9@Q@` z{=8EehatdPPRI`L4hUc#20$6VOqtuA_M`p+H#FB|1!?NqQ7F7j3mcZC8v1B@VV${J z#=&PQw{F+U`5va*uei{tYNe++@mofFu#3dcg?WpUc|jl^C6gZ~L1(jdw3f9n@lKu^ zZY|m;oFn~BrQ>V`^tIVbsl!ii7!Jz4X%@G=ZE$9y?Z(&>#QP91oX-MI2=BEXBKd6;M6_ua1dC~kp?0YuqJ!Mhy*mLe4rfRFae2XjR z90hY1vm{rNCD+!NNP~x-B44~EWnbK50b^Cd`3vWaLjy|GXvM)Ty@Cy)Q1a`vhVhkM zos|~&%kT|#x06>oo}PRCpv%fAYNWH-BA(agMWQ7*Jvz}cjI~pWxhoF;lAAW^Opg|A zQatWuz&T^N^p_jz#c`$y8F7|uSI)_bH&EIZ{h*)~x4kZQ7JpHTRU28e@+e zHJG)#;{%+|#j^60C882h$q`0djOQzvlGScE#!lcgCy{BUvd@dGr##5B-lBVlW2B$_C^?plVTq2HQtGX{G zay7y&O}@b392buRy@0#uhe9W}b4j9$drH*%xnmPLadgUYA(G1Xhbr!+i8Y-;-7b%g zZr-y@4&a}E^5r6bTr^|M>Ren-dmJ)3GE6V0uPN5Rx>y#8e04$r&8u7>6Wuq_V8BYv z7jv$1ug#pG?uVIOsE^{644|{!w$9g1#E5#_Q{=aDS2PIFwxtPm zo60s`RqA_q)0mu%;c*U|U+RfwB|SmzS@~%wcg#fO?gk(G>}`S;K9O$sg0)K2iBP?v zPPdovRJ9f(gUf5PJW$1@VZLLHJ^4VwnQC_#B%y?}cZNYR-yq_f^*3 z{S=-X9QcIy0xZFExU0E3A?zZCu~omlV{S?ZFIrOiwCST#Y8!Ys!UwdGrh5A}?HwPQ z(KENdt424s%fw}w>Nb1Wp0wtetx7V8=dQ-Rzhw%`JCQA=Ja{JkqR?lqoS&_2GzI5E z6SmfB2KZlHdY_Z1mVEXV-~4{b9dyDc1;^`kyvunH&oY3o-r@mxOZin+*1OPI4$zx^7e72|USN zb54){6yRsX7%t~^^)@liE zE8$1y%)*v`gukbcj%RyaQYgRHT&I?L=UZvMMJggO?%m}>E1d2N?5^d(50yD0 zm(r=xykXh6G_R`d1y?k4hH=QqwVH7CZ?o}IZ_WBbuuesVDJlNA(MSz6qqXFvT~$2` zLp>gE{u&D9Yb;-ScTy|qF2CtO=}uXUQlBJe3eCv7{Ti$?lATO!y84paH*j0Usrie9 zbJT{Kv8Gb2D{AJWWdPfZk@XsJx|D2#vHEnqNtjPqE;sJ!rkOyQNU>QAv+Fd=l9)9Q zlSnPbvWjg|?EUDSuXceL^|jrhRt)VeJt=7aqR{=%Z>PtQ)hLEBTX5E834XXv&b<}T8%!irerg)9 zuR2^@vG9p6y*50&QhHFDYad#=Ra)<*S6 z2`BFfd{mGAfr84a`Ho`{SWL zY_C3d4o_^{{C;L%0e@nAt2gLG`>fBsiJ(n__fAoW53FYmS9VABJ4coES$t7!l7M*H z$C2kdFULW=RLYAVza)xpQ3m)KLEtWvhHE2(FNZGApopD)mRxYhD;%Ts-o>yzvOU&k z1}>i^&C)*0EM{&Ys_QP!_ItmUcq%azk68TLri9rmKT~T(q^O#x7#bLPIrhlKz09Dj zdSJ_{^RZau1UvTDVeMkz<3Q036Vv(GAgu~l!n2yuRg~8!xzXp_j#XUs7Xws%61|l= zua(t|{Jd(Y&P{`Gq{5}&4q-9Nz~gzeh3(6oSq%=>$j3wyyd$-*Z=>bhUtD}s;j)o( z>?>iah=N)r*CtnIbolJ?Q#bF*bCQ`KS322?JKvQDyRVVou|$c-(`>X}5E?ZLYdh6W zpES3WCrYa-s;eq|9M7+i&+t{0#UY$&{@5d*a}i&eBJf;ibR~qmltU3GK*l~lBomod z_5RBzb<)Rj(sy(W4u<^en>z0AZ|VkqZg_~2D}ktp_q6eFa3i_`*$Kd_mX!-0A}>dJ zL^K?%@g7!02UmhV-owETL`q+<_XHz>20$q<@9JwJhC(91VhAV#41;4ZU>p`^22mwi zIXl?M5$v7uU?>P8=V^l{5W!d+3L_c}De8&ZPr#9EY>NG1}(tvtW46AU`=O+E1_G@&?uuvf2GHe^|`g^ZhIv->uCS2e+Eah;HTK zudgdj_9-bITv3$MrD4dBT`VZ;Svz*d%f)G}^JlelP-o^;Pzl&NQouwvH>|xJWa>m< zn$tB1uYMYNEc(DGiiJ?=y;ACBE?k3*+|!-Lo!3&IJGIS+vr1|Um~p<7&iwX6TFJt# zKugVAfwnQR+FiPz=fpEXC&tUihP4c`Hvm$*BJ`vK%}-i3-=OJV=Pqu==A72cTx;U`xE_ zX@UXJ1!Q-CNhm`Cm?ISg76*o-pkNdfdeo0VAdcFkcTyqEA<+YE5UEB3;K9J*M|?=* zBp#%9(mWC$vtP*j`$T|A2SyJBF>t+1a3Ce^fREt!PeA^b^7EGl{$}y_q0(|Cy6RgI zJjL|z_Fm3b9{=??q2Vy#IFa^+!Qp__(3t<}JON|4-wzZH@%w=y&HU{|{eFP{Ott}1 zl>U!6 z8jQlg!B_+gi~(lA;V3WyunVc6P_UyhI2sE^05&?RF>nxx{{N?t=t<>wS|kDq!UAeV zBY*;7L2zI$3JuW1P+%k$kO8PsXe1bc zL>{%#FhFk9uYy7#kJ>l{3K#?Q1x0~K^ha}Hz!|^*{Ev76`vmlfA+1eX3qUCn1LzUp zPtxO24Xh6Zk(}rM^7ns^|7pG7wR+ShSqqQ|1+0h#w2DBHPTj9vldMPTBb`DdpkWd{ z0?5I z`C9>zto?^2k9d;!{#i(J9kq|pNJ8YVLb4{w>SzQEM6%p}D_FpSBx{l^`Fr`(l7G}k z==sBvzj%`P{!vKlAGJxA{;iO#d4v)e6!f1S3Py52dcU1-6!1<8{J`;60#f2g{uxL= zgagTOq}D$|De^Z`z&V3SQvq+MR)0p6Er_K#VAPdaiRI#%|0h>n#9 zDXgPO3?O=VPggGw8@wkNMp{7+qKUV4up-?SNYFzA5@T?1Gl(34;7ar~0Ugnhe6O;H ztCt%fwmuLy06*zQ;%VatQP2mj4c-nmcs*r#h$@%}#1t7Bhyoxmu%svW-}@)Y@W=lD zlFt+LA3}ltmI|2gmslh@)E#U+O~6NrBfToPdI4L5{ib5z2>x>oV{L0kybTd>xPJsb zC07p@5aj5FL}Ctu!o;y)tT+M%@cXxVfOz*SQxt|JdC-67iIQ~xSE6YDi2@zBEw2R+ zO6y~+=gB|*mx%nEQIGb2q~^Z_1EQ%to(NGQUE=frkxhV$m^cjZV?Z1fhm(8-3?q&M zYT%@Zqi{GtUL^yFfiHlHUm5{|p&$tpV8EVWm=FkJ2$oX1q@*MR#G>DtdL;P$g(+YQ z5Jfpbs5lM?wEzzPuHhsf>R}H)LMRk$U!zzc z06}{IaYqOQLA2fQ1UVbh&DG?W9U;b7u7U|(&d$I7L4kc~SP|?6@dUBU`lK~&JX}de zBZ=*RcLo6P2QEQo5JkN|fvknc+se7Q;jI9ClJNZti~l$szoo4z0NK63wfaasP$&um zg<{}PAZi0O5~u-xh=Bu&0gjukmks_;I;6rc8ZJLv%v{^$eRKxq7TA1TcJvkwNvK>_`fp18-+C;r_>a>D=a!=VA_k)FTj;^4r2>)(9{9ALzM z&;dFCg2BK0FhI!uS0AA4|LlWfe_NO6VFmoK@gTWpeFuNwLWzawxw-=WT zuwOrWAgYRB6R;ElE)U0DLdgLYRskjthhD-cK$Q?U1snhYC8)d%=>H}mIWk388-;6l z8z)aM7cc~lMPpFFB@BzfA~6_692BmoAdi+qUQ(2k!^j~Of4Lni58@F-U@#;K3lb2x Iq^$({f5Aoqy#N3J literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/thumbnails/0000002.png b/src/documents/tests/samples/documents/thumbnails/0000002.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a76840103812c371dd78f2e8bc704ea3eec1ba GIT binary patch literal 7913 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 Ruvbl>s;I6|ENAlKe*j2SP+b53 literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/thumbnails/0000003.png b/src/documents/tests/samples/documents/thumbnails/0000003.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a76840103812c371dd78f2e8bc704ea3eec1ba GIT binary patch literal 7913 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 Ruvbl>s;I6|ENAlKe*j2SP+b53 literal 0 HcmV?d00001 diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index d6ab7eadd..d6e7ad6e0 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -3,6 +3,8 @@ import os import shutil import tempfile +from pathlib import Path +from unittest import mock from django.core.management import call_command from django.test import TestCase, override_settings @@ -15,49 +17,60 @@ class TestExportImport(DirectoriesMixin, TestCase): - @override_settings( - PASSPHRASE="test" - ) - def test_exporter(self): - shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) - shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) - - file = os.path.join(self.dirs.originals_dir, "0000001.pdf") - - d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow", filename="0000001.pdf", mime_type="application/pdf") - d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) - t1 = Tag.objects.create(name="t") - dt1 = DocumentType.objects.create(name="dt") - c1 = Correspondent.objects.create(name="c") + def setUp(self) -> None: + self.target = tempfile.mkdtemp() + self.addCleanup(shutil.rmtree, self.target) + + self.d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow1", filename="0000001.pdf", mime_type="application/pdf") + self.d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow2", filename="0000002.pdf", mime_type="application/pdf") + self.d3 = Document.objects.create(content="Content", checksum="d38d7ed02e988e072caf924e0f3fcb76", title="wow2", filename="0000003.pdf", mime_type="application/pdf") + self.t1 = Tag.objects.create(name="t") + self.dt1 = DocumentType.objects.create(name="dt") + self.c1 = Correspondent.objects.create(name="c") + + self.d1.tags.add(self.t1) + self.d1.correspondent = self.c1 + self.d1.document_type = self.dt1 + self.d1.save() + super(TestExportImport, self).setUp() + + def _do_export(self, use_filename_format=False, compare_checksums=False): + args = ['document_exporter', self.target] + if use_filename_format: + args += ["--use-filename-format"] + if compare_checksums: + args += ["--compare-checksums"] + + call_command(*args) + + with open(os.path.join(self.target, "manifest.json")) as f: + manifest = json.load(f) - d1.tags.add(t1) - d1.correspondents = c1 - d1.document_type = dt1 - d1.save() - d2.save() + return manifest - target = tempfile.mkdtemp() - self.addCleanup(shutil.rmtree, target) + def test_exporter(self, use_filename_format=False): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) - call_command('document_exporter', target) + manifest = self._do_export(use_filename_format=use_filename_format) - with open(os.path.join(target, "manifest.json")) as f: - manifest = json.load(f) + self.assertEqual(len(manifest), 6) + self.assertEqual(len(list(filter(lambda e: e['model'] == 'documents.document', manifest))), 3) - self.assertEqual(len(manifest), 5) + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) for element in manifest: if element['model'] == 'documents.document': - fname = os.path.join(target, element[document_exporter.EXPORTER_FILE_NAME]) + fname = os.path.join(self.target, element[document_exporter.EXPORTER_FILE_NAME]) self.assertTrue(os.path.exists(fname)) - self.assertTrue(os.path.exists(os.path.join(target, element[document_exporter.EXPORTER_THUMBNAIL_NAME]))) + self.assertTrue(os.path.exists(os.path.join(self.target, element[document_exporter.EXPORTER_THUMBNAIL_NAME]))) with open(fname, "rb") as f: checksum = hashlib.md5(f.read()).hexdigest() self.assertEqual(checksum, element['fields']['checksum']) if document_exporter.EXPORTER_ARCHIVE_NAME in element: - fname = os.path.join(target, element[document_exporter.EXPORTER_ARCHIVE_NAME]) + fname = os.path.join(self.target, element[document_exporter.EXPORTER_ARCHIVE_NAME]) self.assertTrue(os.path.exists(fname)) with open(fname, "rb") as f: @@ -65,24 +78,93 @@ def test_exporter(self): self.assertEqual(checksum, element['fields']['archive_checksum']) with paperless_environment() as dirs: - self.assertEqual(Document.objects.count(), 2) + self.assertEqual(Document.objects.count(), 3) Document.objects.all().delete() Correspondent.objects.all().delete() DocumentType.objects.all().delete() Tag.objects.all().delete() self.assertEqual(Document.objects.count(), 0) - call_command('document_importer', target) - self.assertEqual(Document.objects.count(), 2) + call_command('document_importer', self.target) + self.assertEqual(Document.objects.count(), 3) + self.assertEqual(Tag.objects.count(), 1) + self.assertEqual(Correspondent.objects.count(), 1) + self.assertEqual(DocumentType.objects.count(), 1) + self.assertEqual(Document.objects.get(id=self.d1.id).title, "wow1") + self.assertEqual(Document.objects.get(id=self.d2.id).title, "wow2") + self.assertEqual(Document.objects.get(id=self.d3.id).title, "wow2") messages = check_sanity() # everything is alright after the test self.assertEqual(len(messages), 0, str([str(m) for m in messages])) - @override_settings( - PAPERLESS_FILENAME_FORMAT="{title}" - ) def test_exporter_with_filename_format(self): - self.test_exporter() + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + with override_settings(PAPERLESS_FILENAME_FORMAT="{created_year}/{correspondent}/{title}"): + self.test_exporter(use_filename_format=True) + + def test_update_export_changed_time(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + self._do_export() + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export() + m.assert_not_called() + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + Path(self.d1.source_path).touch() + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export() + self.assertEqual(m.call_count, 1) + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + def test_update_export_changed_checksum(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + self._do_export() + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export() + m.assert_not_called() + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + self.d2.checksum = "asdfasdgf3" + self.d2.save() + + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: + self._do_export(compare_checksums=True) + self.assertEqual(m.call_count, 1) + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + @override_settings(PAPERLESS_FILENAME_FORMAT="{title}/{correspondent}") + def test_update_export_changed_location(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + m = self._do_export(use_filename_format=True) + self.assertTrue(os.path.isfile(os.path.join(self.target, "wow1", "c.pdf"))) + + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + + self.d1.title = "new_title" + self.d1.save() + self._do_export(use_filename_format=True) + self.assertFalse(os.path.isfile(os.path.join(self.target, "wow1", "c.pdf"))) + self.assertFalse(os.path.isdir(os.path.join(self.target, "wow1"))) + self.assertTrue(os.path.isfile(os.path.join(self.target, "new_title", "c.pdf"))) + self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) def test_export_missing_files(self): From 0927f9d477d9120c2776b6f214b01ee110f8e19c Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 14:16:32 +0100 Subject: [PATCH 27/61] some bug fixes and tests --- src/documents/file_handling.py | 4 ++-- .../management/commands/document_exporter.py | 6 +++++- .../samples/documents/originals/0000002.pdf.gpg | Bin 18961 -> 0 bytes .../samples/documents/originals/0000004.pdf.gpg | Bin 0 -> 17779 bytes .../{0000002.png.gpg => 0000004.png.gpg} | Bin src/documents/tests/test_management_exporter.py | 16 ++++++++++++---- 6 files changed, 19 insertions(+), 7 deletions(-) delete mode 100644 src/documents/tests/samples/documents/originals/0000002.pdf.gpg create mode 100644 src/documents/tests/samples/documents/originals/0000004.pdf.gpg rename src/documents/tests/samples/documents/thumbnails/{0000002.png.gpg => 0000004.png.gpg} (100%) diff --git a/src/documents/file_handling.py b/src/documents/file_handling.py index a5274a5f4..dd798e8ab 100644 --- a/src/documents/file_handling.py +++ b/src/documents/file_handling.py @@ -91,7 +91,7 @@ def generate_unique_filename(doc, root): return new_filename -def generate_filename(doc, counter=0): +def generate_filename(doc, counter=0, append_gpg=True): path = "" try: @@ -151,7 +151,7 @@ def generate_filename(doc, counter=0): filename = f"{doc.pk:07}{counter_str}{doc.file_type}" # Append .gpg for encrypted files - if doc.storage_type == doc.STORAGE_TYPE_GPG: + if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG: filename += ".gpg" return filename diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index e2313e86a..250ae1adf 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -108,7 +108,8 @@ def dump(self): while True: if self.use_filename_format: base_name = generate_filename( - document, counter=filename_counter) + document, counter=filename_counter, + append_gpg=False) else: base_name = document.get_public_filename( counter=filename_counter) @@ -139,15 +140,18 @@ def dump(self): t = int(time.mktime(document.created.timetuple())) if document.storage_type == Document.STORAGE_TYPE_GPG: + os.makedirs(os.path.dirname(original_target), exist_ok=True) with open(original_target, "wb") as f: f.write(GnuPG.decrypted(document.source_file)) os.utime(original_target, times=(t, t)) + os.makedirs(os.path.dirname(thumbnail_target), exist_ok=True) with open(thumbnail_target, "wb") as f: f.write(GnuPG.decrypted(document.thumbnail_file)) os.utime(thumbnail_target, times=(t, t)) if archive_target: + os.makedirs(os.path.dirname(archive_target), exist_ok=True) with open(archive_target, "wb") as f: f.write(GnuPG.decrypted(document.archive_path)) os.utime(archive_target, times=(t, t)) diff --git a/src/documents/tests/samples/documents/originals/0000002.pdf.gpg b/src/documents/tests/samples/documents/originals/0000002.pdf.gpg deleted file mode 100644 index 0322a8039f0a1e401142c7fb9d5dc08c60ad105c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18961 zcmV(lK=i+i4Fm@R0@fmhlmb2@qV&@30X}=TA!MckkQ={n3_(37H zMcCZAX00DiNf+|llnul@B&doiE}<1}I*XA%fvL*7(d)xeJrh&(e~6XJiUD=a7i0dw}pE=Z>LkH||J@ z-((I_!zPP%_k}br0++oqJt*eP*d3?H>0DiXCN)ZN&H$z8%hvaW;XjZ5Z z)Xc#V?pBpJo;BHndD$vG_W348k56<_Qf%VawcpJgZ%9HAJ zd%2qJ2eP_9U?b8}*>GwnSY`VwM0Xi@iI;U4sZ&l~dE$*@Yu_A+Isa1rU5bE6L~R9a zz9cmUUn(p%V^!c95`l*D`0bM>Zv>*6Pxly!-CEc8^vgVw<|2!VwmxUHKUQeSD;Pmm zq9q_>+1pMQAp`kec4;=tjnkBjhl*IB)UsId%7w%6QP6?&vC@W7`|WU{3GpQnO`Mni zBmZZN+!~t{PE`{n$8dn{4y_HEKBVwQb3fH6l)7wNlZ_Y&LeyrF(6V)%7HK!V5Dcrsni z7r77Bi(C?PNm`m*c!89Oj^WP&iz?p0sb>|m(wOSv>W@MFC z2c*fiQlrE0s%9y~wCCV(aS410hIp>mJ2{U<0X2IyY3J?8U5&naS|f7z5>HLp1|FY% zrL6NnD~|$VJpzfMS%*a|KkmUG)!v!9C74l^Y&Z|1V#0r9ryU7nPiH zBul5SUMjD<_Ax=JUyGy{Qy060nTY;&=I@tMN`4ZX@{I6vTMPE9h}7YKTB`Ytv(j5T zXROKkc%!Kdz*1n%tz@|38zZM4KCIr^@SK1%~vMCJ(jPQ3PY z6xH`}@Q)xOq7q5Tf;Lgm@s-Zwg!!eDnHE-PE$yXyqW%Zkz7fIM}I+HoQ+QqO%zK>V#CFeilx_5Q-K9H_3f^FLq$#!%YlfFzAh?f)|4evvgOP)qj3iokPThBdkhGOa6&e>( zGGv8kj~kElbn^^YW?0~v6ihM*W2X#oX&l{#KLVWYgT2VUHPljrACUl`rhtwyFx#)u zscq`PtPyCf1W@M3t>Dt1Z@R^zYa!<)PC!#dBtliDAZ}9 z!&C)pN4Mg@t{_Z`fyl@a*b$lJ_Cs~Wn6j%e;yPZg+Wv)cUGXW-o_l*?0Wi_22TEJf z_l?Rs@D8tyf>p>VdAB)aazsVWkuDtn7>@p>iLtTdPniaj($av5C#iwH7rPtG#J|B+ zSY5~+R57zkbMQ580oBOG0!7ed$ve6!TQ^*`Gpo2YZK%Of=IFhbRpUO4#5~4-WgG(S z{o^Rd7aG%es%=c18q7B=e+$J%Q#-YnwsY=6JKPh$*1G4*_rJ{r+-N)k%RCWsB3(Ym zrh5r<%yBo$9*_4{Q3{*w`L8x5&DDS*!@D89`j87);!1QMY2~mD?-{$?5vkJHlQ8~o_rsk>jLV5aPW)&v0}{wf>IsD? zabQ*Ai&t(>?cWC*+zAAuCQ*ulIH~%P4&q?o8U>4#|J+Iy(Z!IYWj1&fl|#&q50CAo zk)j&5DJU*M^{PoH2$9OT|APUuc8y{@3bwA_jS!7g_KrEZ&+G$hIRY{U$s$1Xsp7>V z;{v2H!m~9U=%g}^{sckgp16%AnvLr)*++DQ6eOG7j}tX%Z9YA=qMauYPWW9{8yqqZ zt;?eveXx>*R0PXOgffn`HGIPEU=B`2#{UiDlCiBwFuNtz%crg!B%?cHp^*{ zjCS$5IQUo!F@&I4`9%IXH3VVd8IiBoogNjU{JG)~2fG-D9bZVO zKs_i@d9%^~F+@R>!h`(4ff^8PAc-RKhYURT%I~4_2%? zi~9xNt$5Z6^20|xO`B~QamW|t^6FS2^ogIcwD(;Q1LfIUGU?8*-$S9=M^M{eb)2}R?ub5(f;+K}hY zCxCdq0?+K)9=xxAI*xA!rFK#4Qe=gQ0fjAYQ&l>nZEj$Z>pLX0qC zC>M4kq(5&4I6+Dgoz@X`iu9bnVJrW#MzJXn_P>T`=TBI9jruhM>sFju@hl^%KhtZ& zE;|&e;ss)#uZ&z28xdUwxV>`YduD25Mt;Qjo-3%%+qM!+-KwIH}#hVv`&J27cc)S$J{j}@2m~6Zm z5FU>^RRwW>pogsIba&kh5i}C)I2({#gfrSM>88$;u;EXx$a909m@9Gmqo6A&3a{25?sgs(Z3E$KpTY10(`7cNaVX7%EKS-%@T$ zZ6#G@Zq9Ktb^;=hX$0K-aE=X5lCRv9ifF68ij@Rm_`+j4j@9lZcg}(xpqJ!IDzA$` zM~BSE!%2cB>%-y~xvebhHW4?z;kt#J&$!uEg>MdfCexUw)V#QQ5vr4rPY{WurTX3? z&?%&d-C)E1>|YBA1hRjz(p6;AqNqin(&iRT{ERpH-53LwpV~8W+mm1fyw(N?xYyKh z&YG%RL&gVbuf^JmbKn{IY5aW;1sNJ>Q=6^T_8u%X!=9t8+JF_jPeh^84}=bZ-yR`8 z!sY#`tpUnWOzJ|W!$_#{$E?v2ww-iKiG;*s!=AX1_-UqHI=+(s=d%axS1K6`a-$SL zpbq}Afy^}oxFI~H&5?C}9=vzmfO&g36D2%t93q@KKG6FvEYIl_HNmb(MM zg4-?i`t_XUq(;|F=NS~XeWKczEp(+P(*q*?kdty-Q3=jWMM$SHI2V5|j}YB+ii--k z725y8p`UVE!s_c<%j$zDuJWdKkoZ|fq<4&YWJ@P<@U}|(zg%k9s%Pf8CaG#{U`e)$+aup zova8$He5Ns+%y`2`lx%A$GOeWnJ}D%ZkMlzvj87LY4*o?(9U*CI>!3@Mav5o>^Y0ufYY452Jzji#OmHZi@*ZZub zuvIm2xGmXQY<-aNl01E*7&TcCgoKN>{x8mMZ#44!XidALDxR>?AD?yyPd zd=E;@hp0c1QQG`*Fq5Q}ubVno7V}zP$?{_S=pGYxD)m}fF|*bT7;7VrjmndP_0-NW zKXr3%WqOR}-5w&kExog-=ryPch|mygpT)f>Av7Pc!JrlTN#>zh7xk<^C#Dll>|52L z<~}FL$TDwGqj+)5d+?L6&X&Tg!1$yFgPr=0&!Q^SwMMsF2JN5TXXXG77m#a`*`;WL zANWZ_R#`!(;+W6W>+9gr)*D|9cV_&dr6rJpz*Bsj?*Y@?@DM$#szJ-MQw-57{$iLHh5NG$p%`v<9w&2bot{(%&b5 zbECkQmaEJNb>;>95K|QRI~AlV&9Hlyv6@#ZQ3p@VrddE$<&&Y!p|V>`@aW>}yzPvt znwLZVd%OVYf)=OA1&qjbKvE|Hz-*H9KL(&XLJy=Pqx3Zvbk(IKOnG(Z_YlAxC+tLa zp0-FMpeILPEvPL&2qK&}D4C)(2Tu5;>}SH)5R=L1Pq~@3-Fd2o8jY+wXdkTGKpL-0 zdWn|#a18%c1k$~bXS|ef7Afw?*4%CRAEEvn26H3P>vQ$XpMU^Y*eUt&1fz^%`_#;) z2u`X`emRforzTHF_kV|(-B;whf#64CM8mVWbD#OJBoZU<{D|U%#50!L=ymcu<#%D~ zG_%8(#bZJYgzEN)o)&jBh{5esc+u-)Xt!_j>B1fJ;X-S~&zL@0Hd!ws2w%A{gGi#)Wt!sd8^lnq zGF+SR@`6*Ah!=lEPCR1LF)3jwvctBsw&uizF6ps6v?X2aVM2(sg8&95sTIy!jXHj5j@5On_isF@GjVa@fJS%>p9uw$DPHh(}C~DDwwE0YDFg~at@+t`G z7pbc>wBl#7)M2UbxU#VlW;sk=H^`M;&Oe~Vy>6@nKK=`(4_O`s4~rEdA0^rEpXHFm z?0|KmS75)L1XXKyXc0P*&O;Ax`Z_Lr4|RV~RJ1BBpOM$pHUzftU!taKHv^aYXlv7@ zCCeF}+k%>-8u=6z34aYPj8;aaQ$Lq2CsClMj>8k}Bn|Snhfw=4E;U>L6Mz9=o|NdT zl!?{%uhoo19k?D^_C1{lyWqZrnkyYJCv?Lo-OVmW!byM=T?Z)gM5fI)3wgyc+U`&i z!H_n9x#vr)FV?VuKeQwGRHwss&7Qt_Mdh0dos;==<$cJf?DAc1N3veBeyle=kt(Sk zYFp{?=enPte?OzC4WliocbVWbQ_Oeq|3!tAY`Q7YTa~7SR^NJijOz%#QNnU4UMG`A zUxu?>z1WNLrVMFW{d_+$&-}Lk2Q`FQOGSK2W|aAz+PgX8ov>cp54wW4W7JszSx0n0 zhgi7hGG8eQ40Dc?^;|`cd{UG{1inY3Nm_9HVLHyelPEK;9_P>}%*U-g4JB}*k=wa2 z?Q%cbW1%i=Gw>UzXU1TUl201jVP`fvAr+wWe}~Iz7IqCR+V)@BgyDQm0YqX!nJEC( zogxq>%8F9~ei@@Mc^5~G6-+;xZ10r6<3hBu9<-n`7QnonV^Mn{$q*xD)!&S|PEzBJkGW zLY5_JPj3@K#C9aZ3Rl6QEjV~gbXK@3Aa=c?*bGgR+V`aaDQ|1ev7JJ)ht{EYN1d&3 zEC>ePZI@?93BK^PB~QasLoKq&b+R4rZHv{H$ozaJu_Wm{>B`-X^vY>d1fH9)~Ktot^bp|=5C)Bs^JC}iydVNNtqnrI<0 zYR!%ZJe1VDf0whXlJUrv@o@-y27O1LV7wb@?ouG6>X_Emr|U1Brt-ci-+D+#21hDv zN#hmN!yh#RU|FR&WW-lW20-8i3oc|^(NuccgC^ z&a*TS88Vsv=sT4JO&yd^#EQw?zA^4KFK}8{b@v)}dQf4Rq#Xwa#qI&H2wt}cy7Eoc zo?~K;=Y*b#nGrE@Pbi5?oxTlgZ3%BY(tgqn9Z_36%2Jn@2Y9pR|d)H zeQZ!(P@{eqOhx8 zT#Pq=J%7moBhD9MG&cqapOl4KQdmcP`t>1l?q>@C1Ch{0574Yoos~F&A`YeqpW^x3sx7|uI zb&TajpGd8woPlzGgcbi8Q8X!6o z<;-y{&oNAcZ9})_8+W+;7fzp?9#=#n>Q~EZZB1Qj9@V6fpNR&}N#ho;d!EL{9?yya z580ixt@sZ~4?wRuwuukDfi+4Dprm^kZZOQKj-pGTQ+)T|*L8lvcjAY=FDR!Wd8a5$ zqI0~M&LB(VyMKxE9rsjTfYJalR$ozRRYQTp;6GbC4ld>*OZ`-Q??WrjfEs8GH|c2O zXaRO$49iMs46$DcdG+Q*9@JQ2%($O8=w+l)w|9#j!6@>hfNon4NaEEW7@|)uNAa#k zbEMmCF}UGtbj6|E$Wy25wr5g`_^8#hr=~x_g4sptC>!f5APJ8P(nAkgV2o-!*QC)$ z10`K%O05i zTg}L+_9C+s;q+_HOE4+LU!ob(F3VqLmIRoSufWOegOU?twcH2M81w-e%=ipOAM`PX z36d9z#nZ#{o%p@fZ)gPc3AoarB*v!iv6XUmbg0yjzSETv;f}o?r{XGeZ3U)tgFb`N zvQFrwnxRYXd)4YDekaci>+3teKMu`OyJ63l@D^K5jdy2*7&-Hz{n$cPy(z638GI>o z1XOwVBN8D!2r&;vt|_CWZo4hNF!Ea4$?UYR?X1I)DRwr?raN{Z$w?S+46wHw|NP@nWLFoel>lsnwT#`uo z|9~UrY&B2A10xw3+-XqV!yZUBpRCI#(mXB0);p)$WgjDQs;}O}+FN!>v%tn92C={b zi|P8Bheyaek3A{SI2baeyt5$9aHVURut>H$h`4nEGBgM1P+ll# z={TaI>1F)`Lm=L$kY(_bAD1&|5vJq7GB5y84QrjN!}0dJJX$Yrw+)U;hiZeuBil;Mv3N<81-0rEqR=AgNNUEo46DlDV_ z!!+ywL4TQ@^(`w0>>I6{sv|3OZ70o-+7Ys4txza=JW^pjWM>VK-?K0b?G`tN+|=ntR@0OLeLif! z?FNjd8z0Zv<(KwSW{LSvKx~izLb<4r*;=hL_V^;&@bu9bIdXB6-?LWlH zm+HvRUL2Z1q^L<|d^NeBZufCIocdcxJ~F8gtXA6PIq)dWLQN=v7mX{GrOD#7vWeS3 zHWbQDBuA6B`ptkW3{nwP@qONMWI1(cX1sf!Bq%XMsE(|fZUDC&Nib?d(rKFrEGQjX z+JWsXlXKJ#nhfQ^X80M`OZ&BN;DNv)%%Z2nH%$yj3q zFZOTK70z132O;rKIzJ?iv7qgpWpZaU_kz}n0^}OgH2Y9kwA}iVi)t^%_*gI|ltdgl9pv)d3dU%W0Ty@&EVQpYRNPuFZ zVk71(biqOfC@sIyCOszc9pIoY5Wot+ZCBj{DLZC=5N9HEnolK2Zj07Pf^=aZD$Pw} zxa~RD)uzMYCs|-Y@7PMbNv$s#5K+7+8OBY;V~2HcvTRhk+X*DQ#t9&G`vyI^BO#2rtI}>;7OkT|7>ZcNmmlj3{$>~fc0Z$2$XPqs<;?Yh%8ta=X%QB5)ELtj4cP2$ zCc+vjgl+oPG}iM{2bFFmA?*Uk)1Vl{`vVPmWI_qVTfm$vLpqlmkEKhEIW_BvfAaGw2{PN;u`Lxw4=;z4owM1Q4QxdbaCt$4|Xy=toQLmAF| z2sodB8!N?fpR?tI!8TFDtq!5u=xqGtQG8hW7pM8lRRbR$_rSHH@PxlV{G31B#|f|o zuE#A*b*AoO&wlO=v*iBm>>P45^FEa`M_+@bm#NSwvCqLi9+?wk1u5 z_Ng@)JdNwo-i{BBDV3g`7=A%D9<#0Y2VqiQ_T-!u4?5us33*4%Z=|5457Qnxd+h!) z6;=b#(F`#*2U7i%5T=`Vl^{ho(raoVqTQy$Qft+fuOG?$Ox!<^5JxHB=lD{`K`ce) ztU90%$~5wsBekTvo6CZ|{wPAvTt;OMGYSlFwjzIqUz|?bz(tjM=-zKp6n>Of4FXPI zKE55v7=H+Z<{^89T;Kr6SF)z>lt^TgJz(O+yi9V~sZgM(VsbUZzm&T1-X1^qk!|Ti z@7=>rwrJl9E6shg^Qf@=s(oqR`MW#GD5_Zq_}4XmzrLy_70GZ`m3il}8<3ZcDk4Er zAut1+ntDVqj3Cg3^~dETYc zU9x9x?l*Df90$ng_QbyZr5H~V=@KhV?07&~2k zjq(qsg9MJhTYAYPit=T*-8`p+=}Bl6rKfQK0LcXZT{uQ{^+6kO7udI4pDyh_D$C#_ zz(Kp^>K;ITZ3oVL98&XNzp8IHJ`aw_Gk(^nVb7bry}%4cYjcx@e6bay)S zD$GVH>tHBISFknB!%o+5Ymou(cVvm5Q-=aW2mQZjP3NE0KM_+I% z8O*yF)C;@*U9yx85-f=SuMz76Nmk$oIs7cTm_1k5_SeuMNwkA3LS#cvt2m#r z*vLWF+@9%n9N0+ti`x~7gxhUxY*=TTzm>k-H7#`Cf`H>6&v#>`#1(MJ=G0rf?geZFQWMkyvVX7&(S-Wo51-O&2?OI@9SN&o!Z_Ys{ z0)AZgdc=$H)|f1aU@0<`^6tXui(|^?m6zyyXFs&V@XM5qbu{|Hyd6txPqq{38#*^D zC#0___5u7G42Z!M+fJx*uPPiveaKWXN7dnZ>7T&w^>6I_ZyG1ElLiQtJI#Le+UV z#iP>Yb1h$+vTo4OODctJJw@7pC_?AuLF6-#bbcy3+Q85x$gTI!NsifhW4qb%Dbf>a z@D$P?w0bN@8&RIi19r&X0hZJc7!twa@kj#R7^uy;0t2^G2&~A#*O0Yx| zA+o?Fae92lEr#P{n0R~z+G*l>9aQm&K%t>Wa^5^|5r9aFg!$zuM$u6meE4HLamljN z8j+QqE2t+cy~VYPq*9k^Vd~=X2K%F?b+E{f1)S$o3nhlKeP_>{j}z$EQbVYezMC;> zRoDMJp@J)XXBace((-#)kF`-O_Rblw_xuMOaL(%5-r0ahm++)eTxL3`@&-G6ymjRu z=ZeAH9lWCdCd#}mC_+y4EqwbO@hUHHPX+JrYd>Qm=I5GA*{%KxRDXQ_ z;7|HOug3}n*IGMY%y;H2h7Ih=CQP%nTmWe_%|3IP3+CQw^S|0AFOl|-5w@CSR8@5p zH3s116I@XIna!c8@*0}}Wz?rn*+EHaqU`2xUo!pkm3q4Z4FR)9t`6Y<36$7>K;R$N zEa>L*qp(@EVx-!CaX~xwp3>6F(OiZQ!@id4WS?2VUr@+*j#br`M?knSd#Nfgk5XVZ z;(i(xV}V@9i~89%Q`bP{f)rLd7+qi56}NncX9+7Xd`}YXrLVA86_}|l-c0gZI=ezs z@j;d$+j+39`-aiTgyyG8?_nN!gIq2K?oVFyxJU*TVN&37 zY?txVTncOyem8RKH7=4I>sJ+|%V1V|!Z3C8J$W0~%PSA^*5LhdoK!e;{ge%5m=F|( z4szdD?_44u@}jYpeAJoX7iuxp4st9UBBsIBZzDLjqI+1ah9(>a)T8Nzj;%c5V-`gI z?eKTik|RS&n4@!7T6dwTM9gr@zC96nNGUJk8i4h@qZ$6xtmRO1e1UA=qhf|Sq8RYj z1m^@W*GUbeb4$;n$K-s=k$CF6X5#cb)-=m`+D@X`mgG!B0pk@wkzoz^4-O0}lG8u0Ega{Sx!kRna*YB*k7Rt>Na1P__tpBh zJ&yj0eCa#M8Ie*O)e>|EB^PbWXA?8nxh#5-2N8)fg5 zN-H`iVeHdNKr9z8HKdQ+K3KwSq`tp_%(<#Tjs-ut=VG}J+m$X!~7Lp;8 z(R5s$Uq;Dkm)0CL@WA{t4*&U#F^*dNn?|&7WyqVffNt=-c?#h32)l%}V0&}@%hg_D zqM2;+@{V_5{LG=vDsr`=?>#R9o4X)aFJi1%CzENw(dj+w0$HIDoWVXL_FOv zQ)Z(i|7aKGfi0(kAHFAI{Kw!P>M}6;<|@U~hv_rT*|M8`om3^&cx#tPxwK|L1`U%@ z411)=Q$nmFzYa;cLrgXP14_9cLz@}2@(h#BT25ab#$pu(ZNq}{Sb@_QN@>S{tRVxp zj~9J^K>-uE-EQo;*aGnC=PC%VHDjWRC|ip&s+nZkJG0NKL}i8q50iU%e)YddMPr|0 zdyPz9Ur>FF`y7Ba)JW;ZK)@ob3CK`Gd%!n*|DmT)HhafriOdvLsWP!G`M_HiJTG4c z#8&AP;>r+>lIpwyOI>fm2*pA9U%v~PQ2_RAo=WWfMBfYW40N^GLYwZgUV0qjMSmTt z!%l2RH0LIgl}Y?eQ3?Zj!KjV(BJ|tJ3#lkAB8F&;S(XhyqW1XEL9!uBVrbc41z#j$ z#&a{N^`K~6nIJM2jU{8{T($k{&9IqVLs}d7&%~668s`Ux#ibC~@jH_J4loMj59Dm@ zkn}Z)K5AfV8RtMWOCbIq@;72oSSCefj;mZPDRZ z4-;inx0qy*8gDF~szSD-#k8H_0_^!bZfp39R(lVyZ zYJj*^*&3y>@*15XV{!V4J^?j)G=&TrjI**NqcFl^nTyStG)u%nnZh#CB$ye)XO{T? zw;+eg%}-Ni1ZcsR$bT0YCnw=HuZz4Yb)pvhEG8vwvLnkT28DzZ%Soj>R+5O^&l!Sd zp6I41I&A~W&wW3OtBMP~{pIG8P|p&6@tGOLIZ7mE*FOUWB7PPcjp~hmz2w2Xgc&Ri z@q9gXHX8I09q|m1slLN0*eWnX`>=%KHj4`DN3wzVgnj>s1B4}Y8;5WSHaxcWrCu&J zCzg4AS?bV|5hmB;2nskE7(TZ?Y&IOH5@+ASe|}k*NE$Cch(Cw((|^?c)lUDa1$hn8 z%~P-DH*^OA-P=ow*>UODU+m}z(D6Nb3?6R?nuFsx5}QfX7_YfW;#D-)=epIveEDm2 zS<-!VSq*v&?w*8=6?WW}UbQvA z?91D-__bhW%nB0@5i%_NkxwBb4i#7QW>SY&aLFSK6;cWP6vZOT5bvzc@>4j;mg!`= zx<1)EWj4lH_9Di)WYeD#;t1yG=L9Yb^LC!50|xHpCSxk;swh3Ayx-=DqcYpjmT6Ev zC%vZYbZtVw&8sc=fkj5A0=S2x7DDBw?*{Wa=xiGs(MPG?$2mJr(%J|BF(WNK!^eNA z45L`#7?Vfr7lNj1;kR4#)Znabd4841iZ+6$sHundkpi`o3&H?7^+h=p zKg;?St_&g%nfa4#9z;Yi3F)Xzd?Ijx2P&=;$oCCm*Q6>z%!`*uX9+HvM zGs)#hg(qMFtA%g+grUexVnwj%ZW0&Q*XQDZ{=uH`sCmMep>94<2UBn9@rrqV#0(2k z@X?{lj#dnh#IA0?ziL_yYK_Rk2fT@M;&sHM6;cvt4vD79(^FoNJpVVy^a=GK9aL{B zym@l0EM}h{FOTZ8oM)P&YZBuU7iK>knJb8)iVH0^RLLJ6($)MqDCYBbj?I>C8b?fp zf@oj*t+g03La8~rMw>=zhhUY2ozSZYELv5YpY4O6Hh8SQ-A5HpoWS23OuemSDx#*g z0SNC=vo+?cF!KBY*zCJ-Ez4kISa`)rp}ay&RJMXcX5&!T2kaguhxLd}d`WYjw^P(z z%T(WT0B%734b38+Aq--+f6el_u9<-Kt7$wLknxQ(R3lrOKb!)t2veJx&vD^RBY%_i zx~xE|zZ&wc)q1n$ZroGNDY&9Q27~v<5lgT}Iw;6PLVJGU6pESx>?{WdAEZU(9wm2` zPe&Anm>GYd47cOkXX*4*;d3Chhg}9#``$pdEgm~!iJ6mhg~hL~Xc7e2%bm;_YCA1~ zSIzyEAtNLD&sA3lj_70F<#7imLe*u>7a#*~(hlaA+x~7mtS+zDJN@`+~n0B`MbWW~`?0omYQP{O;#UKz>XdxIfm8 zD*^nG(9QYnxTQ5IS_hwMG4nash)Mfp3Gxw9XrY*UguJW)vdmzKC-r(#!Sc_bP)BKy zL>km3by%y*F6{P4_tm7e=oX;W{wbfgjv<$djnS%$bUHatc1>LOtp*~Oh9&TO zTHoy}d4$Nb;6QVK2|?1~)Y#xCe5^zsJX>MJHDsCW;GZ_cFn!MDY#yKChZ~@Wy*iw6 z%lC#bsP%eNaY&Mm){1(vgJ>w2T0^#<2YR+&^?TNz^<2VLU${a*BuCsABH|Ug!2DEM{Tc*u5Gu>v$7F4-Ziu#*DcW@#ee=bX z&ri;DxHt=#yn)LzLv5U2{7LChB6$dy)hlqKQcCCLG^KlVfZX8I`Q015a<+&R@7qGb zBRum8&B+5kbvQ5kBLc8_y(Xaxfa4jbc&lSl!O!+`v6w-UvS?KR`!z6bGFaQYpLF~o zJrwq8T-nD-1b4je*!m)g_3nskQ>PZ(qzh5JB(9|y4K@n@v;T1sJV4CZFx2zxqf^lt z+nB<2*YqlqXZ~9iNz9cg*|BiA0&q0Q^Xc z&(O2+hm=`Bt$;?^pPn$1(2lv-S1-Cr13c{*gDK_iHPe(h>FJ{$d}AoQbsMkwYnQ?+a%|yRBpZ4Oy2r7KfdNj7OHF0NjF__b*xk-OG2xYez=`;%2%L z^3=;3Mqq6eWvotp1;nDE59HttkhkJuSjXuJTfvu>A~73qol!$=E<&pHEh&&9cu7xh z+Ah6DZBwxrM6hhtKV}xz)Z%Gg3DfO0c7-Lq=V&6K=m2=R1z9xZEc~#7Y1AJ*msT%0 z+wmHWtR-w(k|#tS?Z4UYoKf)l=M()x7XZ7u!Ncob{1@}R>_^kx*5Iw+qgrw z2J|Tky;rwoXbb16Xem!QL$u@uc-fNWR;nh9L{P@=b0}5m1PJnXC@AzX)Bg>K=nx7( z$_N?U;4YSrr9lc1Y51zD=x}^YF}#26Lj|jUtqdj2-|dxuIZ`mb+=RRrO^)mlHepgv zv#wT9jQfQ>owNlOBWsh#S_l+(W=AOzkB@~+AWVN;SG6YNOw1RwLvuAiG)cStRsNjUHt_Mz_J3`srV{KuC!1WJH_9jMF-Y#xLb-sfVrOqA$z-6246 z83#Jxc*@uVb9(Yh!M;m;s}wmmC^ zKgwoD4nB-XbU9!sCll`N*8@{7=FKaS*aUl~601X%Yc={jVFpG~&d95M&dt5mIKy`n z2~Zu7_E)*Zf~OVz-l&9F1bI?u$J&knC_JKxiLhrjas-DNYQ!CR=Vg3Wf9IZt(P&wHA8AlQw!TUd8DKtLo`ccatH{NObkLnSZ@=}WaxJ9)CT7z`{~Yn{FyLw zIJjGQ+QyJEs%Y~w|70(KVpUG~#j8N*<2})BR4lmzJtJ`+*k1^fockhHD?Qlyc$JJw zK;p6`zm?Bp>lG<*QVF7UL$Q)?Zqf-w+oFv&-%d1|`3&M`#u89c*kU=|P}Qd2wP-JK zD?)UNIs?P{!AruXGCYn|RYQT0DzNl${s^#)VjU}nKCSEDHEi$lOr-c6g;>3AYW$=|0&ZoQ!7NH1f$ zIq>W1S%}ra*isOPOWpx^^IK$W$9CYf#YF+zdQ$4jFpU~)1C{A=vNeF~1#AY==j8s@ z8r3qp4G_0lfCbWm`4bfRteo@JhwVgZVpVZe2X549PkH(mGn2bUDuXL-lC?D}II0eH`64;S*Bco}2pd6r z%6N^&7Eqg1E3q(eClSwdbI-e0&}EAa>Kw?)zng^Wh0L}==MlPxaMSpWU^0pHOk@ki z{{Ix~GD!lxc(2(VbQL%p)&lCM$sn%YrlR0IfzeIyev~xmRR|0rs^6ljIR+AB$~;1k znT@TceRTXRXiDTkb~4|PfS_ug>FB5bwO=5^m5viMnHyv4vR61osoZ$J@I-fE2hri; zJVmD|WxRWUIG^eWTQFCBSjiFc#Xj~BI@Jin;h;lK7SZi9E+7=!b0~11m zSB-B;Y_s3+#UA^mVL!+U1H~o1%evA}V!J~T=Rg-7o-X!4fWteM@XKkd%(~{jyX*&a zB77ZQ5+yLn5phEny}}yoxHD>W2z5!l7MBv=Usci7d* z^s`Ay^-*?^cIdYh7J(g(6Fj5+uX}6^YdvIfB>k>x&A?35$`R|pE4MLIy2W6=u+Irv zE=PRWds(qSm0LUsa5(zf`E$rh$3@vVQ#hz35S+8U+fKfQ(GG1LNpt@46Ql##%Cj9Zd8sD&8tx1>!S`)N#XX`0f(Dy|t&sA`s)0*1xcv^(x+zv+&epQ&R`E=_Z zLxVjZW|U7XifD{^U&i2P+>HSpl-l0c{!7f!i!0K0>xO|(3U0)G<>DmFr1aMSTM(jP znK!3Bx4R3!M}5xn1IQlKXA90jPLMYIC6R`?PMn`pS*xkkvI z?1W;VC+ivxb$X7&yo*N?rRJ~iJ$W=19#&I3Ku!w1DdHiqc#H%^XG->%S;B(U?i2HV8qrXq zz>*t{9L+2~tRo~(+$}9%gAQb?DEYpr_v@dvN|pmdVGFN=nH7;Fg0<;#-OUKh-K@a{ zPY)xi$otN>Iw2B*uT?R^-_mxTMMM3^%c&K!W`clTj2%QTfZ# zlXBGpX7Jh|2Um|JIsSFcUR<%!`$|))x}KtU_^arx6Z9sQE?PveirPcYyBv593xkkA z{N5#%{MZ0PCf~`4xKLX7Wi;J;mL; z@5SBEQB?A2F{gI{GJ))EsV=nKvB5u|ol20_4aru$l^a=}z4&d{lYEt)Ng}(}iGugc zs#_w-sV?lMg)4={bRPrpA!!2%@WdzlYVs8PF#!#M!`s zr3}1nbm5?(%g$6-q!Hq3(+=RBl#k*ywq-}X{f98S>2zDo6&PU4HF>r{w@|#Gae$K`5-!? zG{9cR_!kJ!Hxmc3k2!-_=*dSDX9r;6?OB`#(~#ugfUHY@#O)nfVWKVqRx`T+;nyPH zsmII51!50l6++-^Qb({gN;AI#?0U5WgrKII7=WH9N z7ke3=sJvJCi6CPOQu%@55iu}vPZ~aVMM|E?iJefDi46aGSR@@1yR|HCjYvuk6F4Nz zAgRXAwh?w6A-P8LCk5*^_x6+jBC*UEKU2Ky+9}+xzl$$loQY5mXN*6w|k z1b=i*a|X7@oZ@c_we`S)HPCIB4M(|vjgB8N?m=IkP0Wdyo0&4)nqmB>2yoOjM5!sl zSN~qaGQgnpUalvWmhP}S>bnV0&WF6j<0{!HdMm~ln`DH0TP>0y8X_jtH+Vt@1I|&7 zX8TGY%}+B@iD)QJ6~^BUK2{iKSKQ9#@e`AX@F$hYxK*7F`uCn+lUG!Dtg7T2n&&j` z!ZL|^g)WPaPSRxnDNm&EeL2Aqso_?`Eltztc+P%`*2vlHcC5yUjRq?xSAW1pxu^3q zF!i=FP%`T^pw(Nc`uUK3-xy9W1-EQ%Eg5dkOV~+gRNqjr<5aXu(9;%43bP}ke_UYC zCTVq6uZT(tx)@ZSJop}jNJ=q1Hj5LU_j>$w27-H~eM=co3EQH*j@?uJbYvW_D!cME?G@NqypkQ%{d`fg7H# zorK$Gm<&3}s}f45<`(v0p5cIka+DYiXhGhgxQGeLpZX@cVYWK5SDNefWnMsK17n>c z3Qr2Bm(o~R#QV|}@Zm&T#yxR&uwa8JQu)WZSuHRbuTKPONTAWd@&dS{;dFtoT=p`P zv@*2MIHJcyHMHLC!#0;#;PAyoZ}%gu-AL-5vz<~ABaXZHHmp-Cx{dOx6{i=1q7lhW zfOERFb(jk#vQ&oJn87TCb>zz{qnGFU<3Q$e+EMpB;mml@`e+|#O1HC5=JvC$_NtkJ ztX21YIFomir9{O2KF?EhIPiyBYEJ)>jhj?iRR`T5dHT&I@eC>II|-s0vM!i%6$KHe z&32#AAnK~vj-ccq>cr~hjqk)rO9x3|(a|#WKa0+O=T*D6C=svWaJCh0Od+og%nUr@ zXlN=udi0hit<(~*Y1F)F;Jm}J`>XDBemjAOTn(eJGETy(#R!3+=IvokZvhgk25X%r z*iS1k2HVuS-u1BBd;R$26(ozjbXGk7(JlXQG7yQKn!~1Htm79@WHw%Wo*S7ip^zKS z5CUN=0WX)a_Q&{uL=+{~n}GJGusW(#^8`Tjb9~=IH(Iv-_lk*K*V+|xq;ap0iHfe) zHtFDSWO#Wc&Yi{##~V7a#FI+>J=49)!n^Gfv=MZ(^rk%^@;E;Uo-|7@^4REDoibVZ z_Ry+!W%t<8?z|k6Pgo6>E)*^7$pPwVIC$_fG9|(o_=t2>g`=Amw_CI)US*W}@9YQ% zpGGB>v}K-nQYnB_x`jEzt#B5Yqqc;w8ZeTl(&@ypzhsN_E^uLLKj<98yQK6j&{OS= zS)l*F0(HGOg{Zg2(5mXTjZxki91j|d+?xi;q`hrboC!5cwf`LN{;O+A26GJQL3ZKC zXrKUQkA14&E%`e`igS;80ZiGFt4JRw46yoM@%W^zS7Gjl%_HG^dCnw zv*wh5sVEq#WKV=7r>LAoWxH+7f#yCSn`Hw@?PKt|P}rzTg2X-X95@E_Nr3v6QvHtPM!uNu32dk+fK&g$tL~GoMtJ`GG2@dyu2Wv1m_d zR!A09i_|wOoWob9VNB+DEME8m+kySmCKuVEViD;SCv`_^T4(um{TL0Sen@m{m=c8a zIee&R>@G(RS5+0{6@sc+E-I*;7L&nGz7t^qRGPmsT94*?Xg(lIeB} z`j{lDD7hv~tR3xS>~bY^d0$C@rehnZG?(J2>5s8>V|GQ} z78|xLXc9V>g>A|E?k3j-1`$)R9Zyy15BE|dL6NM;cn@&mu2UL8mkFJ8Dt*SBb@8~z(Q<`&Rb;Dq5C zWy(U0bhKnX$){+JTBZ|tE(Sf;&rQeAp5AJz9A&r0D*_}W(2S&e^o1^Wy*R7?DEDSS zX$eXluYAR&%`0DEUNlw0_-I$ znM2gWKxv`OLVcUbLGW67u3&Nezg~TYNIH)ktS3s{9rFNEXgNjB0?cDG2#ac7p3~Mu z5%Xj*&IF>1$F<^3iaCxON(>m~p_(|v;J{0BeN+3%88EANyL&(Q9XGIKr`F2*NKkhs zJ=kKHZRt=sAWTW(tPDnl-q3~ruDE7h5+<=|%4u5p4X*G$2%Z^Q+t)B*M3L1v7lMOj zRZ4L7Y}MM1cpeWA)DUJ=7Zv^50v?WP%?|8%)kQd$oMSQlVFtxH1G&aCl!Eltf8G!W>dyvW6v1v@N+~g=I>QXp$Fq0_G$#qijHj?$Fa%X70_`F>cYFw zYS$QodL~xyvZRWjvX>~W&cyY2%DyKi$NwCC1Ykc7M+a_X+dYB@-LbvfAlXQ0_=V2Oi2 ztH9=%tjh9)Wp?St1J7R|s@4P3L`GoXO?zW4gmsKDX&IQsb&qpKrkfSIDgYV;A|XQCRFJstt?;`^RXks3eb-I%JLrMJdYw{9x_xS8|`f`Fd7I= z<(v1oEa)L-0v6d+6{*qzpTYRqaE&T&Foe&AU}^59R?-e_i8!N-Kaw-h#`@AY#81Q) zT^okUpUrtM{=~b0<^l$=+fp5F{kR^?I~IEac*x%*lXj>dSN9JDJ?GPC41|GWY5~)) z7=K}00e4*mhjC(wNT7OEyBR+=6A}1TyWEc9`B4t-*ST$GYz|_$g0~A(;55S#y23BkS=}=5{L8BI4 zmmn>c{2*dgjPndL+0kCQFIW5IPO~u|Ry6**q0u%4V*;{2=ScN#%u4sFOrF&u98N5N zq3M|!%J(o-gepQzZ>AQo21xvz)at1&Q>r+sb=zYa$zmJDhoKPdws8$L0&^0EC0YKW z50e&mgT2*5jfqTr1zIG{TD~bqrAyBf`zsIN=31c%9IKH(9KhNm0CNF|!Zz;L!W>_R znvv%m``!%<$er4U!AMtaMEm^4Gw@Ej#U)GwB|!(qsvt0yL4y27Bb`}eK^fYW#$V~4 zWGG_gX3MT#g7O(1LC$RIAW&smBzMkxkwGIbnHJ{hjchgdC^%`DyJN+?N>LJP+xIYY z&j$zJgPo*@;q3GID-gc4QI_N5Vqyas1x8CHEb_v#B_cqLyE#&8&JOTr^`7I&U0>Ki zfS{oee%$%uQEVr&LMr|8DNgP5Hi+|A^mDfp<&7e-E?QcQTRtvSf6ei10p216r2Q%C zh^d(MPRrG^dKk5Zw?nUP7|GN72{HJguUzTW&oiX?W@Xq z*>ZfCuO^{x8y65CbdM3z79BoG)oA-yroBc^R9O1x;qcsGP4*$E08%E<&WaAm&awY- zE&=&pbKMFT=xV@`)>&nFO*%065k4K(C!2r&!~YA%N&71&7<(zOPxX@ z>(8s8-*{*!9DFC-jpjw-PTFKW3@!0es-7_ZdgHAe;voH-SaP}03Ke6ZjMWX_4F9>G z6op}{-3dwh%%<14)8*OUxiIIY$%Y%@ZVLp&=EBs!Nl;r{#JL{e-()?S*v*i?r`u|s z^&2tTN>i59o~r#Saxs|^*iqtT6+~a3cPvUN3QLx)p{Uz9wB3WGI~nPaw8@X(;gOKN ztZW$Z`~EAO)(%oa6FJNf6ymilews^f8GCBOuL*WCH_^JSe^d>IPPzuZrn zZ_1Av6UFf|&pO_mdCQHID|BOSAwsFAv!zKgCtMUbIYLo=-RQ1-j#JF?2-B(~EKRkY zMN^nuB(`Z<$L~NqQ68asz>o|iR7?N&7Cdd^+#o7wr6jgsXY|TmAW9+AC1btX@2f@r zCUcSym43d|3L9WXp=5sqyN%I{9aUjwiEt%|Y+#DjCLH2%xRT`3iUGBzS=;LD(CKV$ zxF0gc?F;xKt+ZYr`TvDqU2hkY3deW~liV_dam7XweU?cC`IR8fEtayikC{;Kz)95W zd-tq((W%Tlr+t-vuYQ@O=b|H7{B?v853zVl1DTGH(dC`|6dy)n!Kp=_Ei&dHBn)BO zM40s@#5Bh&nS>*|lzr-e_Z58ZLD}T+-l^@}yo+#K*cDFvo(7X_-r*z3|uD3Hu1H_KMODiQkEw39?(q*kX&wHDkOb(t6nH>qR&c16;`-Q9DDtZ>6f z52(3rxnrT800WQ$(3jk%&H10V%nHAYNsvIU3Vzn}(6~JxF4Dn;AI}nxPdHMbZboY+ zk2%AktuvlrFS_jgYKi?b8uME=btCiDArVC@s$zi3ZRCr&QaX>B8~JrbZU9@H$RH`n zK@UR|rCVgwX)TATuf}>C_Hb96*(Vnp9b5CDj#_|;ixInoxO@U8C$~u|>5qw{OF7pH zA67o4_iu%+NnHtZ3JyGC8xAd8uc?&kZ1a$$lf6ir-E+qjU00+&Zu^8{|Ji1ocy?ag zd}yv^FM{O=xV0XVVCtUm)gLgw6y+ZU`dlb`>so@%rPwJ+U(;^p!q>Xqg zJf^$Maf_ZQGi~R8{T|ZT-NICHIpGr3%EPqzR3Vanq!!C?F-NZpDTE*m6lUAV^7-PN*t#!jdzM?9`h!v?BBy!VLZ#vC0ENEDm9u8jRkv z2Ld_Rr{8|!1OV1(y~8?4QTVC>V)>+4;iY*YuzHpmVa;l@#i*1-BwmO7YUr-Kgls9F zpFK_a$aexc=|R>ix|{(C`PUwvdTS1RM}1VI2I7~OA5pc3cOxVYHshOt6?Ao*OxR?t z*e19XXkL8<^1+Q|ElU2m_aQCu2kSzmwPRk)lUaM{X$HHTyy58jeoAX{&qdDYUn2=j zOo;&#t%2r4FL%>`YMf*Xs@+*`I-^JHo!!3NuDp-&plmDN)l4mdxR+QEh#@z$)qq+} z9Hmu&iaQ1o0uGx4w5=dSKAOFlozO-OSB+_|9Lwaa6$*mL9{ShkXggz!6J8G{;uP*+ z?F@5W$TuLV^Q*5kpR#Fp;>XVTc`G=th1t@sKk(T+ds@)aiise)fT z2muT4hDmCl-AJ<;eonXoP(mh!&f7!?Y&7`5^(A?0k9aQAep?lrMWx*oJT1Co!qRW| zdmfr#7nn1CQ{UXm&1jf*x}F+KM(!}QSwL2c|MJ_8roUhYKBPo`ZeOoe)L-^y_i5P^ z7cb+?k*lyDEWF#>BpQj9Hwkp># zvgccnQ6YW%wB}*kQ%MJ##u!Hv;NklG);Cbe%w)!xiOJ~2lNj_WHs4P72GV=}u^-_$ zjIY=08edcIRjOrU1g_q3Qof`BeZ=tqX>_eI_W_g53b7P}jeg1YaEPNX*UL zKTP)Ad4G-es2!(pkA+{H>_*UTUWWaU1n_(PWZ2-KJ)QC)lf&%MV$T9wm4H^c;g2$< z%H*b5bD}@X3XRicXjaE#9##@y{GhZj6#bq{Zh6=?vu%0eza4A zh~9Z#jEn^UlifFsi)?|0g6l!!K-QkO)0x=1orp9FCq8p|HR4z~^57++VCCH$h}I}+ z^T%q|G4tL+Gm}Ks($rm7nyt|I!=sn1+GH^~f`}}_XtmD<8z} z#TptO!!4js5*oGeaa(U%aus0G7rf!K(pUg5lxqj|^@Q;=TR4nFP7OgWf(Ot1|2;3-UbavWmV~cXk#3skEM!5ktW+St$cl2 z15zHdvL>f48FeFg7ZF3t(V>M&$2vsJ2EicJ46sc3x|K`qPjT(imnrQ}rQ$s2Lt3D+ zj(wA}>LlxF`iVu>KvuSN*e|DG;wPVO=M*?cix(nIUKh%dCA8SQerH0A8;6Gwy5 zEDgy;&w?8acr)6RT9PZ2)g+$3j{e;iFOxw&@-o+D;AB`pSuDttg9n_HE^LWa5?r1i z5NN&mD*O{67L_)P_fw~A%DWWX3iq(Hq7Jfsl1JcVVX}mdR_vc03t%!+YvirGaeL0j zjjBVD&9o0bF0{iv{BLe0^98$0RU)>ykD8T4GyEMKCX!3cvuF z)XA*0C=eqer`gG`karxSI3AXF+IQ;91?mSUxqY~&n3b@YZkIOlYON4w1d9-NxFjMk zto5M-LIC(!xC^@7_w0?!9*mJ{Lac(Bs@O0fyPUw-2!v{OBjeYW7}12v;32%wQ2<-` z&j0|qttm|*r5o+uV`6^hERYyi8;tv6h~C@hBFfyNQ3YAJjl7SuJg^>n#5@`I)A z^uMzte9_uQP8apRPr*>^|1dvLI}P7qB%^V_4Z^1P_0!CWd~0XOH&;OVb+|{9!R2j$ zO!C=Sl*5wV>)z%I%+twKZ&sGPa1+zQOm*kP?&}2W8L*NozP7qu3<}#?AQsWo5?six z(17$oXQ3&>+_kh>|4VdTPto@8InO2ImUQ?tD^pE5%N@wvbl}l*D&-s@^D=YYw1GeM zMbh$sV+uSP8+PbjiCC{r`6sc}!|n`6&U=AmkN08E1ZglA*DlP7>61duBb$DuNytGI&(V!T}TXLH-Gh2D~^@ zypZ`t$t&kP@tcF|i+U>=qDrm6fA}a2h#kOjnu}iq=q`|$x|FO10yMU@yv7B+QN&Dt(-(0tW&rekCEFCG0_U<#bNJjuf-F|{bRsE!~K zP#(D@a}r6Nj2E%&&s*`P{IOWpX_PT?3_N5PFGoz+`hzYhkM;X$MUM-cw{9k!l*#Sa zuK_!1M8H3m8Ft~aB?jy}fPP+<4cj%^)YQS}JZzD2bSfKh zkU2aK?Q5CR!f-c>iL2*ux5WK^+}w36?;HZ2#wcoHg#JK(zr9Ygnr*iJVtm2F^p^3j zi{4K6w|tD1os&^9WaV5_hZIAM!{lGVPe7Ez zxJR=4<-s#U@I4o`dUPS#k7nR104D<#u1uwFKDV})AHVN&h7*omg^F0njp1ThTD zKWAGIfHhlX>(CM@&ZxQWd5iWF7ekaFb+pV~BsA5!7!FIbRXDpyZcL;zYVJx72i%`2 zG`Elv2M@1x>$qq;ZS2cZnwgf0pkVPWS6~YUm0FJ42Kky;|F7OZ%8YN#yxoK>9qETF z@%HgCF&I|yavjrZi>ZTy*MZs(xxxl&T+md9&L~3c@GMC5jb>`C_I(F%?OHo`kaiC z_244p=uOQnotY48yxlX=xS9%w6gZt{=ooQZ42lRW6tmMynZU&=tr9DHcgA(i|zt?S$#kPRm0LSEU336Kuk*Yjij}Xvq)(szyEYAw?)IGg?f33aGaW|y*uRN9)^=Ddq5Z(rP@)xil~5E4IsmvdFRB%X=r0NOfuZI$4lq>`+jqkORM7FN z282h_lcdwyrerg^8Ki$6oswCV8>e;fX*cJJB&lESz;~od6;S-tKoa`8v>)DeJTB%} z5BeKD_n%Xbmf`J`Q~`PePE_<2<65d@s~|_Sl3fUm6B2uY&$jQuX$L79EUvR+DVqBd zoSKiu_+@_d|MNNOZ-J8~fpP5XWex`ic;CPh%DY)fqN04nb;I)oqv(!pQL|UmP8^Jg zKkb$xXzFnQ_6d#x&@?#n$eCYwET#@36qQflFmym)9eE;+6@Z7u;`huaX$J7=te|C4 z7Su`>c-pn(w`cIl9b~Y59s7lws_39pH;ILvcD@Y3=h8js;cRL|$JJo|P$L}aL+#as zlBN^)iBcsJ{HBmIu6=-5Rx zTR2?JJ|}1gDRme+Jo2Te_SYirg>zdP}tW zSdCs5%QC$9SuS1VTXYFG&7RYXiv=aj+($OUl^L}GP(C+Mn+pyp?GN>!Mqwv|M|h^+ zm`^`6)p7s zW-TJl|B@dtps*1&aex^Z3_>o@X{5{$#zTVffBl7(>cTqxD-iQ-xV?!Lcn3%6so{>F z!K$ps*!%POwn9YKCefC4C8Np4FJ;mg?GXaAHS!S99Q7t+XD%00tScG=P`b=6WJw*Q zAHgo=KOn-Bg%c#YP|;;@KSz(RgoX)lytvqt+}SSKVV5Tl3-OUunugAI=4qAD(_pD3 zV^5?`M;gO>9Lv|5!)$2@YK=db#mp{`99rs6I}5tfh@iz4RTC_71rK{kEYld^L6rr) zr-mIE*rQM@jQmU|t^#f{Hci7Fph}}|uwGJ|3j3kQ4$p_!xof2`8iJrxrGzcBc%-J{ zM8F7ux+~D)gVkVqO;hBr?A2C0$QiUby;-bUpz~GwmZgqBa66YrxqUt7bkt8H8O&AL z!_v4QXK=zVOCh7?*>ofmV?2Rfy6&2a=3OR%#7t?;>Z$Fz*2euQk8l-ZZsUn;?D3VUWC_#DN;kW4 z%9D0OKNaQpdz7N}&E}uy^frtNj*93Uz^h;=7uuWPV6^-S`rT0_Wg&<;osc^8pBdqW z*gz(sish1$9{_1C_T^b$T*0Ej*}SJoUE3v>#UsBg(2O7-o5~0_|Q*1WfXw;)ar?$gJ z0S*zQ3V^DcD@X7nt_aU3SCv+d?|WGd_P`zK90$H)i_5f8+t^I~7lo^-(u{3ZP$oGz zz$}>S^kdm|0+wD1(-^;eHnzidX)vNRpJIq38*VFqW9qw{RFe`Sw8Kqvjs6kQTr|hj zg|e1-rM5FpuZ`;bj)3k)r`%ex6cFiv^JMfM*R3Gxt+0QX%^w6x>)gL7mr%pn&n(<6 zUYUN;b&WRt6?Esp$d-$mZ3SUmmWt`5A~h|rfx3Q%_GQ2Ms5w+ z#lq`mCJ1)F*{v&zFO`e9D4qi8L`AdC*i0RZKq+ybf-bQIr8QVH#^v+z&j!W=JNM0U zg_R^N2*7kN3yTTOYE&t+e$h@nS|zwl*tK+nF)QWE%kn1XzMLsX(J4klVSlJ#&oO`K zqv);CkwBI}RlBw3!1*r1DoE9w~hyn3wdHs zl9yYY(Y!JDEm8dOs5;f=I?YxOEqKUv*W4U=Fbs-4glM*g|at z*jD319pOY^q~$`myVwD~d(**zM&J9=IXt@kJ%b~mU(ICszrO1@Y?G6QGukbIY-An6 zu&utwWC*PYP>a9^bld-7!_bcVAzNXFfh!Fu&r%7x5}_=egu>I;VdJs86$RNIFQkP=4h7{&{5u>WjrNbCo!ZC)TM zSR1WFuv~y)h*?|y`+=2A27%C}JW18Cw`Van9}|T$?RBR%L|%U@j@BH2+bu6OC?dfC zB+vL{gBa)8N~M-Lzy%G(#6s8!327%Xr9FMGoFgEi_~DiJiHB#}-3IZD>+)r1my*g9N9HKhyW!dwP# zX*Zf>>E^?DCXltT0Sz4A8QpO%&*_g8hF@|CQrpP}PY}QnH@aetNU*8k{%p;M&`g{$ zes>2Q@;Wf&wOr$5nQepZ0y#|d!xB_L;@t&9E3vM$H|iIV)YvCO@<8M<{C*@$U^#Bi%|73nO3Ag8>StS*~WOp1&N|yA&HLvT1lFY2DC@Ki{ttCBw z75Kd)pjODZ3X!-^dGmiW59UcW+&}?}TlU_V7h4UsTElpk7g-imitA9AU?9khoA(8_ z^3+Shz##k>#3of|m{=UHFKO|+G}flwqr^g)Z!c$FAVsiWA-LV`G?2Co(h0sC9jYmY zPEyfsO5Yx#B;^wsy9tCMR9(mk7#Fr%$#(QH$&Ms1r}Ind*j9`Dx~sUe?IZfZa`J>yL)ColN% zb+II2(d|4#9&uyk#R>iBVP2IE0+^gTet55AUEF_iu|wG?QyZRDU8T2d%~|u{+8-)3 z2X~XC_`I@?i+Aq2p&j!HqSv#3v%T<{^6g6&^ABMK)U)7kt*et$0v+iL0^j43ywV>n=x56l>;^hE>28*^iB)c zS1e6J8LTo%5qS(CF?s0*$z^Vh%}i7o#3*j&_KTD>xht;6cdM(HZ)1C>+zQL9KD{|uQ=yR7>k`u1u(k8W+cW> zVRu%mo2%_Hjc5|D(u&#=>NQ*-JDKJfi7vt6<3!vNS!WG-Th`Qj=w2yq6#LjBv{ev5}BN(6<# z5jx7I+ZK-dM*Gh1_;T2yT_z4^Jv@GI!VH0Q6#y(Bh~op$x8fx9O(~XSX6*MP=eON; z(rM+xhBL=zm#PA4wI|~ev_ECR1oml7)Q?d|?NTs5E3tP*K*2z{_nB6A%l4xtMsk_D zpC~x+z`Vp=IPO+geHSd;_3ls2!(H}68A&T{Ff=!Lw_TDbb)(HKK&V?OlaA(!^Nky@ z9GLUyNi2Cu)e(nW@2Zz$J93J-XfC%Wu$|=9xpn2Ag0l@mdq&C9i8w%IjG%;0nY~;U z6Vnu>h4)zSm}Gq#;QEWNw-lGN6V+oH)IzYZm(XN+*smMyA?NfxR^f4{C$5$)C%dx+ zSin)pClLgy(0()0`mF}NTc@gef?Un1EbfL{#C~da3-)<#aaCS0h?FkgDB^Q7>00zU z$zdRLFEC}R%8m^m*#m!jm(aKc?5=Ms72VrvkmuK@M6)@FngyC|h5FulbbA4VFFn*6 zk?=vdm1rosCn46{opux4?RnLS!FDSyVu*DQGEdO!OTsf1mz{5-yjtzUT*Sp)Y=R2i z%wsZE)_}|-BiJ>?goY3h3yAMB)w8Wlj3`VjJ#$?fIDf(2DezRI-OD0)1xP2LE}LPO8Vb4c{DEGA!%@A^YENo@xU%pR%;GAn`q>8- z>P2OH(N=>0eYzp8BmDFIQbsvr_rs6vv2H*hw;6#e&sJNyI?!G=<3u0G6p5&aKQYz} zu%KDZ=}z*(Cf+PhzUdz_oLMPKE(%!*xaRJVc#zo}xT;gNRKDl+S$q^xA;?`HYqJ(U zqY;C4W=rh`!Joa|S8GtMX3rZ1q7r#ZuG0KuA<1KPx|2c8(a(q|wL0`&)L;9V-szBj zdy6yyVZFLKcjaoL^yS7_-a=2TH*YvOsGO8f8VLiv(fLJ zFirYf1R_E}(H)x5%=g8t0g*rtoyffi2#XV6mbkfi+`8n5o37)&YCg>3#1Er#uOi?@ z%g)p1s=CPOfP`OF-S8Mh;VseKDB;ok%__HXzfHkv!70u%j>m++yrDWkEmecv?bNZ9 zL8F%^zm$q0SM(q&mwd;JmYe|$TJiW>V67J2^iIt${^MS1%tXELY_(u zkKkQ)`Vf*ucoA9A;6d7KLz!C&7R4S+k~xN?2_PurfvcHK#;o31W)Ij=X&}z zFykE4;}DMmOZQYbMcPc&DB^@3B>GU}GZ*&q!yXZqE=Pc;Jwg8{@Shco5#j(=o=J|F z&!wwxy-s8SkLy>V2uS2mJ=*lxiDGohLhPDm&vCBY#TRk|Mx}CdtbEN=Y@Gne4N+>y z{zVwsO-Av<#jJ)gd?(p}x8{q;;jDK4Y6>u}OZohQ8`q~+9+8$ikO`&kTqWiSE%wnTt-2&M4u@vPsQ}z~9mGF@22kRhXM0EEq_l z5Jy~BI+05VOx*`nP(nYo&&0=a3QxC4RHNriXS_~TT?aInu})L!>!n4Jt;_v{!g2+9d(P(PH36>b%)GAuLp6#tXRbeIoIap+!gTc)z_ z4I2g4MEv`%>2+*=J$e|u^bRo2eJ*Am*9co$TqU&ErEz`oh6oD5XT%e;sy} zzq)e@wG5NEG6681gt6FE{f)NaPl(t@7EZs!x^m7;FomNL1hl-9DWm~stv=y`$@?Ot zdwTdE*88&_apK6G0aV1!=t8rcc(}w@#A}sy3}Z69D`EGw?ghS0;^+VQVzYw4je;i3 zaG*YpUoeOeSS^IpWK~{2u(ampvTn^fwSzKzT9(4h8{+#k7OU0zuK5fikT~TL_{lX# z%G(*Mf%-L(dKq>aY|dRCK`eh~L$FU`WyHN&H8tGoECs^)_uf%n!oX_+2zbNKBD?_~ zJP3{<^|u2l{(!GaY(`zJV$d`~eD*eQG_u24N=>RAzL&+C>Kn`5*uB>&DXJ9k<)wHN zw4huWn=@(vrYTT-#=cj>2{&o7V09+e+i0q|_690?6SxdeiRQWxF7)HNV^sN!@(j&G z3ru0g$`nI+5;9wh3=XK+X#y_6D zSa%aeP3q~HW}uLJWu{HibGZCHK{Z$fYZYzsLwZ^T-)&H{EWgVG!{C3e11=tpX#!YW zpQ4SB{O2T=S5?&lZaNk>U$Rs}2bV-2f4pk+wLuai-GH#%E`hN)l&C`ZAsvOfE#S*v z+2v2S&lg1xdI%;s_%ibnvdZEttd3&d{INbiJASBVA0`8F}ARWUjH> zv?pRy$zL$??De}SsS&;YSJ81AlkUoF{@1~0>v9L3{pP|IO5kLFduG!6e$oTz+}F?7 z=0D>lqHJKfW1e0w`5V=0sCYD!P-XojfrR)r{|c{W3yl!o=0gSGfak{^G!% z>^(Z|{cw(16uxY%&%viU=hf^d$x{Ue+VD91717$OWMSh6YJna;n2*swmX4U3KXw9A^~rGq&xA~jxM})w z?+Q~^$4{X;exFG4Q%+6L5noXrO3bP1n8L)AAhDXxb5|%&AqP>yyKXdRd6?T3T+h}~ zWHiz;QC}_d{e;dADaMKSyJf7oN(uHfciO0Dw8;N31*3b z*Q(xs971R*Wx|&r&5v>hV(V~j^EF+@ciV`yct)C3BuyXeQM$M$>piyd@p!>9sT22%3wn?C?I6N5@iBXl@6($bI;Li|$7AKw&EkN2G&=aRV2^5d5*Rp!N+#<#8{uN4azkLxLh}Ip9o>tC zY)V49$Jg*FY*ozBz0hV?=FCv--sGulJrObb_L=!wG#J zZlCu`T={btQ5XLGJq?MGmhWoY{cz7@Op~x;bcae(gGv`9!b+z&i=y3mcAl5?S#w~J z{TP5>U!lo}6wuR3phW%79nMi(43hDL;XiVC75sQDAYOsdjGwpT@bmibEOl*f<9$7o zL%Tz=@{IRW4Pjtv0JC{sX6sPaC1*{}B z7Qt9VZFPVaP!ID!p}s7{BJgwFx=`RLcx;yV?pr{+l!@s{+Tlm*zJ~95u5`ST@8^4g zN9KYiJpL^?j5ZgXTj~C%LGP8*aSP|H8aThbqv>z0;m#k%F9ggA) zLP$9z#~X%4B{~g#+#^5o;_$>YZ>F8$?Q{5vhj&VCe z{;{Iv%cu_#O=hI9ov%X$)Ugd&;sDCMs=i!#z7??4nx&_2x!#;V%n?goyV$RhT}zDu zWf?TITX8%L6pk0kc1Mkd0e}qt|LCU>zN)ptG9l|01KHs13>KL<8EhzU=CdTw^D|tS zZ9HoT0;7qe$Nr@@ufeed`a*ASYrc>|8Yh!Of%oWUfbO;Bo~MgA;dd?Z-E0P(_&pAS zXH=_Mm@M?aS}>E-k?s<)W#b#B?9|D zS`hB66%``l!46PGoShh;faXM!uzy(*kCBK?G5*DHMZ^p9Pl{`5P%L5K*@{rdgxr>L zH`H?z0+1iW*CdtAoVWkyL;VX5JQi3jB4zMilp>X!JC14D^zl74kcq2G%F^QBeH|PW zj1A>-*Q>q2e|V)fU{y<7rqqIYh^FUGgwOa5WizsCI?h|{B5y8r6BbwcWtwW8FogWm zY_%O!qvQN;R4E1pMZZoiaso0H6~n6YIWXG64SAGoxC&K|q|#AS%nZZs`Pfj!{o|*9 z^dPhbw6N78OFRIlRsid-+9jkM7hlg+PVSh+0#KibF+;Ls^qyR}AqOc~-t8v@y4g~& z*+Vt;yvgJiv=);J2$hhP(EiIPd9HhFuPc$24!+bfawB+~25mR_;D5$DcEPJIC0=xi z{H>S{P#ueSM#Ubty1}v?x!9HuWb& z%;D&X)?U5`z@z~|iy0e=3fXvb5j5sJ16HMuc<}{SSM$yNUnDI>@uWiq*ni&pf5rvh zf~unCF^>)pnA_1ap`Nb{24$UxLJ{U2d)g%g^uB6S_(t;b4EB2Q12a^}6v=$Lf1Ol% zy@xq>m*i)dDi&iFx9g}carmA@uC@WuBIrlwkbXKy;GA$x}yq$vgVcnpxu zOS9}tdhDf5-p{0Uk!%eO=BG~AihXJr_KFKUi+1F2C2j4*k>3Fnvqz^ec;jkW5E_kv zO$R?1<;8s=_B(V&QB3As;#&nzc>q##;fjag|iQRxV3ff(a6*V79r%2FYEifswH( z8m)%`QyeLhL?YaPP!WD@lQC%Qjiq8m`N&YE-ZWr>8Ca~aX2({H_!~U*s>v-UXpm%b zmdB`QRTsZ;Kapj8tv47R^UGPwqVB7u*yaIb1)KT=sv+vdErTA72?c9nt1d#d4(8!p z4t7L;{s4}?9`xwd64h47b*maoa{vc>$zL}@{J;KvEa}(6+*H>67OuBM?ao;S_e&+8 z*uIu$@=z~(s92BjkkcFLC_FQ0*2 zaD|(WQK+$BFKd#GZK-{h`DH$T7M&VVaxr9VgWui)z8c-VJ*Sk^$+DDCG0magyMU!* zUC{qvDOpLXQ(d*hM+*yCCE}(%o>5{TK}L)p&$(&KOqA75vYnY&|pth0ft%} zBZD`3h*k|5SG$5jFRhv$6U`bG%%CkliCTQrtqF#ZCwq!!Q#V*@-b+>Y>oMwseo>ql zjan;_+jO7)rjB@h_Mov)3s7fH!tRApCIJ> z?EWx|mrdPToJrpgqlJ`hl#r3uw>#N$J^paH_}H1VOc$CdMOZPm8^lF=4)jS`Dh!Y0 zrZ;_nZT4aGdYLQ)xBH4#!bFJxUTv;cZikvpKMYFCLOcQ7_!YP+5l0-wIo-;~@_lC! zS$B_Uj~md7zELO2YULU+9XD}hNq|0TD0;HB-I;^TokrMizWdXVVB2a z8DnIe(B`Nt&A%Xa@<^)jJv+|8p3^Q5qQoGR=c5aPqudTepobA7-AeRPcu~Z^ElIE} z3yEw;7;LKl&BFAA^iJ>pyfo&N^d22XmE7Mdp0J@7YOc zU_SYb2|gF}aaWb}Wnzdj>hD(8BEml~f^WB9Kn~$$x@zj-MQ*qd-LLcgf{|7aJm?}J K)LVU4SZMNPYqQP( literal 0 HcmV?d00001 diff --git a/src/documents/tests/samples/documents/thumbnails/0000002.png.gpg b/src/documents/tests/samples/documents/thumbnails/0000004.png.gpg similarity index 100% rename from src/documents/tests/samples/documents/thumbnails/0000002.png.gpg rename to src/documents/tests/samples/documents/thumbnails/0000004.png.gpg diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index d6e7ad6e0..e3fef6b78 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -24,6 +24,8 @@ def setUp(self) -> None: self.d1 = Document.objects.create(content="Content", checksum="42995833e01aea9b3edee44bbfdd7ce1", archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b", title="wow1", filename="0000001.pdf", mime_type="application/pdf") self.d2 = Document.objects.create(content="Content", checksum="9c9691e51741c1f4f41a20896af31770", title="wow2", filename="0000002.pdf", mime_type="application/pdf") self.d3 = Document.objects.create(content="Content", checksum="d38d7ed02e988e072caf924e0f3fcb76", title="wow2", filename="0000003.pdf", mime_type="application/pdf") + self.d4 = Document.objects.create(content="Content", checksum="82186aaa94f0b98697d704b90fd1c072", title="wow_dec", filename="0000004.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) + self.t1 = Tag.objects.create(name="t") self.dt1 = DocumentType.objects.create(name="dt") self.c1 = Correspondent.objects.create(name="c") @@ -34,6 +36,9 @@ def setUp(self) -> None: self.d1.save() super(TestExportImport, self).setUp() + @override_settings( + PASSPHRASE="test" + ) def _do_export(self, use_filename_format=False, compare_checksums=False): args = ['document_exporter', self.target] if use_filename_format: @@ -54,8 +59,8 @@ def test_exporter(self, use_filename_format=False): manifest = self._do_export(use_filename_format=use_filename_format) - self.assertEqual(len(manifest), 6) - self.assertEqual(len(list(filter(lambda e: e['model'] == 'documents.document', manifest))), 3) + self.assertEqual(len(manifest), 7) + self.assertEqual(len(list(filter(lambda e: e['model'] == 'documents.document', manifest))), 4) self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) @@ -69,6 +74,8 @@ def test_exporter(self, use_filename_format=False): checksum = hashlib.md5(f.read()).hexdigest() self.assertEqual(checksum, element['fields']['checksum']) + self.assertEqual(element['fields']['storage_type'], Document.STORAGE_TYPE_UNENCRYPTED) + if document_exporter.EXPORTER_ARCHIVE_NAME in element: fname = os.path.join(self.target, element[document_exporter.EXPORTER_ARCHIVE_NAME]) self.assertTrue(os.path.exists(fname)) @@ -78,7 +85,7 @@ def test_exporter(self, use_filename_format=False): self.assertEqual(checksum, element['fields']['archive_checksum']) with paperless_environment() as dirs: - self.assertEqual(Document.objects.count(), 3) + self.assertEqual(Document.objects.count(), 4) Document.objects.all().delete() Correspondent.objects.all().delete() DocumentType.objects.all().delete() @@ -86,13 +93,14 @@ def test_exporter(self, use_filename_format=False): self.assertEqual(Document.objects.count(), 0) call_command('document_importer', self.target) - self.assertEqual(Document.objects.count(), 3) + self.assertEqual(Document.objects.count(), 4) self.assertEqual(Tag.objects.count(), 1) self.assertEqual(Correspondent.objects.count(), 1) self.assertEqual(DocumentType.objects.count(), 1) self.assertEqual(Document.objects.get(id=self.d1.id).title, "wow1") self.assertEqual(Document.objects.get(id=self.d2.id).title, "wow2") self.assertEqual(Document.objects.get(id=self.d3.id).title, "wow2") + self.assertEqual(Document.objects.get(id=self.d4.id).title, "wow_dec") messages = check_sanity() # everything is alright after the test self.assertEqual(len(messages), 0, str([str(m) for m in messages])) From a71a9910840a0c15666c15c313087269b17bbc20 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 14:20:45 +0100 Subject: [PATCH 28/61] fix some test cases due to renamed test files --- src/documents/tests/test_management.py | 10 +++++----- src/documents/tests/test_migrations.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/documents/tests/test_management.py b/src/documents/tests/test_management.py index 58aaf9342..a71baa8ba 100644 --- a/src/documents/tests/test_management.py +++ b/src/documents/tests/test_management.py @@ -70,18 +70,18 @@ def test_decrypt(self, m): PASSPHRASE="test" ).enable() - doc = Document.objects.create(checksum="9c9691e51741c1f4f41a20896af31770", title="wow", filename="0000002.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) + doc = Document.objects.create(checksum="82186aaa94f0b98697d704b90fd1c072", title="wow", filename="0000004.pdf.gpg", mime_type="application/pdf", storage_type=Document.STORAGE_TYPE_GPG) - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000002.pdf.gpg"), os.path.join(originals_dir, "0000002.pdf.gpg")) - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", f"0000002.png.gpg"), os.path.join(thumb_dir, f"{doc.id:07}.png.gpg")) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000004.pdf.gpg"), os.path.join(originals_dir, "0000004.pdf.gpg")) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "thumbnails", f"0000004.png.gpg"), os.path.join(thumb_dir, f"{doc.id:07}.png.gpg")) call_command('decrypt_documents') doc.refresh_from_db() self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) - self.assertEqual(doc.filename, "0000002.pdf") - self.assertTrue(os.path.isfile(os.path.join(originals_dir, "0000002.pdf"))) + self.assertEqual(doc.filename, "0000004.pdf") + self.assertTrue(os.path.isfile(os.path.join(originals_dir, "0000004.pdf"))) self.assertTrue(os.path.isfile(doc.source_path)) self.assertTrue(os.path.isfile(os.path.join(thumb_dir, f"{doc.id:07}.png"))) self.assertTrue(os.path.isfile(doc.thumbnail_path)) diff --git a/src/documents/tests/test_migrations.py b/src/documents/tests/test_migrations.py index 33ba41444..ee7180c63 100644 --- a/src/documents/tests/test_migrations.py +++ b/src/documents/tests/test_migrations.py @@ -98,7 +98,7 @@ def setUpBeforeMigration(self, apps): doc2 = Document.objects.create(checksum="B", file_type="pdf", storage_type=STORAGE_TYPE_GPG) self.doc2_id = doc2.id - shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000002.pdf.gpg"), source_path_before(doc2)) + shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "documents", "originals", "0000004.pdf.gpg"), source_path_before(doc2)) def testMimeTypesMigrated(self): Document = self.apps.get_model('documents', 'Document') From 6c972bd08a40d6fc05042df5a60316a6800f9204 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 14:47:19 +0100 Subject: [PATCH 29/61] more tests --- .../tests/test_management_exporter.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index e3fef6b78..f859649ed 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -12,6 +12,7 @@ from documents.management.commands import document_exporter from documents.models import Document, Tag, DocumentType, Correspondent from documents.sanity_checker import check_sanity +from documents.settings import EXPORTER_FILE_NAME from documents.tests.utils import DirectoriesMixin, paperless_environment @@ -36,6 +37,13 @@ def setUp(self) -> None: self.d1.save() super(TestExportImport, self).setUp() + def _get_document_from_manifest(self, manifest, id): + f = list(filter(lambda d: d['model'] == "documents.document" and d['pk'] == id, manifest)) + if len(f) == 1: + return f[0] + else: + raise ValueError(f"document with id {id} does not exist in manifest") + @override_settings( PASSPHRASE="test" ) @@ -119,11 +127,14 @@ def test_update_export_changed_time(self): self._do_export() self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + st_mtime_1 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime + with mock.patch("documents.management.commands.document_exporter.shutil.copy2") as m: self._do_export() m.assert_not_called() self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + st_mtime_2 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime Path(self.d1.source_path).touch() @@ -131,8 +142,12 @@ def test_update_export_changed_time(self): self._do_export() self.assertEqual(m.call_count, 1) + st_mtime_3 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + self.assertNotEqual(st_mtime_1, st_mtime_2) + self.assertNotEqual(st_mtime_2, st_mtime_3) + def test_update_export_changed_checksum(self): shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) @@ -156,6 +171,23 @@ def test_update_export_changed_checksum(self): self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + def test_update_export_deleted_document(self): + shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) + shutil.copytree(os.path.join(os.path.dirname(__file__), "samples", "documents"), os.path.join(self.dirs.media_dir, "documents")) + + manifest = self._do_export() + + self.assertTrue(len(manifest), 7) + doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id) + self.assertTrue(os.path.isfile(os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]))) + self.d3.delete() + + manifest2 = self._do_export() + self.assertRaises(ValueError, self._get_document_from_manifest, manifest, self.d3.id) + self.assertFalse(os.path.isfile(os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]))) + + self.assertTrue(len(manifest), 6) + @override_settings(PAPERLESS_FILENAME_FORMAT="{title}/{correspondent}") def test_update_export_changed_location(self): shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) From 72616def4f23f3bff67c52b03329025471fff01e Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 15:18:03 +0100 Subject: [PATCH 30/61] locking for the document archiver --- src/documents/management/commands/document_archiver.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/documents/management/commands/document_archiver.py b/src/documents/management/commands/document_archiver.py index 7b9a123d9..01bd819c5 100644 --- a/src/documents/management/commands/document_archiver.py +++ b/src/documents/management/commands/document_archiver.py @@ -11,6 +11,7 @@ from django.conf import settings from django.core.management.base import BaseCommand from django.db import transaction +from filelock import FileLock from whoosh.writing import AsyncWriter from documents.models import Document @@ -47,8 +48,10 @@ def handle_document(document_id): archive_checksum=checksum, content=parser.get_text() ) - create_source_path_directory(document.archive_path) - shutil.move(parser.get_archive_path(), document.archive_path) + with FileLock(settings.MEDIA_LOCK): + create_source_path_directory(document.archive_path) + shutil.move(parser.get_archive_path(), + document.archive_path) with AsyncWriter(index.open_index()) as writer: index.update_document(writer, document) From b9725437d935e2d7286e7295d41483faf2e26f1b Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 15:33:05 +0100 Subject: [PATCH 31/61] delete option for exporter, short options, tests --- .../management/commands/document_exporter.py | 31 +++++++++++++------ .../tests/test_management_exporter.py | 13 ++++++-- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/documents/management/commands/document_exporter.py b/src/documents/management/commands/document_exporter.py index 250ae1adf..37fcf2024 100644 --- a/src/documents/management/commands/document_exporter.py +++ b/src/documents/management/commands/document_exporter.py @@ -31,7 +31,7 @@ def add_arguments(self, parser): parser.add_argument("target") parser.add_argument( - "--compare-checksums", + "-c", "--compare-checksums", default=False, action="store_true", help="Compare file checksums when determining whether to export " @@ -40,13 +40,22 @@ def add_arguments(self, parser): ) parser.add_argument( - "--use-filename-format", + "-f", "--use-filename-format", default=False, action="store_true", help="Use PAPERLESS_FILENAME_FORMAT for storing files in the " "export directory, if configured." ) + parser.add_argument( + "-d", "--delete", + default=False, + action="store_true", + help="After exporting, delete files in the export directory that " + "do not belong to the current export, such as files from " + "deleted documents." + ) + def __init__(self, *args, **kwargs): BaseCommand.__init__(self, *args, **kwargs) self.target = None @@ -54,12 +63,14 @@ def __init__(self, *args, **kwargs): self.exported_files = [] self.compare_checksums = False self.use_filename_format = False + self.delete = False def handle(self, *args, **options): self.target = options["target"] self.compare_checksums = options['compare_checksums'] self.use_filename_format = options['use_filename_format'] + self.delete = options['delete'] if not os.path.exists(self.target): raise CommandError("That path doesn't exist") @@ -176,15 +187,17 @@ def dump(self): with open(manifest_path, "w") as f: json.dump(manifest, f, indent=2) - if manifest_path in self.files_in_export_dir: - self.files_in_export_dir.remove(manifest_path) + if self.delete: + # 5. Remove files which we did not explicitly export in this run + + if manifest_path in self.files_in_export_dir: + self.files_in_export_dir.remove(manifest_path) - # 5. Remove files which we did not explicitly export in this run - for f in self.files_in_export_dir: - os.remove(f) + for f in self.files_in_export_dir: + os.remove(f) - delete_empty_directories(os.path.abspath(os.path.dirname(f)), - os.path.abspath(self.target)) + delete_empty_directories(os.path.abspath(os.path.dirname(f)), + os.path.abspath(self.target)) def check_and_copy(self, source, source_checksum, target): if os.path.abspath(target) in self.files_in_export_dir: diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index f859649ed..212b87ec1 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -47,12 +47,14 @@ def _get_document_from_manifest(self, manifest, id): @override_settings( PASSPHRASE="test" ) - def _do_export(self, use_filename_format=False, compare_checksums=False): + def _do_export(self, use_filename_format=False, compare_checksums=False, delete=False): args = ['document_exporter', self.target] if use_filename_format: args += ["--use-filename-format"] if compare_checksums: args += ["--compare-checksums"] + if delete: + args += ["--delete"] call_command(*args) @@ -182,8 +184,11 @@ def test_update_export_deleted_document(self): self.assertTrue(os.path.isfile(os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]))) self.d3.delete() - manifest2 = self._do_export() + manifest = self._do_export() self.assertRaises(ValueError, self._get_document_from_manifest, manifest, self.d3.id) + self.assertTrue(os.path.isfile(os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]))) + + manifest = self._do_export(delete=True) self.assertFalse(os.path.isfile(os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]))) self.assertTrue(len(manifest), 6) @@ -200,11 +205,13 @@ def test_update_export_changed_location(self): self.d1.title = "new_title" self.d1.save() - self._do_export(use_filename_format=True) + self._do_export(use_filename_format=True, delete=True) self.assertFalse(os.path.isfile(os.path.join(self.target, "wow1", "c.pdf"))) self.assertFalse(os.path.isdir(os.path.join(self.target, "wow1"))) self.assertTrue(os.path.isfile(os.path.join(self.target, "new_title", "c.pdf"))) self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + self.assertTrue(os.path.isfile(os.path.join(self.target, "wow2", "none.pdf"))) + self.assertTrue(os.path.isfile(os.path.join(self.target, "wow2", "none_01.pdf"))) def test_export_missing_files(self): From 385076cf2898511f68f745582e83a393fc2f447d Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 15:43:48 +0100 Subject: [PATCH 32/61] documentation and changelog --- docs/administration.rst | 27 ++++++++++++++++++++++++++- docs/changelog.rst | 11 ++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/docs/administration.rst b/docs/administration.rst index 32741300a..52598245c 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -20,6 +20,8 @@ Options available to any installation of paperless: metadata to a specific folder. You may import your documents into a fresh instance of paperless again or store your documents in another DMS with this export. +* The document exporter is also able to update an already existing export. + Therefore, incremental backups with ``rsync`` are entirely possible. Options available to docker installations: @@ -157,8 +159,13 @@ backup or migration to another DMS. .. code:: - document_exporter target + document_exporter target [-c] [-f] [-d] + optional arguments: + -c, --compare-checksums + -f, --use-filename-format + -d, --delete + ``target`` is a folder to which the data gets written. This includes documents, thumbnails and a ``manifest.json`` file. The manifest contains all metadata from the database (correspondents, tags, etc). @@ -167,6 +174,24 @@ When you use the provided docker compose script, specify ``../export`` as the target. This path inside the container is automatically mounted on your host on the folder ``export``. +If the target directory already exists and contains files, paperless will assume +that the contents of the export directory are a previous export and will attempt +to update the previous export. Paperless will only export changed and added files. +Paperless determines whether a file has changed by inspecting the file attributes +"date/time modified" and "size". If that does not work out for you, specify +``--compare-checksums`` and paperless will attempt to compare file checksums instead. +This is slower. + +Paperless will not remove any existing files in the export directory. If you want +paperless to also remove files that do not belong to the current export such as files +from deleted documents, specify ``--delete``. Be careful when pointing paperless to +a directory that already contains other files. + +The filenames generated by this command follow the format +``[date created] [correspondent] [title].[extension]``. +If you want paperless to use ``PAPERLESS_FILENAME_FORMAT`` for exported filenames +instead, specify ``--use-filename-format``. + .. _utilities-importer: diff --git a/docs/changelog.rst b/docs/changelog.rst index f5bf41b25..43836dea9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,13 +11,22 @@ paperless-ng 1.0.0 Nothing special about this release, but since there are relatively few bug reports coming in, I think that this is reasonably stable. -* Changes and additions +* Document export + + * The document exporter has been rewritten to support updating an already existing export in place. + * The document exporter supports naming exported files according to ``PAPERLESS_FILENAME_FORMAT``. + * The document exporter locks the media directory and the database during execution to ensure that + the resulting export is consistent. + * See the :ref:`updated documentation ` for more details. + +* Other changes and additions * Added a language selector to the settings. * Added date format options to the settings. * Range selection with shift clicking is now possible in the document list. + paperless-ng 0.9.14 ################### From 4fda8f334815175912d9ddd97f6621791f6089c3 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 22:23:19 +0100 Subject: [PATCH 33/61] changelog --- docs/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 43836dea9..721e9520e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -14,6 +14,7 @@ Nothing special about this release, but since there are relatively few bug repor * Document export * The document exporter has been rewritten to support updating an already existing export in place. + This enables incremental backups with ``rsync``. * The document exporter supports naming exported files according to ``PAPERLESS_FILENAME_FORMAT``. * The document exporter locks the media directory and the database during execution to ensure that the resulting export is consistent. From e1e11de2b546f87a6ed035feed07f868b29abcb7 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 22:23:53 +0100 Subject: [PATCH 34/61] mail tests --- src/paperless_mail/tests/test_mail.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index 0104f5023..bec0ff4b4 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -1,3 +1,4 @@ +import os import uuid from collections import namedtuple from typing import ContextManager @@ -9,6 +10,7 @@ from imap_tools import MailMessageFlags, MailboxFolderSelectError from documents.models import Correspondent +from documents.tests.utils import DirectoriesMixin from paperless_mail import tasks from paperless_mail.mail import MailError, MailAccountHandler from paperless_mail.models import MailRule, MailAccount @@ -130,7 +132,7 @@ def fake_magic_from_buffer(buffer, mime=False): @mock.patch('paperless_mail.mail.magic.from_buffer', fake_magic_from_buffer) -class TestMail(TestCase): +class TestMail(DirectoriesMixin, TestCase): def setUp(self): patcher = mock.patch('paperless_mail.mail.MailBox') @@ -146,6 +148,7 @@ def setUp(self): self.reset_bogus_mailbox() self.mail_account_handler = MailAccountHandler() + super(TestMail, self).setUp() def reset_bogus_mailbox(self): self.bogus_mailbox.messages = [] @@ -220,9 +223,13 @@ def test_handle_message(self): args1, kwargs1 = self.async_task.call_args_list[0] args2, kwargs2 = self.async_task.call_args_list[1] + self.assertTrue(os.path.isfile(kwargs1['path']), kwargs1['path']) + self.assertEqual(kwargs1['override_title'], "file_0") self.assertEqual(kwargs1['override_filename'], "file_0.pdf") + self.assertTrue(os.path.isfile(kwargs2['path']), kwargs1['path']) + self.assertEqual(kwargs2['override_title'], "file_1") self.assertEqual(kwargs2['override_filename'], "file_1.pdf") @@ -253,6 +260,7 @@ def test_handle_unknown_mime_type(self): self.assertEqual(self.async_task.call_count, 1) args, kwargs = self.async_task.call_args + self.assertTrue(os.path.isfile(kwargs['path']), kwargs['path']) self.assertEqual(kwargs['override_filename'], "f1.pdf") def test_handle_disposition(self): From bf2e98527cd53dfef146308f06ac01f1ac669dab Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Mon, 18 Jan 2021 22:30:06 +0100 Subject: [PATCH 35/61] updated messages --- src-ui/messages.xlf | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index a98c076ec..d71bdbc2e 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -475,35 +475,35 @@ Saved view "" deleted. src/app/components/manage/settings/settings.component.ts - 58 + 62 Settings saved successfully. src/app/components/manage/settings/settings.component.ts - 74 + 78 Use system language src/app/components/manage/settings/settings.component.ts - 78 + 82 Use date format of display language src/app/components/manage/settings/settings.component.ts - 82 + 86 Error while storing settings on server: src/app/components/manage/settings/settings.component.ts - 98 + 102 @@ -562,22 +562,22 @@ 45 - - Short: + + Short: src/app/components/manage/settings/settings.component.html 51 - - Medium: + + Medium: src/app/components/manage/settings/settings.component.html 55 - - Long: + + Long: src/app/components/manage/settings/settings.component.html 59 @@ -755,21 +755,21 @@ Matching algorithm src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 11 + 10 Matching pattern src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 12 + 11 Case insensitive src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 13 + 12 @@ -1649,21 +1649,21 @@ Create new item src/app/components/common/edit-dialog/edit-dialog.component.ts - 43 + 50 Edit item src/app/components/common/edit-dialog/edit-dialog.component.ts - 47 + 54 Could not save element: src/app/components/common/edit-dialog/edit-dialog.component.ts - 51 + 58 From bab53838ecb034ab5102b20d209306d151b61d80 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 21:30:59 +0000 Subject: [PATCH 36/61] Translate /src-ui/messages.xlf in de translation completed for the source file '/src-ui/messages.xlf' on the 'de' language. --- src-ui/src/locale/messages.de.xlf | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src-ui/src/locale/messages.de.xlf b/src-ui/src/locale/messages.de.xlf index a5b28274f..80cfefab9 100644 --- a/src-ui/src/locale/messages.de.xlf +++ b/src-ui/src/locale/messages.de.xlf @@ -542,7 +542,7 @@ Gespeicherte Ansicht "" gelöscht. src/app/components/manage/settings/settings.component.ts - 58 + 62 @@ -550,7 +550,7 @@ Einstellungen erfolgreich gespeichert. src/app/components/manage/settings/settings.component.ts - 74 + 78 @@ -558,7 +558,7 @@ Benutze Systemsprache src/app/components/manage/settings/settings.component.ts - 78 + 82 @@ -566,7 +566,7 @@ Benutze Datumsformat der Anzeigesprache src/app/components/manage/settings/settings.component.ts - 82 + 86 @@ -574,7 +574,7 @@ Fehler beim Speichern der Einstellungen auf dem Server: src/app/components/manage/settings/settings.component.ts - 98 + 102 @@ -641,25 +641,25 @@ 45 - - Short: - Kurz: + + Short: + Kurz: src/app/components/manage/settings/settings.component.html 51 - - Medium: - Mittel: + + Medium: + Mittel: src/app/components/manage/settings/settings.component.html 55 - - Long: - Lang: + + Long: + Lang: src/app/components/manage/settings/settings.component.html 59 @@ -862,7 +862,7 @@ Zuweisungsalgorithmus src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 11 + 10 @@ -870,7 +870,7 @@ Zuweisungsmuster src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 12 + 11 @@ -878,7 +878,7 @@ Groß-/Kleinschreibung irrelevant src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 13 + 12 @@ -1883,7 +1883,7 @@ Neues Element erstellen src/app/components/common/edit-dialog/edit-dialog.component.ts - 43 + 50 @@ -1891,7 +1891,7 @@ Element bearbeiten src/app/components/common/edit-dialog/edit-dialog.component.ts - 47 + 54 @@ -1899,7 +1899,7 @@ Konnte Element nicht speichern: src/app/components/common/edit-dialog/edit-dialog.component.ts - 51 + 58 @@ -2016,7 +2016,7 @@ Fuzzy: Document contains a word similar to this word - Ungenau: Dokument enthält ein zum folgdendem Wort ähnliches Wort + Ungenau: Dokument enthält ein zum folgendem Wort ähnliches Wort src/app/data/matching-model.ts 16 From a8ec9f29cdb73a92ffd8e59e54a324cbee596177 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 21:31:31 +0000 Subject: [PATCH 37/61] Translate /src-ui/messages.xlf in nl_NL translation completed for the source file '/src-ui/messages.xlf' on the 'nl_NL' language. --- src-ui/src/locale/messages.nl_NL.xlf | 40 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src-ui/src/locale/messages.nl_NL.xlf b/src-ui/src/locale/messages.nl_NL.xlf index c77a77bea..39545c0e2 100644 --- a/src-ui/src/locale/messages.nl_NL.xlf +++ b/src-ui/src/locale/messages.nl_NL.xlf @@ -542,7 +542,7 @@ Opgeslagen view "" verwijderd. src/app/components/manage/settings/settings.component.ts - 58 + 62 @@ -550,7 +550,7 @@ Instellingen succesvol opgeslagen. src/app/components/manage/settings/settings.component.ts - 74 + 78 @@ -558,7 +558,7 @@ Gebruik de systeemtaal src/app/components/manage/settings/settings.component.ts - 78 + 82 @@ -566,7 +566,7 @@ Datumopmaak van weergavetaal gebruiken src/app/components/manage/settings/settings.component.ts - 82 + 86 @@ -574,7 +574,7 @@ Fout bij het opslaan van de instellingen: src/app/components/manage/settings/settings.component.ts - 98 + 102 @@ -641,25 +641,25 @@ 45 - - Short: - Kort: + + Short: + Kort: src/app/components/manage/settings/settings.component.html 51 - - Medium: - Medium: + + Medium: + Medium: src/app/components/manage/settings/settings.component.html 55 - - Long: - Lang: + + Long: + Lang: src/app/components/manage/settings/settings.component.html 59 @@ -862,7 +862,7 @@ Algoritme voor het bepalen van de overeenkomst src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 11 + 10 @@ -870,7 +870,7 @@ Patroon voor overeenkomst src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 12 + 11 @@ -878,7 +878,7 @@ Niet hoofdlettergevoelig src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 13 + 12 @@ -1883,7 +1883,7 @@ Maak nieuw item src/app/components/common/edit-dialog/edit-dialog.component.ts - 43 + 50 @@ -1891,7 +1891,7 @@ Item bewerken src/app/components/common/edit-dialog/edit-dialog.component.ts - 47 + 54 @@ -1899,7 +1899,7 @@ Kon het element niet opslaan: src/app/components/common/edit-dialog/edit-dialog.component.ts - 51 + 58 From 0af0dffeda646df4baea141c9af8d7d08ec37164 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Mon, 18 Jan 2021 23:28:17 +0000 Subject: [PATCH 38/61] Translate /src-ui/messages.xlf in fr translation completed for the source file '/src-ui/messages.xlf' on the 'fr' language. --- src-ui/src/locale/messages.fr.xlf | 130 ++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/src-ui/src/locale/messages.fr.xlf b/src-ui/src/locale/messages.fr.xlf index 06ca713a4..ee97f7ce0 100644 --- a/src-ui/src/locale/messages.fr.xlf +++ b/src-ui/src/locale/messages.fr.xlf @@ -542,7 +542,7 @@ Vue "" supprimée. src/app/components/manage/settings/settings.component.ts - 56 + 62 @@ -550,7 +550,23 @@ Paramètres enregistrés avec succès. src/app/components/manage/settings/settings.component.ts - 70 + 78 + + + + Use system language + Utiliser la langue du système + + src/app/components/manage/settings/settings.component.ts + 82 + + + + Use date format of display language + Utiliser le format de date de la langue d'affichage + + src/app/components/manage/settings/settings.component.ts + 86 @@ -558,7 +574,7 @@ Une erreur s'est produite lors de l'enregistrement des paramètres sur le serveur : src/app/components/manage/settings/settings.component.ts - 86 + 102 @@ -582,7 +598,7 @@ Vues enregistrées src/app/components/manage/settings/settings.component.html - 79 + 114 @@ -609,20 +625,60 @@ 25 + + Date display + Affichage de la date + + src/app/components/manage/settings/settings.component.html + 32 + + + + Date format + Format de date + + src/app/components/manage/settings/settings.component.html + 45 + + + + Short: + Court : + + src/app/components/manage/settings/settings.component.html + 51 + + + + Medium: + Moyen : + + src/app/components/manage/settings/settings.component.html + 55 + + + + Long: + Long : + + src/app/components/manage/settings/settings.component.html + 59 + + Items per page Éléments par page src/app/components/manage/settings/settings.component.html - 32 + 67 Document editor - Éditeur de documents + Visionneuse de documents src/app/components/manage/settings/settings.component.html - 48 + 83 @@ -630,7 +686,7 @@ Utiliser la visionneuse PDF fournie par le navigateur src/app/components/manage/settings/settings.component.html - 52 + 87 @@ -638,7 +694,7 @@ Cette option est généralement plus rapide pour l'affichage de grands documents PDF, mais elle peut ne pas fonctionner sur certains navigateurs. src/app/components/manage/settings/settings.component.html - 52 + 87 @@ -646,7 +702,7 @@ Mode sombre src/app/components/manage/settings/settings.component.html - 59 + 94 @@ -654,7 +710,7 @@ Utiliser les paramètres du système src/app/components/manage/settings/settings.component.html - 62 + 97 @@ -662,7 +718,7 @@ Activer le mode sombre src/app/components/manage/settings/settings.component.html - 63 + 98 @@ -670,7 +726,7 @@ Edition en masse src/app/components/manage/settings/settings.component.html - 67 + 102 @@ -678,7 +734,7 @@ Afficher les messages de confirmation src/app/components/manage/settings/settings.component.html - 71 + 106 @@ -686,7 +742,7 @@ La suppression de documents requiert toujours une confirmation. src/app/components/manage/settings/settings.component.html - 71 + 106 @@ -694,7 +750,7 @@ Appliquer lors de la fermeture src/app/components/manage/settings/settings.component.html - 72 + 107 @@ -702,7 +758,7 @@ Apparaît sur src/app/components/manage/settings/settings.component.html - 91 + 126 @@ -710,7 +766,7 @@ Montrer sur le tableau de bord src/app/components/manage/settings/settings.component.html - 94 + 129 @@ -718,7 +774,7 @@ Montrer dans la barre latérale src/app/components/manage/settings/settings.component.html - 98 + 133 @@ -726,7 +782,7 @@ Aucune vue sauvegardée n'est définie. src/app/components/manage/settings/settings.component.html - 108 + 143 @@ -806,7 +862,7 @@ Algorithme de rapprochement src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 11 + 10 @@ -814,7 +870,7 @@ Modèle de rapprochement src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 12 + 11 @@ -822,7 +878,7 @@ Insensible à la casse src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html - 13 + 12 @@ -1179,9 +1235,9 @@ 50 - - Created: - Créé le : + + Created: + Créé le : src/app/components/document-list/document-card-large/document-card-large.component.html 67 @@ -1614,20 +1670,12 @@ 12 - - Use system language - Utiliser la langue du système - - src/app/services/settings.service.ts - 64 - - English Anglais src/app/services/settings.service.ts - 65 + 74 @@ -1635,7 +1683,7 @@ Allemand src/app/services/settings.service.ts - 66 + 75 @@ -1643,7 +1691,7 @@ Néerlandais src/app/services/settings.service.ts - 67 + 76 @@ -1651,7 +1699,7 @@ Français src/app/services/settings.service.ts - 68 + 77 @@ -1835,7 +1883,7 @@ Créer un nouvel élément src/app/components/common/edit-dialog/edit-dialog.component.ts - 43 + 50 @@ -1843,7 +1891,7 @@ Éditer l'élément src/app/components/common/edit-dialog/edit-dialog.component.ts - 47 + 54 @@ -1851,7 +1899,7 @@ Impossible d'enregistrer l'élément : src/app/components/common/edit-dialog/edit-dialog.component.ts - 51 + 58 From 0b949a14c07c65a44e4ae2c502863238f45cec94 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Tue, 19 Jan 2021 14:00:15 +0100 Subject: [PATCH 39/61] add filtering to management pages #356 --- src-ui/messages.xlf | 40 +++++++++---------- .../correspondent-list.component.html | 11 ++++- .../document-type-list.component.html | 12 ++++-- .../generic-list/generic-list.component.ts | 39 +++++++++++++++--- .../manage/tag-list/tag-list.component.html | 12 ++++-- .../rest/abstract-name-filter-service.ts | 14 +++++++ .../services/rest/correspondent.service.ts | 4 +- .../services/rest/document-type.service.ts | 4 +- src-ui/src/app/services/rest/tag.service.ts | 4 +- 9 files changed, 101 insertions(+), 39 deletions(-) create mode 100644 src-ui/src/app/services/rest/abstract-name-filter-service.ts diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index d71bdbc2e..1e89a9408 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -394,53 +394,60 @@ 2 + + Filter by: + + src/app/components/manage/tag-list/tag-list.component.html + 8 + + Name src/app/components/manage/tag-list/tag-list.component.html - 13 + 9 Color src/app/components/manage/tag-list/tag-list.component.html - 14 + 20 Matching src/app/components/manage/tag-list/tag-list.component.html - 15 + 21 Document count src/app/components/manage/tag-list/tag-list.component.html - 16 + 22 Actions src/app/components/manage/tag-list/tag-list.component.html - 17 + 23 Documents src/app/components/manage/tag-list/tag-list.component.html - 32 + 38 Edit src/app/components/manage/tag-list/tag-list.component.html - 37 + 43 @@ -713,7 +720,7 @@ Last correspondence src/app/components/manage/correspondent-list/correspondent-list.component.html - 15 + 22 @@ -976,13 +983,6 @@ 46 - - Filter by: - - src/app/components/document-list/filter-editor/filter-editor.component.html - 4 - - Filter tags @@ -1670,35 +1670,35 @@ Automatic src/app/components/manage/generic-list/generic-list.component.ts - 33 + 39 Do you really want to delete this element? src/app/components/manage/generic-list/generic-list.component.ts - 76 + 97 Associated documents will not be deleted. src/app/components/manage/generic-list/generic-list.component.ts - 83 + 104 Delete src/app/components/manage/generic-list/generic-list.component.ts - 85 + 106 Error while deleting element: src/app/components/manage/generic-list/generic-list.component.ts - 93 + 114 diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html index ffe260d73..4cbafb817 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.html @@ -2,8 +2,15 @@ -
- +
+
+
+ + +
+
+ +
diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html index dd6e86592..613cc4fd4 100644 --- a/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html +++ b/src-ui/src/app/components/manage/document-type-list/document-type-list.component.html @@ -2,9 +2,15 @@ -
- +
+
+
+ + +
+
+ +
diff --git a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts index e1d5226f3..7c5dbc8e3 100644 --- a/src-ui/src/app/components/manage/generic-list/generic-list.component.ts +++ b/src-ui/src/app/components/manage/generic-list/generic-list.component.ts @@ -1,17 +1,19 @@ -import { Directive, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { Directive, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'; import { ObjectWithId } from 'src/app/data/object-with-id'; import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive'; -import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; +import { AbstractNameFilterService } from 'src/app/services/rest/abstract-name-filter-service'; import { ToastService } from 'src/app/services/toast.service'; import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'; @Directive() -export abstract class GenericListComponent implements OnInit { +export abstract class GenericListComponent implements OnInit, OnDestroy { constructor( - private service: AbstractPaperlessService, + private service: AbstractNameFilterService, private modalService: NgbModal, private editDialogComponent: any, private toastService: ToastService) { @@ -28,6 +30,10 @@ export abstract class GenericListComponent implements On public sortField: string public sortReverse: boolean + private nameFilterDebounce: Subject + private subscription: Subscription + private _nameFilter: string + getMatching(o: MatchingModel) { if (o.matching_algorithm == MATCH_AUTO) { return $localize`Automatic` @@ -44,12 +50,27 @@ export abstract class GenericListComponent implements On this.reloadData() } + ngOnInit(): void { this.reloadData() + + this.nameFilterDebounce = new Subject() + + this.subscription = this.nameFilterDebounce.pipe( + debounceTime(400), + distinctUntilChanged() + ).subscribe(title => { + this._nameFilter = title + this.reloadData() + }) + } + + ngOnDestroy() { + this.subscription.unsubscribe() } reloadData() { - this.service.list(this.page, null, this.sortField, this.sortReverse).subscribe(c => { + this.service.listFiltered(this.page, null, this.sortField, this.sortReverse, this._nameFilter).subscribe(c => { this.data = c.results this.collectionSize = c.count }); @@ -95,4 +116,12 @@ export abstract class GenericListComponent implements On } ) } + + get nameFilter() { + return this._nameFilter + } + + set nameFilter(nameFilter: string) { + this.nameFilterDebounce.next(nameFilter) + } } diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.html b/src-ui/src/app/components/manage/tag-list/tag-list.component.html index 4af22b3cd..036beccbc 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.html +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.html @@ -2,9 +2,15 @@ -
- +
+
+
+ + +
+
+ +
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 new file mode 100644 index 000000000..d605fef49 --- /dev/null +++ b/src-ui/src/app/services/rest/abstract-name-filter-service.ts @@ -0,0 +1,14 @@ +import { ObjectWithId } from 'src/app/data/object-with-id' +import { AbstractPaperlessService } from './abstract-paperless-service' + +export abstract class AbstractNameFilterService extends AbstractPaperlessService { + + listFiltered(page?: number, pageSize?: number, sortField?: string, sortReverse?: boolean, nameFilter?: string) { + let params = {} + if (nameFilter) { + params = {'name__icontains': nameFilter} + } + return this.list(page, pageSize, sortField, sortReverse, params) + } + +} diff --git a/src-ui/src/app/services/rest/correspondent.service.ts b/src-ui/src/app/services/rest/correspondent.service.ts index a609b7dd8..7eac24971 100644 --- a/src-ui/src/app/services/rest/correspondent.service.ts +++ b/src-ui/src/app/services/rest/correspondent.service.ts @@ -1,12 +1,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; -import { AbstractPaperlessService } from './abstract-paperless-service'; +import { AbstractNameFilterService } from './abstract-name-filter-service'; @Injectable({ providedIn: 'root' }) -export class CorrespondentService extends AbstractPaperlessService { +export class CorrespondentService extends AbstractNameFilterService { constructor(http: HttpClient) { super(http, 'correspondents') diff --git a/src-ui/src/app/services/rest/document-type.service.ts b/src-ui/src/app/services/rest/document-type.service.ts index a3ba0d858..4f5b7d0ce 100644 --- a/src-ui/src/app/services/rest/document-type.service.ts +++ b/src-ui/src/app/services/rest/document-type.service.ts @@ -1,12 +1,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; -import { AbstractPaperlessService } from './abstract-paperless-service'; +import { AbstractNameFilterService } from './abstract-name-filter-service'; @Injectable({ providedIn: 'root' }) -export class DocumentTypeService extends AbstractPaperlessService { +export class DocumentTypeService extends AbstractNameFilterService { constructor(http: HttpClient) { super(http, 'document_types') diff --git a/src-ui/src/app/services/rest/tag.service.ts b/src-ui/src/app/services/rest/tag.service.ts index b4151dbb9..7bc55b0c9 100644 --- a/src-ui/src/app/services/rest/tag.service.ts +++ b/src-ui/src/app/services/rest/tag.service.ts @@ -1,12 +1,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { PaperlessTag } from 'src/app/data/paperless-tag'; -import { AbstractPaperlessService } from './abstract-paperless-service'; +import { AbstractNameFilterService } from './abstract-name-filter-service'; @Injectable({ providedIn: 'root' }) -export class TagService extends AbstractPaperlessService { +export class TagService extends AbstractNameFilterService { constructor(http: HttpClient) { super(http, 'tags') From 5355f2b027e448b1e142ed8ed2dbb0ce010d3dd5 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Tue, 19 Jan 2021 14:43:55 +0100 Subject: [PATCH 40/61] fixes #351 --- .../management/commands/document_consumer.py | 44 ++++++++++++++++--- .../tests/test_management_consumer.py | 27 +++++++++++- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index d0c045db2..595bd39cd 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -5,7 +5,6 @@ from django.conf import settings from django.core.management.base import BaseCommand, CommandError -from django.utils.text import slugify from django_q.tasks import async_task from watchdog.events import FileSystemEventHandler from watchdog.observers.polling import PollingObserver @@ -71,6 +70,31 @@ def _consume(filepath): "Error while consuming document: {}".format(e)) +def _test_inotify(directory): + if not INotify: + return False + + test_file = os.path.join(directory, "__inotify_test_file__") + inotify = INotify() + descriptor = None + try: + inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO + descriptor = inotify.add_watch(directory, inotify_flags) + Path(test_file).touch() + events = inotify.read(timeout=1000) + return len(events) == 1 + except Exception as e: + logger.warning( + f"Error while checking inotify availability: {str(e)}") + return False + finally: + if descriptor: + inotify.rm_watch(descriptor) + inotify.close() + if os.path.isfile(test_file): + os.unlink(test_file) + + def _consume_wait_unmodified(file, num_tries=20, wait_time=1): mtime = -1 current_try = 0 @@ -154,17 +178,25 @@ def handle(self, *args, **options): if options["oneshot"]: return - if settings.CONSUMER_POLLING == 0 and INotify: - self.handle_inotify(directory, recursive) + if settings.CONSUMER_POLLING == 0: + if _test_inotify(directory): + self.handle_inotify(directory, recursive) + else: + logger.warning( + f"Inotify notifications are not available on {directory}, " + f"falling back to polling every 10 seconds") + self.handle_polling( + directory, recursive, 10) else: - self.handle_polling(directory, recursive) + self.handle_polling( + directory, recursive, settings.CONSUMER_POLLING) logger.debug("Consumer exiting.") - def handle_polling(self, directory, recursive): + def handle_polling(self, directory, recursive, timeout): logging.getLogger(__name__).info( f"Polling directory for changes: {directory}") - self.observer = PollingObserver(timeout=settings.CONSUMER_POLLING) + self.observer = PollingObserver(timeout=timeout) self.observer.schedule(Handler(), directory, recursive=recursive) self.observer.start() try: diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index b6a61a167..0680e7f56 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -7,8 +7,9 @@ from django.conf import settings from django.core.management import call_command, CommandError -from django.test import override_settings, TransactionTestCase +from django.test import override_settings, TransactionTestCase, TestCase +from documents.management.commands.document_consumer import _test_inotify from documents.models import Tag from documents.consumer import ConsumerError from documents.management.commands import document_consumer @@ -260,3 +261,27 @@ def test_consume_file_with_path_tags(self): @override_settings(CONSUMER_POLLING=1) def test_consume_file_with_path_tags_polling(self): self.test_consume_file_with_path_tags() + + +class TestInotify(DirectoriesMixin, TestCase): + + def test_inotify(self): + self.assertTrue(_test_inotify(self.dirs.consumption_dir)) + + @mock.patch("documents.management.commands.document_consumer.Path.touch") + def test_inotify_error(self, m): + m.side_effect = OSError("Permission error") + self.assertFalse(_test_inotify(self.dirs.consumption_dir)) + + @mock.patch("documents.management.commands.document_consumer.Command.handle_polling") + @mock.patch("documents.management.commands.document_consumer.Command.handle_inotify") + @mock.patch("documents.management.commands.document_consumer._test_inotify") + def test_polling_fallback(self, test_inotify, handle_inotify, handle_polling): + test_inotify.return_value = False + + cmd = document_consumer.Command() + cmd.handle(directory=settings.CONSUMPTION_DIR, oneshot=False) + + test_inotify.assert_called_once() + handle_polling.assert_called_once() + handle_inotify.assert_not_called() From 5e841531be10fee6ebb061c3088b51391d241e1e Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Tue, 19 Jan 2021 16:14:28 +0100 Subject: [PATCH 41/61] more test --- src/documents/tests/test_management_exporter.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/documents/tests/test_management_exporter.py b/src/documents/tests/test_management_exporter.py index 212b87ec1..986e458b9 100644 --- a/src/documents/tests/test_management_exporter.py +++ b/src/documents/tests/test_management_exporter.py @@ -74,6 +74,11 @@ def test_exporter(self, use_filename_format=False): self.assertTrue(os.path.exists(os.path.join(self.target, "manifest.json"))) + self.assertEqual(self._get_document_from_manifest(manifest, self.d1.id)['fields']['title'], "wow1") + self.assertEqual(self._get_document_from_manifest(manifest, self.d2.id)['fields']['title'], "wow2") + self.assertEqual(self._get_document_from_manifest(manifest, self.d3.id)['fields']['title'], "wow2") + self.assertEqual(self._get_document_from_manifest(manifest, self.d4.id)['fields']['title'], "wow_dec") + for element in manifest: if element['model'] == 'documents.document': fname = os.path.join(self.target, element[document_exporter.EXPORTER_FILE_NAME]) From 7834d369c67a2e1b4abbc74a9240db616a72bcd7 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Tue, 19 Jan 2021 16:23:49 +0100 Subject: [PATCH 42/61] txt preview #338 --- .../document-detail/document-detail.component.html | 6 +++++- 1 file changed, 5 insertions(+), 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 253d5b167..639b9e260 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 @@ -141,6 +141,10 @@ - + + + + + From c808011b75abe61e47a69003f7e951cb10491bb3 Mon Sep 17 00:00:00 2001 From: Jonas Winkler <17569239+jonaswinkler@users.noreply.github.com> Date: Wed, 20 Jan 2021 11:37:32 +0100 Subject: [PATCH 43/61] Update Crowdin configuration file --- crowdin.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..6291aa2b0 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,5 @@ +files: + - source: /src/documents/locale/en-us/LC_MESSAGES/django.po + translation: /src/documents/locale/%two_letters_code%/LC_MESSAGES/django.po + - source: /src-ui/messages.xlf + translation: /src-ui/src/locale/messages.%two_letters_code%.xlf From f06edebbd863fdcfbf63f74badf4ac3c9a20f922 Mon Sep 17 00:00:00 2001 From: Jonas Winkler <17569239+jonaswinkler@users.noreply.github.com> Date: Wed, 20 Jan 2021 11:39:20 +0100 Subject: [PATCH 44/61] Update Crowdin configuration file --- crowdin.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crowdin.yml b/crowdin.yml index 6291aa2b0..f5a7765dd 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,5 +1,5 @@ files: - - source: /src/documents/locale/en-us/LC_MESSAGES/django.po - translation: /src/documents/locale/%two_letters_code%/LC_MESSAGES/django.po + - source: /src/locale/en-us/LC_MESSAGES/django.po + translation: /src/locale/%two_letters_code%/LC_MESSAGES/django.po - source: /src-ui/messages.xlf translation: /src-ui/src/locale/messages.%two_letters_code%.xlf From 08046cb83f95dbcb9ebe9bba37d8d2a9c77c0b67 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 20 Jan 2021 11:56:09 +0100 Subject: [PATCH 45/61] revert changes for #351 --- .../management/commands/document_consumer.py | 41 ++----------------- .../tests/test_management_consumer.py | 27 +----------- 2 files changed, 5 insertions(+), 63 deletions(-) diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 595bd39cd..690f5299a 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -70,31 +70,6 @@ def _consume(filepath): "Error while consuming document: {}".format(e)) -def _test_inotify(directory): - if not INotify: - return False - - test_file = os.path.join(directory, "__inotify_test_file__") - inotify = INotify() - descriptor = None - try: - inotify_flags = flags.CLOSE_WRITE | flags.MOVED_TO - descriptor = inotify.add_watch(directory, inotify_flags) - Path(test_file).touch() - events = inotify.read(timeout=1000) - return len(events) == 1 - except Exception as e: - logger.warning( - f"Error while checking inotify availability: {str(e)}") - return False - finally: - if descriptor: - inotify.rm_watch(descriptor) - inotify.close() - if os.path.isfile(test_file): - os.unlink(test_file) - - def _consume_wait_unmodified(file, num_tries=20, wait_time=1): mtime = -1 current_try = 0 @@ -179,24 +154,16 @@ def handle(self, *args, **options): return if settings.CONSUMER_POLLING == 0: - if _test_inotify(directory): - self.handle_inotify(directory, recursive) - else: - logger.warning( - f"Inotify notifications are not available on {directory}, " - f"falling back to polling every 10 seconds") - self.handle_polling( - directory, recursive, 10) + self.handle_inotify(directory, recursive) else: - self.handle_polling( - directory, recursive, settings.CONSUMER_POLLING) + self.handle_polling(directory, recursive) logger.debug("Consumer exiting.") - def handle_polling(self, directory, recursive, timeout): + def handle_polling(self, directory, recursive): logging.getLogger(__name__).info( f"Polling directory for changes: {directory}") - self.observer = PollingObserver(timeout=timeout) + self.observer = PollingObserver(timeout=settings.CONSUMER_POLLING) self.observer.schedule(Handler(), directory, recursive=recursive) self.observer.start() try: diff --git a/src/documents/tests/test_management_consumer.py b/src/documents/tests/test_management_consumer.py index 0680e7f56..b6a61a167 100644 --- a/src/documents/tests/test_management_consumer.py +++ b/src/documents/tests/test_management_consumer.py @@ -7,9 +7,8 @@ from django.conf import settings from django.core.management import call_command, CommandError -from django.test import override_settings, TransactionTestCase, TestCase +from django.test import override_settings, TransactionTestCase -from documents.management.commands.document_consumer import _test_inotify from documents.models import Tag from documents.consumer import ConsumerError from documents.management.commands import document_consumer @@ -261,27 +260,3 @@ def test_consume_file_with_path_tags(self): @override_settings(CONSUMER_POLLING=1) def test_consume_file_with_path_tags_polling(self): self.test_consume_file_with_path_tags() - - -class TestInotify(DirectoriesMixin, TestCase): - - def test_inotify(self): - self.assertTrue(_test_inotify(self.dirs.consumption_dir)) - - @mock.patch("documents.management.commands.document_consumer.Path.touch") - def test_inotify_error(self, m): - m.side_effect = OSError("Permission error") - self.assertFalse(_test_inotify(self.dirs.consumption_dir)) - - @mock.patch("documents.management.commands.document_consumer.Command.handle_polling") - @mock.patch("documents.management.commands.document_consumer.Command.handle_inotify") - @mock.patch("documents.management.commands.document_consumer._test_inotify") - def test_polling_fallback(self, test_inotify, handle_inotify, handle_polling): - test_inotify.return_value = False - - cmd = document_consumer.Command() - cmd.handle(directory=settings.CONSUMPTION_DIR, oneshot=False) - - test_inotify.assert_called_once() - handle_polling.assert_called_once() - handle_inotify.assert_not_called() From 9f1436a865621d313de7c6fd4eccd3957eb7b7ea Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 20 Jan 2021 12:34:01 +0100 Subject: [PATCH 46/61] more test --- src/documents/tests/test_api.py | 47 +++++++++++++++++++++++++++++ src/documents/tests/test_parsers.py | 1 + 2 files changed, 48 insertions(+) diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index 5f81b83b6..6b9cf14e0 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -923,6 +923,14 @@ def test_api_invalid_correspondent(self): doc2 = Document.objects.get(id=self.doc2.id) self.assertEqual(doc2.correspondent, self.c1) + def test_api_no_correspondent(self): + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "set_correspondent", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + def test_api_invalid_document_type(self): self.assertEqual(self.doc2.document_type, self.dt1) response = self.client.post("/api/documents/bulk_edit/", json.dumps({ @@ -935,6 +943,14 @@ def test_api_invalid_document_type(self): doc2 = Document.objects.get(id=self.doc2.id) self.assertEqual(doc2.document_type, self.dt1) + def test_api_no_document_type(self): + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "set_document_type", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + def test_api_add_invalid_tag(self): self.assertEqual(list(self.doc2.tags.all()), [self.t1]) response = self.client.post("/api/documents/bulk_edit/", json.dumps({ @@ -946,6 +962,14 @@ def test_api_add_invalid_tag(self): self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + def test_api_add_tag_no_tag(self): + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "add_tag", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + def test_api_delete_invalid_tag(self): self.assertEqual(list(self.doc2.tags.all()), [self.t1]) response = self.client.post("/api/documents/bulk_edit/", json.dumps({ @@ -957,6 +981,14 @@ def test_api_delete_invalid_tag(self): self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + def test_api_delete_tag_no_tag(self): + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "remove_tag", + "parameters": {} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + def test_api_modify_invalid_tags(self): self.assertEqual(list(self.doc2.tags.all()), [self.t1]) response = self.client.post("/api/documents/bulk_edit/", json.dumps({ @@ -966,6 +998,21 @@ def test_api_modify_invalid_tags(self): }), content_type='application/json') self.assertEqual(response.status_code, 400) + def test_api_modify_tags_no_tags(self): + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "modify_tags", + "parameters": {"remove_tags": [1123123]} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc2.id], + "method": "modify_tags", + "parameters": {'add_tags': [self.t2.id, 1657]} + }), content_type='application/json') + self.assertEqual(response.status_code, 400) + def test_api_selection_data_empty(self): response = self.client.post("/api/documents/selection_data/", json.dumps({ "documents": [] diff --git a/src/documents/tests/test_parsers.py b/src/documents/tests/test_parsers.py index 805e4beac..392c0504f 100644 --- a/src/documents/tests/test_parsers.py +++ b/src/documents/tests/test_parsers.py @@ -120,3 +120,4 @@ def test_file_extensions(self): self.assertTrue(is_file_ext_supported('.pdf')) self.assertFalse(is_file_ext_supported('.hsdfh')) + self.assertFalse(is_file_ext_supported('')) From f6a2cc74e84847fbd01b0d16e3ed6b3eee76ee85 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 20 Jan 2021 14:25:52 +0100 Subject: [PATCH 47/61] clicking on small and large cards changed --- .../document-card-large/document-card-large.component.html | 4 ++-- .../document-card-small/document-card-small.component.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index 85964f528..d26c17159 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -1,6 +1,6 @@ -
+
-
+
diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index a0d0299a5..882de71ed 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,6 +1,6 @@
-
-
+
+
From 43685baff32b1e5c6cee764bd0f95764917cddf6 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 20 Jan 2021 14:29:30 +0100 Subject: [PATCH 48/61] fix tag clicking on document table list --- .../app/components/document-list/document-list.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index a29e146bf..cfc2e655d 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -152,7 +152,7 @@
{{d.title | documentTitle}} - + From 482ef6313da570e6bbe84b4253200efff0377f33 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 20 Jan 2021 14:35:38 +0100 Subject: [PATCH 49/61] changelog --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 721e9520e..28bcc00e6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,6 @@ Changelog ********* - paperless-ng 1.0.0 ################## @@ -25,7 +24,8 @@ Nothing special about this release, but since there are relatively few bug repor * Added a language selector to the settings. * Added date format options to the settings. * Range selection with shift clicking is now possible in the document list. - + * Filtering correspondent, type and tag management pages by name. + * Focus "Name" field in dialogs by default. paperless-ng 0.9.14 From 03e159f67c623d8fc79f4a8c9ca01640a250b4a6 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 20 Jan 2021 13:44:24 +0000 Subject: [PATCH 50/61] Translate /src-ui/messages.xlf in nl_NL at least 99% translated for the source file '/src-ui/messages.xlf' on the 'nl_NL' language. Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format --- src-ui/src/locale/messages.nl_NL.xlf | 42 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src-ui/src/locale/messages.nl_NL.xlf b/src-ui/src/locale/messages.nl_NL.xlf index 39545c0e2..49f3010ad 100644 --- a/src-ui/src/locale/messages.nl_NL.xlf +++ b/src-ui/src/locale/messages.nl_NL.xlf @@ -449,12 +449,20 @@ 2 + + Filter by: + Filter op: + + src/app/components/manage/tag-list/tag-list.component.html + 8 + + Name Naam src/app/components/manage/tag-list/tag-list.component.html - 13 + 9 @@ -462,7 +470,7 @@ Kleur src/app/components/manage/tag-list/tag-list.component.html - 14 + 20 @@ -470,7 +478,7 @@ Overeenkomend src/app/components/manage/tag-list/tag-list.component.html - 15 + 21 @@ -478,7 +486,7 @@ Aantal documenten src/app/components/manage/tag-list/tag-list.component.html - 16 + 22 @@ -486,7 +494,7 @@ Acties src/app/components/manage/tag-list/tag-list.component.html - 17 + 23 @@ -494,7 +502,7 @@ Documenten src/app/components/manage/tag-list/tag-list.component.html - 32 + 38 @@ -502,7 +510,7 @@ Bewerk src/app/components/manage/tag-list/tag-list.component.html - 37 + 43 @@ -814,7 +822,7 @@ Laatste correspondentie src/app/components/manage/correspondent-list/correspondent-list.component.html - 15 + 22 @@ -1114,14 +1122,6 @@ 46 - - Filter by: - Filter op: - - src/app/components/document-list/filter-editor/filter-editor.component.html - 4 - - Filter tags Etiketten filteren @@ -1907,7 +1907,7 @@ Automatisch src/app/components/manage/generic-list/generic-list.component.ts - 33 + 39 @@ -1915,7 +1915,7 @@ Wil je dit element echt verwijderen? src/app/components/manage/generic-list/generic-list.component.ts - 76 + 97 @@ -1923,7 +1923,7 @@ Geassocieerde documenten zullen niet verwijderd worden. src/app/components/manage/generic-list/generic-list.component.ts - 83 + 104 @@ -1931,7 +1931,7 @@ Verwijderen src/app/components/manage/generic-list/generic-list.component.ts - 85 + 106 @@ -1939,7 +1939,7 @@ Fout bij het verwijderen van het element: src/app/components/manage/generic-list/generic-list.component.ts - 93 + 114 From 54916fec2ed1b963cbe2cc177a96faca4c588c4b Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 20 Jan 2021 13:44:31 +0000 Subject: [PATCH 51/61] Translate /src-ui/messages.xlf in fr at least 99% translated for the source file '/src-ui/messages.xlf' on the 'fr' language. Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format --- src-ui/src/locale/messages.fr.xlf | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src-ui/src/locale/messages.fr.xlf b/src-ui/src/locale/messages.fr.xlf index ee97f7ce0..0ee824d5b 100644 --- a/src-ui/src/locale/messages.fr.xlf +++ b/src-ui/src/locale/messages.fr.xlf @@ -449,12 +449,20 @@ 2 + + Filter by: + Filtrer par : + + src/app/components/manage/tag-list/tag-list.component.html + 8 + + Name Nom src/app/components/manage/tag-list/tag-list.component.html - 13 + 9 @@ -462,7 +470,7 @@ Couleur src/app/components/manage/tag-list/tag-list.component.html - 14 + 20 @@ -470,7 +478,7 @@ Rapprochement src/app/components/manage/tag-list/tag-list.component.html - 15 + 21 @@ -478,7 +486,7 @@ Nombre de documents src/app/components/manage/tag-list/tag-list.component.html - 16 + 22 @@ -486,7 +494,7 @@ Actions src/app/components/manage/tag-list/tag-list.component.html - 17 + 23 @@ -494,7 +502,7 @@ Documents src/app/components/manage/tag-list/tag-list.component.html - 32 + 38 @@ -502,7 +510,7 @@ Éditer src/app/components/manage/tag-list/tag-list.component.html - 37 + 43 @@ -814,7 +822,7 @@ Dernière correspondance src/app/components/manage/correspondent-list/correspondent-list.component.html - 15 + 22 @@ -1114,14 +1122,6 @@ 46 - - Filter by: - Filtrer par : - - src/app/components/document-list/filter-editor/filter-editor.component.html - 4 - - Filter tags Filtrer les étiquettes @@ -1907,7 +1907,7 @@ Automatique src/app/components/manage/generic-list/generic-list.component.ts - 33 + 39 @@ -1915,7 +1915,7 @@ Voulez-vous vraiment supprimer cet élément ? src/app/components/manage/generic-list/generic-list.component.ts - 76 + 97 @@ -1923,7 +1923,7 @@ Les documents associés ne seront pas supprimés. src/app/components/manage/generic-list/generic-list.component.ts - 83 + 104 @@ -1931,7 +1931,7 @@ Supprimer src/app/components/manage/generic-list/generic-list.component.ts - 85 + 106 @@ -1939,7 +1939,7 @@ Une erreur s'est produite lors de la suppression de l'élément : src/app/components/manage/generic-list/generic-list.component.ts - 93 + 114 From 937cdaccadc303eff159086bd86c8fcf831162b0 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Wed, 20 Jan 2021 13:44:37 +0000 Subject: [PATCH 52/61] Translate /src-ui/messages.xlf in de at least 99% translated for the source file '/src-ui/messages.xlf' on the 'de' language. Manual sync of partially translated files: untranslated content is included with an empty translation or source language content depending on file format --- src-ui/src/locale/messages.de.xlf | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src-ui/src/locale/messages.de.xlf b/src-ui/src/locale/messages.de.xlf index 80cfefab9..e1539618c 100644 --- a/src-ui/src/locale/messages.de.xlf +++ b/src-ui/src/locale/messages.de.xlf @@ -449,12 +449,20 @@ 2 + + Filter by: + Filtern nach: + + src/app/components/manage/tag-list/tag-list.component.html + 8 + + Name Name src/app/components/manage/tag-list/tag-list.component.html - 13 + 9 @@ -462,7 +470,7 @@ Farbe src/app/components/manage/tag-list/tag-list.component.html - 14 + 20 @@ -470,7 +478,7 @@ Zuweisung src/app/components/manage/tag-list/tag-list.component.html - 15 + 21 @@ -478,7 +486,7 @@ Anzahl Dokumente src/app/components/manage/tag-list/tag-list.component.html - 16 + 22 @@ -486,7 +494,7 @@ Aktionen src/app/components/manage/tag-list/tag-list.component.html - 17 + 23 @@ -494,7 +502,7 @@ Dokumente src/app/components/manage/tag-list/tag-list.component.html - 32 + 38 @@ -502,7 +510,7 @@ Bearbeiten src/app/components/manage/tag-list/tag-list.component.html - 37 + 43 @@ -814,7 +822,7 @@ Letzter Kontakt src/app/components/manage/correspondent-list/correspondent-list.component.html - 15 + 22 @@ -1114,14 +1122,6 @@ 46 - - Filter by: - Filtern nach: - - src/app/components/document-list/filter-editor/filter-editor.component.html - 4 - - Filter tags Tags filtern @@ -1907,7 +1907,7 @@ Automatisch src/app/components/manage/generic-list/generic-list.component.ts - 33 + 39 @@ -1915,7 +1915,7 @@ Möchten Sie dieses Element wirklich löschen? src/app/components/manage/generic-list/generic-list.component.ts - 76 + 97 @@ -1923,7 +1923,7 @@ Assoziierte Dokumente werden nicht gelöscht. src/app/components/manage/generic-list/generic-list.component.ts - 83 + 104 @@ -1931,7 +1931,7 @@ Löschen src/app/components/manage/generic-list/generic-list.component.ts - 85 + 106 @@ -1939,7 +1939,7 @@ Fehler beim Löschen des Elements: src/app/components/manage/generic-list/generic-list.component.ts - 93 + 114 From 9575b0fe3210faff9b41ca52d8081ebbc3b9fcba Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Wed, 20 Jan 2021 20:28:42 +0100 Subject: [PATCH 53/61] fixes #397 --- src/documents/models.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/documents/models.py b/src/documents/models.py index 928b8aa4d..86878dd7e 100755 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -63,12 +63,6 @@ class Meta: def __str__(self): return self.name - def save(self, *args, **kwargs): - - self.match = self.match.lower() - - models.Model.save(self, *args, **kwargs) - class Correspondent(MatchingModel): From ddf386c4d4aa61f59c671a54be07e5880ec95276 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 21 Jan 2021 01:02:41 +0100 Subject: [PATCH 54/61] add temporary directory during startup and ensure correct permissions --- docker/docker-entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index 8e1a21757..5919b14aa 100644 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -79,7 +79,11 @@ initialize() { fi done + echo "creating directory /tmp/paperless" + mkdir -p /tmp/paperless + chown -R paperless:paperless ../ + chown -R paperless:paperless /tmp/paperless migrations From aeb10d34079bb62d5d748698062de0bfa9a20585 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 21 Jan 2021 12:07:50 +0100 Subject: [PATCH 55/61] fixes duplicate documents with tags__id__in --- src/documents/tests/test_api.py | 6 ++++++ src/documents/views.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index 6b9cf14e0..2b332a873 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -228,6 +228,12 @@ def test_document_filters(self): self.assertEqual(len(results), 2) self.assertCountEqual([results[0]['id'], results[1]['id']], [doc1.id, doc3.id]) + response = self.client.get("/api/documents/?tags__id__in={},{}".format(tag_2.id, tag_3.id)) + self.assertEqual(response.status_code, 200) + results = response.data['results'] + self.assertEqual(len(results), 2) + self.assertCountEqual([results[0]['id'], results[1]['id']], [doc2.id, doc3.id]) + response = self.client.get("/api/documents/?tags__id__all={},{}".format(tag_2.id, tag_3.id)) self.assertEqual(response.status_code, 200) results = response.data['results'] diff --git a/src/documents/views.py b/src/documents/views.py index eb9078f75..b99bf11c7 100755 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -159,6 +159,9 @@ class DocumentViewSet(RetrieveModelMixin, "added", "archive_serial_number") + def get_queryset(self): + return Document.objects.distinct() + def get_serializer(self, *args, **kwargs): fields_param = self.request.query_params.get('fields', None) if fields_param: From 11f9616411612e501755b314bd73a3263fb99151 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 21 Jan 2021 21:05:16 +0100 Subject: [PATCH 56/61] improve locale selection options --- src-ui/messages.xlf | 14 +++++++------- .../manage/settings/settings.component.html | 4 ++-- .../manage/settings/settings.component.ts | 5 +++-- src-ui/src/app/services/settings.service.ts | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 1e89a9408..6175cf700 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -482,35 +482,35 @@ Saved view "" deleted. src/app/components/manage/settings/settings.component.ts - 62 + 63 Settings saved successfully. src/app/components/manage/settings/settings.component.ts - 78 + 79 Use system language src/app/components/manage/settings/settings.component.ts - 82 + 83 Use date format of display language src/app/components/manage/settings/settings.component.ts - 86 + 87 Error while storing settings on server: src/app/components/manage/settings/settings.component.ts - 102 + 103 @@ -1463,8 +1463,8 @@ 12 - - English + + English (US) src/app/services/settings.service.ts 74 diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html index 13ffb4517..4ed5ad1ae 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.html +++ b/src-ui/src/app/components/manage/settings/settings.component.html @@ -19,7 +19,7 @@

Appearance

You need to reload the page after applying a new language. @@ -34,7 +34,7 @@

Appearance

diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index 3c4de59b6..47f714c21 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, Renderer2 } from '@angular/core'; +import { Component, Inject, LOCALE_ID, OnInit, Renderer2 } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; @@ -38,7 +38,8 @@ export class SettingsComponent implements OnInit { public savedViewService: SavedViewService, private documentListViewService: DocumentListViewService, private toastService: ToastService, - private settings: SettingsService + private settings: SettingsService, + @Inject(LOCALE_ID) public currentLocale: string ) { } ngOnInit() { diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index d2a190c1f..041fb51ca 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -71,7 +71,7 @@ export class SettingsService { getLanguageOptions(): LanguageOption[] { return [ - {code: "en-us", name: $localize`English`, englishName: "English"}, + {code: "en-US", name: $localize`English (US)`, englishName: "English (US)"}, {code: "de", name: $localize`German`, englishName: "German"}, {code: "nl", name: $localize`Dutch`, englishName: "Dutch"}, {code: "fr", name: $localize`French`, englishName: "French"} From 3b22d7131dfd924909da3be7f8a0713f54b60a92 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 21 Jan 2021 20:08:36 +0000 Subject: [PATCH 57/61] Translate /src-ui/messages.xlf in nl_NL translation completed for the source file '/src-ui/messages.xlf' on the 'nl_NL' language. --- src-ui/src/locale/messages.nl_NL.xlf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src-ui/src/locale/messages.nl_NL.xlf b/src-ui/src/locale/messages.nl_NL.xlf index 49f3010ad..3753bc92c 100644 --- a/src-ui/src/locale/messages.nl_NL.xlf +++ b/src-ui/src/locale/messages.nl_NL.xlf @@ -550,7 +550,7 @@ Opgeslagen view "" verwijderd. src/app/components/manage/settings/settings.component.ts - 62 + 63 @@ -558,7 +558,7 @@ Instellingen succesvol opgeslagen. src/app/components/manage/settings/settings.component.ts - 78 + 79 @@ -566,7 +566,7 @@ Gebruik de systeemtaal src/app/components/manage/settings/settings.component.ts - 82 + 83 @@ -574,7 +574,7 @@ Datumopmaak van weergavetaal gebruiken src/app/components/manage/settings/settings.component.ts - 86 + 87 @@ -582,7 +582,7 @@ Fout bij het opslaan van de instellingen: src/app/components/manage/settings/settings.component.ts - 102 + 103 @@ -1670,9 +1670,9 @@ 12 - - English - Engels + + English (US) + Engels (US) src/app/services/settings.service.ts 74 From 099b84a66dd19a8dd44e2eea613c6b040b871bba Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 21 Jan 2021 20:08:46 +0000 Subject: [PATCH 58/61] Translate /src-ui/messages.xlf in fr translation completed for the source file '/src-ui/messages.xlf' on the 'fr' language. --- src-ui/src/locale/messages.fr.xlf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src-ui/src/locale/messages.fr.xlf b/src-ui/src/locale/messages.fr.xlf index 0ee824d5b..4f09eab72 100644 --- a/src-ui/src/locale/messages.fr.xlf +++ b/src-ui/src/locale/messages.fr.xlf @@ -550,7 +550,7 @@ Vue "" supprimée. src/app/components/manage/settings/settings.component.ts - 62 + 63 @@ -558,7 +558,7 @@ Paramètres enregistrés avec succès. src/app/components/manage/settings/settings.component.ts - 78 + 79 @@ -566,7 +566,7 @@ Utiliser la langue du système src/app/components/manage/settings/settings.component.ts - 82 + 83 @@ -574,7 +574,7 @@ Utiliser le format de date de la langue d'affichage src/app/components/manage/settings/settings.component.ts - 86 + 87 @@ -582,7 +582,7 @@ Une erreur s'est produite lors de l'enregistrement des paramètres sur le serveur : src/app/components/manage/settings/settings.component.ts - 102 + 103 @@ -1670,9 +1670,9 @@ 12 - - English - Anglais + + English (US) + Anglais (US) src/app/services/settings.service.ts 74 From 18ebb58dc90cdfb1ddb665ae5cc30918b550efbe Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Thu, 21 Jan 2021 20:08:58 +0000 Subject: [PATCH 59/61] Translate /src-ui/messages.xlf in de translation completed for the source file '/src-ui/messages.xlf' on the 'de' language. --- src-ui/src/locale/messages.de.xlf | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src-ui/src/locale/messages.de.xlf b/src-ui/src/locale/messages.de.xlf index e1539618c..a898e4630 100644 --- a/src-ui/src/locale/messages.de.xlf +++ b/src-ui/src/locale/messages.de.xlf @@ -550,7 +550,7 @@ Gespeicherte Ansicht "" gelöscht. src/app/components/manage/settings/settings.component.ts - 62 + 63 @@ -558,7 +558,7 @@ Einstellungen erfolgreich gespeichert. src/app/components/manage/settings/settings.component.ts - 78 + 79 @@ -566,7 +566,7 @@ Benutze Systemsprache src/app/components/manage/settings/settings.component.ts - 82 + 83 @@ -574,7 +574,7 @@ Benutze Datumsformat der Anzeigesprache src/app/components/manage/settings/settings.component.ts - 86 + 87 @@ -582,7 +582,7 @@ Fehler beim Speichern der Einstellungen auf dem Server: src/app/components/manage/settings/settings.component.ts - 102 + 103 @@ -1670,9 +1670,9 @@ 12 - - English - Englisch + + English (US) + Englisch (US) src/app/services/settings.service.ts 74 From 87ad2ce1d68be2068992461077721ccea8b90866 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 21 Jan 2021 21:32:08 +0100 Subject: [PATCH 60/61] added a note regarding updates after 0.9.13 --- docs/administration.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/administration.rst b/docs/administration.rst index bb543e3db..14b986e82 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -90,10 +90,28 @@ B. If you built the image yourself, do the following: $ docker-compose build $ docker-compose up -Running `docker-compose up` will also apply any new database migrations. +Running ``docker-compose up`` will also apply any new database migrations. If you see everything working, press CTRL+C once to gracefully stop paperless. Then you can start paperless-ng with ``-d`` to have it run in the background. + .. note:: + + In version 0.9.14, the update process was changed. In 0.9.13 and earlier, the + docker-compose files specified exact versions and pull won't automatically + update to newer versions. In order to enable updates as described above, either + get the new ``docker-compose.yml`` file from `here `_ + or edit the ``docker-compose.yml`` file, find the line that says + + .. code:: + + image: jonaswinkler/paperless-ng:0.9.x + + and replace the version with ``latest``: + + .. code:: + + image: jonaswinkler/paperless-ng:latest + Bare Metal Route ================ From 227f7b6946c119fd3b69f9313ea49c08f3d22310 Mon Sep 17 00:00:00 2001 From: jonaswinkler Date: Thu, 21 Jan 2021 22:29:47 +0100 Subject: [PATCH 61/61] revert a change --- src/documents/management/commands/document_consumer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/documents/management/commands/document_consumer.py b/src/documents/management/commands/document_consumer.py index 690f5299a..9ba1b1a87 100644 --- a/src/documents/management/commands/document_consumer.py +++ b/src/documents/management/commands/document_consumer.py @@ -153,7 +153,7 @@ def handle(self, *args, **options): if options["oneshot"]: return - if settings.CONSUMER_POLLING == 0: + if settings.CONSUMER_POLLING == 0 and INotify: self.handle_inotify(directory, recursive) else: self.handle_polling(directory, recursive)