mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into feature/dark-mode
This commit is contained in:
		| @@ -1,4 +1,4 @@ | ||||
| bind = '[::]:8000' | ||||
| bind = ['[::]:8000', 'localhost:8000'] | ||||
| backlog = 2048 | ||||
| workers = 3 | ||||
| worker_class = 'sync' | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -22,6 +22,7 @@ RUN apt-get update \ | ||||
| 		libpq-dev \ | ||||
| 		libqpdf-dev \ | ||||
| 		libxml2 \ | ||||
| 		libxslt1-dev \ | ||||
| 		optipng \ | ||||
| 		pngquant \ | ||||
| 		qpdf \ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 <administration-index>`. | ||||
|  | ||||
| paperless-ng 0.9.9 | ||||
| ################## | ||||
|   | ||||
| @@ -400,6 +400,15 @@ PAPERLESS_FILENAME_DATE_ORDER=<format> | ||||
|  | ||||
|     Defaults to none, which disables this feature. | ||||
|  | ||||
| PAPERLESS_THUMBNAIL_FONT_NAME=<filename> | ||||
|     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 | ||||
| ######## | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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<ChangedItems>() | ||||
|  | ||||
| @@ -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()) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -132,7 +132,7 @@ | ||||
|             <div [ngbNavOutlet]="nav" class="mt-2"></div> | ||||
|  | ||||
|             <button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n>Discard</button>  | ||||
|             <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n>Save & edit next</button>  | ||||
|             <button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n>Save & next</button>  | ||||
|             <button type="submit" class="btn btn-primary" i18n>Save</button>  | ||||
|         </form> | ||||
|     </div> | ||||
|   | ||||
| @@ -30,6 +30,7 @@ | ||||
|         [items]="tags" | ||||
|         [editing]="true" | ||||
|         [multiple]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (open)="openTagsDropdown()" | ||||
|         [(selectionModel)]="tagSelectionModel" | ||||
|         (apply)="setTags($event)"> | ||||
| @@ -37,6 +38,7 @@ | ||||
|       <app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" | ||||
|         [items]="correspondents" | ||||
|         [editing]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (open)="openCorrespondentDropdown()" | ||||
|         [(selectionModel)]="correspondentSelectionModel" | ||||
|         (apply)="setCorrespondents($event)"> | ||||
| @@ -44,6 +46,7 @@ | ||||
|       <app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill" | ||||
|         [items]="documentTypes" | ||||
|         [editing]="true" | ||||
|         [applyOnClose]="applyOnClose" | ||||
|         (open)="openDocumentTypeDropdown()" | ||||
|         [(selectionModel)]="documentTypeSelectionModel" | ||||
|         (apply)="setDocumentTypes($event)"> | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -84,7 +84,7 @@ | ||||
|  | ||||
| <div class="d-flex justify-content-between align-items-center"> | ||||
|   <p i18n *ngIf="list.selected.size > 0">Selected {{list.selected.size}} of {{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p> | ||||
|   <p i18n *ngIf="list.selected.size == 0">{{list.collectionSize || 0}} {list.collectionSize, plural, =1 {document} other {documents}}</p> | ||||
|   <p *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {1 document} other {{{list.collectionSize || 0}} documents}}</p> | ||||
|   <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" | ||||
|   [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination> | ||||
| </div> | ||||
|   | ||||
| @@ -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<FilterRule[]>() | ||||
|  | ||||
|   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<FilterRule[]>() | ||||
|  | ||||
|   updateRules() { | ||||
|     this.filterRulesChange.next(this.filterRules) | ||||
|   } | ||||
|  | ||||
|   hasFilters() { | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|   </div> | ||||
|   <div class="modal-body"> | ||||
|     <app-input-text i18n-title title="Name" formControlName="name"></app-input-text> | ||||
|     <app-input-check i18n-title title="Show in side bar" formControlName="showInSideBar"></app-input-check> | ||||
|     <app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check> | ||||
|     <app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|   | ||||
| @@ -9,8 +9,8 @@ | ||||
|      | ||||
|     <app-input-text i18n-title title="Name" formControlName="name"></app-input-text> | ||||
|     <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> | ||||
|     <app-input-text i18n-title title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text> | ||||
|     <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check> | ||||
|     <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text> | ||||
|     <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button> | ||||
|   | ||||
| @@ -9,8 +9,8 @@ | ||||
|        | ||||
|       <app-input-text i18n-title title="Name" formControlName="name"></app-input-text> | ||||
|       <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> | ||||
|       <app-input-text i18n-title title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text> | ||||
|       <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check> | ||||
|       <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text> | ||||
|       <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check> | ||||
|  | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <app-page-header title="Document types"> | ||||
| <app-page-header title="Document types" i18n-title> | ||||
|   <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button> | ||||
| </app-page-header> | ||||
|  | ||||
|   | ||||
| @@ -39,8 +39,15 @@ | ||||
|               <label class="custom-control-label" for="darkModeEnabled">Enabled</label> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|    | ||||
|         </div> | ||||
|  | ||||
|         <h4 i18n>Bulk editing</h4> | ||||
|  | ||||
|         <app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check> | ||||
|         <app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check> | ||||
|  | ||||
|       </ng-template> | ||||
|     </li> | ||||
|     <li [ngbNavItem]="2"> | ||||
|   | ||||
| @@ -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.`) | ||||
|   | ||||
| @@ -10,7 +10,7 @@ | ||||
|  | ||||
|  | ||||
|       <div class="form-group paperless-input-select"> | ||||
|         <label for="colour">Colour</label> | ||||
|         <label for="colour" i18n>Color</label> | ||||
|         <ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false"> | ||||
|           <ng-template ng-option-tmp ng-label-tmp let-item="item"> | ||||
|             <span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span> | ||||
| @@ -18,13 +18,13 @@ | ||||
|         </ng-select> | ||||
|       </div> | ||||
|       | ||||
|       <app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check> | ||||
|       <app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> | ||||
|       <app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text> | ||||
|       <app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check> | ||||
|       <app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check> | ||||
|       <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select> | ||||
|       <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text> | ||||
|       <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button> | ||||
|       <button type="submit" class="btn btn-primary">Save</button> | ||||
|       <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button> | ||||
|       <button type="submit" class="btn btn-primary" i18n>Save</button> | ||||
|     </div> | ||||
|   </form> | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| <app-page-header title="Tags"> | ||||
|   <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()"> | ||||
|     Create | ||||
|   </button> | ||||
| <app-page-header title="Tags" i18n-title> | ||||
|   <button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" i18n>Create</button> | ||||
| </app-page-header> | ||||
|  | ||||
| <div class="row m-0 justify-content-end"> | ||||
| @@ -12,11 +10,11 @@ | ||||
| <table class="table table-striped border shadow-sm"> | ||||
|   <thead> | ||||
|     <tr> | ||||
|       <th scope="col" sortable="name" (sort)="onSort($event)">Name</th> | ||||
|       <th scope="col">Colour</th> | ||||
|       <th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th> | ||||
|       <th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th> | ||||
|       <th scope="col">Actions</th> | ||||
|       <th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th> | ||||
|       <th scope="col" i18n>Color</th> | ||||
|       <th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th> | ||||
|       <th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th> | ||||
|       <th scope="col" i18n>Actions</th> | ||||
|     </tr> | ||||
|   </thead> | ||||
|   <tbody> | ||||
| @@ -31,21 +29,18 @@ | ||||
|           <button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(tag)"> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16"> | ||||
|               <path fill-rule="evenodd" d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2h-11z"/> | ||||
|             </svg> | ||||
|             Documents | ||||
|             </svg> <ng-container i18n>Documents</ng-container> | ||||
|           </button> | ||||
|           <button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)"> | ||||
|             <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||
|               <path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/> | ||||
|             </svg> | ||||
|             Edit | ||||
|             </svg> <ng-container i18n>Edit</ng-container> | ||||
|           </button> | ||||
|           <button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)"> | ||||
|             <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"> | ||||
|               <path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/> | ||||
|               <path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/> | ||||
|             </svg> | ||||
|             Delete | ||||
|             </svg> <ng-container i18n>Delete</ng-container> | ||||
|           </button> | ||||
|         </div> | ||||
|       </td> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| <app-page-header title="Search results"> | ||||
| <app-page-header i18n-title title="Search results"> | ||||
| </app-page-header> | ||||
|  | ||||
| <div *ngIf="errorMessage" class="alert alert-danger" i18n>Invalid search query: {{errorMessage}}</div> | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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<number>() | ||||
| @@ -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 { | ||||
|   | ||||
							
								
								
									
										16
									
								
								src-ui/src/app/services/settings.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src-ui/src/app/services/settings.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										60
									
								
								src-ui/src/app/services/settings.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src-ui/src/app/services/settings.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
|   } | ||||
| } | ||||
| @@ -2,5 +2,5 @@ export const environment = { | ||||
|   production: true, | ||||
|   apiBaseUrl: "/api/", | ||||
|   appTitle: "Paperless-ng", | ||||
|   version: "0.9.9" | ||||
|   version: "0.9.10" | ||||
| }; | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								src/documents/tests/samples/test_with_bom.pdf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/documents/tests/samples/test_with_bom.pdf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -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) | ||||
|   | ||||
| @@ -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]) | ||||
|   | ||||
| @@ -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") | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = (0, 9, 9) | ||||
| __version__ = (0, 9, 10) | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon