mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -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]
|
||||||
@@ -204,9 +202,15 @@ lint.per-file-ignores."docker/wait-for-redis.py" = [
|
|||||||
"INP001",
|
"INP001",
|
||||||
"T201",
|
"T201",
|
||||||
]
|
]
|
||||||
|
lint.per-file-ignores."src/documents/file_handling.py" = [
|
||||||
|
"PTH",
|
||||||
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
|
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
|
lint.per-file-ignores."src/documents/management/commands/document_exporter.py" = [
|
||||||
|
"PTH",
|
||||||
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
|
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
@@ -216,6 +220,9 @@ lint.per-file-ignores."src/documents/models.py" = [
|
|||||||
lint.per-file-ignores."src/documents/parsers.py" = [
|
lint.per-file-ignores."src/documents/parsers.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
|
lint.per-file-ignores."src/documents/signals/handlers.py" = [
|
||||||
|
"PTH",
|
||||||
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
||||||
"RUF001",
|
"RUF001",
|
||||||
]
|
]
|
||||||
@@ -232,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,
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -8,15 +7,19 @@ from documents.templating.filepath import validate_filepath_template_and_render
|
|||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
|
|
||||||
|
|
||||||
def create_source_path_directory(source_path: Path) -> None:
|
def create_source_path_directory(source_path):
|
||||||
source_path.parent.mkdir(parents=True, exist_ok=True)
|
os.makedirs(os.path.dirname(source_path), exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def delete_empty_directories(directory: Path, root: Path) -> None:
|
def delete_empty_directories(directory, root):
|
||||||
if not directory.is_dir():
|
if not os.path.isdir(directory):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not directory.is_relative_to(root):
|
# Go up in the directory hierarchy and try to delete all directories
|
||||||
|
directory = os.path.normpath(directory)
|
||||||
|
root = os.path.normpath(root)
|
||||||
|
|
||||||
|
if not directory.startswith(root + os.path.sep):
|
||||||
# don't do anything outside our originals folder.
|
# don't do anything outside our originals folder.
|
||||||
|
|
||||||
# append os.path.set so that we avoid these cases:
|
# append os.path.set so that we avoid these cases:
|
||||||
@@ -24,12 +27,11 @@ def delete_empty_directories(directory: Path, root: Path) -> None:
|
|||||||
# root = /home/originals ("/" gets appended and startswith fails)
|
# root = /home/originals ("/" gets appended and startswith fails)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Go up in the directory hierarchy and try to delete all directories
|
|
||||||
while directory != root:
|
while directory != root:
|
||||||
if not list(directory.iterdir()):
|
if not os.listdir(directory):
|
||||||
# it's empty
|
# it's empty
|
||||||
try:
|
try:
|
||||||
directory.rmdir()
|
os.rmdir(directory)
|
||||||
except OSError:
|
except OSError:
|
||||||
# whatever. empty directories aren't that bad anyway.
|
# whatever. empty directories aren't that bad anyway.
|
||||||
return
|
return
|
||||||
@@ -38,10 +40,10 @@ def delete_empty_directories(directory: Path, root: Path) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# go one level up
|
# go one level up
|
||||||
directory = directory.parent
|
directory = os.path.normpath(os.path.dirname(directory))
|
||||||
|
|
||||||
|
|
||||||
def generate_unique_filename(doc, *, archive_filename=False) -> Path:
|
def generate_unique_filename(doc, *, archive_filename=False):
|
||||||
"""
|
"""
|
||||||
Generates a unique filename for doc in settings.ORIGINALS_DIR.
|
Generates a unique filename for doc in settings.ORIGINALS_DIR.
|
||||||
|
|
||||||
@@ -54,32 +56,21 @@ def generate_unique_filename(doc, *, archive_filename=False) -> Path:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if archive_filename:
|
if archive_filename:
|
||||||
old_filename: Path | None = (
|
old_filename = doc.archive_filename
|
||||||
Path(doc.archive_filename) if doc.archive_filename else None
|
|
||||||
)
|
|
||||||
root = settings.ARCHIVE_DIR
|
root = settings.ARCHIVE_DIR
|
||||||
else:
|
else:
|
||||||
old_filename = Path(doc.filename) if doc.filename else None
|
old_filename = doc.filename
|
||||||
root = settings.ORIGINALS_DIR
|
root = settings.ORIGINALS_DIR
|
||||||
|
|
||||||
# If generating archive filenames, try to make a name that is similar to
|
# If generating archive filenames, try to make a name that is similar to
|
||||||
# the original filename first.
|
# the original filename first.
|
||||||
|
|
||||||
if archive_filename and doc.filename:
|
if archive_filename and doc.filename:
|
||||||
# Generate the full path using the same logic as generate_filename
|
new_filename = os.path.splitext(doc.filename)[0] + ".pdf"
|
||||||
base_generated = generate_filename(doc, archive_filename=archive_filename)
|
if new_filename == old_filename or not os.path.exists(
|
||||||
|
os.path.join(root, new_filename),
|
||||||
# Try to create a simple PDF version based on the original filename
|
):
|
||||||
# but preserve any directory structure from the template
|
return new_filename
|
||||||
if str(base_generated.parent) != ".":
|
|
||||||
# Has directory structure, preserve it
|
|
||||||
simple_pdf_name = base_generated.parent / (Path(doc.filename).stem + ".pdf")
|
|
||||||
else:
|
|
||||||
# No directory structure
|
|
||||||
simple_pdf_name = Path(Path(doc.filename).stem + ".pdf")
|
|
||||||
|
|
||||||
if simple_pdf_name == old_filename or not (root / simple_pdf_name).exists():
|
|
||||||
return simple_pdf_name
|
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
|
|
||||||
@@ -93,7 +84,7 @@ def generate_unique_filename(doc, *, archive_filename=False) -> Path:
|
|||||||
# still the same as before.
|
# still the same as before.
|
||||||
return new_filename
|
return new_filename
|
||||||
|
|
||||||
if (root / new_filename).exists():
|
if os.path.exists(os.path.join(root, new_filename)):
|
||||||
counter += 1
|
counter += 1
|
||||||
else:
|
else:
|
||||||
return new_filename
|
return new_filename
|
||||||
@@ -105,8 +96,8 @@ def generate_filename(
|
|||||||
counter=0,
|
counter=0,
|
||||||
append_gpg=True,
|
append_gpg=True,
|
||||||
archive_filename=False,
|
archive_filename=False,
|
||||||
) -> Path:
|
):
|
||||||
base_path: Path | None = None
|
path = ""
|
||||||
|
|
||||||
def format_filename(document: Document, template_str: str) -> str | None:
|
def format_filename(document: Document, template_str: str) -> str | None:
|
||||||
rendered_filename = validate_filepath_template_and_render(
|
rendered_filename = validate_filepath_template_and_render(
|
||||||
@@ -143,34 +134,17 @@ def generate_filename(
|
|||||||
|
|
||||||
# If we have one, render it
|
# If we have one, render it
|
||||||
if filename_format is not None:
|
if filename_format is not None:
|
||||||
rendered_path: str | None = format_filename(doc, filename_format)
|
path = format_filename(doc, filename_format)
|
||||||
if rendered_path:
|
|
||||||
base_path = Path(rendered_path)
|
|
||||||
|
|
||||||
counter_str = f"_{counter:02}" if counter else ""
|
counter_str = f"_{counter:02}" if counter else ""
|
||||||
filetype_str = ".pdf" if archive_filename else doc.file_type
|
filetype_str = ".pdf" if archive_filename else doc.file_type
|
||||||
|
|
||||||
if base_path:
|
if path:
|
||||||
# Split the path into directory and filename parts
|
filename = f"{path}{counter_str}{filetype_str}"
|
||||||
directory = base_path.parent
|
|
||||||
# Use the full name (not just stem) as the base filename
|
|
||||||
base_filename = base_path.name
|
|
||||||
|
|
||||||
# Build the final filename with counter and filetype
|
|
||||||
final_filename = f"{base_filename}{counter_str}{filetype_str}"
|
|
||||||
|
|
||||||
# If we have a directory component, include it
|
|
||||||
if str(directory) != ".":
|
|
||||||
full_path = directory / final_filename
|
|
||||||
else:
|
|
||||||
full_path = Path(final_filename)
|
|
||||||
else:
|
else:
|
||||||
# No template, use document ID
|
filename = f"{doc.pk:07}{counter_str}{filetype_str}"
|
||||||
final_filename = f"{doc.pk:07}{counter_str}{filetype_str}"
|
|
||||||
full_path = Path(final_filename)
|
|
||||||
|
|
||||||
# Add GPG extension if needed
|
|
||||||
if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG:
|
if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG:
|
||||||
full_path = full_path.with_suffix(full_path.suffix + ".gpg")
|
filename += ".gpg"
|
||||||
|
|
||||||
return full_path
|
return filename
|
||||||
|
@@ -236,7 +236,10 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
# now make an archive in the original target, with all files stored
|
# now make an archive in the original target, with all files stored
|
||||||
if self.zip_export and temp_dir is not None:
|
if self.zip_export and temp_dir is not None:
|
||||||
shutil.make_archive(
|
shutil.make_archive(
|
||||||
self.original_target / options["zip_name"],
|
os.path.join(
|
||||||
|
self.original_target,
|
||||||
|
options["zip_name"],
|
||||||
|
),
|
||||||
format="zip",
|
format="zip",
|
||||||
root_dir=temp_dir.name,
|
root_dir=temp_dir.name,
|
||||||
)
|
)
|
||||||
@@ -339,7 +342,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.split_manifest:
|
if self.split_manifest:
|
||||||
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
|
manifest_name = Path(base_name + "-manifest.json")
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
manifest_name = Path("json") / manifest_name
|
manifest_name = Path("json") / manifest_name
|
||||||
manifest_name = (self.target / manifest_name).resolve()
|
manifest_name = (self.target / manifest_name).resolve()
|
||||||
@@ -413,7 +416,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
else:
|
else:
|
||||||
item.unlink()
|
item.unlink()
|
||||||
|
|
||||||
def generate_base_name(self, document: Document) -> Path:
|
def generate_base_name(self, document: Document) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a unique name for the document, one which hasn't already been exported (or will be)
|
Generates a unique name for the document, one which hasn't already been exported (or will be)
|
||||||
"""
|
"""
|
||||||
@@ -433,12 +436,12 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
filename_counter += 1
|
filename_counter += 1
|
||||||
return Path(base_name)
|
return base_name
|
||||||
|
|
||||||
def generate_document_targets(
|
def generate_document_targets(
|
||||||
self,
|
self,
|
||||||
document: Document,
|
document: Document,
|
||||||
base_name: Path,
|
base_name: str,
|
||||||
document_dict: dict,
|
document_dict: dict,
|
||||||
) -> tuple[Path, Path | None, Path | None]:
|
) -> tuple[Path, Path | None, Path | None]:
|
||||||
"""
|
"""
|
||||||
@@ -446,25 +449,25 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
"""
|
"""
|
||||||
original_name = base_name
|
original_name = base_name
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
original_name = Path("originals") / original_name
|
original_name = os.path.join("originals", original_name)
|
||||||
original_target = (self.target / original_name).resolve()
|
original_target = (self.target / Path(original_name)).resolve()
|
||||||
document_dict[EXPORTER_FILE_NAME] = str(original_name)
|
document_dict[EXPORTER_FILE_NAME] = original_name
|
||||||
|
|
||||||
if not self.no_thumbnail:
|
if not self.no_thumbnail:
|
||||||
thumbnail_name = base_name.parent / (base_name.stem + "-thumbnail.webp")
|
thumbnail_name = base_name + "-thumbnail.webp"
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
thumbnail_name = Path("thumbnails") / thumbnail_name
|
thumbnail_name = os.path.join("thumbnails", thumbnail_name)
|
||||||
thumbnail_target = (self.target / thumbnail_name).resolve()
|
thumbnail_target = (self.target / Path(thumbnail_name)).resolve()
|
||||||
document_dict[EXPORTER_THUMBNAIL_NAME] = str(thumbnail_name)
|
document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_name
|
||||||
else:
|
else:
|
||||||
thumbnail_target = None
|
thumbnail_target = None
|
||||||
|
|
||||||
if not self.no_archive and document.has_archive_version:
|
if not self.no_archive and document.has_archive_version:
|
||||||
archive_name = base_name.parent / (base_name.stem + "-archive.pdf")
|
archive_name = base_name + "-archive.pdf"
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
archive_name = Path("archive") / archive_name
|
archive_name = os.path.join("archive", archive_name)
|
||||||
archive_target = (self.target / archive_name).resolve()
|
archive_target = (self.target / Path(archive_name)).resolve()
|
||||||
document_dict[EXPORTER_ARCHIVE_NAME] = str(archive_name)
|
document_dict[EXPORTER_ARCHIVE_NAME] = archive_name
|
||||||
else:
|
else:
|
||||||
archive_target = None
|
archive_target = None
|
||||||
|
|
||||||
@@ -569,7 +572,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
perform_copy = False
|
perform_copy = False
|
||||||
|
|
||||||
if target.exists():
|
if target.exists():
|
||||||
source_stat = source.stat()
|
source_stat = os.stat(source)
|
||||||
target_stat = target.stat()
|
target_stat = target.stat()
|
||||||
if self.compare_checksums and source_checksum:
|
if self.compare_checksums and source_checksum:
|
||||||
target_checksum = hashlib.md5(target.read_bytes()).hexdigest()
|
target_checksum = hashlib.md5(target.read_bytes()).hexdigest()
|
||||||
|
@@ -63,11 +63,11 @@ class Document:
|
|||||||
/ "documents"
|
/ "documents"
|
||||||
/ "originals"
|
/ "originals"
|
||||||
/ f"{self.pk:07}.{self.file_type}.gpg"
|
/ f"{self.pk:07}.{self.file_type}.gpg"
|
||||||
)
|
).as_posix()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_file(self):
|
def source_file(self):
|
||||||
return self.source_path.open("rb")
|
return Path(self.source_path).open("rb")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -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
|
||||||
@@ -51,6 +49,8 @@ from documents.permissions import set_permissions_for_object
|
|||||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
@@ -327,16 +327,15 @@ def cleanup_document_deletion(sender, instance, **kwargs):
|
|||||||
# Find a non-conflicting filename in case a document with the same
|
# Find a non-conflicting filename in case a document with the same
|
||||||
# name was moved to trash earlier
|
# name was moved to trash earlier
|
||||||
counter = 0
|
counter = 0
|
||||||
old_filename = Path(instance.source_path).name
|
old_filename = os.path.split(instance.source_path)[1]
|
||||||
old_filebase = Path(old_filename).stem
|
(old_filebase, old_fileext) = os.path.splitext(old_filename)
|
||||||
old_fileext = Path(old_filename).suffix
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
new_file_path = settings.EMPTY_TRASH_DIR / (
|
new_file_path = settings.EMPTY_TRASH_DIR / (
|
||||||
old_filebase + (f"_{counter:02}" if counter else "") + old_fileext
|
old_filebase + (f"_{counter:02}" if counter else "") + old_fileext
|
||||||
)
|
)
|
||||||
|
|
||||||
if new_file_path.exists():
|
if os.path.exists(new_file_path):
|
||||||
counter += 1
|
counter += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@@ -360,26 +359,26 @@ def cleanup_document_deletion(sender, instance, **kwargs):
|
|||||||
files += (instance.source_path,)
|
files += (instance.source_path,)
|
||||||
|
|
||||||
for filename in files:
|
for filename in files:
|
||||||
if filename and filename.is_file():
|
if filename and os.path.isfile(filename):
|
||||||
try:
|
try:
|
||||||
filename.unlink()
|
os.unlink(filename)
|
||||||
logger.debug(f"Deleted file {filename}.")
|
logger.debug(f"Deleted file {filename}.")
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"While deleting document {instance!s}, the file "
|
f"While deleting document {instance!s}, the file "
|
||||||
f"{filename} could not be deleted: {e}",
|
f"{filename} could not be deleted: {e}",
|
||||||
)
|
)
|
||||||
elif filename and not filename.is_file():
|
elif filename and not os.path.isfile(filename):
|
||||||
logger.warning(f"Expected {filename} to exist, but it did not")
|
logger.warning(f"Expected {filename} to exist, but it did not")
|
||||||
|
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(instance.source_path).parent,
|
os.path.dirname(instance.source_path),
|
||||||
root=settings.ORIGINALS_DIR,
|
root=settings.ORIGINALS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if instance.has_archive_version:
|
if instance.has_archive_version:
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(instance.archive_path).parent,
|
os.path.dirname(instance.archive_path),
|
||||||
root=settings.ARCHIVE_DIR,
|
root=settings.ARCHIVE_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -400,14 +399,14 @@ def update_filename_and_move_files(
|
|||||||
if isinstance(instance, CustomFieldInstance):
|
if isinstance(instance, CustomFieldInstance):
|
||||||
instance = instance.document
|
instance = instance.document
|
||||||
|
|
||||||
def validate_move(instance, old_path: Path, new_path: Path):
|
def validate_move(instance, old_path, new_path):
|
||||||
if not old_path.is_file():
|
if not os.path.isfile(old_path):
|
||||||
# Can't do anything if the old file does not exist anymore.
|
# Can't do anything if the old file does not exist anymore.
|
||||||
msg = f"Document {instance!s}: File {old_path} doesn't exist."
|
msg = f"Document {instance!s}: File {old_path} doesn't exist."
|
||||||
logger.fatal(msg)
|
logger.fatal(msg)
|
||||||
raise CannotMoveFilesException(msg)
|
raise CannotMoveFilesException(msg)
|
||||||
|
|
||||||
if new_path.is_file():
|
if os.path.isfile(new_path):
|
||||||
# Can't do anything if the new file already exists. Skip updating file.
|
# Can't do anything if the new file already exists. Skip updating file.
|
||||||
msg = f"Document {instance!s}: Cannot rename file since target path {new_path} already exists."
|
msg = f"Document {instance!s}: Cannot rename file since target path {new_path} already exists."
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
@@ -435,20 +434,16 @@ def update_filename_and_move_files(
|
|||||||
old_filename = instance.filename
|
old_filename = instance.filename
|
||||||
old_source_path = instance.source_path
|
old_source_path = instance.source_path
|
||||||
|
|
||||||
# Need to convert to string to be able to save it to the db
|
instance.filename = generate_unique_filename(instance)
|
||||||
instance.filename = str(generate_unique_filename(instance))
|
|
||||||
move_original = old_filename != instance.filename
|
move_original = old_filename != instance.filename
|
||||||
|
|
||||||
old_archive_filename = instance.archive_filename
|
old_archive_filename = instance.archive_filename
|
||||||
old_archive_path = instance.archive_path
|
old_archive_path = instance.archive_path
|
||||||
|
|
||||||
if instance.has_archive_version:
|
if instance.has_archive_version:
|
||||||
# Need to convert to string to be able to save it to the db
|
instance.archive_filename = generate_unique_filename(
|
||||||
instance.archive_filename = str(
|
instance,
|
||||||
generate_unique_filename(
|
archive_filename=True,
|
||||||
instance,
|
|
||||||
archive_filename=True,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
move_archive = old_archive_filename != instance.archive_filename
|
move_archive = old_archive_filename != instance.archive_filename
|
||||||
@@ -490,11 +485,11 @@ def update_filename_and_move_files(
|
|||||||
|
|
||||||
# Try to move files to their original location.
|
# Try to move files to their original location.
|
||||||
try:
|
try:
|
||||||
if move_original and instance.source_path.is_file():
|
if move_original and os.path.isfile(instance.source_path):
|
||||||
logger.info("Restoring previous original path")
|
logger.info("Restoring previous original path")
|
||||||
shutil.move(instance.source_path, old_source_path)
|
shutil.move(instance.source_path, old_source_path)
|
||||||
|
|
||||||
if move_archive and instance.archive_path.is_file():
|
if move_archive and os.path.isfile(instance.archive_path):
|
||||||
logger.info("Restoring previous archive path")
|
logger.info("Restoring previous archive path")
|
||||||
shutil.move(instance.archive_path, old_archive_path)
|
shutil.move(instance.archive_path, old_archive_path)
|
||||||
|
|
||||||
@@ -515,15 +510,17 @@ def update_filename_and_move_files(
|
|||||||
|
|
||||||
# finally, remove any empty sub folders. This will do nothing if
|
# finally, remove any empty sub folders. This will do nothing if
|
||||||
# something has failed above.
|
# something has failed above.
|
||||||
if not old_source_path.is_file():
|
if not os.path.isfile(old_source_path):
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(old_source_path).parent,
|
os.path.dirname(old_source_path),
|
||||||
root=settings.ORIGINALS_DIR,
|
root=settings.ORIGINALS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if instance.has_archive_version and not old_archive_path.is_file():
|
if instance.has_archive_version and not os.path.isfile(
|
||||||
|
old_archive_path,
|
||||||
|
):
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(old_archive_path).parent,
|
os.path.dirname(old_archive_path),
|
||||||
root=settings.ARCHIVE_DIR,
|
root=settings.ARCHIVE_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1220,7 +1217,10 @@ def run_workflows(
|
|||||||
)
|
)
|
||||||
files = None
|
files = None
|
||||||
if action.webhook.include_document:
|
if action.webhook.include_document:
|
||||||
with original_file.open("rb") as f:
|
with open(
|
||||||
|
original_file,
|
||||||
|
"rb",
|
||||||
|
) as f:
|
||||||
files = {
|
files = {
|
||||||
"file": (
|
"file": (
|
||||||
filename,
|
filename,
|
||||||
@@ -1439,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()
|
|
||||||
|
@@ -41,9 +41,11 @@ class TestDocument(TestCase):
|
|||||||
Path(file_path).touch()
|
Path(file_path).touch()
|
||||||
Path(thumb_path).touch()
|
Path(thumb_path).touch()
|
||||||
|
|
||||||
with mock.patch("documents.signals.handlers.Path.unlink") as mock_unlink:
|
with mock.patch("documents.signals.handlers.os.unlink") as mock_unlink:
|
||||||
document.delete()
|
document.delete()
|
||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
|
mock_unlink.assert_any_call(file_path)
|
||||||
|
mock_unlink.assert_any_call(thumb_path)
|
||||||
self.assertEqual(mock_unlink.call_count, 2)
|
self.assertEqual(mock_unlink.call_count, 2)
|
||||||
|
|
||||||
def test_document_soft_delete(self):
|
def test_document_soft_delete(self):
|
||||||
@@ -61,7 +63,7 @@ class TestDocument(TestCase):
|
|||||||
Path(file_path).touch()
|
Path(file_path).touch()
|
||||||
Path(thumb_path).touch()
|
Path(thumb_path).touch()
|
||||||
|
|
||||||
with mock.patch("documents.signals.handlers.Path.unlink") as mock_unlink:
|
with mock.patch("documents.signals.handlers.os.unlink") as mock_unlink:
|
||||||
document.delete()
|
document.delete()
|
||||||
self.assertEqual(mock_unlink.call_count, 0)
|
self.assertEqual(mock_unlink.call_count, 0)
|
||||||
|
|
||||||
|
@@ -34,12 +34,12 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
document.save()
|
document.save()
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path(f"{document.pk:07d}.pdf"))
|
self.assertEqual(generate_filename(document), f"{document.pk:07d}.pdf")
|
||||||
|
|
||||||
document.storage_type = Document.STORAGE_TYPE_GPG
|
document.storage_type = Document.STORAGE_TYPE_GPG
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(document),
|
generate_filename(document),
|
||||||
Path(f"{document.pk:07d}.pdf.gpg"),
|
f"{document.pk:07d}.pdf.gpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
||||||
@@ -58,12 +58,12 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
|
|
||||||
# Enable encryption and check again
|
# Enable encryption and check again
|
||||||
document.storage_type = Document.STORAGE_TYPE_GPG
|
document.storage_type = Document.STORAGE_TYPE_GPG
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf.gpg"))
|
self.assertEqual(document.filename, "none/none.pdf.gpg")
|
||||||
|
|
||||||
document.save()
|
document.save()
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
document.source_path.touch()
|
document.source_path.touch()
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
Path(document.source_path).touch()
|
Path(document.source_path).touch()
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
|
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
|
|
||||||
@@ -269,11 +269,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
dt = DocumentType.objects.create(name="my_doc_type")
|
dt = DocumentType.objects.create(name="my_doc_type")
|
||||||
d = Document.objects.create(title="the_doc", mime_type="application/pdf")
|
d = Document.objects.create(title="the_doc", mime_type="application/pdf")
|
||||||
|
|
||||||
self.assertEqual(generate_filename(d), Path("none - the_doc.pdf"))
|
self.assertEqual(generate_filename(d), "none - the_doc.pdf")
|
||||||
|
|
||||||
d.document_type = dt
|
d.document_type = dt
|
||||||
|
|
||||||
self.assertEqual(generate_filename(d), Path("my_doc_type - the_doc.pdf"))
|
self.assertEqual(generate_filename(d), "my_doc_type - the_doc.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{asn} - {title}")
|
@override_settings(FILENAME_FORMAT="{asn} - {title}")
|
||||||
def test_asn(self):
|
def test_asn(self):
|
||||||
@@ -289,8 +289,8 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
archive_serial_number=None,
|
archive_serial_number=None,
|
||||||
checksum="B",
|
checksum="B",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(d1), Path("652 - the_doc.pdf"))
|
self.assertEqual(generate_filename(d1), "652 - the_doc.pdf")
|
||||||
self.assertEqual(generate_filename(d2), Path("none - the_doc.pdf"))
|
self.assertEqual(generate_filename(d2), "none - the_doc.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title} {tag_list}")
|
@override_settings(FILENAME_FORMAT="{title} {tag_list}")
|
||||||
def test_tag_list(self):
|
def test_tag_list(self):
|
||||||
@@ -298,7 +298,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
doc.tags.create(name="tag2")
|
doc.tags.create(name="tag2")
|
||||||
doc.tags.create(name="tag1")
|
doc.tags.create(name="tag1")
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), Path("doc1 tag1,tag2.pdf"))
|
self.assertEqual(generate_filename(doc), "doc1 tag1,tag2.pdf")
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="doc2",
|
title="doc2",
|
||||||
@@ -306,7 +306,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), Path("doc2.pdf"))
|
self.assertEqual(generate_filename(doc), "doc2.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="//etc/something/{title}")
|
@override_settings(FILENAME_FORMAT="//etc/something/{title}")
|
||||||
def test_filename_relative(self):
|
def test_filename_relative(self):
|
||||||
@@ -330,11 +330,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
created=d1,
|
created=d1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("2020-03-06.pdf"))
|
self.assertEqual(generate_filename(doc1), "2020-03-06.pdf")
|
||||||
|
|
||||||
doc1.created = datetime.date(2020, 11, 16)
|
doc1.created = datetime.date(2020, 11, 16)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("2020-11-16.pdf"))
|
self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{added_year}-{added_month}-{added_day}",
|
FILENAME_FORMAT="{added_year}-{added_month}-{added_day}",
|
||||||
@@ -347,11 +347,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
added=d1,
|
added=d1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("232-01-09.pdf"))
|
self.assertEqual(generate_filename(doc1), "232-01-09.pdf")
|
||||||
|
|
||||||
doc1.added = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1))
|
doc1.added = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1))
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("2020-11-16.pdf"))
|
self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}",
|
FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}",
|
||||||
@@ -389,11 +389,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
document.pk = 13579
|
document.pk = 13579
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0013579.pdf"))
|
self.assertEqual(generate_filename(document), "0013579.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT=None)
|
@override_settings(FILENAME_FORMAT=None)
|
||||||
def test_format_none(self):
|
def test_format_none(self):
|
||||||
@@ -402,7 +402,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
def test_try_delete_empty_directories(self):
|
def test_try_delete_empty_directories(self):
|
||||||
# Create our working directory
|
# Create our working directory
|
||||||
@@ -428,7 +428,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{created__year}")
|
@override_settings(FILENAME_FORMAT="{created__year}")
|
||||||
def test_invalid_format_key(self):
|
def test_invalid_format_key(self):
|
||||||
@@ -437,7 +437,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title}")
|
@override_settings(FILENAME_FORMAT="{title}")
|
||||||
def test_duplicates(self):
|
def test_duplicates(self):
|
||||||
@@ -564,7 +564,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
value_select="abc123",
|
value_select="abc123",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), Path("document_apple.pdf"))
|
self.assertEqual(generate_filename(doc), "document_apple.pdf")
|
||||||
|
|
||||||
# handler should not have been called
|
# handler should not have been called
|
||||||
self.assertEqual(m.call_count, 0)
|
self.assertEqual(m.call_count, 0)
|
||||||
@@ -576,7 +576,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
cf.save()
|
cf.save()
|
||||||
self.assertEqual(generate_filename(doc), Path("document_aubergine.pdf"))
|
self.assertEqual(generate_filename(doc), "document_aubergine.pdf")
|
||||||
# handler should have been called
|
# handler should have been called
|
||||||
self.assertEqual(m.call_count, 1)
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
@@ -897,7 +897,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=1,
|
pk=1,
|
||||||
checksum="1",
|
checksum="1",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("This. is the title.pdf"))
|
self.assertEqual(generate_filename(doc), "This. is the title.pdf")
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="my\\invalid/../title:yay",
|
title="my\\invalid/../title:yay",
|
||||||
@@ -905,7 +905,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("my-invalid-..-title-yay.pdf"))
|
self.assertEqual(generate_filename(doc), "my-invalid-..-title-yay.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{created}")
|
@override_settings(FILENAME_FORMAT="{created}")
|
||||||
def test_date(self):
|
def test_date(self):
|
||||||
@@ -916,7 +916,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("2020-05-21.pdf"))
|
self.assertEqual(generate_filename(doc), "2020-05-21.pdf")
|
||||||
|
|
||||||
def test_dynamic_path(self):
|
def test_dynamic_path(self):
|
||||||
"""
|
"""
|
||||||
@@ -935,7 +935,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="2",
|
checksum="2",
|
||||||
storage_path=StoragePath.objects.create(path="TestFolder/{{created}}"),
|
storage_path=StoragePath.objects.create(path="TestFolder/{{created}}"),
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("TestFolder/2020-06-25.pdf"))
|
self.assertEqual(generate_filename(doc), "TestFolder/2020-06-25.pdf")
|
||||||
|
|
||||||
def test_dynamic_path_with_none(self):
|
def test_dynamic_path_with_none(self):
|
||||||
"""
|
"""
|
||||||
@@ -956,7 +956,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="2",
|
checksum="2",
|
||||||
storage_path=StoragePath.objects.create(path="{{asn}} - {{created}}"),
|
storage_path=StoragePath.objects.create(path="{{asn}} - {{created}}"),
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("none - 2020-06-25.pdf"))
|
self.assertEqual(generate_filename(doc), "none - 2020-06-25.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT_REMOVE_NONE=True,
|
FILENAME_FORMAT_REMOVE_NONE=True,
|
||||||
@@ -984,7 +984,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="2",
|
checksum="2",
|
||||||
storage_path=sp,
|
storage_path=sp,
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("TestFolder/2020-06-25.pdf"))
|
self.assertEqual(generate_filename(doc), "TestFolder/2020-06-25.pdf")
|
||||||
|
|
||||||
# Special case, undefined variable, then defined at the start of the template
|
# Special case, undefined variable, then defined at the start of the template
|
||||||
# This could lead to an absolute path after we remove the leading -none-, but leave the leading /
|
# This could lead to an absolute path after we remove the leading -none-, but leave the leading /
|
||||||
@@ -993,7 +993,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
"{{ owner_username }}/{{ created_year }}/{{ correspondent }}/{{ title }}"
|
"{{ owner_username }}/{{ created_year }}/{{ correspondent }}/{{ title }}"
|
||||||
)
|
)
|
||||||
sp.save()
|
sp.save()
|
||||||
self.assertEqual(generate_filename(doc), Path("2020/does not matter.pdf"))
|
self.assertEqual(generate_filename(doc), "2020/does not matter.pdf")
|
||||||
|
|
||||||
def test_multiple_doc_paths(self):
|
def test_multiple_doc_paths(self):
|
||||||
"""
|
"""
|
||||||
@@ -1028,14 +1028,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(generate_filename(doc_a), "ThisIsAFolder/4/2020-06-25.pdf")
|
||||||
generate_filename(doc_a),
|
self.assertEqual(generate_filename(doc_b), "SomeImportantNone/2020-07-25.pdf")
|
||||||
Path("ThisIsAFolder/4/2020-06-25.pdf"),
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
generate_filename(doc_b),
|
|
||||||
Path("SomeImportantNone/2020-07-25.pdf"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT=None,
|
FILENAME_FORMAT=None,
|
||||||
@@ -1070,11 +1064,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc_a), Path("0000002.pdf"))
|
self.assertEqual(generate_filename(doc_a), "0000002.pdf")
|
||||||
self.assertEqual(
|
self.assertEqual(generate_filename(doc_b), "SomeImportantNone/2020-07-25.pdf")
|
||||||
generate_filename(doc_b),
|
|
||||||
Path("SomeImportantNone/2020-07-25.pdf"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{created_year_short}/{created_month_name_short}/{created_month_name}/{title}",
|
FILENAME_FORMAT="{created_year_short}/{created_month_name_short}/{created_month_name}/{title}",
|
||||||
@@ -1087,7 +1078,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("89/Dec/December/The Title.pdf"))
|
self.assertEqual(generate_filename(doc), "89/Dec/December/The Title.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{added_year_short}/{added_month_name}/{added_month_name_short}/{title}",
|
FILENAME_FORMAT="{added_year_short}/{added_month_name}/{added_month_name_short}/{title}",
|
||||||
@@ -1100,7 +1091,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("84/August/Aug/The Title.pdf"))
|
self.assertEqual(generate_filename(doc), "84/August/Aug/The Title.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{owner_username}/{title}",
|
FILENAME_FORMAT="{owner_username}/{title}",
|
||||||
@@ -1133,8 +1124,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="3",
|
checksum="3",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(owned_doc), Path("user1/The Title.pdf"))
|
self.assertEqual(generate_filename(owned_doc), "user1/The Title.pdf")
|
||||||
self.assertEqual(generate_filename(no_owner_doc), Path("none/does matter.pdf"))
|
self.assertEqual(generate_filename(no_owner_doc), "none/does matter.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{original_name}",
|
FILENAME_FORMAT="{original_name}",
|
||||||
@@ -1180,20 +1171,17 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
original_filename="logs.txt",
|
original_filename="logs.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc_with_original), Path("someepdf.pdf"))
|
self.assertEqual(generate_filename(doc_with_original), "someepdf.pdf")
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(tricky_with_original),
|
generate_filename(tricky_with_original),
|
||||||
Path("some pdf with spaces and stuff.pdf"),
|
"some pdf with spaces and stuff.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(no_original), Path("none.pdf"))
|
self.assertEqual(generate_filename(no_original), "none.pdf")
|
||||||
|
|
||||||
self.assertEqual(generate_filename(text_doc), Path("logs.txt"))
|
self.assertEqual(generate_filename(text_doc), "logs.txt")
|
||||||
self.assertEqual(
|
self.assertEqual(generate_filename(text_doc, archive_filename=True), "logs.pdf")
|
||||||
generate_filename(text_doc, archive_filename=True),
|
|
||||||
Path("logs.pdf"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="XX{correspondent}/{title}",
|
FILENAME_FORMAT="XX{correspondent}/{title}",
|
||||||
@@ -1218,7 +1206,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("XX/doc1.pdf"))
|
self.assertEqual(document.filename, "XX/doc1.pdf")
|
||||||
|
|
||||||
def test_complex_template_strings(self):
|
def test_complex_template_strings(self):
|
||||||
"""
|
"""
|
||||||
@@ -1256,19 +1244,19 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/some where/2020-06-25/Does Matter.pdf"),
|
"somepath/some where/2020-06-25/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
doc_a.checksum = "5"
|
doc_a.checksum = "5"
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/2024-10-01/Does Matter.pdf"),
|
"somepath/2024-10-01/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
sp.path = "{{ document.title|lower }}{{ document.archive_serial_number - 2 }}"
|
sp.path = "{{ document.title|lower }}{{ document.archive_serial_number - 2 }}"
|
||||||
sp.save()
|
sp.save()
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc_a), Path("does matter23.pdf"))
|
self.assertEqual(generate_filename(doc_a), "does matter23.pdf")
|
||||||
|
|
||||||
sp.path = """
|
sp.path = """
|
||||||
somepath/
|
somepath/
|
||||||
@@ -1287,13 +1275,13 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
sp.save()
|
sp.save()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/asn-000-200/Does Matter/Does Matter.pdf"),
|
"somepath/asn-000-200/Does Matter/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
doc_a.archive_serial_number = 301
|
doc_a.archive_serial_number = 301
|
||||||
doc_a.save()
|
doc_a.save()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/asn-201-400/asn-3xx/Does Matter.pdf"),
|
"somepath/asn-201-400/asn-3xx/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@@ -1322,7 +1310,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
with self.assertLogs(level=logging.WARNING) as capture:
|
with self.assertLogs(level=logging.WARNING) as capture:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("0000002.pdf"),
|
"0000002.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(capture.output), 1)
|
self.assertEqual(len(capture.output), 1)
|
||||||
@@ -1357,7 +1345,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
with self.assertLogs(level=logging.WARNING) as capture:
|
with self.assertLogs(level=logging.WARNING) as capture:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("0000002.pdf"),
|
"0000002.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(capture.output), 1)
|
self.assertEqual(len(capture.output), 1)
|
||||||
@@ -1425,7 +1413,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("invoices/1234.pdf"),
|
"invoices/1234.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1439,7 +1427,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("Some Title_ChoiceOne.pdf"),
|
"Some Title_ChoiceOne.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for handling Nones well
|
# Check for handling Nones well
|
||||||
@@ -1448,7 +1436,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("Some Title_Default Value.pdf"),
|
"Some Title_Default Value.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
cf.name = "Invoice Number"
|
cf.name = "Invoice Number"
|
||||||
@@ -1461,7 +1449,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("invoices/4567.pdf"),
|
"invoices/4567.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1469,7 +1457,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("invoices/0.pdf"),
|
"invoices/0.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_datetime_filter(self):
|
def test_datetime_filter(self):
|
||||||
@@ -1508,7 +1496,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("2020/Some Title.pdf"),
|
"2020/Some Title.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1516,7 +1504,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("2020-06-25/Some Title.pdf"),
|
"2020-06-25/Some Title.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1524,7 +1512,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("2024-10-01/Some Title.pdf"),
|
"2024-10-01/Some Title.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_slugify_filter(self):
|
def test_slugify_filter(self):
|
||||||
@@ -1551,7 +1539,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc),
|
generate_filename(doc),
|
||||||
Path("some-title-with-special-characters.pdf"),
|
"some-title-with-special-characters.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with correspondent name containing spaces and special chars
|
# Test with correspondent name containing spaces and special chars
|
||||||
@@ -1565,7 +1553,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc),
|
generate_filename(doc),
|
||||||
Path("johns-office-workplace/some-title-with-special-characters.pdf"),
|
"johns-office-workplace/some-title-with-special-characters.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with custom fields
|
# Test with custom fields
|
||||||
@@ -1584,5 +1572,5 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc),
|
generate_filename(doc),
|
||||||
Path("brussels-belgium/some-title-with-special-characters.pdf"),
|
"brussels-belgium/some-title-with-special-characters.pdf",
|
||||||
)
|
)
|
||||||
|
@@ -209,7 +209,7 @@ class TestExportImport(
|
|||||||
4,
|
4,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self._get_document_from_manifest(manifest, self.d1.id)["fields"]["title"],
|
self._get_document_from_manifest(manifest, self.d1.id)["fields"]["title"],
|
||||||
@@ -235,7 +235,9 @@ class TestExportImport(
|
|||||||
).as_posix()
|
).as_posix()
|
||||||
self.assertIsFile(fname)
|
self.assertIsFile(fname)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME],
|
(
|
||||||
|
self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME]
|
||||||
|
).as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
with Path(fname).open("rb") as f:
|
with Path(fname).open("rb") as f:
|
||||||
@@ -250,7 +252,7 @@ class TestExportImport(
|
|||||||
if document_exporter.EXPORTER_ARCHIVE_NAME in element:
|
if document_exporter.EXPORTER_ARCHIVE_NAME in element:
|
||||||
fname = (
|
fname = (
|
||||||
self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
|
self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
|
||||||
)
|
).as_posix()
|
||||||
self.assertIsFile(fname)
|
self.assertIsFile(fname)
|
||||||
|
|
||||||
with Path(fname).open("rb") as f:
|
with Path(fname).open("rb") as f:
|
||||||
@@ -310,7 +312,7 @@ class TestExportImport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._do_export()
|
self._do_export()
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
|
st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
|
||||||
|
|
||||||
@@ -320,7 +322,7 @@ class TestExportImport(
|
|||||||
self._do_export()
|
self._do_export()
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
st_mtime_2 = (self.target / "manifest.json").stat().st_mtime
|
st_mtime_2 = (self.target / "manifest.json").stat().st_mtime
|
||||||
|
|
||||||
Path(self.d1.source_path).touch()
|
Path(self.d1.source_path).touch()
|
||||||
@@ -332,7 +334,7 @@ class TestExportImport(
|
|||||||
self.assertEqual(m.call_count, 1)
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
st_mtime_3 = (self.target / "manifest.json").stat().st_mtime
|
st_mtime_3 = (self.target / "manifest.json").stat().st_mtime
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.assertNotEqual(st_mtime_1, st_mtime_2)
|
self.assertNotEqual(st_mtime_1, st_mtime_2)
|
||||||
self.assertNotEqual(st_mtime_2, st_mtime_3)
|
self.assertNotEqual(st_mtime_2, st_mtime_3)
|
||||||
@@ -350,7 +352,7 @@ class TestExportImport(
|
|||||||
|
|
||||||
self._do_export()
|
self._do_export()
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"documents.management.commands.document_exporter.copy_file_with_basic_stats",
|
"documents.management.commands.document_exporter.copy_file_with_basic_stats",
|
||||||
@@ -358,7 +360,7 @@ class TestExportImport(
|
|||||||
self._do_export()
|
self._do_export()
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.d2.checksum = "asdfasdgf3"
|
self.d2.checksum = "asdfasdgf3"
|
||||||
self.d2.save()
|
self.d2.save()
|
||||||
@@ -369,7 +371,7 @@ class TestExportImport(
|
|||||||
self._do_export(compare_checksums=True)
|
self._do_export(compare_checksums=True)
|
||||||
self.assertEqual(m.call_count, 1)
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
def test_update_export_deleted_document(self):
|
def test_update_export_deleted_document(self):
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
@@ -383,7 +385,7 @@ class TestExportImport(
|
|||||||
self.assertTrue(len(manifest), 7)
|
self.assertTrue(len(manifest), 7)
|
||||||
doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id)
|
doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
str(self.target / doc_from_manifest[EXPORTER_FILE_NAME]),
|
(self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
|
||||||
)
|
)
|
||||||
self.d3.delete()
|
self.d3.delete()
|
||||||
|
|
||||||
@@ -395,12 +397,12 @@ class TestExportImport(
|
|||||||
self.d3.id,
|
self.d3.id,
|
||||||
)
|
)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
self.target / doc_from_manifest[EXPORTER_FILE_NAME],
|
(self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
manifest = self._do_export(delete=True)
|
manifest = self._do_export(delete=True)
|
||||||
self.assertIsNotFile(
|
self.assertIsNotFile(
|
||||||
self.target / doc_from_manifest[EXPORTER_FILE_NAME],
|
(self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(len(manifest), 6)
|
self.assertTrue(len(manifest), 6)
|
||||||
@@ -414,20 +416,20 @@ class TestExportImport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._do_export(use_filename_format=True)
|
self._do_export(use_filename_format=True)
|
||||||
self.assertIsFile(self.target / "wow1" / "c.pdf")
|
self.assertIsFile((self.target / "wow1" / "c.pdf").as_posix())
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.d1.title = "new_title"
|
self.d1.title = "new_title"
|
||||||
self.d1.save()
|
self.d1.save()
|
||||||
self._do_export(use_filename_format=True, delete=True)
|
self._do_export(use_filename_format=True, delete=True)
|
||||||
self.assertIsNotFile(self.target / "wow1" / "c.pdf")
|
self.assertIsNotFile((self.target / "wow1" / "c.pdf").as_posix())
|
||||||
self.assertIsNotDir(self.target / "wow1")
|
self.assertIsNotDir((self.target / "wow1").as_posix())
|
||||||
self.assertIsFile(self.target / "new_title" / "c.pdf")
|
self.assertIsFile((self.target / "new_title" / "c.pdf").as_posix())
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
self.assertIsFile(self.target / "wow2" / "none.pdf")
|
self.assertIsFile((self.target / "wow2" / "none.pdf").as_posix())
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
self.target / "wow2" / "none_01.pdf",
|
(self.target / "wow2" / "none_01.pdf").as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_export_missing_files(self):
|
def test_export_missing_files(self):
|
||||||
|
@@ -20,7 +20,7 @@ def source_path_before(self):
|
|||||||
if self.storage_type == STORAGE_TYPE_GPG:
|
if self.storage_type == STORAGE_TYPE_GPG:
|
||||||
fname += ".gpg"
|
fname += ".gpg"
|
||||||
|
|
||||||
return Path(settings.ORIGINALS_DIR) / fname
|
return (Path(settings.ORIGINALS_DIR) / fname).as_posix()
|
||||||
|
|
||||||
|
|
||||||
def file_type_after(self):
|
def file_type_after(self):
|
||||||
@@ -35,7 +35,7 @@ def source_path_after(doc):
|
|||||||
if doc.storage_type == STORAGE_TYPE_GPG:
|
if doc.storage_type == STORAGE_TYPE_GPG:
|
||||||
fname += ".gpg" # pragma: no cover
|
fname += ".gpg" # pragma: no cover
|
||||||
|
|
||||||
return Path(settings.ORIGINALS_DIR) / fname
|
return (Path(settings.ORIGINALS_DIR) / fname).as_posix()
|
||||||
|
|
||||||
|
|
||||||
@override_settings(PASSPHRASE="test")
|
@override_settings(PASSPHRASE="test")
|
||||||
|
@@ -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