Compare commits

..

1 Commits

Author SHA1 Message Date
shamoon
7710848a25 Enhancement: doc_id placeholder support in workflow templates 2026-01-15 19:49:46 -08:00
15 changed files with 452 additions and 585 deletions

View File

@@ -35,7 +35,7 @@ jobs:
contents: read contents: read
packages: write packages: write
outputs: outputs:
should-push: ${{ steps.check-push.outputs.should-push }} can-push: ${{ steps.check-push.outputs.can-push }}
push-external: ${{ steps.check-push.outputs.push-external }} push-external: ${{ steps.check-push.outputs.push-external }}
repository: ${{ steps.repo.outputs.name }} repository: ${{ steps.repo.outputs.name }}
ref-name: ${{ steps.ref.outputs.name }} ref-name: ${{ steps.ref.outputs.name }}
@@ -59,28 +59,16 @@ jobs:
env: env:
REF_NAME: ${{ steps.ref.outputs.name }} REF_NAME: ${{ steps.ref.outputs.name }}
run: | run: |
# should-push: Should we push to GHCR? # can-push: Can we push to GHCR?
# True for: # True for: pushes, or PRs from the same repo (not forks)
# 1. Pushes (tags/dev/beta) - filtered via the workflow triggers can_push=${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
# 2. Internal PRs where the branch name starts with 'feature-' - filtered here when a PR is synced echo "can-push=${can_push}"
echo "can-push=${can_push}" >> $GITHUB_OUTPUT
should_push="false"
if [[ "${{ github.event_name }}" == "push" ]]; then
should_push="true"
elif [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" == "${{ github.repository }}" ]]; then
if [[ "${REF_NAME}" == feature-* || "${REF_NAME}" == fix-* ]]; then
should_push="true"
fi
fi
echo "should-push=${should_push}"
echo "should-push=${should_push}" >> $GITHUB_OUTPUT
# push-external: Should we also push to Docker Hub and Quay.io? # push-external: Should we also push to Docker Hub and Quay.io?
# Only for main repo on dev/beta branches or version tags # Only for main repo on dev/beta branches or version tags
push_external="false" push_external="false"
if [[ "${should_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then if [[ "${can_push}" == "true" && "${{ github.repository_owner }}" == "paperless-ngx" ]]; then
case "${REF_NAME}" in case "${REF_NAME}" in
dev|beta) dev|beta)
push_external="true" push_external="true"
@@ -137,20 +125,20 @@ jobs:
labels: ${{ steps.docker-meta.outputs.labels }} labels: ${{ steps.docker-meta.outputs.labels }}
build-args: | build-args: |
PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }} PNGX_TAG_VERSION=${{ steps.docker-meta.outputs.version }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.should-push }} outputs: type=image,name=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }},push-by-digest=true,name-canonical=true,push=${{ steps.check-push.outputs.can-push }}
cache-from: | cache-from: |
type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.cache-ref }}-${{ matrix.arch }} type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:${{ steps.ref.outputs.cache-ref }}-${{ matrix.arch }}
type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }} type=registry,ref=${{ env.REGISTRY }}/${{ steps.repo.outputs.name }}/cache/app:dev-${{ matrix.arch }}
cache-to: ${{ steps.check-push.outputs.should-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }} cache-to: ${{ steps.check-push.outputs.can-push == 'true' && format('type=registry,mode=max,ref={0}/{1}/cache/app:{2}-{3}', env.REGISTRY, steps.repo.outputs.name, steps.ref.outputs.cache-ref, matrix.arch) || '' }}
- name: Export digest - name: Export digest
if: steps.check-push.outputs.should-push == 'true' if: steps.check-push.outputs.can-push == 'true'
run: | run: |
mkdir -p /tmp/digests mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}" digest="${{ steps.build.outputs.digest }}"
echo "digest=${digest}" echo "digest=${digest}"
touch "/tmp/digests/${digest#sha256:}" touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest - name: Upload digest
if: steps.check-push.outputs.should-push == 'true' if: steps.check-push.outputs.can-push == 'true'
uses: actions/upload-artifact@v6.0.0 uses: actions/upload-artifact@v6.0.0
with: with:
name: digests-${{ matrix.arch }} name: digests-${{ matrix.arch }}
@@ -161,7 +149,7 @@ jobs:
name: Merge and Push Manifest name: Merge and Push Manifest
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: build-arch needs: build-arch
if: needs.build-arch.outputs.should-push == 'true' if: needs.build-arch.outputs.can-push == 'true'
permissions: permissions:
contents: read contents: read
packages: write packages: write

View File

@@ -30,7 +30,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.9.26-python3.12-trixie-slim AS s6-overlay-base FROM ghcr.io/astral-sh/uv:0.9.15-python3.12-trixie-slim AS s6-overlay-base
WORKDIR /usr/src/s6 WORKDIR /usr/src/s6

View File

@@ -597,6 +597,7 @@ The following placeholders are only available for "added" or "updated" triggers
- `{{created_day}}`: created day - `{{created_day}}`: created day
- `{{created_time}}`: created time in HH:MM format - `{{created_time}}`: created time in HH:MM format
- `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set. - `{{doc_url}}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
- `{{doc_id}}`: Document ID
##### Examples ##### Examples

View File

@@ -28,7 +28,7 @@ dependencies = [
# Only patch versions are guaranteed to not introduce breaking changes. # Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.5", "django~=5.2.5",
"django-allauth[mfa,socialaccount]~=65.12.1", "django-allauth[mfa,socialaccount]~=65.12.1",
"django-auditlog~=3.4.1", "django-auditlog~=3.3.0",
"django-cachalot~=2.8.0", "django-cachalot~=2.8.0",
"django-celery-results~=2.6.0", "django-celery-results~=2.6.0",
"django-compression-middleware~=0.5.0", "django-compression-middleware~=0.5.0",
@@ -47,20 +47,20 @@ dependencies = [
"faiss-cpu>=1.10", "faiss-cpu>=1.10",
"filelock~=3.20.0", "filelock~=3.20.0",
"flower~=2.0.1", "flower~=2.0.1",
"gotenberg-client~=0.13.1", "gotenberg-client~=0.12.0",
"httpx-oauth~=0.16", "httpx-oauth~=0.16",
"imap-tools~=1.11.0", "imap-tools~=1.11.0",
"inotifyrecursive~=0.3", "inotifyrecursive~=0.3",
"jinja2~=3.1.5", "jinja2~=3.1.5",
"langdetect~=1.0.9", "langdetect~=1.0.9",
"llama-index-core>=0.14.12", "llama-index-core>=0.12.33.post1",
"llama-index-embeddings-huggingface>=0.6.1", "llama-index-embeddings-huggingface>=0.5.3",
"llama-index-embeddings-openai>=0.5.1", "llama-index-embeddings-openai>=0.3.1",
"llama-index-llms-ollama>=0.9.1", "llama-index-llms-ollama>=0.5.4",
"llama-index-llms-openai>=0.6.13", "llama-index-llms-openai>=0.3.38",
"llama-index-vector-stores-faiss>=0.5.2", "llama-index-vector-stores-faiss>=0.3",
"nltk~=3.9.1", "nltk~=3.9.1",
"ocrmypdf~=16.13.0", "ocrmypdf~=16.12.0",
"openai>=1.76", "openai>=1.76",
"pathvalidate~=3.3.1", "pathvalidate~=3.3.1",
"pdf2image~=1.17.0", "pdf2image~=1.17.0",
@@ -77,7 +77,7 @@ dependencies = [
"sentence-transformers>=4.1", "sentence-transformers>=4.1",
"setproctitle~=1.3.4", "setproctitle~=1.3.4",
"tika-client~=0.10.0", "tika-client~=0.10.0",
"torch~=2.9.1", "torch~=2.7.0",
"tqdm~=4.67.1", "tqdm~=4.67.1",
"watchdog~=6.0", "watchdog~=6.0",
"whitenoise~=6.9", "whitenoise~=6.9",
@@ -92,7 +92,7 @@ optional-dependencies.postgres = [
"psycopg[c,pool]==3.2.12", "psycopg[c,pool]==3.2.12",
# Direct dependency for proper resolution of the pre-built wheels # Direct dependency for proper resolution of the pre-built wheels
"psycopg-c==3.2.12", "psycopg-c==3.2.12",
"psycopg-pool==3.3", "psycopg-pool==3.2.7",
] ]
optional-dependencies.webserver = [ optional-dependencies.webserver = [
"granian[uvloop]~=2.5.1", "granian[uvloop]~=2.5.1",
@@ -127,7 +127,7 @@ testing = [
] ]
lint = [ lint = [
"pre-commit~=4.5.1", "pre-commit~=4.4.0",
"pre-commit-uv~=4.2.0", "pre-commit-uv~=4.2.0",
"ruff~=0.14.0", "ruff~=0.14.0",
] ]

View File

@@ -28,7 +28,7 @@
</button> </button>
</ng-template> </ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm"> <ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div class="tag-option-row d-flex align-items-center" [class.w-auto]="!getTag(item.id)?.parent"> <div class="tag-option-row d-flex align-items-center">
@if (item.id && tags) { @if (item.id && tags) {
@if (getTag(item.id)?.parent) { @if (getTag(item.id)?.parent) {
<i-bs name="list-nested" class="me-1"></i-bs> <i-bs name="list-nested" class="me-1"></i-bs>

View File

@@ -23,7 +23,7 @@
// Dropdown hierarchy reveal for ng-select options // Dropdown hierarchy reveal for ng-select options
::ng-deep .ng-dropdown-panel .ng-option { ::ng-deep .ng-dropdown-panel .ng-option {
overflow-x: scroll !important; overflow-x: scroll;
.tag-option-row { .tag-option-row {
font-size: 1rem; font-size: 1rem;

View File

@@ -285,10 +285,10 @@ export class DocumentDetailComponent
if ( if (
element && element &&
element.nativeElement.offsetParent !== null && element.nativeElement.offsetParent !== null &&
this.nav?.activeId == DocumentDetailNavIDs.Preview this.nav?.activeId == 4
) { ) {
// its visible // its visible
setTimeout(() => this.nav?.select(DocumentDetailNavIDs.Details)) setTimeout(() => this.nav?.select(1))
} }
} }

View File

@@ -6,7 +6,7 @@ from django.db import models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("documents", "1075_workflowaction_order"), ("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"),
] ]
operations = [ operations = [

View File

@@ -12,7 +12,7 @@ def populate_action_order(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("documents", "1074_workflowrun_deleted_at_workflowrun_restored_at_and_more"), ("documents", "1075_alter_paperlesstask_task_name"),
] ]
operations = [ operations = [

View File

@@ -40,6 +40,7 @@ def parse_w_workflow_placeholders(
created: date | None = None, created: date | None = None,
doc_title: str | None = None, doc_title: str | None = None,
doc_url: str | None = None, doc_url: str | None = None,
doc_id: int | None = None,
) -> str: ) -> str:
""" """
Available title placeholders for Workflows depend on what has already been assigned, Available title placeholders for Workflows depend on what has already been assigned,
@@ -79,6 +80,8 @@ def parse_w_workflow_placeholders(
formatting.update({"doc_title": doc_title}) formatting.update({"doc_title": doc_title})
if doc_url is not None: if doc_url is not None:
formatting.update({"doc_url": doc_url}) formatting.update({"doc_url": doc_url})
if doc_id is not None:
formatting.update({"doc_id": str(doc_id)})
logger.debug(f"Parsing Workflow Jinja template: {text}") logger.debug(f"Parsing Workflow Jinja template: {text}")
try: try:

View File

@@ -3298,7 +3298,7 @@ class TestWorkflows(
) )
webhook_action = WorkflowActionWebhook.objects.create( webhook_action = WorkflowActionWebhook.objects.create(
use_params=False, use_params=False,
body="Test message: {{doc_url}}", body="Test message: {{doc_url}} with id {{doc_id}}",
url="http://paperless-ngx.com", url="http://paperless-ngx.com",
include_document=False, include_document=False,
) )
@@ -3328,7 +3328,10 @@ class TestWorkflows(
mock_post.assert_called_once_with( mock_post.assert_called_once_with(
url="http://paperless-ngx.com", url="http://paperless-ngx.com",
data=f"Test message: http://localhost:8000/paperless/documents/{doc.id}/", data=(
f"Test message: http://localhost:8000/paperless/documents/{doc.id}/"
f" with id {doc.id}"
),
headers={}, headers={},
files=None, files=None,
as_json=False, as_json=False,

View File

@@ -44,6 +44,7 @@ def build_workflow_action_context(
"current_filename": document.filename or "", "current_filename": document.filename or "",
"added": timezone.localtime(document.added), "added": timezone.localtime(document.added),
"created": document.created, "created": document.created,
"id": document.pk,
} }
correspondent_obj = ( correspondent_obj = (
@@ -75,6 +76,7 @@ def build_workflow_action_context(
"current_filename": filename, "current_filename": filename,
"added": timezone.localtime(timezone.now()), "added": timezone.localtime(timezone.now()),
"created": overrides.created if overrides else None, "created": overrides.created if overrides else None,
"id": "",
} }
@@ -109,6 +111,7 @@ def execute_email_action(
context["created"], context["created"],
context["title"], context["title"],
context["doc_url"], context["doc_url"],
context["id"],
) )
if action.email.subject if action.email.subject
else "" else ""
@@ -125,6 +128,7 @@ def execute_email_action(
context["created"], context["created"],
context["title"], context["title"],
context["doc_url"], context["doc_url"],
context["id"],
) )
if action.email.body if action.email.body
else "" else ""
@@ -203,6 +207,7 @@ def execute_webhook_action(
context["created"], context["created"],
context["title"], context["title"],
context["doc_url"], context["doc_url"],
context["id"],
) )
except Exception as e: except Exception as e:
logger.error( logger.error(
@@ -221,6 +226,7 @@ def execute_webhook_action(
context["created"], context["created"],
context["title"], context["title"],
context["doc_url"], context["doc_url"],
context["id"],
) )
headers = {} headers = {}
if action.webhook.headers: if action.webhook.headers:

View File

@@ -55,6 +55,9 @@ def apply_assignment_to_document(
document.original_filename or "", document.original_filename or "",
document.filename or "", document.filename or "",
document.created, document.created,
"", # dont pass the title to avoid recursion
"", # no urls in titles
document.pk,
) )
except Exception: # pragma: no cover except Exception: # pragma: no cover
logger.exception( logger.exception(

View File

@@ -11,12 +11,14 @@ from paperless_ai.chat import stream_chat_with_documents
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_embed_model(): def patch_embed_model():
from llama_index.core import settings as llama_settings from llama_index.core import settings as llama_settings
from llama_index.core.embeddings.mock_embed_model import MockEmbedding
# Use a real BaseEmbedding subclass to satisfy llama-index 0.14 validation mock_embed_model = MagicMock()
llama_settings.Settings.embed_model = MockEmbedding(embed_dim=1536) mock_embed_model._get_text_embedding_batch.return_value = [
[0.1] * 1536,
] # 1 vector per input
llama_settings.Settings._embed_model = mock_embed_model
yield yield
llama_settings.Settings.embed_model = None llama_settings.Settings._embed_model = None
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)

933
uv.lock generated

File diff suppressed because it is too large Load Diff