Skip to content

Commit 8ff7fa1

Browse files
authored
Merge pull request #200 from pneumaticapp/backend/workflow/45866__skip_task_if_workflow_starter
45866 backend [ workflow ] Skip task if workflow starter is a performer
2 parents e263540 + dfc7f42 commit 8ff7fa1

22 files changed

Lines changed: 904 additions & 10 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 2.2 on 2026-04-10 15:01
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('processes', '0250_workflowevent_type_add_task_delegated'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='task',
15+
name='skip_for_starter',
16+
field=models.BooleanField(default=False),
17+
),
18+
migrations.AddField(
19+
model_name='tasktemplate',
20+
name='skip_for_starter',
21+
field=models.BooleanField(default=False),
22+
),
23+
]

backend/src/processes/models/mixins.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ class Meta:
162162
validators=[MinValueValidator(1)],
163163
)
164164
require_completion_by_all = models.BooleanField(default=False)
165+
skip_for_starter = models.BooleanField(default=False)
165166
revert_task = models.CharField(
166167
max_length=255,
167168
null=True,

backend/src/processes/serializers/templates/task.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Meta:
7171
'number',
7272
'description',
7373
'require_completion_by_all',
74+
'skip_for_starter',
7475
'delay',
7576
'fields',
7677
'conditions',
@@ -87,6 +88,7 @@ class Meta:
8788
'number',
8889
'description',
8990
'require_completion_by_all',
91+
'skip_for_starter',
9092
'delay',
9193
'api_name',
9294
'template',

backend/src/processes/services/tasks/task.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def _create_instance(
8282
require_completion_by_all=(
8383
instance_template.require_completion_by_all
8484
),
85+
skip_for_starter=instance_template.skip_for_starter,
8586
is_urgent=workflow.is_urgent,
8687
checklists_total=ChecklistTemplateSelection.objects.filter(
8788
checklist__task=instance_template,

backend/src/processes/services/tasks/task_version.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ def _create_or_update_instance(
241241
'require_completion_by_all': data[
242242
'require_completion_by_all'
243243
],
244+
'skip_for_starter': data['skip_for_starter'],
244245
'name_template': data['name'],
245246
'name': insert_fields_values_to_text(
246247
text=data['name'],

backend/src/processes/services/versioning/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class Meta:
191191
'clear_description',
192192
'number',
193193
'require_completion_by_all',
194+
'skip_for_starter',
194195
'fields',
195196
'delay',
196197
'conditions',

backend/src/processes/services/workflow_action.py

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,20 @@ def continue_task(self, task: Task, is_returned: bool = False):
464464
.by_workflow(self.workflow.id).with_tasks_after(task)
465465
.update(is_completed=False, date_completed=None)
466466
)
467+
if task.skip_for_starter and task.require_completion_by_all:
468+
starter = self.workflow.workflow_starter
469+
if starter:
470+
(
471+
TaskPerformer.objects
472+
.by_task(task.id)
473+
.exclude_directly_deleted()
474+
.by_user_or_group(starter.id)
475+
.update(
476+
is_completed=True,
477+
date_completed=timezone.now(),
478+
)
479+
)
480+
467481
# if task force snoozed then start task event already exists
468482
# but if task returned then
469483
if not task_start_event_already_exist:
@@ -479,6 +493,7 @@ def continue_task(self, task: Task, is_returned: bool = False):
479493
TaskPerformer.objects
480494
.by_task(task.id)
481495
.exclude_directly_deleted()
496+
.not_completed()
482497
.get_user_ids_emails_subscriber_set()
483498
)
484499

@@ -615,6 +630,21 @@ def delay_task(self, task: Task, delay: Delay):
615630
delay=delay,
616631
)
617632

633+
def _skip_task_for_starter(
634+
self,
635+
task: Task,
636+
is_returned: bool,
637+
):
638+
WorkflowEventService.task_skip_event(task)
639+
if is_returned and task.parents:
640+
task.status = TaskStatus.PENDING
641+
task.save(update_fields=['status'])
642+
self._start_prev_tasks(task)
643+
else:
644+
task.status = TaskStatus.SKIPPED
645+
task.save(update_fields=['status'])
646+
self._start_next_tasks(parent_task=task)
647+
618648
def start_task(
619649
self,
620650
task: Task,
@@ -642,16 +672,41 @@ def start_task(
642672
self._start_prev_tasks(task)
643673
else:
644674
self._start_next_tasks(parent_task=task)
645-
else: # noqa: PLR5501
646-
if is_returned:
647-
self.continue_workflow(task=task, is_returned=is_returned)
675+
return
676+
if task.skip_for_starter and not self.workflow.is_external:
677+
starter = self.workflow.workflow_starter
678+
starter_perf = (
679+
TaskPerformer.objects
680+
.by_task(task.id)
681+
.exclude_directly_deleted()
682+
.by_user_or_group(starter.id)
683+
.first()
684+
)
685+
if starter_perf:
686+
if not task.require_completion_by_all:
687+
self._skip_task_for_starter(task, is_returned)
688+
return
689+
690+
has_other_performers = (
691+
TaskPerformer.objects
692+
.by_task(task.id)
693+
.exclude_directly_deleted()
694+
.exclude(pk=starter_perf.pk)
695+
.exists()
696+
)
697+
if not has_other_performers:
698+
self._skip_task_for_starter(task, is_returned)
699+
return
700+
701+
if is_returned:
702+
self.continue_workflow(task=task, is_returned=is_returned)
703+
else:
704+
delay = task.get_active_delay()
705+
if delay:
706+
self.delay_task(task=task, delay=delay)
707+
self._start_next_tasks()
648708
else:
649-
delay = task.get_active_delay()
650-
if delay:
651-
self.delay_task(task=task, delay=delay)
652-
self._start_next_tasks()
653-
else:
654-
self.continue_workflow(task=task, is_returned=is_returned)
709+
self.continue_workflow(task=task, is_returned=is_returned)
655710

656711
def complete_task(self, task: Task, by_user: bool = False):
657712

backend/src/processes/tests/test_services/test_tasks/test_task_service.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,46 @@ def test_create_instance__all_fields__ok(mocker):
9292
assert task.description_template == description
9393
assert task.number == 1
9494
assert task.require_completion_by_all is True
95+
assert task.skip_for_starter is False
9596
assert task.is_urgent is True
9697
assert task.checklists_total == 2
9798
clear_mock.assert_called_once_with(description)
9899

99100

101+
def test_create_instance__skip_flag_true__ok(mocker):
102+
103+
# arrange
104+
user = create_test_user()
105+
template = create_test_template(user=user, tasks_count=1)
106+
template_task = template.tasks.get(number=1)
107+
template_task.skip_for_starter = True
108+
template_task.save(
109+
update_fields=['skip_for_starter'],
110+
)
111+
workflow = create_test_workflow(user, template=template)
112+
workflow.tasks.delete()
113+
clear_description = 'clear'
114+
mocker.patch(
115+
'src.services.markdown.MarkdownService.clear',
116+
return_value=clear_description,
117+
)
118+
service = TaskService(
119+
user=user,
120+
instance=None,
121+
)
122+
123+
# act
124+
service._create_instance(
125+
instance_template=template_task,
126+
workflow=workflow,
127+
)
128+
129+
# assert
130+
task = service.instance
131+
assert task.skip_for_starter is True
132+
assert task.require_completion_by_all is False
133+
134+
100135
def test_get_task_due_date__raw_due_date_not_exist__return_none():
101136

102137
# arrange

backend/src/processes/tests/test_services/test_tasks/test_task_version_service.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,53 @@ def test_create_or_update_instance__update_all_fields__ok(mocker):
101101
assert task.description_template == description
102102
assert task.number == 1
103103
assert task.require_completion_by_all is True
104+
assert task.skip_for_starter is False
104105
assert task.is_urgent is True
105106
clear_mock.assert_called_once_with(description)
106107

107108

109+
def test_create_or_update__skip_flag_true__ok(mocker):
110+
111+
# arrange
112+
user = create_test_owner()
113+
template = create_test_template(user=user, tasks_count=1)
114+
workflow = create_test_workflow(
115+
user=user,
116+
template=template,
117+
is_urgent=True,
118+
)
119+
clear_description = 'clear'
120+
mocker.patch(
121+
'src.services.markdown.MarkdownService.clear',
122+
return_value=clear_description,
123+
)
124+
template_task = template.tasks.get(number=1)
125+
template_task.skip_for_starter = True
126+
template_task.save(
127+
update_fields=['skip_for_starter'],
128+
)
129+
task_data = TaskSchemaV1(instance=template_task).data
130+
131+
task = workflow.tasks.get(number=1)
132+
service = TaskUpdateVersionService(
133+
user=user,
134+
instance=task,
135+
auth_type=AuthTokenType.USER,
136+
is_superuser=False,
137+
)
138+
139+
# act
140+
task = service._create_or_update_instance(
141+
data=task_data,
142+
workflow=workflow,
143+
fields_values={},
144+
)
145+
146+
# assert
147+
assert task.skip_for_starter is True
148+
assert task.require_completion_by_all is False
149+
150+
108151
def test_create_or_update_instance__remove_revert_task__ok():
109152

110153
# arrange

0 commit comments

Comments
 (0)