From a6f456d5dba075fcd7267c7ffefa61281c44ba50 Mon Sep 17 00:00:00 2001 From: Felix Vollmer Date: Wed, 9 Apr 2025 09:37:46 +0200 Subject: [PATCH 1/4] Add StartWorkflowMixin This allows to start new workflows from views that don't inherit from BaseCreateView. This is useful if you e.g. want to start a view from json input and don't need forms. --- joeflow/models.py | 3 ++- joeflow/tasks/human.py | 4 ++-- joeflow/views.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/joeflow/models.py b/joeflow/models.py index 1ff6ece..f12c6d6 100644 --- a/joeflow/models.py +++ b/joeflow/models.py @@ -19,6 +19,7 @@ from .conf import settings from .typing import HUMAN, MACHINE from .utils import NoDashDiGraph +from .views import StartWorkflowMixin logger = logging.getLogger(__name__) @@ -139,7 +140,7 @@ def urls(cls): urls = [] for name, node in cls.get_nodes(): if isinstance(node, View): - if isinstance(node, BaseCreateView): + if isinstance(node, (BaseCreateView, StartWorkflowMixin)): route = f"{name}/" else: route = f"{name}//" diff --git a/joeflow/tasks/human.py b/joeflow/tasks/human.py index 1094d45..0951e0f 100644 --- a/joeflow/tasks/human.py +++ b/joeflow/tasks/human.py @@ -2,7 +2,7 @@ from django.views import generic -from joeflow.views import TaskViewMixin +from joeflow.views import TaskViewMixin, StartWorkflowMixin __all__ = ( "StartView", @@ -10,7 +10,7 @@ ) -class StartView(TaskViewMixin, generic.CreateView): +class StartView(TaskViewMixin, StartWorkflowMixin, generic.CreateView): """ Start a new workflow by a human with a view. diff --git a/joeflow/views.py b/joeflow/views.py index 7f8ac2c..c6dc048 100644 --- a/joeflow/views.py +++ b/joeflow/views.py @@ -29,6 +29,22 @@ def get_template_names(self): return names +class StartWorkflowMixin: + """ + Use this mixin to create a start workflow with a view. Example: + + class MyStartWorkflowView(StartWorkflowMixin, TaskViewMixin, View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body) + workflow_id = self.start_workflow(data) + except Exception as e: + return HttpResponseBadRequest("Failed to start workflow") + + return JsonResponse({'message': 'Workflow started successfully.', 'id': workflow_id}, status=201) + """ + + class TaskViewMixin(WorkflowTemplateNameViewMixin, RevisionMixin): name = None path = "" From d047d86b5da77d4c5d9de2d9efe9a130c2062f02 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Mon, 5 May 2025 22:01:28 +0200 Subject: [PATCH 2/4] Fix dostring format --- joeflow/views.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/joeflow/views.py b/joeflow/views.py index c6dc048..1291e02 100644 --- a/joeflow/views.py +++ b/joeflow/views.py @@ -31,17 +31,18 @@ def get_template_names(self): class StartWorkflowMixin: """ - Use this mixin to create a start workflow with a view. Example: - - class MyStartWorkflowView(StartWorkflowMixin, TaskViewMixin, View): - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body) - workflow_id = self.start_workflow(data) - except Exception as e: - return HttpResponseBadRequest("Failed to start workflow") - - return JsonResponse({'message': 'Workflow started successfully.', 'id': workflow_id}, status=201) + View-mixin to create a start workflow. + + Example: + class MyStartWorkflowView(StartWorkflowMixin, TaskViewMixin, View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body) + workflow_id = self.start_workflow(data) + except Exception as e: + return HttpResponseBadRequest("Failed to start workflow") + + return JsonResponse({'message': 'Workflow started successfully.', 'id': workflow_id}, status=201) """ From f5b6f9e204f53a9f27a42d6c7980a69957fa5d94 Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Mon, 5 May 2025 22:05:40 +0200 Subject: [PATCH 3/4] Fix import order --- joeflow/tasks/human.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joeflow/tasks/human.py b/joeflow/tasks/human.py index 0951e0f..af523c9 100644 --- a/joeflow/tasks/human.py +++ b/joeflow/tasks/human.py @@ -2,7 +2,7 @@ from django.views import generic -from joeflow.views import TaskViewMixin, StartWorkflowMixin +from joeflow.views import StartWorkflowMixin, TaskViewMixin __all__ = ( "StartView", From 4ed389ea436572fcaef587b7d76a06e23396fc5a Mon Sep 17 00:00:00 2001 From: Johannes Maron Date: Mon, 5 May 2025 22:27:51 +0200 Subject: [PATCH 4/4] Add tests and improve integration --- joeflow/models.py | 4 +-- joeflow/tasks/human.py | 4 +-- joeflow/views.py | 36 ++++++++++++---------- tests/fixtures/simpleworkflow.dot | 2 ++ tests/fixtures/simpleworkflow_instance.dot | 2 ++ tests/test_models.py | 1 + tests/testapp/workflows.py | 8 +++++ 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/joeflow/models.py b/joeflow/models.py index f12c6d6..cfe51e1 100644 --- a/joeflow/models.py +++ b/joeflow/models.py @@ -19,7 +19,7 @@ from .conf import settings from .typing import HUMAN, MACHINE from .utils import NoDashDiGraph -from .views import StartWorkflowMixin +from .views import StartViewMixin logger = logging.getLogger(__name__) @@ -140,7 +140,7 @@ def urls(cls): urls = [] for name, node in cls.get_nodes(): if isinstance(node, View): - if isinstance(node, (BaseCreateView, StartWorkflowMixin)): + if isinstance(node, (BaseCreateView, StartViewMixin)): route = f"{name}/" else: route = f"{name}//" diff --git a/joeflow/tasks/human.py b/joeflow/tasks/human.py index af523c9..a82e45c 100644 --- a/joeflow/tasks/human.py +++ b/joeflow/tasks/human.py @@ -2,7 +2,7 @@ from django.views import generic -from joeflow.views import StartWorkflowMixin, TaskViewMixin +from joeflow.views import StartViewMixin, TaskViewMixin __all__ = ( "StartView", @@ -10,7 +10,7 @@ ) -class StartView(TaskViewMixin, StartWorkflowMixin, generic.CreateView): +class StartView(StartViewMixin, generic.CreateView): """ Start a new workflow by a human with a view. diff --git a/joeflow/views.py b/joeflow/views.py index 1291e02..a66adb1 100644 --- a/joeflow/views.py +++ b/joeflow/views.py @@ -29,23 +29,6 @@ def get_template_names(self): return names -class StartWorkflowMixin: - """ - View-mixin to create a start workflow. - - Example: - class MyStartWorkflowView(StartWorkflowMixin, TaskViewMixin, View): - def post(self, request, *args, **kwargs): - try: - data = json.loads(request.body) - workflow_id = self.start_workflow(data) - except Exception as e: - return HttpResponseBadRequest("Failed to start workflow") - - return JsonResponse({'message': 'Workflow started successfully.', 'id': workflow_id}, status=201) - """ - - class TaskViewMixin(WorkflowTemplateNameViewMixin, RevisionMixin): name = None path = "" @@ -114,6 +97,25 @@ def create_task(self, workflow, prev_task): ) +class StartViewMixin(TaskViewMixin): + """ + View-mixin to create a start workflow. + + Example: + class MyStartWorkflowView(StartViewMixin, View): + def post(self, request, *args, **kwargs): + try: + data = json.loads(request.body) + workflow_id = self.start_workflow(data) + except Exception as e: + return HttpResponseBadRequest("Failed to start workflow") + + return JsonResponse({'message': 'Workflow started successfully.', 'id': workflow_id}, status=201) + """ + + model = None + + class WorkflowDetailView(WorkflowTemplateNameViewMixin, generic.DetailView): pass diff --git a/tests/fixtures/simpleworkflow.dot b/tests/fixtures/simpleworkflow.dot index 5253bcc..25ebeb5 100644 --- a/tests/fixtures/simpleworkflow.dot +++ b/tests/fixtures/simpleworkflow.dot @@ -5,7 +5,9 @@ digraph { "save the princess" [color=black fontcolor=black style="filled, rounded"] "start method" [color=black fontcolor=black style=filled] "start view" [color=black fontcolor=black style="filled, rounded"] + "custom start view" [color=black fontcolor=black style="filled, rounded"] "save the princess" -> end [color=black] "start method" -> "save the princess" [color=black] "start view" -> "save the princess" [color=black] + "custom start view" -> "save the princess" [color=black] } diff --git a/tests/fixtures/simpleworkflow_instance.dot b/tests/fixtures/simpleworkflow_instance.dot index ca04368..623f51c 100644 --- a/tests/fixtures/simpleworkflow_instance.dot +++ b/tests/fixtures/simpleworkflow_instance.dot @@ -5,7 +5,9 @@ digraph { "save the princess" [color=black fontcolor=black href="/simple/save_the_princess/2/" peripheries=1 style="filled, rounded, bold"] "start method" [color=black fontcolor=black peripheries=1 style=filled] "start view" [color="#888888" fontcolor="#888888" style="filled, rounded"] + "custom start view" [color="#888888" fontcolor="#888888" style="filled, rounded"] "save the princess" -> end [color="#888888"] "start method" -> "save the princess" [color=black] "start view" -> "save the princess" [color="#888888"] + "custom start view" -> "save the princess" [color="#888888"] } diff --git a/tests/test_models.py b/tests/test_models.py index d8801af..00f96b0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -207,6 +207,7 @@ def test_urls(self): assert namespace == "simpleworkflow" names = {pattern.name for pattern in patterns} assert names == { + "custom_start_view", "save_the_princess", "start_view", "override", diff --git a/tests/testapp/workflows.py b/tests/testapp/workflows.py index c0427c2..bb97a83 100644 --- a/tests/testapp/workflows.py +++ b/tests/testapp/workflows.py @@ -1,8 +1,10 @@ from datetime import timedelta from django.core.mail import send_mail +from django.views import generic from joeflow import tasks +from joeflow.views import StartViewMixin from . import models from .views import UpdateWithPrevUserView @@ -40,8 +42,13 @@ class Meta: proxy = True +class CustomStartViewTaskView(StartViewMixin, generic.View): + pass + + class SimpleWorkflow(models.SimpleWorkflowState): start_view = tasks.StartView(fields="__all__", path="custom/postfix/") + custom_start_view = CustomStartViewTaskView() start_method = tasks.Start() save_the_princess = tasks.UpdateView(fields="__all__") @@ -50,6 +57,7 @@ def end(self): edges = ( (start_view, save_the_princess), + (custom_start_view, save_the_princess), (start_method, save_the_princess), (save_the_princess, end), )