Compare commits

..
Author SHA1 Message Date
github-actions[bot]GitHubgithub-actions <41898282+github-actions[bot]@users.noreply.github.com>shamoon
65fe6aacc7 Documentation: Add v3.0.3 changelog (#13300)
* Changelog v3.0.3 - GHA

* Update changelog.md

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-07-25 18:57:03 -07:00
shamoon b5a23cff2f Bump version to 3.0.3 2026-07-25 16:32:39 -07:00
12 changed files with 55 additions and 98 deletions
+34
View File
@@ -1,5 +1,39 @@
# Changelog
## paperless-ngx 3.0.3
### Bug Fixes
- Fixhancement: PAPERLESS\_ALLAUTH\_TRUSTED\_PROXY\_COUNT [@shamoon](https://github.com/shamoon) ([#13281](https://github.com/paperless-ngx/paperless-ngx/pull/13281))
- Fix: preserve document fields during Gotenberg conversion to PDF [@stumpylog](https://github.com/stumpylog) ([#13271](https://github.com/paperless-ngx/paperless-ngx/pull/13271))
- Fix: Makes the email date aware as soon as possible during parsing [@stumpylog](https://github.com/stumpylog) ([#13266](https://github.com/paperless-ngx/paperless-ngx/pull/13266))
- Fix: Handle a plain string as Celery sometimes provides for the traceback [@stumpylog](https://github.com/stumpylog) ([#13267](https://github.com/paperless-ngx/paperless-ngx/pull/13267))
- Fix: Emit the torch index into the requirements.txt for people still using it [@stumpylog](https://github.com/stumpylog) ([#13265](https://github.com/paperless-ngx/paperless-ngx/pull/13265))
- Fix: handle notes without a user when building the search index [@matthiasmast](https://github.com/matthiasmast) ([#13260](https://github.com/paperless-ngx/paperless-ngx/pull/13260))
### Documentation
- Docs: warn bare-metal users about stale files when upgrading [@stumpylog](https://github.com/stumpylog) ([#13296](https://github.com/paperless-ngx/paperless-ngx/pull/13296))
- Documentation: Add the NumPy CPU baseline increase to the migration guide [@stumpylog](https://github.com/stumpylog) ([#13269](https://github.com/paperless-ngx/paperless-ngx/pull/13269))
### Maintenance
- Fix: Emit the torch index into the requirements.txt for people still using it [@stumpylog](https://github.com/stumpylog) ([#13265](https://github.com/paperless-ngx/paperless-ngx/pull/13265))
### All App Changes
<details>
<summary>6 changes</summary>
- Fixhancement: PAPERLESS\_ALLAUTH\_TRUSTED\_PROXY\_COUNT [@shamoon](https://github.com/shamoon) ([#13281](https://github.com/paperless-ngx/paperless-ngx/pull/13281))
- Tweak: adjust top navbar wrapping when AI chat button visible [@shamoon](https://github.com/shamoon) ([#13280](https://github.com/paperless-ngx/paperless-ngx/pull/13280))
- Fix: preserve document fields during Gotenberg conversion to PDF [@stumpylog](https://github.com/stumpylog) ([#13271](https://github.com/paperless-ngx/paperless-ngx/pull/13271))
- Fix: Makes the email date aware as soon as possible during parsing [@stumpylog](https://github.com/stumpylog) ([#13266](https://github.com/paperless-ngx/paperless-ngx/pull/13266))
- Fix: Handle a plain string as Celery sometimes provides for the traceback [@stumpylog](https://github.com/stumpylog) ([#13267](https://github.com/paperless-ngx/paperless-ngx/pull/13267))
- Fix: handle notes without a user when building the search index [@matthiasmast](https://github.com/matthiasmast) ([#13260](https://github.com/paperless-ngx/paperless-ngx/pull/13260))
</details>
## paperless-ngx 3.0.2
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
version = "3.0.2"
version = "3.0.3"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.11"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
"version": "3.0.2",
"version": "3.0.3",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
@@ -108,18 +108,6 @@ describe('PermissionsService', () => {
actionKey: 'View', // PermissionAction.View
typeKey: 'Document', // PermissionType.Document
})
expect(
permissionsService.getPermissionKeys('view_global_statistics')
).toEqual({
actionKey: 'View', // PermissionAction.View
typeKey: 'GlobalStatistics', // PermissionType.GlobalStatistics
})
expect(
permissionsService.getPermissionKeys('view_system_monitoring')
).toEqual({
actionKey: 'View', // PermissionAction.View
typeKey: 'SystemMonitoring', // PermissionType.SystemMonitoring
})
})
it('correctly checks explicit global permissions', () => {
+10 -7
View File
@@ -110,16 +110,19 @@ export class PermissionsService {
actionKey: string
typeKey: string
} {
const matches = permissionStr.match(/(.+)_/)
let typeKey
let actionKey
const actionIndex = Object.values(PermissionAction).findIndex((action) =>
permissionStr.startsWith(`${action}_`)
)
if (actionIndex > -1) {
const action = Object.values(PermissionAction)[actionIndex]
actionKey = Object.keys(PermissionAction)[actionIndex]
if (matches?.length > 0) {
const action = matches[1]
const actionIndex = Object.values(PermissionAction).indexOf(
action as PermissionAction
)
if (actionIndex > -1) {
actionKey = Object.keys(PermissionAction)[actionIndex]
}
const typeIndex = Object.values(PermissionType).indexOf(
permissionStr.replace(`${action}_`, '%s_') as PermissionType
permissionStr.replace(action, '%s') as PermissionType
)
if (typeIndex > -1) {
typeKey = Object.keys(PermissionType)[typeIndex]
+1 -1
View File
@@ -8,7 +8,7 @@ export const environment = {
apiVersion: '10', // match src/paperless/settings.py
appTitle: DEFAULT_APP_TITLE,
tag: 'prod',
version: '3.0.2',
version: '3.0.3',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',
@@ -172,39 +172,3 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
self.assertIsNotNone(
response.data["settings"]["outlook_oauth_url"],
)
@override_settings(
OAUTH_CALLBACK_BASE_URL="http://localhost:8000",
GMAIL_OAUTH_CLIENT_ID="abc123",
GMAIL_OAUTH_CLIENT_SECRET="def456",
GMAIL_OAUTH_ENABLED=True,
OUTLOOK_OAUTH_CLIENT_ID="ghi789",
OUTLOOK_OAUTH_CLIENT_SECRET="jkl012",
OUTLOOK_OAUTH_ENABLED=True,
)
def test_settings_oauth_state_reused_across_calls(self) -> None:
"""
GIVEN:
- A mail account oauth flow has been started, storing a state in the session
WHEN:
- The ui_settings endpoint is called again before the flow completes
THEN:
- The same oauth state is reused, not replaced with a new one
"""
response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
first_state = self.client.session["oauth_state"]
first_gmail_url = response.data["settings"]["gmail_oauth_url"]
first_outlook_url = response.data["settings"]["outlook_oauth_url"]
response = self.client.get(self.ENDPOINT, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(self.client.session["oauth_state"], first_state)
self.assertEqual(
response.data["settings"]["gmail_oauth_url"],
first_gmail_url,
)
self.assertEqual(
response.data["settings"]["outlook_oauth_url"],
first_outlook_url,
)
+3 -7
View File
@@ -4002,19 +4002,15 @@ class UiSettingsView(GenericAPIView[Any]):
ui_settings["auditlog_enabled"] = settings.AUDIT_LOG_ENABLED
if settings.GMAIL_OAUTH_ENABLED or settings.OUTLOOK_OAUTH_ENABLED:
# Reuse an in-flight state instead of minting a new one on every
# settings fetch, or a concurrent request can invalidate a login
# that's still in progress.
manager = PaperlessMailOAuth2Manager(
state=request.session.get("oauth_state"),
)
request.session["oauth_state"] = manager.state
manager = PaperlessMailOAuth2Manager()
if settings.GMAIL_OAUTH_ENABLED:
ui_settings["gmail_oauth_url"] = manager.get_gmail_authorization_url()
request.session["oauth_state"] = manager.state
if settings.OUTLOOK_OAUTH_ENABLED:
ui_settings["outlook_oauth_url"] = (
manager.get_outlook_authorization_url()
)
request.session["oauth_state"] = manager.state
ui_settings["email_enabled"] = settings.EMAIL_ENABLED
+1 -1
View File
@@ -1,6 +1,6 @@
from typing import Final
__version__: Final[tuple[int, int, int]] = (3, 0, 2)
__version__: Final[tuple[int, int, int]] = (3, 0, 3)
# Version string like X.Y.Z
__full_version_str__: Final[str] = ".".join(map(str, __version__))
# Version string like X.Y
@@ -138,16 +138,6 @@ class TestMailOAuth(
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
)
# State is single-use and was cleared by the callback above, so a new
# flow needs a new state stored in the session.
session = self.client.session
session.update(
{
"oauth_state": "test_state",
},
)
session.save()
# Test Outlook OAuth callback
response = self.client.get(
"/api/oauth/callback/?code=test_code&state=test_state",
@@ -190,16 +180,6 @@ class TestMailOAuth(
MailAccount.objects.filter(imap_server="imap.gmail.com").exists(),
)
# State is single-use and was cleared by the callback above, so a
# new flow needs a new state stored in the session.
session = self.client.session
session.update(
{
"oauth_state": "test_state",
},
)
session.save()
# Test Outlook OAuth callback
response = self.client.get(
"/api/oauth/callback/?code=test_code&state=test_state",
+3 -11
View File
@@ -4,7 +4,6 @@ from datetime import timedelta
from http import HTTPStatus
from typing import Any
from django.conf import settings
from django.http import HttpResponseBadRequest
from django.http import HttpResponseForbidden
from django.http import HttpResponseRedirect
@@ -257,14 +256,9 @@ class OauthCallbackView(GenericAPIView[Any]):
)
return HttpResponseBadRequest("Invalid request, see logs for more detail")
session_state = request.session.get("oauth_state")
if not session_state and not settings.DEBUG:
logger.error(
"Invalid oauth callback request: no state in session",
)
return HttpResponseBadRequest("Invalid request, see logs for more detail")
oauth_manager = PaperlessMailOAuth2Manager(state=session_state)
oauth_manager = PaperlessMailOAuth2Manager(
state=request.session.get("oauth_state"),
)
state = request.query_params.get("state", "")
if not oauth_manager.validate_state(state):
@@ -321,5 +315,3 @@ class OauthCallbackView(GenericAPIView[Any]):
return HttpResponseRedirect(
f"{oauth_manager.oauth_redirect_url}?oauth_success=0",
)
finally:
request.session.pop("oauth_state", None)
Generated
+1 -1
View File
@@ -2880,7 +2880,7 @@ wheels = [
[[package]]
name = "paperless-ngx"
version = "3.0.2"
version = "3.0.3"
source = { virtual = "." }
dependencies = [
{ name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },