diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py index 88d881664..5c9c0eb37 100644 --- a/docker/gunicorn.conf.py +++ b/docker/gunicorn.conf.py @@ -1,4 +1,4 @@ -bind = '[::]:8000' +bind = ['[::]:8000', 'localhost:8000'] backlog = 2048 workers = 3 worker_class = 'sync' diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml index e7a32bec0..c2b599e52 100644 --- a/docker/hub/docker-compose.postgres.yml +++ b/docker/hub/docker-compose.postgres.yml @@ -15,7 +15,7 @@ services: POSTGRES_PASSWORD: paperless webserver: - image: jonaswinkler/paperless-ng:0.9.9 + image: jonaswinkler/paperless-ng:0.9.10 restart: always depends_on: - db diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml index 98b7d70a2..429d42c06 100644 --- a/docker/hub/docker-compose.sqlite.yml +++ b/docker/hub/docker-compose.sqlite.yml @@ -5,7 +5,7 @@ services: restart: always webserver: - image: jonaswinkler/paperless-ng:0.9.9 + image: jonaswinkler/paperless-ng:0.9.10 restart: always depends_on: - broker diff --git a/docker/local/Dockerfile b/docker/local/Dockerfile index 9fc71abc7..1fd5e24fa 100644 --- a/docker/local/Dockerfile +++ b/docker/local/Dockerfile @@ -22,6 +22,7 @@ RUN apt-get update \ libpq-dev \ libqpdf-dev \ libxml2 \ + libxslt1-dev \ optipng \ pngquant \ qpdf \ diff --git a/docker/supervisord.conf b/docker/supervisord.conf index ff3ed4311..9b97b6825 100644 --- a/docker/supervisord.conf +++ b/docker/supervisord.conf @@ -8,7 +8,7 @@ loglevel=info ; log level; default info; others: debug,warn,trace user=root [program:gunicorn] -command=gunicorn -c /usr/src/paperless/gunicorn.conf.py -b '[::]:8000' paperless.wsgi +command=gunicorn -c /usr/src/paperless/gunicorn.conf.py paperless.wsgi user=paperless stdout_logfile=/dev/stdout diff --git a/docs/changelog.rst b/docs/changelog.rst index e63c19d7d..4357a981b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,35 @@ Changelog ********* +paperless-ng 0.9.10 +################### + +* Bulk editing + + * Thanks to `Michael Shamoon`_, we've got a new interface for the bulk editor. + * There are some configuration options in the settings to alter the behavior. + +* Other changes and additions + + * The Paperless-ng logo now navigates to the dashboard. + * Filter for documents that don't have any correspondents, types or tags assigned. + * Tags, types and correspondents are now sorted case insensitive. + * Lots of preparation work for localization support. + +* Fixes + + * Added missing dependencies for Raspberry Pi builds. + * Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts. + * An issue with the search index reporting missing documents after bulk deletes was fixed. + +.. note:: + + 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 `. paperless-ng 0.9.9 ################## diff --git a/docs/configuration.rst b/docs/configuration.rst index efc1a9db1..5ccb80b3a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -400,6 +400,15 @@ PAPERLESS_FILENAME_DATE_ORDER= Defaults to none, which disables this feature. +PAPERLESS_THUMBNAIL_FONT_NAME= + Paperless creates thumbnails for plain text files by rendering the content + of the file on an image and uses a predefined font for that. This + font can be changed here. + + Note that this won't have any effect on already generated thumbnails. + + Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``. + Binaries ######## diff --git a/paperless.conf.example b/paperless.conf.example index 910fc22a0..139453cf3 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -54,6 +54,7 @@ #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh #PAPERLESS_FILENAME_DATE_ORDER=YMD #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] +#PAPERLESS_THUMBNAIL_FONT_NAME= # Binaries diff --git a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts index 1bf5d0216..27472bdc7 100644 --- a/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts +++ b/src-ui/src/app/components/common/date-dropdown/date-dropdown.component.ts @@ -21,10 +21,10 @@ const LAST_YEAR = 3 export class DateDropdownComponent implements OnInit, OnDestroy { quickFilters = [ - {id: LAST_7_DAYS, name: "Last 7 days"}, - {id: LAST_MONTH, name: "Last month"}, - {id: LAST_3_MONTHS, name: "Last 3 months"}, - {id: LAST_YEAR, name: "Last year"} + {id: LAST_7_DAYS, name: $localize`Last 7 days`}, + {id: LAST_MONTH, name: $localize`Last month`}, + {id: LAST_3_MONTHS, name: $localize`Last 3 months`}, + {id: LAST_YEAR, name: $localize`Last year`} ] @Input() diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index 06b2333d5..915d10677 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -142,7 +142,7 @@ export class FilterableDropdownComponent { if (items) { this._selectionModel.items = Array.from(items) this._selectionModel.items.unshift({ - name: "None", + name: $localize`Not assigned`, id: null }) } @@ -195,6 +195,9 @@ export class FilterableDropdownComponent { @Input() editing = false + @Input() + applyOnClose = false + @Output() apply = new EventEmitter() @@ -208,7 +211,9 @@ export class FilterableDropdownComponent { applyClicked() { if (this.selectionModel.isDirty()) { this.dropdown.close() - this.apply.emit(this.selectionModel.diff()) + if (!this.applyOnClose) { + this.apply.emit(this.selectionModel.diff()) + } } } @@ -223,6 +228,9 @@ export class FilterableDropdownComponent { this.open.next() } else { this.filterText = '' + if (this.applyOnClose && this.selectionModel.isDirty()) { + this.apply.emit(this.selectionModel.diff()) + } } } 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 daf01249f..eae3367c1 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 @@ -132,7 +132,7 @@
  -   +     diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html index e08d50c47..62a2bb95d 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html @@ -30,6 +30,7 @@ [items]="tags" [editing]="true" [multiple]="true" + [applyOnClose]="applyOnClose" (open)="openTagsDropdown()" [(selectionModel)]="tagSelectionModel" (apply)="setTags($event)"> @@ -37,6 +38,7 @@ @@ -44,6 +46,7 @@ diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 1e2e8496f..88efcee06 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -15,6 +15,7 @@ import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component'; import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'; import { MatchingModel } from 'src/app/data/matching-model'; +import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; @Component({ selector: 'app-bulk-editor', @@ -38,9 +39,13 @@ export class BulkEditorComponent { public list: DocumentListViewService, private documentService: DocumentService, private modalService: NgbModal, - private openDocumentService: OpenDocumentsService + private openDocumentService: OpenDocumentsService, + private settings: SettingsService ) { } + applyOnClose: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE) + showConfirmationDialogs: boolean = this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS) + ngOnInit() { this.tagService.listAll().subscribe(result => this.tags = result.results) this.correspondentService.listAll().subscribe(result => this.correspondents = result.results) @@ -51,10 +56,10 @@ export class BulkEditorComponent { return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe( tap(() => { this.list.reload() + this.list.reduceSelectionToFilter() this.list.selected.forEach(id => { this.openDocumentService.refreshDocument(id) }) - this.list.selectNone() }) ) } @@ -105,30 +110,39 @@ export class BulkEditorComponent { setTags(changedTags: ChangedItems) { if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return - let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) - modal.componentInstance.title = $localize`Confirm tags assignment` - if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) { - let tag = changedTags.itemsToAdd[0] - modal.componentInstance.message = $localize`This operation will add the tag ${tag.name} to all ${this.list.selected.size} selected document(s).` - } else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) { - modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to all ${this.list.selected.size} selected document(s).` - } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) { - let tag = changedTags.itemsToAdd[0] - modal.componentInstance.message = $localize`This operation will remove the tag ${tag.name} from all ${this.list.selected.size} selected document(s).` - } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) { - modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from all ${this.list.selected.size} selected document(s).` + if (this.showConfirmationDialogs) { + let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) + modal.componentInstance.title = $localize`Confirm tags assignment` + if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) { + let tag = changedTags.itemsToAdd[0] + modal.componentInstance.message = $localize`This operation will add the tag ${tag.name} to all ${this.list.selected.size} selected document(s).` + } else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) { + modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to all ${this.list.selected.size} selected document(s).` + } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) { + let tag = changedTags.itemsToRemove[0] + modal.componentInstance.message = $localize`This operation will remove the tag ${tag.name} from all ${this.list.selected.size} selected document(s).` + } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) { + modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from all ${this.list.selected.size} selected document(s).` + } else { + modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on all ${this.list.selected.size} selected document(s).` + } + + modal.componentInstance.btnClass = "btn-warning" + modal.componentInstance.btnCaption = $localize`Confirm` + modal.componentInstance.confirmClicked.subscribe(() => { + this.performSetTags(modal, changedTags) + }) } else { - modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on all ${this.list.selected.size} selected document(s).` + this.performSetTags(null, changedTags) } - - modal.componentInstance.btnClass = "btn-warning" - modal.componentInstance.btnCaption = $localize`Confirm` - modal.componentInstance.confirmClicked.subscribe(() => { - this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe( - response => { - this.tagService.clearCache() + } + + private performSetTags(modal, changedTags: ChangedItems) { + this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe( + response => { + if (modal) { modal.close() - }) + } } ) } @@ -136,47 +150,67 @@ export class BulkEditorComponent { setCorrespondents(changedCorrespondents: ChangedItems) { if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return - let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) - modal.componentInstance.title = $localize`Confirm correspondent assignment` let correspondent = changedCorrespondents.itemsToAdd.length > 0 ? changedCorrespondents.itemsToAdd[0] : null - if (correspondent) { - modal.componentInstance.message = $localize`This operation will assign the correspondent ${correspondent.name} to all ${this.list.selected.size} selected document(s).` + + if (this.showConfirmationDialogs) { + let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) + modal.componentInstance.title = $localize`Confirm correspondent assignment` + if (correspondent) { + modal.componentInstance.message = $localize`This operation will assign the correspondent ${correspondent.name} to all ${this.list.selected.size} selected document(s).` + } else { + modal.componentInstance.message = $localize`This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).` + } + modal.componentInstance.btnClass = "btn-warning" + modal.componentInstance.btnCaption = $localize`Confirm` + modal.componentInstance.confirmClicked.subscribe(() => { + this.performSetCorrespondents(modal, correspondent) + }) } else { - modal.componentInstance.message = $localize`This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).` + this.performSetCorrespondents(null, correspondent) } - modal.componentInstance.btnClass = "btn-warning" - modal.componentInstance.btnCaption = $localize`Confirm` - modal.componentInstance.confirmClicked.subscribe(() => { - this.executeBulkOperation('set_correspondent', {"correspondent": correspondent?.id}).subscribe( - response => { - this.correspondentService.clearCache() + } + + private performSetCorrespondents(modal, correspondent: MatchingModel) { + this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe( + response => { + if (modal) { modal.close() } - ) - }) + } + ) } setDocumentTypes(changedDocumentTypes: ChangedItems) { if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return - let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) - modal.componentInstance.title = $localize`Confirm document type assignment` let documentType = changedDocumentTypes.itemsToAdd.length > 0 ? changedDocumentTypes.itemsToAdd[0] : null - if (documentType) { - modal.componentInstance.message = $localize`This operation will assign the document type ${documentType.name} to all ${this.list.selected.size} selected document(s).` + + if (this.showConfirmationDialogs) { + let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) + modal.componentInstance.title = $localize`Confirm document type assignment` + if (documentType) { + modal.componentInstance.message = $localize`This operation will assign the document type ${documentType.name} to all ${this.list.selected.size} selected document(s).` + } else { + modal.componentInstance.message = $localize`This operation will remove the document type from all ${this.list.selected.size} selected document(s).` + } + modal.componentInstance.btnClass = "btn-warning" + modal.componentInstance.btnCaption = $localize`Confirm` + modal.componentInstance.confirmClicked.subscribe(() => { + this.performSetDocumentTypes(modal, documentType) + }) } else { - modal.componentInstance.message = $localize`This operation will remove the document type from all ${this.list.selected.size} selected document(s).` + this.performSetDocumentTypes(null, documentType) } - modal.componentInstance.btnClass = "btn-warning" - modal.componentInstance.btnCaption = $localize`Confirm` - modal.componentInstance.confirmClicked.subscribe(() => { - this.executeBulkOperation('set_document_type', {"document_type": documentType?.id}).subscribe( - response => { - this.documentService.clearCache() + } + + private performSetDocumentTypes(modal, documentType) { + this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe( + response => { + if (modal) { modal.close() } - ) - }) + } + ) } applyDelete() { 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 40a08c105..bc1047ba9 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 @@ -84,7 +84,7 @@

Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}

-

{{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}

+

{list.collectionSize, plural, =1 {1 document} other {{{list.collectionSize || 0}} documents}}

diff --git a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts index d4cac1a9d..4b62d6a51 100644 --- a/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts +++ b/src-ui/src/app/components/document-list/filter-editor/filter-editor.component.ts @@ -62,6 +62,10 @@ export class FilterEditorComponent implements OnInit, OnDestroy { @Input() set filterRules (value: FilterRule[]) { + this.documentTypeSelectionModel.clear(false) + this.tagSelectionModel.clear(false) + this.correspondentSelectionModel.clear(false) + value.forEach(rule => { switch (rule.rule_type) { case FILTER_TITLE: @@ -80,22 +84,22 @@ export class FilterEditorComponent implements OnInit, OnDestroy { this.dateAddedBefore = rule.value break case FILTER_HAS_TAG: - this.tagSelectionModel.set(+rule.value, ToggleableItemState.Selected, false) + this.tagSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false) + break + case FILTER_HAS_ANY_TAG: + this.tagSelectionModel.set(null, ToggleableItemState.Selected, false) break case FILTER_CORRESPONDENT: - this.correspondentSelectionModel.set(+rule.value, ToggleableItemState.Selected, false) + this.correspondentSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false) break case FILTER_DOCUMENT_TYPE: - this.documentTypeSelectionModel.set(+rule.value, ToggleableItemState.Selected, false) + this.documentTypeSelectionModel.set(rule.value ? +rule.value : null, ToggleableItemState.Selected, false) break } }) } - @Output() - filterRulesChange = new EventEmitter() - - updateRules() { + get filterRules() { let filterRules: FilterRule[] = [] if (this._titleFilter) { filterRules.push({rule_type: FILTER_TITLE, value: this._titleFilter}) @@ -125,7 +129,14 @@ export class FilterEditorComponent implements OnInit, OnDestroy { if (this.dateAddedAfter) { filterRules.push({rule_type: FILTER_ADDED_AFTER, value: this.dateAddedAfter}) } - this.filterRulesChange.next(filterRules) + return filterRules + } + + @Output() + filterRulesChange = new EventEmitter() + + updateRules() { + this.filterRulesChange.next(this.filterRules) } hasFilters() { 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 9a2a37e36..9e28448a2 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 @@ -7,7 +7,7 @@ + + +

Bulk editing

+ + + +
  • 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 a514658fb..df3015e21 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -1,9 +1,9 @@ import { Component, OnInit, Renderer2 } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; -import { GENERAL_SETTINGS } from 'src/app/data/storage-keys'; 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 { ToastService } from 'src/app/services/toast.service'; import { AppViewService } from 'src/app/services/app-view.service'; @@ -17,13 +17,11 @@ export class SettingsComponent implements OnInit { savedViewGroup = new FormGroup({}) settingsForm = new FormGroup({ - 'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT), - 'darkModeUseSystem': new FormControl( - localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM) == undefined ? GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM_DEFAULT : JSON.parse(localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM)) - ), - 'darkModeEnabled': new FormControl( - localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_ENABLED) == undefined ? GENERAL_SETTINGS.DARK_MODE_ENABLED_DEFAULT : JSON.parse(localStorage.getItem(GENERAL_SETTINGS.DARK_MODE_ENABLED)) - ), + 'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)), + 'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)), + 'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)), + 'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)), + 'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)), 'savedViews': this.savedViewGroup }) @@ -33,6 +31,7 @@ export class SettingsComponent implements OnInit { public savedViewService: SavedViewService, private documentListViewService: DocumentListViewService, private toastService: ToastService, + private settings: SettingsService, private appViewService: AppViewService ) { } @@ -67,9 +66,11 @@ export class SettingsComponent implements OnInit { } private saveLocalSettings() { - localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) - localStorage.setItem(GENERAL_SETTINGS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem) - localStorage.setItem(GENERAL_SETTINGS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString()) + this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose) + this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs) + this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) + 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.documentListViewService.updatePageSize() this.appViewService.updateDarkModeSettings() this.toastService.showInfo($localize`Settings saved successfully.`) diff --git a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html index 138d3e7cd..bbb96fb6b 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html +++ b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html @@ -10,7 +10,7 @@
    - + {{item.name}} @@ -18,13 +18,13 @@
    - - - - + + + + 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 bbe2c6dd2..43126f7b2 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 @@ -1,7 +1,5 @@ - - + +
    @@ -12,11 +10,11 @@ - - - - - + + + + + @@ -31,21 +29,18 @@ diff --git a/src-ui/src/app/components/search/search.component.html b/src-ui/src/app/components/search/search.component.html index 7047a3144..547c8a475 100644 --- a/src-ui/src/app/components/search/search.component.html +++ b/src-ui/src/app/components/search/search.component.html @@ -1,4 +1,4 @@ - +
    Invalid search query: {{errorMessage}}
    diff --git a/src-ui/src/app/data/storage-keys.ts b/src-ui/src/app/data/storage-keys.ts index 057d68d70..ec91c06ac 100644 --- a/src-ui/src/app/data/storage-keys.ts +++ b/src-ui/src/app/data/storage-keys.ts @@ -5,12 +5,3 @@ export const OPEN_DOCUMENT_SERVICE = { export const DOCUMENT_LIST_SERVICE = { CURRENT_VIEW_CONFIG: 'document-list-service:currentViewConfig' } - -export const GENERAL_SETTINGS = { - DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', - DOCUMENT_LIST_SIZE_DEFAULT: 50, - DARK_MODE_USE_SYSTEM: 'general-settings:darkModeUseSystem', - DARK_MODE_USE_SYSTEM_DEFAULT: true, - DARK_MODE_ENABLED: 'general-settings:darkModeEnabled', - DARK_MODE_ENABLED_DEFAULT: false -} 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 b148d4087..dfcf9c0c5 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -3,8 +3,9 @@ import { Observable } from 'rxjs'; import { cloneFilterRules, FilterRule } from '../data/filter-rule'; import { PaperlessDocument } from '../data/paperless-document'; import { PaperlessSavedView } from '../data/paperless-saved-view'; -import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys'; +import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'; import { DocumentService } from './rest/document.service'; +import { SettingsService, SETTINGS_KEYS } from './settings.service'; /** @@ -23,7 +24,7 @@ export class DocumentListViewService { isReloading: boolean = false documents: PaperlessDocument[] = [] currentPage = 1 - currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT + currentPageSize: number = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) collectionSize: number /** @@ -190,7 +191,7 @@ export class DocumentListViewService { } updatePageSize() { - let newPageSize = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT + let newPageSize = this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE) if (newPageSize != this.currentPageSize) { this.currentPageSize = newPageSize } @@ -202,7 +203,7 @@ export class DocumentListViewService { this.selected.clear() } - private reduceSelectionToFilter() { + reduceSelectionToFilter() { if (this.selected.size > 0) { this.documentService.listAllFilteredIds(this.filterRules).subscribe(ids => { let subset = new Set() @@ -239,7 +240,7 @@ export class DocumentListViewService { } } - constructor(private documentService: DocumentService) { + constructor(private documentService: DocumentService, private settings: SettingsService) { let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) if (documentListViewConfigJson) { try { diff --git a/src-ui/src/app/services/settings.service.spec.ts b/src-ui/src/app/services/settings.service.spec.ts new file mode 100644 index 000000000..359cb6b7a --- /dev/null +++ b/src-ui/src/app/services/settings.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SettingsService } from './settings.service'; + +describe('SettingsService', () => { + let service: SettingsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SettingsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts new file mode 100644 index 000000000..00e6ff639 --- /dev/null +++ b/src-ui/src/app/services/settings.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; + +export interface PaperlessSettings { + key: string + type: string + default: any +} + +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', +} + +const SETTINGS: PaperlessSettings[] = [ + {key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true}, + {key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false}, + {key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50} +] + +@Injectable({ + providedIn: 'root' +}) +export class SettingsService { + + constructor() { } + + get(key: string): any { + let setting = SETTINGS.find(s => s.key == key) + + if (!setting) { + return null + } + + let value = localStorage.getItem(key) + + if (value != null) { + switch (setting.type) { + case "boolean": + return JSON.parse(value) + case "number": + return +value + case "string": + return value + default: + return value + } + } else { + return setting.default + } + } + + set(key: string, value: any) { + localStorage.setItem(key, value.toString()) + } + + unset(key: string) { + localStorage.removeItem(key) + } +} diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index ab6b07c73..7b707f014 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.9" + version: "0.9.10" }; diff --git a/src/documents/tests/samples/test_with_bom.pdf b/src/documents/tests/samples/test_with_bom.pdf new file mode 100644 index 000000000..c4a46701c Binary files /dev/null and b/src/documents/tests/samples/test_with_bom.pdf differ diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index adc947a1a..dbe8f881c 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -847,6 +847,21 @@ class TestBulkEdit(DirectoriesMixin, APITestCase): self.assertEqual(args[0], [self.doc1.id]) self.assertEqual(kwargs['tag'], self.t1.id) + @mock.patch("documents.serialisers.bulk_edit.modify_tags") + def test_api_modify_tags(self, m): + m.return_value = "OK" + response = self.client.post("/api/documents/bulk_edit/", json.dumps({ + "documents": [self.doc1.id, self.doc3.id], + "method": "modify_tags", + "parameters": {"add_tags": [self.t1.id], "remove_tags": [self.t2.id]} + }), content_type='application/json') + self.assertEqual(response.status_code, 200) + m.assert_called_once() + args, kwargs = m.call_args + self.assertListEqual(args[0], [self.doc1.id, self.doc3.id]) + self.assertEqual(kwargs['add_tags'], [self.t1.id]) + self.assertEqual(kwargs['remove_tags'], [self.t2.id]) + @mock.patch("documents.serialisers.bulk_edit.delete") def test_api_delete(self, m): m.return_value = "OK" @@ -927,6 +942,38 @@ class TestBulkEdit(DirectoriesMixin, APITestCase): self.assertEqual(list(self.doc2.tags.all()), [self.t1]) + 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({ + "documents": [self.doc2.id], + "method": "modify_tags", + "parameters": {'add_tags': [self.t2.id, 1657], "remove_tags": [1123123]} + }), 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": [] + }), content_type='application/json') + self.assertEqual(response.status_code, 200) + for field, Entity in [('selected_correspondents', Correspondent), ('selected_tags', Tag), ('selected_document_types', DocumentType)]: + self.assertEqual(len(response.data[field]), Entity.objects.count()) + for correspondent in response.data[field]: + self.assertEqual(correspondent['document_count'], 0) + self.assertCountEqual( + map(lambda c: c['id'], response.data[field]), + map(lambda c: c['id'], Entity.objects.values('id'))) + + def test_api_selection_data(self): + response = self.client.post("/api/documents/selection_data/", json.dumps({ + "documents": [self.doc1.id, self.doc2.id, self.doc4.id, self.doc5.id] + }), content_type='application/json') + self.assertEqual(response.status_code, 200) + + self.assertCountEqual(response.data['selected_correspondents'], [{"id": self.c1.id, "document_count": 1}, {"id": self.c2.id, "document_count": 0}]) + self.assertCountEqual(response.data['selected_tags'], [{"id": self.t1.id, "document_count": 2}, {"id": self.t2.id, "document_count": 1}]) + self.assertCountEqual(response.data['selected_document_types'], [{"id": self.c1.id, "document_count": 1}, {"id": self.c2.id, "document_count": 0}]) + class TestApiAuth(APITestCase): @@ -951,3 +998,4 @@ class TestApiAuth(APITestCase): self.assertEqual(self.client.get("/api/search/").status_code, 401) self.assertEqual(self.client.get("/api/search/auto_complete/").status_code, 401) self.assertEqual(self.client.get("/api/documents/bulk_edit/").status_code, 401) + self.assertEqual(self.client.get("/api/documents/selection_data/").status_code, 401) diff --git a/src/documents/tests/test_tasks.py b/src/documents/tests/test_tasks.py index 6d04e58e1..653590707 100644 --- a/src/documents/tests/test_tasks.py +++ b/src/documents/tests/test_tasks.py @@ -1,10 +1,12 @@ from datetime import datetime +from unittest import mock from django.test import TestCase from django.utils import timezone from documents import tasks from documents.models import Document +from documents.sanity_checker import SanityError, SanityFailedError from documents.tests.utils import DirectoriesMixin @@ -22,3 +24,19 @@ class TestTasks(DirectoriesMixin, TestCase): def test_train_classifier(self): tasks.train_classifier() + + @mock.patch("documents.tasks.sanity_checker.check_sanity") + def test_sanity_check(self, m): + m.return_value = [] + tasks.sanity_check() + m.assert_called_once() + m.reset_mock() + m.return_value = [SanityError("")] + self.assertRaises(SanityFailedError, tasks.sanity_check) + m.assert_called_once() + + def test_culk_update_documents(self): + doc1 = Document.objects.create(title="test", content="my document", checksum="wow", added=timezone.now(), + created=timezone.now(), modified=timezone.now()) + + tasks.bulk_update_documents([doc1.pk]) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index c6f7c9357..5af1be85e 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -69,6 +69,8 @@ SCRATCH_DIR = os.getenv("PAPERLESS_SCRATCH_DIR", "/tmp/paperless") # Application Definition # ############################################################################### +env_apps = os.getenv("PAPERLESS_APPS").split(",") if os.getenv("PAPERLESS_APPS") else [] + INSTALLED_APPS = [ "whitenoise.runserver_nostatic", @@ -95,7 +97,7 @@ INSTALLED_APPS = [ "django_q", -] +] + env_apps REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ @@ -420,3 +422,5 @@ for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")): # TODO: this should not have a prefix. # Specify the filename format for out files PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT") + +THUMBNAIL_FONT_NAME = os.getenv("PAPERLESS_THUMBNAIL_FONT_NAME", "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf") diff --git a/src/paperless/version.py b/src/paperless/version.py index b1dfc590c..facb097fc 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1 +1 @@ -__version__ = (0, 9, 9) +__version__ = (0, 9, 10) diff --git a/src/paperless_text/parsers.py b/src/paperless_text/parsers.py index 030c2c2c2..a38bd7a91 100644 --- a/src/paperless_text/parsers.py +++ b/src/paperless_text/parsers.py @@ -1,10 +1,9 @@ import os -import subprocess from PIL import ImageDraw, ImageFont, Image from django.conf import settings -from documents.parsers import DocumentParser, ParseError +from documents.parsers import DocumentParser class TextDocumentParser(DocumentParser): @@ -23,7 +22,8 @@ class TextDocumentParser(DocumentParser): img = Image.new("RGB", (500, 700), color="white") draw = ImageDraw.Draw(img) font = ImageFont.truetype( - "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf", 20, + font=settings.THUMBNAIL_FONT_NAME, + size=20, layout_engine=ImageFont.LAYOUT_BASIC) draw.text((5, 5), read_text(), font=font, fill="black")
    NameColourMatchingDocument countActionsNameColorMatchingDocument countActions