Compare commits

..

4 Commits

Author SHA1 Message Date
GitHub Actions
07c298523a Auto translate strings 2025-08-02 12:55:48 +00:00
Antoine Mérino
0ea159683d Performance: add setting to enable DB connection pooling for PostgreSQL (#10354)
---------

Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2025-08-02 12:54:13 +00:00
GitHub Actions
f0b6e79d14 Auto translate strings 2025-08-02 03:45:01 +00:00
dependabot[bot]
302cb22ec6 Chore(deps-dev): Bump the frontend-jest-dependencies group in /src-ui with 4 updates (#10497)
* Chore(deps-dev): Bump the frontend-jest-dependencies group

Bumps the frontend-jest-dependencies group in /src-ui with 4 updates: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest), [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest), [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) and [jest-preset-angular](https://github.com/thymikee/jest-preset-angular).


Updates `jest` from 29.7.0 to 30.0.5
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest)

Updates `@types/jest` from 29.5.14 to 30.0.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Updates `jest-environment-jsdom` from 29.7.0 to 30.0.5
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest-environment-jsdom)

Updates `jest-preset-angular` from 14.5.5 to 15.0.0
- [Release notes](https://github.com/thymikee/jest-preset-angular/releases)
- [Changelog](https://github.com/thymikee/jest-preset-angular/blob/main/CHANGELOG.md)
- [Commits](https://github.com/thymikee/jest-preset-angular/compare/v14.5.5...v15.0.0)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-jest-dependencies
- dependency-name: "@types/jest"
  dependency-version: 30.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-jest-dependencies
- dependency-name: jest-environment-jsdom
  dependency-version: 30.0.5
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-jest-dependencies
- dependency-name: jest-preset-angular
  dependency-version: 15.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: frontend-jest-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update Jest setup for Node util imports and typings

* Refactor navigation actions to utility functions

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-08-02 03:43:31 +00:00
19 changed files with 1418 additions and 978 deletions

View File

@@ -159,6 +159,23 @@ Available options are `postgresql` and `mariadb`.
Defaults to unset, which uses Djangos 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}
: 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.

View File

@@ -52,6 +52,7 @@ dependencies = [
"ocrmypdf~=16.10.0",
"pathvalidate~=3.3.1",
"pdf2image~=1.17.0",
"psycopg-pool",
"python-dateutil~=2.9.0",
"python-dotenv~=1.1.0",
"python-gnupg~=0.5.4",
@@ -74,9 +75,10 @@ optional-dependencies.mariadb = [
"mysqlclient~=2.2.7",
]
optional-dependencies.postgres = [
"psycopg[c]==3.2.9",
"psycopg[c,pool]==3.2.9",
# Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.2.9",
"psycopg-pool==3.2.6",
]
optional-dependencies.webserver = [
"granian[uvloop]~=2.4.1",

View File

@@ -1507,64 +1507,64 @@
<source>Use system language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">77</context>
<context context-type="linenumber">78</context>
</context-group>
</trans-unit>
<trans-unit id="7729897675462249787" datatype="html">
<source>Use date format of display language</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">80</context>
<context context-type="linenumber">81</context>
</context-group>
</trans-unit>
<trans-unit id="1235706724900303689" datatype="html">
<source>Error retrieving users</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">224</context>
<context context-type="linenumber">225</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">55</context>
<context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="3066660568529853846" datatype="html">
<source>Error retrieving groups</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">243</context>
<context context-type="linenumber">244</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">67</context>
<context context-type="linenumber">68</context>
</context-group>
</trans-unit>
<trans-unit id="7217000812750597833" datatype="html">
<source>Settings were saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">546</context>
<context context-type="linenumber">547</context>
</context-group>
</trans-unit>
<trans-unit id="525012668859298131" datatype="html">
<source>Settings were saved successfully. Reload is required to apply some changes.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">550</context>
<context context-type="linenumber">551</context>
</context-group>
</trans-unit>
<trans-unit id="8491974984518503778" datatype="html">
<source>Reload now</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">551</context>
<context context-type="linenumber">552</context>
</context-group>
</trans-unit>
<trans-unit id="3011185103048412841" datatype="html">
<source>An error occurred while saving settings.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.ts</context>
<context context-type="linenumber">561</context>
<context context-type="linenumber">562</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
@@ -2229,11 +2229,11 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">120</context>
<context context-type="linenumber">123</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">173</context>
<context context-type="linenumber">176</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
@@ -2497,50 +2497,50 @@
<source>Password has been changed, you will be logged out momentarily.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">93</context>
<context context-type="linenumber">94</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">194</context>
<context context-type="linenumber">195</context>
</context-group>
</trans-unit>
<trans-unit id="2753185112875184719" datatype="html">
<source>Saved user &quot;<x id="PH" equiv-text="newUser.username"/>&quot;.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">100</context>
<context context-type="linenumber">103</context>
</context-group>
</trans-unit>
<trans-unit id="3471101514724661554" datatype="html">
<source>Error saving user.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">110</context>
<context context-type="linenumber">113</context>
</context-group>
</trans-unit>
<trans-unit id="5565868288871970148" datatype="html">
<source>Confirm delete user account</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">118</context>
<context context-type="linenumber">121</context>
</context-group>
</trans-unit>
<trans-unit id="8133663925694885325" datatype="html">
<source>This operation will permanently delete this user account.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">119</context>
<context context-type="linenumber">122</context>
</context-group>
</trans-unit>
<trans-unit id="1181910457994920507" datatype="html">
<source>Proceed</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">122</context>
<context context-type="linenumber">125</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">175</context>
<context context-type="linenumber">178</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
@@ -2595,56 +2595,56 @@
<source>Deleted user &quot;<x id="PH" equiv-text="user.username"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">128</context>
<context context-type="linenumber">131</context>
</context-group>
</trans-unit>
<trans-unit id="286457042048584728" datatype="html">
<source>Error deleting user &quot;<x id="PH" equiv-text="user.username"/>&quot;.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">135</context>
<context context-type="linenumber">138</context>
</context-group>
</trans-unit>
<trans-unit id="5766640174051730159" datatype="html">
<source>Saved group &quot;<x id="PH" equiv-text="newGroup.name"/>&quot;.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">155</context>
<context context-type="linenumber">158</context>
</context-group>
</trans-unit>
<trans-unit id="8382042988405122578" datatype="html">
<source>Error saving group.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">163</context>
<context context-type="linenumber">166</context>
</context-group>
</trans-unit>
<trans-unit id="6538873300613683004" datatype="html">
<source>Confirm delete user group</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">171</context>
<context context-type="linenumber">174</context>
</context-group>
</trans-unit>
<trans-unit id="7710984639498518244" datatype="html">
<source>This operation will permanently delete this user group.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">172</context>
<context context-type="linenumber">175</context>
</context-group>
</trans-unit>
<trans-unit id="3756187211130340490" datatype="html">
<source>Deleted group &quot;<x id="PH" equiv-text="group.name"/>&quot;</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">181</context>
<context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit id="1697803415975901060" datatype="html">
<source>Error deleting group &quot;<x id="PH" equiv-text="group.name"/>&quot;.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.ts</context>
<context context-type="linenumber">188</context>
<context context-type="linenumber">191</context>
</context-group>
</trans-unit>
<trans-unit id="7931334600001636863" datatype="html">
@@ -5828,85 +5828,85 @@
<source>Emails must match</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">142</context>
<context context-type="linenumber">143</context>
</context-group>
</trans-unit>
<trans-unit id="5281933990298241826" datatype="html">
<source>Passwords must match</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">170</context>
<context context-type="linenumber">171</context>
</context-group>
</trans-unit>
<trans-unit id="4219429959475101385" datatype="html">
<source>Profile updated successfully</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">191</context>
<context context-type="linenumber">192</context>
</context-group>
</trans-unit>
<trans-unit id="3417726855410304962" datatype="html">
<source>Error saving profile</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">203</context>
<context context-type="linenumber">206</context>
</context-group>
</trans-unit>
<trans-unit id="154249228726292516" datatype="html">
<source>Error generating auth token</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">220</context>
<context context-type="linenumber">223</context>
</context-group>
</trans-unit>
<trans-unit id="4153637646944982460" datatype="html">
<source>Error disconnecting social account</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">245</context>
<context context-type="linenumber">248</context>
</context-group>
</trans-unit>
<trans-unit id="5939111172212776886" datatype="html">
<source>Error fetching TOTP settings</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">264</context>
<context context-type="linenumber">267</context>
</context-group>
</trans-unit>
<trans-unit id="1030314492414713260" datatype="html">
<source>TOTP activated successfully</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">285</context>
<context context-type="linenumber">288</context>
</context-group>
</trans-unit>
<trans-unit id="3755006064892435830" datatype="html">
<source>Error activating TOTP</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">287</context>
<context context-type="linenumber">290</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">293</context>
<context context-type="linenumber">296</context>
</context-group>
</trans-unit>
<trans-unit id="5919827473541889422" datatype="html">
<source>TOTP deactivated successfully</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">309</context>
<context context-type="linenumber">312</context>
</context-group>
</trans-unit>
<trans-unit id="6214722303383624015" datatype="html">
<source>Error deactivating TOTP</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">311</context>
<context context-type="linenumber">314</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.ts</context>
<context context-type="linenumber">316</context>
<context context-type="linenumber">319</context>
</context-group>
</trans-unit>
<trans-unit id="6617773613987957957" datatype="html">

View File

@@ -54,16 +54,16 @@
"@angular/compiler-cli": "~20.1.4",
"@codecov/webpack-plugin": "^1.9.1",
"@playwright/test": "^1.54.2",
"@types/jest": "^29.5.14",
"@types/jest": "^30.0.0",
"@types/node": "^24.1.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
"@typescript-eslint/utils": "^8.38.0",
"eslint": "^9.32.0",
"jest": "29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest": "30.0.5",
"jest-environment-jsdom": "^30.0.5",
"jest-junit": "^16.0.0",
"jest-preset-angular": "^14.5.5",
"jest-preset-angular": "^15.0.0",
"jest-websocket-mock": "^2.5.0",
"prettier-plugin-organize-imports": "^4.2.0",
"ts-node": "~10.9.1",

2068
src-ui/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,16 @@
import '@angular/localize/init'
import { jest } from '@jest/globals'
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'
import { TextDecoder, TextEncoder } from 'util'
import { TextDecoder, TextEncoder } from 'node:util'
if (process.env.NODE_ENV === 'test') {
setupZoneTestEnv()
}
global.TextEncoder = TextEncoder
global.TextDecoder = TextDecoder
;(globalThis as any).TextEncoder = TextEncoder as unknown as {
new (): TextEncoder
}
;(globalThis as any).TextDecoder = TextDecoder as unknown as {
new (): TextDecoder
}
import { registerLocaleData } from '@angular/common'
import localeAf from '@angular/common/locales/af'
@@ -116,10 +120,6 @@ if (!URL.revokeObjectURL) {
Object.defineProperty(window.URL, 'revokeObjectURL', { value: jest.fn() })
}
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
})
HTMLCanvasElement.prototype.getContext = <
typeof HTMLCanvasElement.prototype.getContext

View File

@@ -36,6 +36,7 @@ import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.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 { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CheckComponent } from '../../common/input/check/check.component'
@@ -225,6 +226,9 @@ describe('SettingsComponent', () => {
})
it('should offer reload if settings changes require', () => {
const reloadSpy = jest
.spyOn(navUtils, 'locationReload')
.mockImplementation(() => {})
completeSetup()
let toast: Toast
toastService.getToasts().subscribe((t) => (toast = t[0]))
@@ -241,6 +245,7 @@ describe('SettingsComponent', () => {
expect(toast.actionName).toEqual('Reload now')
toast.action()
expect(reloadSpy).toHaveBeenCalled()
})
it('should allow setting theme color, visually apply change immediately but not save', () => {
@@ -269,7 +274,7 @@ describe('SettingsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
})
it('should show errors on load if load groups failure', () => {
@@ -281,7 +286,7 @@ describe('SettingsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
})
it('should load system status on initialize, show errors if needed', () => {

View File

@@ -57,6 +57,7 @@ import {
} from 'src/app/services/settings.service'
import { SystemStatusService } from 'src/app/services/system-status.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 { ColorComponent } from '../../common/input/color/color.component'
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
@@ -550,7 +551,7 @@ export class SettingsComponent
savedToast.content = $localize`Settings were saved successfully. Reload is required to apply some changes.`
savedToast.actionName = $localize`Reload now`
savedToast.action = () => {
location.reload()
locationReload()
}
}

View File

@@ -19,6 +19,7 @@ import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.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 { 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'
@@ -107,7 +108,7 @@ describe('UsersAndGroupsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
@@ -130,7 +131,7 @@ describe('UsersAndGroupsComponent', () => {
throwError(() => new Error('error deleting user'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
@@ -142,19 +143,18 @@ describe('UsersAndGroupsComponent', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
component.editUser(users[0])
const navSpy = jest
.spyOn(navUtils, 'setLocationHref')
.mockImplementation(() => {})
const editDialog = modal.componentInstance as UserEditDialogComponent
editDialog.passwordIsSet = true
settingsService.currentUser = users[0] // simulate logged in as same user
editDialog.succeeded.emit(users[0])
fixture.detectChanges()
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
tick(2600)
expect(window.location.href).toContain('logout')
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}))
it('should support edit / create group, show error if needed', () => {
@@ -166,7 +166,7 @@ describe('UsersAndGroupsComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
editDialog.succeeded.emit(groups[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved group "${groups[0].name}".`
@@ -188,7 +188,7 @@ describe('UsersAndGroupsComponent', () => {
throwError(() => new Error('error deleting group'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
@@ -210,7 +210,7 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(userService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
})
it('should show errors on load if load groups failure', () => {
@@ -222,6 +222,6 @@ describe('UsersAndGroupsComponent', () => {
)
completeSetup(groupService)
fixture.detectChanges()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
})
})

View File

@@ -10,6 +10,7 @@ import { GroupService } from 'src/app/services/rest/group.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.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 { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
@@ -93,7 +94,9 @@ export class UsersAndGroupsComponent
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
setLocationHref(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}, 2500)
} else {
this.toastService.showInfo(

View File

@@ -18,6 +18,7 @@ import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
import { ProfileService } from 'src/app/services/profile.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 { PasswordComponent } from '../input/password/password.component'
import { TextComponent } from '../input/text/text.component'
@@ -205,16 +206,15 @@ describe('ProfileEditDialogComponent', () => {
const updateSpy = jest.spyOn(profileService, 'update')
updateSpy.mockReturnValue(of(null))
Object.defineProperty(window, 'location', {
value: {
href: 'http://localhost/',
},
writable: true, // possibility to override
})
const navSpy = jest
.spyOn(navUtils, 'setLocationHref')
.mockImplementation(() => {})
component.save()
expect(updateSpy).toHaveBeenCalled()
tick(2600)
expect(window.location.href).toContain('logout')
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}))
it('should support auth token copy', fakeAsync(() => {

View File

@@ -21,6 +21,7 @@ import {
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { ProfileService } from 'src/app/services/profile.service'
import { ToastService } from 'src/app/services/toast.service'
import { setLocationHref } from 'src/app/utils/navigation'
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
import { ConfirmButtonComponent } from '../confirm-button/confirm-button.component'
import { PasswordComponent } from '../input/password/password.component'
@@ -194,7 +195,9 @@ export class ProfileEditDialogComponent
$localize`Password has been changed, you will be logged out momentarily.`
)
setTimeout(() => {
window.location.href = `${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
setLocationHref(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}, 2500)
}
this.activeModal.close()

View File

@@ -188,7 +188,7 @@ describe('MailComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
editDialog.succeeded.emit(mailAccounts[0] as any)
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved account "${mailAccounts[0].name}".`
@@ -211,7 +211,7 @@ describe('MailComponent', () => {
throwError(() => new Error('error deleting mail account'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()
@@ -246,7 +246,7 @@ describe('MailComponent', () => {
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
editDialog.succeeded.emit(mailRules[0] as any)
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved rule "${mailRules[0].name}".`
@@ -280,7 +280,7 @@ describe('MailComponent', () => {
throwError(() => new Error('error deleting mail rule "rule1"'))
)
deleteDialog.confirm()
expect(toastErrorSpy).toBeCalled()
expect(toastErrorSpy).toHaveBeenCalled()
deleteSpy.mockReturnValueOnce(of(true))
deleteDialog.confirm()
expect(listAllSpy).toHaveBeenCalled()

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
export function setLocationHref(url: string) {
window.location.href = url
}
export function locationReload() {
window.location.reload()
}

View File

@@ -3,7 +3,8 @@
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jest"
"jest",
"node",
],
"module": "commonjs",
"emitDecoratorMetadata": true,

View File

@@ -12,11 +12,13 @@ from celery.signals import before_task_publish
from celery.signals import task_failure
from celery.signals import task_postrun
from celery.signals import task_prerun
from celery.signals import worker_process_init
from django.conf import settings
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
from django.db import DatabaseError
from django.db import close_old_connections
from django.db import connections
from django.db import models
from django.db.models import Q
from django.dispatch import receiver
@@ -1439,3 +1441,18 @@ def task_failure_handler(
task_instance.save()
except Exception: # pragma: no cover
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()

View File

@@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-08 21:14+0000\n"
"POT-Creation-Date: 2025-08-02 12:55+0000\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@@ -1645,147 +1645,147 @@ msgstr ""
msgid "paperless application settings"
msgstr ""
#: paperless/settings.py:762
#: paperless/settings.py:774
msgid "English (US)"
msgstr ""
#: paperless/settings.py:763
#: paperless/settings.py:775
msgid "Arabic"
msgstr ""
#: paperless/settings.py:764
#: paperless/settings.py:776
msgid "Afrikaans"
msgstr ""
#: paperless/settings.py:765
#: paperless/settings.py:777
msgid "Belarusian"
msgstr ""
#: paperless/settings.py:766
#: paperless/settings.py:778
msgid "Bulgarian"
msgstr ""
#: paperless/settings.py:767
#: paperless/settings.py:779
msgid "Catalan"
msgstr ""
#: paperless/settings.py:768
#: paperless/settings.py:780
msgid "Czech"
msgstr ""
#: paperless/settings.py:769
#: paperless/settings.py:781
msgid "Danish"
msgstr ""
#: paperless/settings.py:770
#: paperless/settings.py:782
msgid "German"
msgstr ""
#: paperless/settings.py:771
#: paperless/settings.py:783
msgid "Greek"
msgstr ""
#: paperless/settings.py:772
#: paperless/settings.py:784
msgid "English (GB)"
msgstr ""
#: paperless/settings.py:773
#: paperless/settings.py:785
msgid "Spanish"
msgstr ""
#: paperless/settings.py:774
#: paperless/settings.py:786
msgid "Persian"
msgstr ""
#: paperless/settings.py:775
#: paperless/settings.py:787
msgid "Finnish"
msgstr ""
#: paperless/settings.py:776
#: paperless/settings.py:788
msgid "French"
msgstr ""
#: paperless/settings.py:777
#: paperless/settings.py:789
msgid "Hungarian"
msgstr ""
#: paperless/settings.py:778
#: paperless/settings.py:790
msgid "Italian"
msgstr ""
#: paperless/settings.py:779
#: paperless/settings.py:791
msgid "Japanese"
msgstr ""
#: paperless/settings.py:780
#: paperless/settings.py:792
msgid "Korean"
msgstr ""
#: paperless/settings.py:781
#: paperless/settings.py:793
msgid "Luxembourgish"
msgstr ""
#: paperless/settings.py:782
#: paperless/settings.py:794
msgid "Norwegian"
msgstr ""
#: paperless/settings.py:783
#: paperless/settings.py:795
msgid "Dutch"
msgstr ""
#: paperless/settings.py:784
#: paperless/settings.py:796
msgid "Polish"
msgstr ""
#: paperless/settings.py:785
#: paperless/settings.py:797
msgid "Portuguese (Brazil)"
msgstr ""
#: paperless/settings.py:786
#: paperless/settings.py:798
msgid "Portuguese"
msgstr ""
#: paperless/settings.py:787
#: paperless/settings.py:799
msgid "Romanian"
msgstr ""
#: paperless/settings.py:788
#: paperless/settings.py:800
msgid "Russian"
msgstr ""
#: paperless/settings.py:789
#: paperless/settings.py:801
msgid "Slovak"
msgstr ""
#: paperless/settings.py:790
#: paperless/settings.py:802
msgid "Slovenian"
msgstr ""
#: paperless/settings.py:791
#: paperless/settings.py:803
msgid "Serbian"
msgstr ""
#: paperless/settings.py:792
#: paperless/settings.py:804
msgid "Swedish"
msgstr ""
#: paperless/settings.py:793
#: paperless/settings.py:805
msgid "Turkish"
msgstr ""
#: paperless/settings.py:794
#: paperless/settings.py:806
msgid "Ukrainian"
msgstr ""
#: paperless/settings.py:795
#: paperless/settings.py:807
msgid "Vietnamese"
msgstr ""
#: paperless/settings.py:796
#: paperless/settings.py:808
msgid "Chinese Simplified"
msgstr ""
#: paperless/settings.py:797
#: paperless/settings.py:809
msgid "Chinese Traditional"
msgstr ""

View File

@@ -703,6 +703,9 @@ def _parse_db_settings() -> dict:
# Leave room for future extensibility
if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
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 = {
"read_default_file": "/etc/mysql/my.cnf",
"charset": "utf8mb4",
@@ -722,6 +725,15 @@ def _parse_db_settings() -> dict:
"sslcert": os.getenv("PAPERLESS_DBSSLCERT", 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"]["OPTIONS"].update(options)

19
uv.lock generated
View File

@@ -1971,10 +1971,11 @@ mariadb = [
{ name = "mysqlclient", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
postgres = [
{ name = "psycopg", extra = ["c"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "psycopg", extra = ["c", "pool"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "psycopg-c", version = "3.2.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" },
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'" },
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
{ name = "psycopg-pool", version = "3.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'darwin'" },
]
webserver = [
{ name = "granian", extra = ["uvloop"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -2079,10 +2080,11 @@ requires-dist = [
{ name = "ocrmypdf", specifier = "~=16.10.0" },
{ name = "pathvalidate", specifier = "~=3.3.1" },
{ name = "pdf2image", specifier = "~=1.17.0" },
{ name = "psycopg", extras = ["c"], marker = "extra == 'postgres'", specifier = "==3.2.9" },
{ name = "psycopg", extras = ["c", "pool"], marker = "extra == 'postgres'", specifier = "==3.2.9" },
{ name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" },
{ name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" },
{ name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.2.9" },
{ name = "psycopg-pool", marker = "extra == 'postgres'" },
{ name = "python-dateutil", specifier = "~=2.9.0" },
{ name = "python-dotenv", specifier = "~=1.1.0" },
{ name = "python-gnupg", specifier = "~=0.5.4" },
@@ -2433,6 +2435,9 @@ c = [
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'aarch64' and sys_platform == 'linux'" },
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
]
pool = [
{ name = "psycopg-pool", version = "3.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'darwin'" },
]
[[package]]
name = "psycopg-c"
@@ -2466,6 +2471,16 @@ wheels = [
{ url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl", hash = "sha256:250c357319242da102047b04c5cc78af872dbf85c2cb05abf114e1fb5f207917" },
]
[[package]]
name = "psycopg-pool"
version = "3.2.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770, upload-time = "2025-02-26T12:03:47.129Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"