mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Merge remote-tracking branch 'upstream/dev' into feature/dark-mode
This commit is contained in:
commit
2e0d36c4d9
@ -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")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user