Compare commits

..

10 Commits

Author SHA1 Message Date
dependabot[bot]
b06c0a0eba docker(deps): Bump astral-sh/uv from 0.6.16-python3.12-bookworm-slim to 0.7.8-python3.12-bookworm-slim (#10056)
* docker(deps): Bump astral-sh/uv

Bumps [astral-sh/uv](https://github.com/astral-sh/uv) from 0.6.16-python3.12-bookworm-slim to 0.7.8-python3.12-bookworm-slim.
- [Release notes](https://github.com/astral-sh/uv/releases)
- [Changelog](https://github.com/astral-sh/uv/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/uv/compare/0.6.16...0.7.8)

---
updated-dependencies:
- dependency-name: astral-sh/uv
  dependency-version: 0.7.8-python3.12-bookworm-slim
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

* Upgrades the CI workflow to also use 0.7.x

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Trenton H <797416+stumpylog@users.noreply.github.com>
2025-05-29 11:17:15 -07:00
matthesrieke
e9746aa0e3 Enhancement: include DOCUMENT_TYPE to post consume scripts (#9977)
* expose DOCUMENT_TYPE to post consume scripts

* Apply suggestions from code review

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-05-28 23:32:59 +00:00
Freilichtbühne
bfaab21589 Fix: Add exception to utime in copy_basic_file_stats (#10070) 2025-05-28 15:13:03 -07:00
GitHub Actions
3849569bd1 Auto translate strings 2025-05-27 23:01:05 +00:00
shamoon
c40a7751b9 Fix: include base href when opening global search result in new window (#10066) 2025-05-27 15:59:17 -07:00
shamoon
f39463ff4e Add a more helpful docstring to schedule logic, scheduled test 2025-05-27 13:05:42 -07:00
shamoon
2ada8ec681 Chore: silence migration result if no docs 2025-05-26 10:31:37 -07:00
github-actions[bot]
4c6fdbb21f Documentation: Add v2.16.2 changelog (#10029)
* Changelog v2.16.2 - 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>
2025-05-24 11:57:09 -07:00
shamoon
889c4378a9 Merge branch 'dev' 2025-05-24 11:55:16 -07:00
shamoon
06dd039083 Revert "Chore: remove invalid branches-ignores"
This reverts commit 28a1b9d1ac.
2025-05-24 11:46:47 -07:00
14 changed files with 136 additions and 16 deletions

View File

@@ -6,9 +6,13 @@ on:
- 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+'
# https://semver.org/#spec-item-9 # https://semver.org/#spec-item-9
- 'v[0-9]+.[0-9]+.[0-9]+-beta.rc[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-beta.rc[0-9]+'
branches-ignore:
- 'translations**'
pull_request: pull_request:
branches-ignore:
- 'translations**'
env: env:
DEFAULT_UV_VERSION: "0.6.x" DEFAULT_UV_VERSION: "0.7.x"
# This is the default version of Python to use in most steps which aren't specific # This is the default version of Python to use in most steps which aren't specific
DEFAULT_PYTHON_VERSION: "3.11" DEFAULT_PYTHON_VERSION: "3.11"
jobs: jobs:

View File

@@ -32,7 +32,7 @@ RUN set -eux \
# Purpose: Installs s6-overlay and rootfs # Purpose: Installs s6-overlay and rootfs
# Comments: # Comments:
# - Don't leave anything extra in here either # - Don't leave anything extra in here either
FROM ghcr.io/astral-sh/uv:0.6.16-python3.12-bookworm-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.7.8-python3.12-bookworm-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6

View File

@@ -179,6 +179,7 @@ variables:
| ---------------------------- | ---------------------------------------------- | | ---------------------------- | ---------------------------------------------- |
| `DOCUMENT_ID` | Database primary key of the document | | `DOCUMENT_ID` | Database primary key of the document |
| `DOCUMENT_FILE_NAME` | Formatted filename, not including paths | | `DOCUMENT_FILE_NAME` | Formatted filename, not including paths |
| `DOCUMENT_TYPE` | The document type (if any) |
| `DOCUMENT_CREATED` | Date & time when document created | | `DOCUMENT_CREATED` | Date & time when document created |
| `DOCUMENT_MODIFIED` | Date & time when document was last modified | | `DOCUMENT_MODIFIED` | Date & time when document was last modified |
| `DOCUMENT_ADDED` | Date & time when document was added | | `DOCUMENT_ADDED` | Date & time when document was added |

View File

@@ -1,5 +1,30 @@
# Changelog # Changelog
## paperless-ngx 2.16.2
### Bug Fixes
- Fix: accept datetime for created [@shamoon](https://github.com/shamoon) ([#10021](https://github.com/paperless-ngx/paperless-ngx/pull/10021))
- Fix: created date fixes in v2.16 [@shamoon](https://github.com/shamoon) ([#10026](https://github.com/paperless-ngx/paperless-ngx/pull/10026))
- Fix: mark fields for created objects as dirty [@shamoon](https://github.com/shamoon) ([#10022](https://github.com/paperless-ngx/paperless-ngx/pull/10022))
- Fix: add fallback to copyfile on PermissionError @samuel-kosmann ([#10023](https://github.com/paperless-ngx/paperless-ngx/pull/10023))
### Dependencies
- Chore: warn users about removal of postgres v13 support [@shamoon](https://github.com/shamoon) ([#9980](https://github.com/paperless-ngx/paperless-ngx/pull/9980))
### All App Changes
<details>
<summary>5 changes</summary>
- Fix: accept datetime for created [@shamoon](https://github.com/shamoon) ([#10021](https://github.com/paperless-ngx/paperless-ngx/pull/10021))
- Fix: add fallback to copyfile on PermissionError @samuel-kosmann ([#10023](https://github.com/paperless-ngx/paperless-ngx/pull/10023))
- Fix: created date fixes in v2.16 [@shamoon](https://github.com/shamoon) ([#10026](https://github.com/paperless-ngx/paperless-ngx/pull/10026))
- Fix: mark fields for created objects as dirty [@shamoon](https://github.com/shamoon) ([#10022](https://github.com/paperless-ngx/paperless-ngx/pull/10022))
- Chore: warn users about removal of postgres v13 support [@shamoon](https://github.com/shamoon) ([#9980](https://github.com/paperless-ngx/paperless-ngx/pull/9980))
</details>
## paperless-ngx 2.16.1 ## paperless-ngx 2.16.1
### Bug Fixes ### Bug Fixes

View File

@@ -6,6 +6,7 @@ A document with an id of ${DOCUMENT_ID} was just consumed. I know the
following additional information about it: following additional information about it:
* Generated File Name: ${DOCUMENT_FILE_NAME} * Generated File Name: ${DOCUMENT_FILE_NAME}
* Document type: ${DOCUMENT_TYPE}
* Archive Path: ${DOCUMENT_ARCHIVE_PATH} * Archive Path: ${DOCUMENT_ARCHIVE_PATH}
* Source Path: ${DOCUMENT_SOURCE_PATH} * Source Path: ${DOCUMENT_SOURCE_PATH}
* Created: ${DOCUMENT_CREATED} * Created: ${DOCUMENT_CREATED}

View File

@@ -1084,7 +1084,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">121</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2818183879511244335" datatype="html"> <trans-unit id="2818183879511244335" datatype="html">
@@ -3092,22 +3092,22 @@
<source>Successfully updated object.</source> <source>Successfully updated object.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">210</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">247</context> <context context-type="linenumber">248</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1801333259018423190" datatype="html"> <trans-unit id="1801333259018423190" datatype="html">
<source>Error occurred saving object.</source> <source>Error occurred saving object.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">212</context> <context context-type="linenumber">213</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context> <context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.ts</context>
<context context-type="linenumber">250</context> <context context-type="linenumber">251</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8193912662253833654" datatype="html"> <trans-unit id="8193912662253833654" datatype="html">

View File

@@ -529,6 +529,17 @@ describe('GlobalSearchComponent', () => {
expect(dispatchSpy).toHaveBeenCalledTimes(2) // once for keydown, second for click expect(dispatchSpy).toHaveBeenCalledTimes(2) // once for keydown, second for click
}) })
it('should support using base href in navigateOrOpenInNewWindow', () => {
jest
.spyOn(component['locationStrategy'], 'getBaseHref')
.mockReturnValue('/base/')
const openSpy = jest.spyOn(window, 'open')
const event = new Event('click')
event['ctrlKey'] = true
component.primaryAction(DataType.Document, { id: 1 }, event as any)
expect(openSpy).toHaveBeenCalledWith('/base/documents/1', '_blank')
})
it('should support title content search and advanced search', () => { it('should support title content search and advanced search', () => {
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter') const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
component.query = 'test' component.query = 'test'

View File

@@ -1,4 +1,4 @@
import { NgTemplateOutlet } from '@angular/common' import { LocationStrategy, NgTemplateOutlet } from '@angular/common'
import { import {
Component, Component,
ElementRef, ElementRef,
@@ -99,7 +99,8 @@ export class GlobalSearchComponent implements OnInit {
private permissionsService: PermissionsService, private permissionsService: PermissionsService,
private toastService: ToastService, private toastService: ToastService,
private hotkeyService: HotKeyService, private hotkeyService: HotKeyService,
private settingsService: SettingsService private settingsService: SettingsService,
private locationStrategy: LocationStrategy
) { ) {
this.queryDebounce = new Subject<string>() this.queryDebounce = new Subject<string>()
@@ -421,10 +422,13 @@ export class GlobalSearchComponent implements OnInit {
extras: Object = {} extras: Object = {}
) { ) {
if (newWindow) { if (newWindow) {
const url = this.router.serializeUrl( const serializedUrl = this.router.serializeUrl(
this.router.createUrlTree(commands, extras) this.router.createUrlTree(commands, extras)
) )
window.open(url, '_blank') const baseHref = this.locationStrategy.getBaseHref()
const fullUrl =
baseHref.replace(/\/+$/, '') + '/' + serializedUrl.replace(/^\/+/, '')
window.open(fullUrl, '_blank')
} else { } else {
this.router.navigate(commands, extras) this.router.navigate(commands, extras)
} }

View File

@@ -303,6 +303,7 @@ class ConsumerPlugin(
script_env = os.environ.copy() script_env = os.environ.copy()
script_env["DOCUMENT_ID"] = str(document.pk) script_env["DOCUMENT_ID"] = str(document.pk)
script_env["DOCUMENT_TYPE"] = str(document.document_type)
script_env["DOCUMENT_CREATED"] = str(document.created) script_env["DOCUMENT_CREATED"] = str(document.created)
script_env["DOCUMENT_MODIFIED"] = str(document.modified) script_env["DOCUMENT_MODIFIED"] = str(document.modified)
script_env["DOCUMENT_ADDED"] = str(document.added) script_env["DOCUMENT_ADDED"] = str(document.added)

View File

@@ -39,7 +39,8 @@ def migrate_date(apps, schema_editor):
f"[1067_alter_document_created] {total_updated} of {total_checked} processed...", f"[1067_alter_document_created] {total_updated} of {total_checked} processed...",
) )
print(f"[1067_alter_document_created] completed for {total_checked} documents.") if total_checked > 0:
print(f"[1067_alter_document_created] completed for {total_checked} documents.")
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@@ -387,6 +387,18 @@ def empty_trash(doc_ids=None):
@shared_task @shared_task
def check_scheduled_workflows(): def check_scheduled_workflows():
"""
Check and run all enabled scheduled workflows.
Scheduled triggers are evaluated based on a target date field (e.g. added, created, modified, or a custom date field),
combined with a day offset.
The offset is mathematically negated resulting in the following behavior:
- Positive offsets mean the workflow should trigger BEFORE the specified date (e.g., offset = +7 → trigger 7 days before)
- Negative offsets mean the workflow should trigger AFTER the specified date (e.g., offset = -7 → trigger 7 days after)
Once a document satisfies this condition, and recurring/non-recurring constraints are met, the workflow is run.
"""
scheduled_workflows: list[Workflow] = ( scheduled_workflows: list[Workflow] = (
Workflow.objects.filter( Workflow.objects.filter(
triggers__type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, triggers__type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,

View File

@@ -1174,12 +1174,16 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
m.assert_called_once() m.assert_called_once()
@mock.patch("documents.consumer.run_subprocess") @mock.patch("documents.consumer.run_subprocess")
def test_post_consume_script_with_correspondent(self, m): def test_post_consume_script_with_correspondent_and_type(self, m):
with tempfile.NamedTemporaryFile() as script: with tempfile.NamedTemporaryFile() as script:
with override_settings(POST_CONSUME_SCRIPT=script.name): with override_settings(POST_CONSUME_SCRIPT=script.name):
c = Correspondent.objects.create(name="my_bank") c = Correspondent.objects.create(name="my_bank")
t = DocumentType.objects.create(
name="Test type",
)
doc = Document.objects.create( doc = Document.objects.create(
title="Test", title="Test",
document_type=t,
mime_type="application/pdf", mime_type="application/pdf",
correspondent=c, correspondent=c,
) )
@@ -1207,6 +1211,7 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
subset = { subset = {
"DOCUMENT_ID": str(doc.pk), "DOCUMENT_ID": str(doc.pk),
"DOCUMENT_TYPE": "Test type",
"DOCUMENT_DOWNLOAD_URL": f"/api/documents/{doc.pk}/download/", "DOCUMENT_DOWNLOAD_URL": f"/api/documents/{doc.pk}/download/",
"DOCUMENT_THUMBNAIL_URL": f"/api/documents/{doc.pk}/thumb/", "DOCUMENT_THUMBNAIL_URL": f"/api/documents/{doc.pk}/thumb/",
"DOCUMENT_CORRESPONDENT": "my_bank", "DOCUMENT_CORRESPONDENT": "my_bank",

View File

@@ -1610,7 +1610,7 @@ class TestWorkflows(
doc.refresh_from_db() doc.refresh_from_db()
self.assertIsNone(doc.owner) self.assertIsNone(doc.owner)
def test_workflow_scheduled_trigger_negative_offset(self): def test_workflow_scheduled_trigger_negative_offset_customfield(self):
""" """
GIVEN: GIVEN:
- Existing workflow with SCHEDULED trigger and negative offset of -7 days (so 7 days after date) - Existing workflow with SCHEDULED trigger and negative offset of -7 days (so 7 days after date)
@@ -1662,6 +1662,55 @@ class TestWorkflows(
doc.refresh_from_db() doc.refresh_from_db()
self.assertEqual(doc.owner, self.user2) self.assertEqual(doc.owner, self.user2)
def test_workflow_scheduled_trigger_negative_offset_created(self):
"""
GIVEN:
- Existing workflow with SCHEDULED trigger and negative offset of -7 days (so 7 days after date)
- Created date set to 8 days ago → trigger time = 1 day ago and 5 days ago
WHEN:
- Scheduled workflows are checked for document
THEN:
- Workflow runs and document owner is updated for the first document, not the second
"""
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
schedule_offset_days=-7,
schedule_date_field=WorkflowTrigger.ScheduleDateField.CREATED,
)
action = WorkflowAction.objects.create(
assign_title="Doc assign owner",
assign_owner=self.user2,
)
w = Workflow.objects.create(
name="Workflow 1",
order=0,
)
w.triggers.add(trigger)
w.actions.add(action)
w.save()
doc = Document.objects.create(
title="sample test",
correspondent=self.c,
original_filename="sample.pdf",
checksum="1",
created=timezone.now().date() - timedelta(days=8),
)
doc2 = Document.objects.create(
title="sample test 2",
correspondent=self.c,
original_filename="sample2.pdf",
checksum="2",
created=timezone.now().date() - timedelta(days=5),
)
tasks.check_scheduled_workflows()
doc.refresh_from_db()
self.assertEqual(doc.owner, self.user2)
doc2.refresh_from_db()
self.assertIsNone(doc2.owner) # has not triggered yet
def test_workflow_enabled_disabled(self): def test_workflow_enabled_disabled(self):
trigger = WorkflowTrigger.objects.create( trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED, type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,

View File

@@ -23,11 +23,17 @@ def copy_basic_file_stats(source: Path | str, dest: Path | str) -> None:
The extended attribute copy does weird things with SELinux and files The extended attribute copy does weird things with SELinux and files
copied from temporary directories and copystat doesn't allow disabling copied from temporary directories and copystat doesn't allow disabling
these copies these copies.
If there is a PermissionError, skip copying file stats.
""" """
source, dest = _coerce_to_path(source, dest) source, dest = _coerce_to_path(source, dest)
src_stat = source.stat() src_stat = source.stat()
utime(dest, ns=(src_stat.st_atime_ns, src_stat.st_mtime_ns))
try:
utime(dest, ns=(src_stat.st_atime_ns, src_stat.st_mtime_ns))
except PermissionError:
pass
def copy_file_with_basic_stats( def copy_file_with_basic_stats(