Support custom fields in workflow title templates

This commit is contained in:
shamoon committed 2026-10-26 21:02:00 -07:00
1 parent 7d8147c21d
commit 7f7c7cc3c7
5 files changed
+94 -27

No files matched your search

+1
View File
@@ -726,6 +726,7 @@ applied. You can use the following placeholders in the template with any trigger
- `{{added_time}}`: added time in HH:MM format
- `{{original_filename}}`: original file name without extension
- `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`)
- `{{custom_fields}}` (title assignments only): Custom field names mapped to their type and value, as in [file name templates](advanced_usage.md#additional-variables). For example, `{{ custom_fields.Invoice.value }}` or `{{ custom_fields | get_cf_value("Invoice", "") }}` for a fallback when the field is missing. In title assignments, fields assigned by the same action are available.
- `{{doc_title}}`: current document title (cannot be used in title assignment)
The following placeholders are only available for "added" or "updated" triggers
+13
View File
@@ -847,6 +847,19 @@ class ConsumerPlugin(
local_added,
self.filename,
self.filename,
custom_fields=[
CustomFieldInstance(
field=field,
**{
CustomFieldInstance.get_value_field_name(
field.data_type,
): self.metadata.custom_fields[field.pk],
},
)
for field in CustomField.objects.filter(
pk__in=self.metadata.custom_fields or {},
)
],
)
def _store(
+6
View File
@@ -1,4 +1,5 @@
import logging
from collections.abc import Iterable
from datetime import date
from datetime import datetime
from pathlib import Path
@@ -13,7 +14,9 @@ from jinja2 import make_logging_undefined
from jinja2.meta import find_undeclared_variables
from jinja2.sandbox import SecurityError
from documents.models import CustomFieldInstance
from documents.templating.environment import _template_environment
from documents.templating.filepath import get_custom_fields_context
from documents.templating.filters import format_datetime
from documents.templating.filters import localize_date
@@ -56,6 +59,7 @@ _known_placeholder_names = {
"doc_title",
"doc_url",
"doc_id",
"custom_fields",
}
@@ -86,6 +90,7 @@ def parse_w_workflow_placeholders(
doc_title: str | None = None,
doc_url: str | None = None,
doc_id: int | None = None,
custom_fields: Iterable[CustomFieldInstance] = (),
) -> str:
"""
Available title placeholders for Workflows depend on what has already been assigned,
@@ -108,6 +113,7 @@ def parse_w_workflow_placeholders(
"original_filename": Path(original_filename).stem,
"filename": Path(filename).stem,
}
formatting.update(get_custom_fields_context(custom_fields))
if created is not None:
formatting.update(
{
+44
View File
@@ -1846,6 +1846,50 @@ class TestWorkflows(
expected_str = f"Document storage path {doc.storage_path} does not match {trigger.filter_has_storage_path}"
self.assertIn(expected_str, cm.output[1])
def test_title_custom_fields(self) -> None:
from documents.consumer import ConsumerPlugin
from documents.templating.workflows import validate_workflow_template
field = CustomField.objects.create(
name="Invoice",
data_type=CustomField.FieldDataType.STRING,
)
template = '{{ custom_fields.Invoice.value }} {{ custom_fields | get_cf_value("Missing", "fallback") }}'
validate_workflow_template(template)
consumer = mock.Mock(
filename="invoice.pdf",
metadata=DocumentMetadataOverrides(custom_fields={field.pk: "INV-123"}),
)
self.assertEqual(
ConsumerPlugin._parse_title_placeholders(consumer, template),
"INV-123 fallback",
)
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
)
action = WorkflowAction.objects.create(assign_title=template)
doc = Document.objects.create(title="Original")
action.assign_custom_fields.add(field)
action.assign_custom_fields_values = {str(field.pk): "INV-123"}
action.save()
workflow = Workflow.objects.create(name="Custom field title", order=0)
workflow.triggers.add(trigger)
workflow.actions.add(action)
document_consumption_finished.send(sender=self.__class__, document=doc)
doc.refresh_from_db()
self.assertEqual(doc.title, "INV-123 fallback")
# Existing fields remain available when the action assigns no custom fields.
action.assign_custom_fields.clear()
CustomFieldInstance.objects.filter(document=doc, field=field).update(
value_text="INV-456",
)
document_consumption_finished.send(sender=self.__class__, document=doc)
doc.refresh_from_db()
self.assertEqual(doc.title, "INV-456 fallback")
def test_document_added_invalid_title_placeholders(self) -> None:
"""
GIVEN:
+30 -27
View File
@@ -43,6 +43,33 @@ def apply_assignment_to_document(
if action.assign_owner:
document.owner = action.assign_owner
if action.has_assign_custom_fields:
for field in action.assign_custom_fields.all():
value_field_name = CustomFieldInstance.get_value_field_name(
data_type=field.data_type,
)
args = {
value_field_name: action.assign_custom_fields_values.get(
str(field.pk),
None,
),
}
# for some reason update_or_create doesn't work here
instance = CustomFieldInstance.objects.filter(
field=field,
document=document,
).first()
# empty string is indistinguishable from no value in the UI
if instance and args[value_field_name] not in (None, ""):
setattr(instance, value_field_name, args[value_field_name])
instance.save()
elif not instance:
CustomFieldInstance.objects.create(
**args,
field=field,
document=document,
)
if action.assign_title:
try:
title = parse_w_workflow_placeholders(
@@ -57,6 +84,9 @@ def apply_assignment_to_document(
"", # dont pass the title to avoid recursion
"", # no urls in titles
document.pk,
custom_fields=CustomFieldInstance.global_objects.filter(
document=document,
).select_related("field"),
)
if title:
document.title = title
@@ -91,33 +121,6 @@ def apply_assignment_to_document(
merge=True,
)
if action.has_assign_custom_fields:
for field in action.assign_custom_fields.all():
value_field_name = CustomFieldInstance.get_value_field_name(
data_type=field.data_type,
)
args = {
value_field_name: action.assign_custom_fields_values.get(
str(field.pk),
None,
),
}
# for some reason update_or_create doesn't work here
instance = CustomFieldInstance.objects.filter(
field=field,
document=document,
).first()
# empty string is indistinguishable from no value in the UI
if instance and args[value_field_name] not in (None, ""):
setattr(instance, value_field_name, args[value_field_name])
instance.save()
elif not instance:
CustomFieldInstance.objects.create(
**args,
field=field,
document=document,
)
def apply_assignment_to_overrides(
action: WorkflowAction,