Enhancement: allow negative workflow trigger offset

This commit is contained in:
shamoon 2025-04-15 12:35:17 -07:00
parent 67a97ffc4d
commit c7b5791b56
No known key found for this signature in database
4 changed files with 126 additions and 24 deletions

View File

@ -0,0 +1,22 @@
# Generated by Django 5.1.7 on 2025-04-15 19:18
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
dependencies = [
("documents", "1065_workflowaction_assign_custom_fields_values"),
]
operations = [
migrations.AlterField(
model_name="workflowtrigger",
name="schedule_offset_days",
field=models.IntegerField(
default=0,
help_text="The number of days to offset the schedule trigger by.",
verbose_name="schedule offset days",
),
),
]

View File

@ -1019,7 +1019,7 @@ class WorkflowTrigger(models.Model):
verbose_name=_("has this correspondent"), verbose_name=_("has this correspondent"),
) )
schedule_offset_days = models.PositiveIntegerField( schedule_offset_days = models.IntegerField(
_("schedule offset days"), _("schedule offset days"),
default=0, default=0,
help_text=_( help_text=_(

View File

@ -1,8 +1,8 @@
import datetime
import hashlib import hashlib
import logging import logging
import shutil import shutil
import uuid import uuid
from datetime import timedelta
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@ -356,7 +356,7 @@ def empty_trash(doc_ids=None):
if doc_ids is not None if doc_ids is not None
else Document.deleted_objects.filter( else Document.deleted_objects.filter(
deleted_at__lt=timezone.localtime(timezone.now()) deleted_at__lt=timezone.localtime(timezone.now())
- timedelta( - datetime.timedelta(
days=settings.EMPTY_TRASH_DELAY, days=settings.EMPTY_TRASH_DELAY,
), ),
) )
@ -396,6 +396,7 @@ def check_scheduled_workflows():
) )
if scheduled_workflows.count() > 0: if scheduled_workflows.count() > 0:
logger.debug(f"Checking {len(scheduled_workflows)} scheduled workflows") logger.debug(f"Checking {len(scheduled_workflows)} scheduled workflows")
now = timezone.now()
for workflow in scheduled_workflows: for workflow in scheduled_workflows:
schedule_triggers = workflow.triggers.filter( schedule_triggers = workflow.triggers.filter(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED, type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
@ -403,31 +404,60 @@ def check_scheduled_workflows():
trigger: WorkflowTrigger trigger: WorkflowTrigger
for trigger in schedule_triggers: for trigger in schedule_triggers:
documents = Document.objects.none() documents = Document.objects.none()
offset_td = timedelta(days=trigger.schedule_offset_days) offset_td = datetime.timedelta(days=-trigger.schedule_offset_days)
threshold = now - offset_td
logger.debug( logger.debug(
f"Checking trigger {trigger} with offset {offset_td} against field: {trigger.schedule_date_field}", f"Trigger {trigger.id}: checking if (date + {offset_td}) <= now ({now})",
) )
match trigger.schedule_date_field: match trigger.schedule_date_field:
case WorkflowTrigger.ScheduleDateField.ADDED: case WorkflowTrigger.ScheduleDateField.ADDED:
documents = Document.objects.filter( documents = Document.objects.filter(added__lte=threshold)
added__lt=timezone.now() - offset_td,
)
case WorkflowTrigger.ScheduleDateField.CREATED: case WorkflowTrigger.ScheduleDateField.CREATED:
documents = Document.objects.filter( documents = Document.objects.filter(created__lte=threshold)
created__lt=timezone.now() - offset_td,
)
case WorkflowTrigger.ScheduleDateField.MODIFIED: case WorkflowTrigger.ScheduleDateField.MODIFIED:
documents = Document.objects.filter( documents = Document.objects.filter(modified__lte=threshold)
modified__lt=timezone.now() - offset_td,
)
case WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD: case WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD:
cf_instances = CustomFieldInstance.objects.filter( # cap earliest date to avoid massive scans
field=trigger.schedule_date_custom_field, earliest_date = now - datetime.timedelta(days=365)
value_date__lt=timezone.now() - offset_td, if offset_td.days < -365:
) logger.warning(
documents = Document.objects.filter( f"Trigger {trigger.id} has large negative offset ({offset_td.days}), "
id__in=cf_instances.values_list("document", flat=True), f"limiting earliest scan date to {earliest_date}",
)
cf_filter_kwargs = {
"field": trigger.schedule_date_custom_field,
"value_date__isnull": False,
"value_date__lte": threshold,
"value_date__gte": earliest_date,
}
recent_cf_instances = CustomFieldInstance.objects.filter(
**cf_filter_kwargs,
) )
matched_ids = [
cfi.document_id
for cfi in recent_cf_instances
if cfi.value_date
and (
timezone.make_aware(
datetime.datetime.combine(
cfi.value_date,
datetime.time.min,
),
)
+ offset_td
<= now
)
]
documents = Document.objects.filter(id__in=matched_ids)
if documents.count() > 0: if documents.count() > 0:
logger.debug( logger.debug(
f"Found {documents.count()} documents for trigger {trigger}", f"Found {documents.count()} documents for trigger {trigger}",
@ -439,18 +469,18 @@ def check_scheduled_workflows():
workflow=workflow, workflow=workflow,
).order_by("-run_at") ).order_by("-run_at")
if not trigger.schedule_is_recurring and workflow_runs.exists(): if not trigger.schedule_is_recurring and workflow_runs.exists():
# schedule is non-recurring and the workflow has already been run
logger.debug( logger.debug(
f"Skipping document {document} for non-recurring workflow {workflow} as it has already been run", f"Skipping document {document} for non-recurring workflow {workflow} as it has already been run",
) )
continue continue
elif (
if (
trigger.schedule_is_recurring trigger.schedule_is_recurring
and workflow_runs.exists() and workflow_runs.exists()
and ( and (
workflow_runs.last().run_at workflow_runs.last().run_at
> timezone.now() > now
- timedelta( - datetime.timedelta(
days=trigger.schedule_recurring_interval_days, days=trigger.schedule_recurring_interval_days,
) )
) )

View File

@ -1600,6 +1600,56 @@ 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):
"""
GIVEN:
- Existing workflow with SCHEDULED trigger and negative offset of -7 days
WHEN:
- Scheduled workflows are checked for document with custom field date 8 days in the past
THEN:
- Workflow runs and document owner is updated
"""
trigger = WorkflowTrigger.objects.create(
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
schedule_offset_days=-7,
schedule_date_field=WorkflowTrigger.ScheduleDateField.CUSTOM_FIELD,
schedule_date_custom_field=self.cf1,
)
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",
)
cfi = CustomFieldInstance.objects.create(
document=doc,
field=self.cf1,
value_date=timezone.now() - timedelta(days=5),
)
tasks.check_scheduled_workflows()
doc.refresh_from_db()
self.assertIsNone(doc.owner) # has not triggered yet
cfi.value_date = timezone.now() - timedelta(days=8)
cfi.save()
tasks.check_scheduled_workflows()
doc.refresh_from_db()
self.assertEqual(doc.owner, self.user2)
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,