mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-05 18:58:34 -05:00
Compare commits
1 Commits
dependabot
...
chore/manu
Author | SHA1 | Date | |
---|---|---|---|
![]() |
36ead3d08e |
@@ -159,23 +159,6 @@ Available options are `postgresql` and `mariadb`.
|
|||||||
|
|
||||||
Defaults to unset, which uses Django’s built-in defaults.
|
Defaults to unset, which uses Django’s built-in defaults.
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_POOLSIZE=<int>`](#PAPERLESS_DB_POOLSIZE) {#PAPERLESS_DB_POOLSIZE}
|
|
||||||
|
|
||||||
: Defines the maximum number of database connections to keep in the pool.
|
|
||||||
|
|
||||||
Only applies to PostgreSQL. This setting is ignored for other database engines.
|
|
||||||
|
|
||||||
The value must be greater than or equal to 1 to be used.
|
|
||||||
Defaults to unset, which disables connection pooling.
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
|
|
||||||
A small pool is typically sufficient — for example, a size of 4.
|
|
||||||
Make sure your PostgreSQL server's max_connections setting is large enough to handle:
|
|
||||||
```(Paperless workers + Celery workers) × pool size + safety margin```
|
|
||||||
For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4:
|
|
||||||
(4 + 2) × 4 + 10 = 34 connections required.
|
|
||||||
|
|
||||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
|
||||||
|
|
||||||
: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.
|
: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.
|
||||||
|
@@ -30,9 +30,6 @@ Each document has data fields that you can assign to them:
|
|||||||
- A _document type_ is used to demarcate the type of a document such
|
- A _document type_ is used to demarcate the type of a document such
|
||||||
as letter, bank statement, invoice, contract, etc. It is used to
|
as letter, bank statement, invoice, contract, etc. It is used to
|
||||||
identify what a document is about.
|
identify what a document is about.
|
||||||
- The document _storage path_ is the location where the document files
|
|
||||||
are stored. See [Storage Paths](advanced_usage.md#storage-paths) for
|
|
||||||
more information.
|
|
||||||
- The _date added_ of a document is the date the document was scanned
|
- The _date added_ of a document is the date the document was scanned
|
||||||
into paperless. You cannot and should not change this date.
|
into paperless. You cannot and should not change this date.
|
||||||
- The _date created_ of a document is the date the document was
|
- The _date created_ of a document is the date the document was
|
||||||
|
@@ -52,7 +52,6 @@ dependencies = [
|
|||||||
"ocrmypdf~=16.10.0",
|
"ocrmypdf~=16.10.0",
|
||||||
"pathvalidate~=3.3.1",
|
"pathvalidate~=3.3.1",
|
||||||
"pdf2image~=1.17.0",
|
"pdf2image~=1.17.0",
|
||||||
"psycopg-pool",
|
|
||||||
"python-dateutil~=2.9.0",
|
"python-dateutil~=2.9.0",
|
||||||
"python-dotenv~=1.1.0",
|
"python-dotenv~=1.1.0",
|
||||||
"python-gnupg~=0.5.4",
|
"python-gnupg~=0.5.4",
|
||||||
@@ -63,7 +62,7 @@ dependencies = [
|
|||||||
"redis[hiredis]~=5.2.1",
|
"redis[hiredis]~=5.2.1",
|
||||||
"scikit-learn~=1.7.0",
|
"scikit-learn~=1.7.0",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"tika-client~=0.10.0",
|
"tika-client~=0.9.0",
|
||||||
"tqdm~=4.67.1",
|
"tqdm~=4.67.1",
|
||||||
"watchdog~=6.0",
|
"watchdog~=6.0",
|
||||||
"whitenoise~=6.9",
|
"whitenoise~=6.9",
|
||||||
@@ -75,13 +74,12 @@ optional-dependencies.mariadb = [
|
|||||||
"mysqlclient~=2.2.7",
|
"mysqlclient~=2.2.7",
|
||||||
]
|
]
|
||||||
optional-dependencies.postgres = [
|
optional-dependencies.postgres = [
|
||||||
"psycopg[c,pool]==3.2.9",
|
"psycopg[c]==3.2.9",
|
||||||
# Direct dependency for proper resolution of the pre-built wheels
|
# Direct dependency for proper resolution of the pre-built wheels
|
||||||
"psycopg-c==3.2.9",
|
"psycopg-c==3.2.9",
|
||||||
"psycopg-pool==3.2.6",
|
|
||||||
]
|
]
|
||||||
optional-dependencies.webserver = [
|
optional-dependencies.webserver = [
|
||||||
"granian[uvloop]~=2.5.0",
|
"granian[uvloop]~=2.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@@ -241,7 +239,6 @@ testpaths = [
|
|||||||
"src/paperless_mail/tests/",
|
"src/paperless_mail/tests/",
|
||||||
"src/paperless_tesseract/tests/",
|
"src/paperless_tesseract/tests/",
|
||||||
"src/paperless_tika/tests",
|
"src/paperless_tika/tests",
|
||||||
"src/paperless_text/tests/",
|
|
||||||
]
|
]
|
||||||
addopts = [
|
addopts = [
|
||||||
"--pythonwarnings=all",
|
"--pythonwarnings=all",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -11,17 +11,17 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^20.1.4",
|
"@angular/cdk": "^20.0.4",
|
||||||
"@angular/common": "~20.1.4",
|
"@angular/common": "~20.0.6",
|
||||||
"@angular/compiler": "~20.1.4",
|
"@angular/compiler": "~20.0.6",
|
||||||
"@angular/core": "~20.1.4",
|
"@angular/core": "~20.0.6",
|
||||||
"@angular/forms": "~20.1.4",
|
"@angular/forms": "~20.0.6",
|
||||||
"@angular/localize": "~20.1.4",
|
"@angular/localize": "~20.0.6",
|
||||||
"@angular/platform-browser": "~20.1.4",
|
"@angular/platform-browser": "~20.0.6",
|
||||||
"@angular/platform-browser-dynamic": "~20.1.4",
|
"@angular/platform-browser-dynamic": "~20.0.6",
|
||||||
"@angular/router": "~20.1.4",
|
"@angular/router": "~20.0.6",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
||||||
"@ng-select/ng-select": "^20.0.1",
|
"@ng-select/ng-select": "^15.1.3",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"ngx-color": "^10.0.0",
|
"ngx-color": "^10.0.0",
|
||||||
"ngx-cookie-service": "^20.0.1",
|
"ngx-cookie-service": "^20.0.1",
|
||||||
"ngx-device-detector": "^10.0.2",
|
"ngx-device-detector": "^10.0.2",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^17.0.1",
|
"ngx-ui-tour-ng-bootstrap": "^17.0.0",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
"utif": "^3.1.0",
|
"utif": "^3.1.0",
|
||||||
@@ -42,33 +42,33 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^20.0.0",
|
"@angular-builders/custom-webpack": "^20.0.0",
|
||||||
"@angular-builders/jest": "^20.0.0",
|
"@angular-builders/jest": "^20.0.0",
|
||||||
"@angular-devkit/core": "^20.1.4",
|
"@angular-devkit/core": "^20.0.4",
|
||||||
"@angular-devkit/schematics": "^20.1.4",
|
"@angular-devkit/schematics": "^20.0.4",
|
||||||
"@angular-eslint/builder": "20.1.1",
|
"@angular-eslint/builder": "20.1.1",
|
||||||
"@angular-eslint/eslint-plugin": "20.1.1",
|
"@angular-eslint/eslint-plugin": "20.1.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "20.1.1",
|
"@angular-eslint/eslint-plugin-template": "20.1.1",
|
||||||
"@angular-eslint/schematics": "20.1.1",
|
"@angular-eslint/schematics": "20.1.1",
|
||||||
"@angular-eslint/template-parser": "20.1.1",
|
"@angular-eslint/template-parser": "20.1.1",
|
||||||
"@angular/build": "^20.1.4",
|
"@angular/build": "^20.0.4",
|
||||||
"@angular/cli": "~20.1.4",
|
"@angular/cli": "~20.0.4",
|
||||||
"@angular/compiler-cli": "~20.1.4",
|
"@angular/compiler-cli": "~20.0.6",
|
||||||
"@codecov/webpack-plugin": "^1.9.1",
|
"@codecov/webpack-plugin": "^1.9.1",
|
||||||
"@playwright/test": "^1.54.2",
|
"@playwright/test": "^1.53.2",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^24.1.0",
|
"@types/node": "^24.0.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||||
"@typescript-eslint/parser": "^8.38.0",
|
"@typescript-eslint/parser": "^8.35.1",
|
||||||
"@typescript-eslint/utils": "^8.38.0",
|
"@typescript-eslint/utils": "^8.35.1",
|
||||||
"eslint": "^9.32.0",
|
"eslint": "^9.30.1",
|
||||||
"jest": "30.0.5",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "^30.0.5",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"jest-preset-angular": "^15.0.0",
|
"jest-preset-angular": "^14.5.5",
|
||||||
"jest-websocket-mock": "^2.5.0",
|
"jest-websocket-mock": "^2.5.0",
|
||||||
"prettier-plugin-organize-imports": "^4.2.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"webpack": "^5.101.0"
|
"webpack": "^5.99.9"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
5220
src-ui/pnpm-lock.yaml
generated
5220
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,12 @@
|
|||||||
import '@angular/localize/init'
|
import '@angular/localize/init'
|
||||||
import { jest } from '@jest/globals'
|
import { jest } from '@jest/globals'
|
||||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
|
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
|
||||||
import { TextDecoder, TextEncoder } from 'node:util'
|
import { TextDecoder, TextEncoder } from 'util'
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
setupZoneTestEnv()
|
setupZoneTestEnv()
|
||||||
}
|
}
|
||||||
;(globalThis as any).TextEncoder = TextEncoder as unknown as {
|
global.TextEncoder = TextEncoder
|
||||||
new (): TextEncoder
|
global.TextDecoder = TextDecoder
|
||||||
}
|
|
||||||
;(globalThis as any).TextDecoder = TextDecoder as unknown as {
|
|
||||||
new (): TextDecoder
|
|
||||||
}
|
|
||||||
|
|
||||||
import { registerLocaleData } from '@angular/common'
|
import { registerLocaleData } from '@angular/common'
|
||||||
import localeAf from '@angular/common/locales/af'
|
import localeAf from '@angular/common/locales/af'
|
||||||
@@ -120,6 +116,10 @@ if (!URL.revokeObjectURL) {
|
|||||||
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
|
||||||
}
|
}
|
||||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
configurable: true,
|
||||||
|
value: { reload: jest.fn() },
|
||||||
|
})
|
||||||
|
|
||||||
HTMLCanvasElement.prototype.getContext = <
|
HTMLCanvasElement.prototype.getContext = <
|
||||||
typeof HTMLCanvasElement.prototype.getContext
|
typeof HTMLCanvasElement.prototype.getContext
|
||||||
|
@@ -50,7 +50,7 @@
|
|||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<div class="btn-toolbar" role="toolbar">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<button type="button" (click)="discardChanges()" class="btn btn-outline-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
|
<button type="button" (click)="discardChanges()" class="btn btn-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
||||||
|
@@ -358,6 +358,6 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
|
|
||||||
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -36,7 +36,6 @@ import { UserService } from 'src/app/services/rest/user.service'
|
|||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service'
|
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||||
import * as navUtils from 'src/app/utils/navigation'
|
|
||||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
@@ -226,9 +225,6 @@ describe('SettingsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should offer reload if settings changes require', () => {
|
it('should offer reload if settings changes require', () => {
|
||||||
const reloadSpy = jest
|
|
||||||
.spyOn(navUtils, 'locationReload')
|
|
||||||
.mockImplementation(() => {})
|
|
||||||
completeSetup()
|
completeSetup()
|
||||||
let toast: Toast
|
let toast: Toast
|
||||||
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
toastService.getToasts().subscribe((t) => (toast = t[0]))
|
||||||
@@ -245,7 +241,6 @@ describe('SettingsComponent', () => {
|
|||||||
|
|
||||||
expect(toast.actionName).toEqual('Reload now')
|
expect(toast.actionName).toEqual('Reload now')
|
||||||
toast.action()
|
toast.action()
|
||||||
expect(reloadSpy).toHaveBeenCalled()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow setting theme color, visually apply change immediately but not save', () => {
|
it('should allow setting theme color, visually apply change immediately but not save', () => {
|
||||||
@@ -274,7 +269,7 @@ describe('SettingsComponent', () => {
|
|||||||
)
|
)
|
||||||
completeSetup(userService)
|
completeSetup(userService)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show errors on load if load groups failure', () => {
|
it('should show errors on load if load groups failure', () => {
|
||||||
@@ -286,7 +281,7 @@ describe('SettingsComponent', () => {
|
|||||||
)
|
)
|
||||||
completeSetup(groupService)
|
completeSetup(groupService)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should load system status on initialize, show errors if needed', () => {
|
it('should load system status on initialize, show errors if needed', () => {
|
||||||
|
@@ -57,7 +57,6 @@ import {
|
|||||||
} from 'src/app/services/settings.service'
|
} from 'src/app/services/settings.service'
|
||||||
import { SystemStatusService } from 'src/app/services/system-status.service'
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
import { Toast, ToastService } from 'src/app/services/toast.service'
|
import { Toast, ToastService } from 'src/app/services/toast.service'
|
||||||
import { locationReload } from 'src/app/utils/navigation'
|
|
||||||
import { CheckComponent } from '../../common/input/check/check.component'
|
import { CheckComponent } from '../../common/input/check/check.component'
|
||||||
import { ColorComponent } from '../../common/input/color/color.component'
|
import { ColorComponent } from '../../common/input/color/color.component'
|
||||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||||
@@ -551,7 +550,7 @@ export class SettingsComponent
|
|||||||
savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`
|
savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`
|
||||||
savedToast.actionName = $localize`Reload now`
|
savedToast.actionName = $localize`Reload now`
|
||||||
savedToast.action = () => {
|
savedToast.action = () => {
|
||||||
locationReload()
|
location.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,7 +19,6 @@ import { GroupService } from 'src/app/services/rest/group.service'
|
|||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import * as navUtils from 'src/app/utils/navigation'
|
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
|
||||||
@@ -108,7 +107,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
editDialog.failed.emit()
|
editDialog.failed.emit()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
settingsService.currentUser = users[1] // simulate logged in as different user
|
settingsService.currentUser = users[1] // simulate logged in as different user
|
||||||
editDialog.succeeded.emit(users[0])
|
editDialog.succeeded.emit(users[0])
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||||
@@ -131,7 +130,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
throwError(() => new Error('error deleting user'))
|
throwError(() => new Error('error deleting user'))
|
||||||
)
|
)
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
deleteSpy.mockReturnValueOnce(of(true))
|
deleteSpy.mockReturnValueOnce(of(true))
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(listAllSpy).toHaveBeenCalled()
|
expect(listAllSpy).toHaveBeenCalled()
|
||||||
@@ -143,18 +142,19 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||||
component.editUser(users[0])
|
component.editUser(users[0])
|
||||||
const navSpy = jest
|
|
||||||
.spyOn(navUtils, 'setLocationHref')
|
|
||||||
.mockImplementation(() => {})
|
|
||||||
const editDialog = modal.componentInstance as UserEditDialogComponent
|
const editDialog = modal.componentInstance as UserEditDialogComponent
|
||||||
editDialog.passwordIsSet = true
|
editDialog.passwordIsSet = true
|
||||||
settingsService.currentUser = users[0] // simulate logged in as same user
|
settingsService.currentUser = users[0] // simulate logged in as same user
|
||||||
editDialog.succeeded.emit(users[0])
|
editDialog.succeeded.emit(users[0])
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
|
Object.defineProperty(window, 'location', {
|
||||||
|
value: {
|
||||||
|
href: 'http://localhost/',
|
||||||
|
},
|
||||||
|
writable: true, // possibility to override
|
||||||
|
})
|
||||||
tick(2600)
|
tick(2600)
|
||||||
expect(navSpy).toHaveBeenCalledWith(
|
expect(window.location.href).toContain('logout')
|
||||||
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
it('should support edit / create group, show error if needed', () => {
|
it('should support edit / create group, show error if needed', () => {
|
||||||
@@ -166,7 +166,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
editDialog.failed.emit()
|
editDialog.failed.emit()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
editDialog.succeeded.emit(groups[0])
|
editDialog.succeeded.emit(groups[0])
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||||
`Saved group "${groups[0].name}".`
|
`Saved group "${groups[0].name}".`
|
||||||
@@ -188,7 +188,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
throwError(() => new Error('error deleting group'))
|
throwError(() => new Error('error deleting group'))
|
||||||
)
|
)
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
deleteSpy.mockReturnValueOnce(of(true))
|
deleteSpy.mockReturnValueOnce(of(true))
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(listAllSpy).toHaveBeenCalled()
|
expect(listAllSpy).toHaveBeenCalled()
|
||||||
@@ -210,7 +210,7 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
)
|
)
|
||||||
completeSetup(userService)
|
completeSetup(userService)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should show errors on load if load groups failure', () => {
|
it('should show errors on load if load groups failure', () => {
|
||||||
@@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => {
|
|||||||
)
|
)
|
||||||
completeSetup(groupService)
|
completeSetup(groupService)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@@ -10,7 +10,6 @@ import { GroupService } from 'src/app/services/rest/group.service'
|
|||||||
import { UserService } from 'src/app/services/rest/user.service'
|
import { UserService } from 'src/app/services/rest/user.service'
|
||||||
import { SettingsService } from 'src/app/services/settings.service'
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { setLocationHref } from 'src/app/utils/navigation'
|
|
||||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||||
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
|
||||||
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
|
||||||
@@ -94,9 +93,7 @@ export class UsersAndGroupsComponent
|
|||||||
$localize`Password has been changed, you will be logged out momentarily.`
|
$localize`Password has been changed, you will be logged out momentarily.`
|
||||||
)
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLocationHref(
|
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
||||||
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
|
||||||
)
|
|
||||||
}, 2500)
|
}, 2500)
|
||||||
} else {
|
} else {
|
||||||
this.toastService.showInfo(
|
this.toastService.showInfo(
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
}
|
}
|
||||||
<div class="list-group-item">
|
<div class="list-group-item">
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<input class="form-control" type="text" spellcheck="false" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
<input class="form-control" type="text" [(ngModel)]="filterText" [placeholder]="filterPlaceholder" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (selectionModel.items) {
|
@if (selectionModel.items) {
|
||||||
|
@@ -18,7 +18,6 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
|||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import * as navUtils from 'src/app/utils/navigation'
|
|
||||||
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
|
||||||
import { PasswordComponent } from '../input/password/password.component'
|
import { PasswordComponent } from '../input/password/password.component'
|
||||||
import { TextComponent } from '../input/text/text.component'
|
import { TextComponent } from '../input/text/text.component'
|
||||||
@@ -206,15 +205,16 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
|
|
||||||
const updateSpy = jest.spyOn(profileService, 'update')
|
const updateSpy = jest.spyOn(profileService, 'update')
|
||||||
updateSpy.mockReturnValue(of(null))
|
updateSpy.mockReturnValue(of(null))
|
||||||
const navSpy = jest
|
Object.defineProperty(window, 'location', {
|
||||||
.spyOn(navUtils, 'setLocationHref')
|
value: {
|
||||||
.mockImplementation(() => {})
|
href: 'http://localhost/',
|
||||||
|
},
|
||||||
|
writable: true, // possibility to override
|
||||||
|
})
|
||||||
component.save()
|
component.save()
|
||||||
expect(updateSpy).toHaveBeenCalled()
|
expect(updateSpy).toHaveBeenCalled()
|
||||||
tick(2600)
|
tick(2600)
|
||||||
expect(navSpy).toHaveBeenCalledWith(
|
expect(window.location.href).toContain('logout')
|
||||||
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
|
||||||
)
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
it('should support auth token copy', fakeAsync(() => {
|
it('should support auth token copy', fakeAsync(() => {
|
||||||
|
@@ -21,7 +21,6 @@ import {
|
|||||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { setLocationHref } from 'src/app/utils/navigation'
|
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
|
||||||
import { PasswordComponent } from '../input/password/password.component'
|
import { PasswordComponent } from '../input/password/password.component'
|
||||||
@@ -195,9 +194,7 @@ export class ProfileEditDialogComponent
|
|||||||
$localize`Password has been changed, you will be logged out momentarily.`
|
$localize`Password has been changed, you will be logged out momentarily.`
|
||||||
)
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setLocationHref(
|
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
||||||
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
|
|
||||||
)
|
|
||||||
}, 2500)
|
}, 2500)
|
||||||
}
|
}
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
|
@@ -188,7 +188,7 @@ describe('MailComponent', () => {
|
|||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
editDialog.failed.emit()
|
editDialog.failed.emit()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
editDialog.succeeded.emit(mailAccounts[0] as any)
|
editDialog.succeeded.emit(mailAccounts[0] as any)
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||||
`Saved account "${mailAccounts[0].name}".`
|
`Saved account "${mailAccounts[0].name}".`
|
||||||
@@ -211,7 +211,7 @@ describe('MailComponent', () => {
|
|||||||
throwError(() => new Error('error deleting mail account'))
|
throwError(() => new Error('error deleting mail account'))
|
||||||
)
|
)
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
deleteSpy.mockReturnValueOnce(of(true))
|
deleteSpy.mockReturnValueOnce(of(true))
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(listAllSpy).toHaveBeenCalled()
|
expect(listAllSpy).toHaveBeenCalled()
|
||||||
@@ -246,7 +246,7 @@ describe('MailComponent', () => {
|
|||||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
editDialog.failed.emit()
|
editDialog.failed.emit()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
editDialog.succeeded.emit(mailRules[0] as any)
|
editDialog.succeeded.emit(mailRules[0] as any)
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||||
`Saved rule "${mailRules[0].name}".`
|
`Saved rule "${mailRules[0].name}".`
|
||||||
@@ -280,7 +280,7 @@ describe('MailComponent', () => {
|
|||||||
throwError(() => new Error('error deleting mail rule "rule1"'))
|
throwError(() => new Error('error deleting mail rule "rule1"'))
|
||||||
)
|
)
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(toastErrorSpy).toHaveBeenCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
deleteSpy.mockReturnValueOnce(of(true))
|
deleteSpy.mockReturnValueOnce(of(true))
|
||||||
deleteDialog.confirm()
|
deleteDialog.confirm()
|
||||||
expect(listAllSpy).toHaveBeenCalled()
|
expect(listAllSpy).toHaveBeenCalled()
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions">
|
<pngx-page-header title="{{ typeNamePlural | titlecase }}">
|
||||||
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
||||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -164,7 +164,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const createButton = fixture.debugElement.queryAll(By.css('button'))[4]
|
const createButton = fixture.debugElement.queryAll(By.css('button'))[3]
|
||||||
createButton.triggerEventHandler('click')
|
createButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@@ -188,7 +188,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const editButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
const editButton = fixture.debugElement.queryAll(By.css('button'))[6]
|
||||||
editButton.triggerEventHandler('click')
|
editButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@@ -213,7 +213,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const deleteSpy = jest.spyOn(tagService, 'delete')
|
const deleteSpy = jest.spyOn(tagService, 'delete')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
|
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
||||||
deleteButton.triggerEventHandler('click')
|
deleteButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@@ -233,7 +233,7 @@ describe('ManagementListComponent', () => {
|
|||||||
|
|
||||||
it('should support quick filter for objects', () => {
|
it('should support quick filter for objects', () => {
|
||||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
const filterButton = fixture.debugElement.queryAll(By.css('button'))[9]
|
const filterButton = fixture.debugElement.queryAll(By.css('button'))[8]
|
||||||
filterButton.triggerEventHandler('click')
|
filterButton.triggerEventHandler('click')
|
||||||
expect(qfSpy).toHaveBeenCalledWith([
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
|
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
|
||||||
|
@@ -70,6 +70,6 @@
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -1,8 +0,0 @@
|
|||||||
/* istanbul ignore file */
|
|
||||||
export function setLocationHref(url: string) {
|
|
||||||
window.location.href = url
|
|
||||||
}
|
|
||||||
|
|
||||||
export function locationReload() {
|
|
||||||
window.location.reload()
|
|
||||||
}
|
|
@@ -3,8 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./out-tsc/spec",
|
"outDir": "./out-tsc/spec",
|
||||||
"types": [
|
"types": [
|
||||||
"jest",
|
"jest"
|
||||||
"node",
|
|
||||||
],
|
],
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
@@ -12,13 +12,11 @@ from celery.signals import before_task_publish
|
|||||||
from celery.signals import task_failure
|
from celery.signals import task_failure
|
||||||
from celery.signals import task_postrun
|
from celery.signals import task_postrun
|
||||||
from celery.signals import task_prerun
|
from celery.signals import task_prerun
|
||||||
from celery.signals import worker_process_init
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db import DatabaseError
|
from django.db import DatabaseError
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
from django.db import connections
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
@@ -1441,18 +1439,3 @@ def task_failure_handler(
|
|||||||
task_instance.save()
|
task_instance.save()
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
logger.exception("Updating PaperlessTask failed")
|
logger.exception("Updating PaperlessTask failed")
|
||||||
|
|
||||||
|
|
||||||
@worker_process_init.connect
|
|
||||||
def close_connection_pool_on_worker_init(**kwargs):
|
|
||||||
"""
|
|
||||||
Close the DB connection pool for each Celery child process after it starts.
|
|
||||||
|
|
||||||
This is necessary because the parent process parse the Django configuration,
|
|
||||||
initializes connection pools then forks.
|
|
||||||
|
|
||||||
Closing these pools after forking ensures child processes have a valid connection.
|
|
||||||
"""
|
|
||||||
for conn in connections.all(initialized_only=True):
|
|
||||||
if conn.alias == "default" and hasattr(conn, "pool") and conn.pool:
|
|
||||||
conn.close_pool()
|
|
||||||
|
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2025-08-02 12:55+0000\n"
|
"POT-Creation-Date: 2025-07-08 21:14+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -1645,147 +1645,147 @@ msgstr ""
|
|||||||
msgid "paperless application settings"
|
msgid "paperless application settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:774
|
#: paperless/settings.py:762
|
||||||
msgid "English (US)"
|
msgid "English (US)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:775
|
#: paperless/settings.py:763
|
||||||
msgid "Arabic"
|
msgid "Arabic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:776
|
#: paperless/settings.py:764
|
||||||
msgid "Afrikaans"
|
msgid "Afrikaans"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:777
|
#: paperless/settings.py:765
|
||||||
msgid "Belarusian"
|
msgid "Belarusian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:778
|
#: paperless/settings.py:766
|
||||||
msgid "Bulgarian"
|
msgid "Bulgarian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:779
|
#: paperless/settings.py:767
|
||||||
msgid "Catalan"
|
msgid "Catalan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:780
|
#: paperless/settings.py:768
|
||||||
msgid "Czech"
|
msgid "Czech"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:781
|
#: paperless/settings.py:769
|
||||||
msgid "Danish"
|
msgid "Danish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:782
|
#: paperless/settings.py:770
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:783
|
#: paperless/settings.py:771
|
||||||
msgid "Greek"
|
msgid "Greek"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:784
|
#: paperless/settings.py:772
|
||||||
msgid "English (GB)"
|
msgid "English (GB)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:785
|
#: paperless/settings.py:773
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:786
|
#: paperless/settings.py:774
|
||||||
msgid "Persian"
|
msgid "Persian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:787
|
#: paperless/settings.py:775
|
||||||
msgid "Finnish"
|
msgid "Finnish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:788
|
#: paperless/settings.py:776
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:789
|
#: paperless/settings.py:777
|
||||||
msgid "Hungarian"
|
msgid "Hungarian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:790
|
#: paperless/settings.py:778
|
||||||
msgid "Italian"
|
msgid "Italian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:791
|
#: paperless/settings.py:779
|
||||||
msgid "Japanese"
|
msgid "Japanese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:792
|
#: paperless/settings.py:780
|
||||||
msgid "Korean"
|
msgid "Korean"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:793
|
#: paperless/settings.py:781
|
||||||
msgid "Luxembourgish"
|
msgid "Luxembourgish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:794
|
#: paperless/settings.py:782
|
||||||
msgid "Norwegian"
|
msgid "Norwegian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:795
|
#: paperless/settings.py:783
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:796
|
#: paperless/settings.py:784
|
||||||
msgid "Polish"
|
msgid "Polish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:797
|
#: paperless/settings.py:785
|
||||||
msgid "Portuguese (Brazil)"
|
msgid "Portuguese (Brazil)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:798
|
#: paperless/settings.py:786
|
||||||
msgid "Portuguese"
|
msgid "Portuguese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:799
|
#: paperless/settings.py:787
|
||||||
msgid "Romanian"
|
msgid "Romanian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:800
|
#: paperless/settings.py:788
|
||||||
msgid "Russian"
|
msgid "Russian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:801
|
#: paperless/settings.py:789
|
||||||
msgid "Slovak"
|
msgid "Slovak"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:802
|
#: paperless/settings.py:790
|
||||||
msgid "Slovenian"
|
msgid "Slovenian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:803
|
#: paperless/settings.py:791
|
||||||
msgid "Serbian"
|
msgid "Serbian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:804
|
#: paperless/settings.py:792
|
||||||
msgid "Swedish"
|
msgid "Swedish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:805
|
#: paperless/settings.py:793
|
||||||
msgid "Turkish"
|
msgid "Turkish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:806
|
#: paperless/settings.py:794
|
||||||
msgid "Ukrainian"
|
msgid "Ukrainian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:807
|
#: paperless/settings.py:795
|
||||||
msgid "Vietnamese"
|
msgid "Vietnamese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:808
|
#: paperless/settings.py:796
|
||||||
msgid "Chinese Simplified"
|
msgid "Chinese Simplified"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings.py:809
|
#: paperless/settings.py:797
|
||||||
msgid "Chinese Traditional"
|
msgid "Chinese Traditional"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@@ -703,9 +703,6 @@ def _parse_db_settings() -> dict:
|
|||||||
# Leave room for future extensibility
|
# Leave room for future extensibility
|
||||||
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
|
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
|
||||||
engine = "django.db.backends.mysql"
|
engine = "django.db.backends.mysql"
|
||||||
# Contrary to Postgres, Django does not natively support connection pooling for MariaDB.
|
|
||||||
# However, since MariaDB uses threads instead of forks, establishing connections is significantly faster
|
|
||||||
# compared to PostgreSQL, so the lack of pooling is not an issue
|
|
||||||
options = {
|
options = {
|
||||||
"read_default_file": "/etc/mysql/my.cnf",
|
"read_default_file": "/etc/mysql/my.cnf",
|
||||||
"charset": "utf8mb4",
|
"charset": "utf8mb4",
|
||||||
@@ -725,15 +722,6 @@ def _parse_db_settings() -> dict:
|
|||||||
"sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
|
"sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
|
||||||
"sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
|
"sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
|
||||||
}
|
}
|
||||||
if int(os.getenv("PAPERLESS_DB_POOLSIZE", 0)) > 0:
|
|
||||||
options.update(
|
|
||||||
{
|
|
||||||
"pool": {
|
|
||||||
"min_size": 1,
|
|
||||||
"max_size": int(os.getenv("PAPERLESS_DB_POOLSIZE")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
databases["default"]["ENGINE"] = engine
|
databases["default"]["ENGINE"] = engine
|
||||||
databases["default"]["OPTIONS"].update(options)
|
databases["default"]["OPTIONS"].update(options)
|
||||||
|
@@ -16,15 +16,7 @@ class TextDocumentParser(DocumentParser):
|
|||||||
logging_name = "paperless.parsing.text"
|
logging_name = "paperless.parsing.text"
|
||||||
|
|
||||||
def get_thumbnail(self, document_path: Path, mime_type, file_name=None) -> Path:
|
def get_thumbnail(self, document_path: Path, mime_type, file_name=None) -> Path:
|
||||||
# Avoid reading entire file into memory
|
text = self.read_file_handle_unicode_errors(document_path)
|
||||||
max_chars = 100_000
|
|
||||||
file_size_limit = 50 * 1024 * 1024
|
|
||||||
|
|
||||||
if document_path.stat().st_size > file_size_limit:
|
|
||||||
text = "[File too large to preview]"
|
|
||||||
else:
|
|
||||||
with Path(document_path).open("r", encoding="utf-8", errors="replace") as f:
|
|
||||||
text = f.read(max_chars)
|
|
||||||
|
|
||||||
img = Image.new("RGB", (500, 700), color="white")
|
img = Image.new("RGB", (500, 700), color="white")
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
@@ -33,7 +25,7 @@ class TextDocumentParser(DocumentParser):
|
|||||||
size=20,
|
size=20,
|
||||||
layout_engine=ImageFont.Layout.BASIC,
|
layout_engine=ImageFont.Layout.BASIC,
|
||||||
)
|
)
|
||||||
draw.multiline_text((5, 5), text, font=font, fill="black", spacing=4)
|
draw.text((5, 5), text, font=font, fill="black")
|
||||||
|
|
||||||
out_path = self.tempdir / "thumb.webp"
|
out_path = self.tempdir / "thumb.webp"
|
||||||
img.save(out_path, format="WEBP")
|
img.save(out_path, format="WEBP")
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
import tempfile
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from paperless_text.parsers import TextDocumentParser
|
from paperless_text.parsers import TextDocumentParser
|
||||||
@@ -36,26 +35,3 @@ class TestTextParser:
|
|||||||
|
|
||||||
assert text_parser.get_text() == "Pantothens<EFBFBD>ure\n"
|
assert text_parser.get_text() == "Pantothens<EFBFBD>ure\n"
|
||||||
assert text_parser.get_archive_path() is None
|
assert text_parser.get_archive_path() is None
|
||||||
|
|
||||||
def test_thumbnail_large_file(self, text_parser: TextDocumentParser):
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- A very large text file (>50MB)
|
|
||||||
WHEN:
|
|
||||||
- A thumbnail is requested
|
|
||||||
THEN:
|
|
||||||
- A thumbnail is created without reading the entire file into memory
|
|
||||||
"""
|
|
||||||
with tempfile.NamedTemporaryFile(
|
|
||||||
delete=False,
|
|
||||||
mode="w",
|
|
||||||
encoding="utf-8",
|
|
||||||
suffix=".txt",
|
|
||||||
) as tmp:
|
|
||||||
tmp.write("A" * (51 * 1024 * 1024)) # 51 MB of 'A'
|
|
||||||
large_file = Path(tmp.name)
|
|
||||||
|
|
||||||
thumb = text_parser.get_thumbnail(large_file, "text/plain")
|
|
||||||
assert thumb.exists()
|
|
||||||
assert thumb.is_file()
|
|
||||||
large_file.unlink()
|
|
||||||
|
Reference in New Issue
Block a user