Skip to content

Commit 2915748

Browse files
authored
Merge pull request #210 from pneumaticapp/backend/templates/46269__manager_performer_type
46269 backend [ users ] Hierarchical approval workflow
2 parents 8e25035 + 8f10699 commit 2915748

26 files changed

Lines changed: 1702 additions & 19 deletions

File tree

backend/src/processes/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,14 @@ class PerformerType:
7171
GROUP = 'group'
7272
WORKFLOW_STARTER = 'workflow_starter'
7373
FIELD = 'field'
74+
MANAGER = 'manager'
7475

7576
choices = (
7677
(USER, USER),
7778
(GROUP, GROUP),
7879
(WORKFLOW_STARTER, WORKFLOW_STARTER),
7980
(FIELD, FIELD),
81+
(MANAGER, MANAGER),
8082
)
8183

8284
filter_choices = (

backend/src/processes/messages/template.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,3 +284,30 @@
284284
MSG_PT_0070 = _(
285285
'Permission denied. You are not a template owner or viewer.',
286286
)
287+
MSG_PT_0071 = _(
288+
'You should set the source step for performer '
289+
'with the type "manager".',
290+
)
291+
MSG_PT_0072 = lambda name: format_lazy(
292+
_(
293+
'Task "{name}": Manager performer '
294+
'cannot reference its own step.',
295+
),
296+
name=name,
297+
)
298+
MSG_PT_0073 = lambda name, step_name: format_lazy(
299+
_(
300+
'Task "{name}": Manager performer references '
301+
'a non-existent step "{step_name}".',
302+
),
303+
name=name,
304+
step_name=step_name,
305+
)
306+
MSG_PT_0074 = lambda name, step_name: format_lazy(
307+
_(
308+
'Task "{name}": Disable "Required completion by all" '
309+
'on step "{step_name}" to use Manager performer.',
310+
),
311+
name=name,
312+
step_name=step_name,
313+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.db import migrations, models
2+
3+
4+
class Migration(migrations.Migration):
5+
6+
dependencies = [
7+
('processes', '0251_add_skip_for_starter'),
8+
]
9+
10+
operations = [
11+
migrations.AddField(
12+
model_name='rawperformertemplate',
13+
name='source_task_api_name',
14+
field=models.CharField(
15+
blank=True,
16+
max_length=200,
17+
null=True,
18+
),
19+
),
20+
migrations.AddField(
21+
model_name='rawperformer',
22+
name='source_task_api_name',
23+
field=models.CharField(
24+
blank=True,
25+
max_length=200,
26+
null=True,
27+
),
28+
),
29+
]

backend/src/processes/models/mixins.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ class Meta:
4040
on_delete=models.CASCADE,
4141
null=True,
4242
)
43+
source_task_api_name = models.CharField(
44+
max_length=200,
45+
null=True,
46+
blank=True,
47+
)
4348

4449

4550
class WorkflowMixin(models.Model):
@@ -283,13 +288,17 @@ def delete_raw_performer(
283288
group: Optional[UserGroup] = None,
284289
field=None,
285290
performer_type: PerformerType = PerformerType.USER,
291+
source_task_api_name: Optional[str] = None,
286292
) -> int:
287293

288294
""" Delete a raw_performer
289295
and returns the number of objects deleted """
290296

291297
if (
292-
performer_type != PerformerType.WORKFLOW_STARTER
298+
performer_type not in (
299+
PerformerType.WORKFLOW_STARTER,
300+
PerformerType.MANAGER,
301+
)
293302
and user is None and group is None and field is None
294303
):
295304
raise Exception(
@@ -301,6 +310,7 @@ def delete_raw_performer(
301310
user=user,
302311
group=group,
303312
field=field,
313+
source_task_api_name=source_task_api_name,
304314
).delete()[0]
305315

306316
def delete_raw_performers(self):

backend/src/processes/models/workflows/task.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# ruff: noqa: PLC0415
22
from collections import defaultdict
3-
from typing import List, Optional, Set, Tuple
3+
from typing import TYPE_CHECKING, List, Optional, Set, Tuple
44

55
from django.contrib.auth import get_user_model
66
from django.db import models
77
from django.utils import timezone
88

9+
from src.accounts.enums import UserStatus
910
from src.accounts.models import (
1011
AccountBaseMixin,
1112
Notification,
@@ -18,6 +19,7 @@
1819
FieldType,
1920
PerformerType,
2021
TaskStatus,
22+
WorkflowEventType,
2123
)
2224
from src.processes.models.mixins import (
2325
ApiNameMixin,
@@ -31,6 +33,9 @@
3133
TasksQuerySet,
3234
)
3335

36+
if TYPE_CHECKING:
37+
from src.processes.models.workflows.raw_performer import RawPerformer
38+
3439
UserModel = get_user_model()
3540

3641

@@ -122,6 +127,7 @@ def _get_raw_performer(
122127
group_id: Optional[int] = None,
123128
user_id: Optional[int] = None,
124129
field=None, # Optional[TaskField]
130+
source_task_api_name: Optional[str] = None,
125131
): # -> RawPerformer
126132

127133
""" Returns new a raw performer object with given data """
@@ -134,6 +140,7 @@ def _get_raw_performer(
134140
field=field,
135141
api_name=api_name,
136142
type=performer_type,
143+
source_task_api_name=source_task_api_name,
137144
)
138145
if user:
139146
result.user = user
@@ -151,12 +158,18 @@ def add_raw_performer(
151158
field=None,
152159
api_name: Optional[str] = None,
153160
performer_type: PerformerType = PerformerType.USER,
161+
source_task_api_name: Optional[str] = None,
154162
): # -> RawPerformer
155163

156164
""" Creates and returns a raw performer for a task with given data
157165
Optionally updates the performers after create raw performers """
158166

159-
if (
167+
if performer_type == PerformerType.MANAGER:
168+
if not source_task_api_name:
169+
raise Exception(
170+
'Manager performer requires source_task_api_name',
171+
)
172+
elif (
160173
performer_type != PerformerType.WORKFLOW_STARTER
161174
and not group_id
162175
and not user
@@ -174,6 +187,7 @@ def add_raw_performer(
174187
group_id=group_id,
175188
user_id=user_id,
176189
field=field,
190+
source_task_api_name=source_task_api_name,
177191
)
178192
raw_performer.save()
179193
return raw_performer
@@ -261,6 +275,7 @@ def update_raw_performers_from_task_template(
261275
'user_id': e.user_id,
262276
'group_id': e.group_id,
263277
'api_name': e.api_name,
278+
'source_task_api_name': e.source_task_api_name,
264279
'field': {
265280
'api_name': e.field.api_name,
266281
} if e.type == PerformerType.FIELD else None,
@@ -309,6 +324,11 @@ def update_raw_performers_from_task_template(
309324
group_id=raw_performer_template.get('group_id'),
310325
field=field,
311326
api_name=raw_performer_template['api_name'],
327+
source_task_api_name=(
328+
raw_performer_template.get(
329+
'source_task_api_name',
330+
)
331+
),
312332
),
313333
)
314334
if new_raw_performers:
@@ -318,6 +338,38 @@ def update_raw_performers_from_task_template(
318338
api_name__in=deleted_raw_performer_api_names,
319339
).delete()
320340

341+
def _resolve_manager(
342+
self,
343+
raw_performer: 'RawPerformer',
344+
) -> Optional[UserModel]:
345+
346+
""" Resolve manager performer: find the user who completed
347+
the source step and return their manager.
348+
Returns None if source step not completed
349+
or completer has no manager. """
350+
351+
source_api_name = raw_performer.source_task_api_name
352+
if not source_api_name:
353+
return None
354+
from src.processes.models.workflows.event import WorkflowEvent
355+
complete_event = (
356+
WorkflowEvent.objects
357+
.filter(
358+
workflow_id=self.workflow_id,
359+
task__api_name=source_api_name,
360+
task__status=TaskStatus.COMPLETED,
361+
type=WorkflowEventType.TASK_COMPLETE,
362+
)
363+
.select_related('user__manager')
364+
.order_by('-created')
365+
.first()
366+
)
367+
if complete_event and complete_event.user:
368+
manager = complete_event.user.manager
369+
if manager and manager.status == UserStatus.ACTIVE:
370+
return manager
371+
return None
372+
321373
def update_performers(
322374
self,
323375
raw_performer=None,
@@ -354,6 +406,7 @@ def update_performers(
354406
defaultdict(list),
355407
defaultdict(list),
356408
)
409+
raw_performers_for_update = []
357410
for raw_performer_ in raw_performers:
358411
if raw_performer_.type == PerformerType.USER:
359412
user_ids[raw_performer_.user_id].append(raw_performer_)
@@ -364,6 +417,13 @@ def update_performers(
364417
elif raw_performer_.type == PerformerType.WORKFLOW_STARTER:
365418
user = self.get_default_performer()
366419
user_ids[user.id].append(raw_performer_)
420+
elif raw_performer_.type == PerformerType.MANAGER:
421+
manager_user = self._resolve_manager(raw_performer_)
422+
if manager_user:
423+
user_ids[manager_user.id].append(raw_performer_)
424+
elif raw_performer_.task_performer_id is not None:
425+
raw_performer_.task_performer_id = None
426+
raw_performers_for_update.append(raw_performer_)
367427

368428
if api_names:
369429
user_fields = self.workflow.get_fields(
@@ -378,7 +438,6 @@ def update_performers(
378438
elif field.group_id:
379439
group_ids[field.group_id].extend(api_names[field.api_name])
380440

381-
raw_performers_for_update = []
382441
created_performers_user_ids = []
383442
created_performers_group_ids = []
384443
if user_ids:
@@ -476,7 +535,9 @@ def _delete_orphaned_performers(self) -> Tuple[List[int], List[int]]:
476535
left pointing to the performer) """
477536

478537
task_performer_ids = list(
479-
self.raw_performers.values_list('task_performer_id', flat=True),
538+
self.raw_performers
539+
.exclude(task_performer_id=None)
540+
.values_list('task_performer_id', flat=True),
480541
)
481542
performers_to_delete = (
482543
TaskPerformer.objects
@@ -501,6 +562,7 @@ def delete_raw_performer(
501562
group: Optional[UserGroup] = None,
502563
field=None,
503564
performer_type: PerformerType = PerformerType.USER,
565+
source_task_api_name: Optional[str] = None,
504566
):
505567

506568
""" Delete a raw_performer
@@ -512,6 +574,7 @@ def delete_raw_performer(
512574
user=user,
513575
group=group,
514576
field=field,
577+
source_task_api_name=source_task_api_name,
515578
)
516579
if deleted_count:
517580
self._delete_orphaned_performers()

0 commit comments

Comments
 (0)