Skip to content

Commit

Permalink
Add support for split and merge workflow patterns (#146)
Browse files Browse the repository at this point in the history
* To split workflow a list of tasks is returned in jexl expression
* To merge multiple tasks of a workflow link to same flow
  • Loading branch information
sliverc committed Dec 11, 2018
1 parent f1b2902 commit 75155a3
Show file tree
Hide file tree
Showing 14 changed files with 345 additions and 100 deletions.
1 change: 1 addition & 0 deletions caluma/form/schema.py
Expand Up @@ -261,6 +261,7 @@ class Meta:
lookup_input_kwarg = "option"
serializer_class = serializers.RemoveOptionSerializer
return_field_name = False
model_operations = ["update"]


class Answer(Node, graphene.Interface):
Expand Down
29 changes: 14 additions & 15 deletions caluma/tests/snapshots/snap_test_schema.py
Expand Up @@ -27,7 +27,7 @@
input AddWorkflowFlowInput {
workflow: ID!
task: ID!
tasks: [ID]!
next: FlowJexl!
clientMutationId: String
}
Expand Down Expand Up @@ -250,9 +250,9 @@
}
type Flow implements Node {
task: Task!
next: FlowJexl!
id: ID!
tasks: [Task]!
}
type FlowConnection {
Expand Down Expand Up @@ -351,7 +351,7 @@
publishWorkflow(input: PublishWorkflowInput!): PublishWorkflowPayload
archiveWorkflow(input: ArchiveWorkflowInput!): ArchiveWorkflowPayload
addWorkflowFlow(input: AddWorkflowFlowInput!): AddWorkflowFlowPayload
removeWorkflowFlow(input: RemoveWorkflowFlowInput!): RemoveWorkflowFlowPayload
removeFlow(input: RemoveFlowInput!): RemoveFlowPayload
saveTask(input: SaveTaskInput!): SaveTaskPayload
archiveTask(input: ArchiveTaskInput!): ArchiveTaskPayload
startCase(input: StartCaseInput!): StartCasePayload
Expand Down Expand Up @@ -511,6 +511,16 @@
id: ID!
}
input RemoveFlowInput {
flow: ID!
clientMutationId: String
}
type RemoveFlowPayload {
flow: Flow
clientMutationId: String
}
input RemoveFormQuestionInput {
form: ID!
question: ID!
Expand All @@ -531,17 +541,6 @@
clientMutationId: String
}
input RemoveWorkflowFlowInput {
workflow: ID!
task: ID!
clientMutationId: String
}
type RemoveWorkflowFlowPayload {
workflow: Workflow
clientMutationId: String
}
input ReorderFormQuestionsInput {
form: ID!
questions: [ID]!
Expand Down Expand Up @@ -916,8 +915,8 @@
isArchived: Boolean!
start: Task!
form: Form
flows(before: String, after: String, first: Int, last: Int, task: ID): FlowConnection
id: ID!
flows(before: String, after: String, first: Int, last: Int, task: ID): FlowConnection
}
type WorkflowConnection {
Expand Down
2 changes: 1 addition & 1 deletion caluma/tests/test_schema.py
Expand Up @@ -8,7 +8,7 @@
DIR_NAME = os.path.dirname(__file__)


@pytest.mark.parametrize("node_type", ["form", "workflow", "flow", "task"])
@pytest.mark.parametrize("node_type", ["form", "workflow", "task"])
def test_schema_node(db, snapshot, request, node_type):
"""
Add your model to parametrize for automatic global node testing.
Expand Down
11 changes: 9 additions & 2 deletions caluma/workflow/factories.py
Expand Up @@ -32,14 +32,21 @@ class Meta:


class FlowFactory(DjangoModelFactory):
workflow = SubFactory(WorkflowFactory)
task = SubFactory(TaskFactory)
next = Faker("slug")

class Meta:
model = models.Flow


class TaskFlowFactory(DjangoModelFactory):
workflow = SubFactory(WorkflowFactory)
task = SubFactory(TaskFactory)
flow = SubFactory(FlowFactory)

class Meta:
model = models.TaskFlow


class CaseFactory(DjangoModelFactory):
workflow = SubFactory(WorkflowFactory)
status = models.Case.STATUS_RUNNING
Expand Down
10 changes: 9 additions & 1 deletion caluma/workflow/filters.py
@@ -1,5 +1,5 @@
from . import models
from ..core.filters import FilterSet, OrderingFilter, SearchFilter
from ..core.filters import FilterSet, GlobalIDFilter, OrderingFilter, SearchFilter


class WorkflowFilterSet(FilterSet):
Expand All @@ -11,6 +11,14 @@ class Meta:
fields = ("slug", "name", "description", "is_published", "is_archived")


class FlowFilterSet(FilterSet):
task = GlobalIDFilter(field_name="task_flows__task")

class Meta:
model = models.Flow
fields = ("task",)


class CaseFilterSet(FilterSet):
order_by = OrderingFilter(label="CaseOrdering", fields=("status",))

Expand Down
14 changes: 13 additions & 1 deletion caluma/workflow/jexl.py
@@ -1,4 +1,5 @@
from functools import partial
from itertools import chain

from pyjexl import JEXL

Expand All @@ -9,8 +10,19 @@ class FlowJexl(JEXL):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.add_transform("task", lambda spec: spec)
self.add_transform("tasks", lambda spec: spec)

def extract_tasks(self, expr):
return self.analyze(
yield from self.analyze(
expr, partial(ExtractTransformSubjectAnalyzer, transforms=["task"])
)

# tasks transforms return a list of literals
yield from (
literal.value
for literal in chain(
*self.analyze(
expr, partial(ExtractTransformSubjectAnalyzer, transforms=["tasks"])
)
)
)
72 changes: 72 additions & 0 deletions caluma/workflow/migrations/0006_auto_20181207_1512.py
@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.17 on 2018-12-07 15:12
from __future__ import unicode_literals

from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [("workflow", "0005_workitem_child_case")]

operations = [
migrations.CreateModel(
name="TaskFlow",
fields=[
("created_at", models.DateTimeField(auto_now_add=True)),
("modified_at", models.DateTimeField(auto_now=True)),
(
"created_by_user",
models.CharField(blank=True, max_length=150, null=True),
),
(
"created_by_group",
models.CharField(blank=True, max_length=150, null=True),
),
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
],
),
migrations.AlterUniqueTogether(name="flow", unique_together=set([])),
migrations.AddField(
model_name="taskflow",
name="flow",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="task_flows",
to="workflow.Flow",
),
),
migrations.AddField(
model_name="taskflow",
name="task",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="task_flows",
to="workflow.Task",
),
),
migrations.AddField(
model_name="taskflow",
name="workflow",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="task_flows",
to="workflow.Workflow",
),
),
migrations.RemoveField(model_name="flow", name="task"),
migrations.RemoveField(model_name="flow", name="workflow"),
migrations.AlterUniqueTogether(
name="taskflow", unique_together=set([("workflow", "task")])
),
]
10 changes: 8 additions & 2 deletions caluma/workflow/models.py
Expand Up @@ -35,10 +35,16 @@ class Workflow(SlugModel):


class Flow(UUIDModel):
workflow = models.ForeignKey(Workflow, related_name="flows")
task = models.ForeignKey(Task, related_name="flows")
next = models.TextField()


class TaskFlow(UUIDModel):
workflow = models.ForeignKey(
Workflow, on_delete=models.CASCADE, related_name="task_flows"
)
task = models.ForeignKey(Task, related_name="task_flows", on_delete=models.CASCADE)
flow = models.ForeignKey(Flow, related_name="task_flows", on_delete=models.CASCADE)

class Meta:
unique_together = ("workflow", "task")

Expand Down
37 changes: 22 additions & 15 deletions caluma/workflow/schema.py
Expand Up @@ -19,30 +19,36 @@ class FlowJexl(graphene.String):
)


class Task(DjangoObjectType):
class Meta:
model = models.Task
exclude_fields = ("task_flows", "work_items")
interfaces = (relay.Node,)


class Flow(DjangoObjectType):
next = FlowJexl(required=True)
tasks = graphene.List(Task, required=True)

def resolve_tasks(self, info, **args):
return models.Task.objects.filter(pk__in=self.task_flows.values("task"))

class Meta:
model = models.Flow
filter_fields = ("task",)
only_fields = ("task", "next")
only_fields = ("tasks", "next")
interfaces = (relay.Node,)


class Workflow(DjangoObjectType):
flows = DjangoFilterConnectionField(Flow)
flows = DjangoFilterConnectionField(Flow, filterset_class=filters.FlowFilterSet)

def resolve_flows(self, info, **args):
return models.Flow.objects.filter(pk__in=self.task_flows.values("flow"))

class Meta:
model = models.Workflow
filter_fields = ("slug", "name", "description", "is_published", "is_archived")
exclude_fields = ("cases",)
interfaces = (relay.Node,)


class Task(DjangoObjectType):
class Meta:
model = models.Task
exclude_fields = ("flows", "work_items")
exclude_fields = ("cases", "task_flows")
interfaces = (relay.Node,)


Expand Down Expand Up @@ -81,10 +87,11 @@ class Meta:
lookup_input_kwarg = "workflow"


class RemoveWorkflowFlow(Mutation):
class RemoveFlow(Mutation):
class Meta:
serializer_class = serializers.RemoveWorkflowFlowSerializer
lookup_input_kwarg = "workflow"
lookup_input_kwarg = "flow"
serializer_class = serializers.RemoveFlowSerializer
model_operations = ["update"]


class SaveTask(UserDefinedPrimaryKeyMixin, Mutation):
Expand Down Expand Up @@ -121,7 +128,7 @@ class Mutation(object):
publish_workflow = PublishWorkflow().Field()
archive_workflow = ArchiveWorkflow().Field()
add_workflow_flow = AddWorkflowFlow().Field()
remove_workflow_flow = RemoveWorkflowFlow().Field()
remove_flow = RemoveFlow().Field()

save_task = SaveTask().Field()
archive_task = ArchiveTask().Field()
Expand Down

0 comments on commit 75155a3

Please sign in to comment.