diff --git a/docs/UsingSpiffdemo/Getting_Started.md b/docs/UsingSpiffdemo/Getting_Started.md
new file mode 100644
index 000000000..4e19bea65
--- /dev/null
+++ b/docs/UsingSpiffdemo/Getting_Started.md
@@ -0,0 +1,192 @@
+# Using Spiffdemo.org
+Spiffdemo is a demo version of Spiffworkflow, offering a limited set of functions and features.
+It provides users a platform to explore workflow concepts through a collection of top-level examples, diagrams, and workflows.
+Users can interact with pre-built models, make modifications, and visualize process flows.
+Spiffdemo serves as a demonstration version of Spiffworkflow, providing a glimpse into its capabilities with limited functionality.
+Spiffworkflow, on the other hand, is a full-fledged tool that offers a broader range of features for creating and managing workflows.
+
+| Category | Spiffdemo (Demo Version) | SpiffWorkflow (Full Version) |
+|---------|-------------------------|------------------------------|
+| Functionality | Limited set of functions and features | Comprehensive set of features and functions for creating, managing, and optimizing process models and workflows |
+| Purpose | Provides a platform for exploration and demonstration of workflow concepts | Enables users to create, manage, and optimize their own process models and workflows for their specific needs and requirements |
+| Features | Limited functionality for interacting with pre-built models and making modifications | Advanced features such as task management, process modeling, diagram editing, and customizable properties |
+
+## How to Login to Spiffdemo
+To begin your journey with Spiffdemo, open your web browser and navigate to the official Spiffdemo website.
+
+On the login screen, you will find the option to log in using Single Sign-On.
+Click the Single Sign-On button and select your preferred login method, such as using your Gmail account.
+
+
+
+
+
+```{admonition} Note: Stay tuned as we expand our sign-on options beyond Gmail.
+More ways to access Spiffdemo are coming your way!
+```
+## Exploring the Examples
+
+When logging into the dashboard, it is crucial to familiarize yourself with the functions it offers and how the underlying engine operates.
+In the demo website, we will explore two examples: the Minimal Example and the Essential Example, to provide a clear understanding of the process.
+
+
+
+
+
+
+### Minimal Example
+
+Let's begin with the Minimal Example, which serves as a "Hello World" process—a simple executable BPMN Diagram designed to demonstrate basic functionality.
+Rather than immediately starting the process, we will first examine its structure.
+
+
+
+#### Access the Process Directory
+
+Clicking on the process name will open the directory dedicated to the Minimal Example process.
+From here, you can start the process if desired, but for the purpose of this example, we will proceed with an explanation of its components and functionality.
+Therefore, Locate and click on the .bpmn file to access the BPMN editor.
+
+
+
+
+
+
+
+The BPMN editor provides a visual representation of the process workflow.
+
+
+
+
+
+
+
+#### Understand the Process Workflow
+
+
+
+The Minimal Example process consists of three key elements: a start event, a manual task, and an end event.
+It is essential to understand the purpose and functionality of the properties panel, which is an integral component of the process diagram.
+Without selecting any specific task within the diagram editor, the properties panel will appear as follows:
+
+
+
+General
+
+- Name field is usually empty unless user wants to provide it.
+It serves as a label or identifier for the process.
+
+- The ID is automatically populated by the system (default behavior) however it can be updated by the user, but it must remain unique within the other processes.
+
+- By default, all processes are executable, which means the engine can run the process.
+
+
+Documentation
+
+- This field can be used to provide any notes related to the process.
+
+
+Data Objects
+
+- Used to configure Data Objects added to the process.
+See full article [here](https://medium.com/@danfunk/understanding-bpmns-data-objects-with-spiffworkflow-26e195e23398).
+
+
+
+**1. Start Event**
+
+
+The first event in the minimal example is the start event.
+Each process diagram begins with a Start Event.
+Now explore the properties panel when you click on the first process of the diagram, “Start Event”.
+
+
+
+General
+
+- The Name for a Start Event is often left blank unless it needs to be named to provide more clarity on the flow or to be able to view this name in Process Instance logs.
+
+- ID is automatically populated by the system (default behavior) however it can be updated by the user, but it must remain unique within the process.
+Often the ID would be updated to allow easier referencing in messages and also Logs as long as it’s unique in the process.
+
+
+Documentation
+
+- This field is used to provide any notes related to the element.
+
+
+ ```{admonition} Note: In the minimal example, the Start Event is a None Start Event.
+This type of Start Event signifies that the process can be initiated without any triggering message or timer event.
+It is worth noting that there are other types of Start Events available, such as Message Start Events and Timer Start Events.
+These advanced Start Events will be discussed in detail in the subsequent sections, providing further insights into their specific use cases and functionalities.
+```
+
+
+**2. Manual Task**
+
+Within the process flow, the next step is a manual task.
+A Manual Task is another type of BPMN Task requiring human involvement.
+Manual Tasks do not require user input other than clicking a button to acknowledge the completion of a task that usually occurs outside of the process.
+
+Now explore the properties panel when you click on the first process of the process of “Show Example Manual Task”.
+
+
+
+Panel General Section
+
+- Enter/Edit the User Task name in this section.
+Alternatively, double-click on the User Task in the diagram.
+
+- The ID is automatically entered and can be edited for improved referencing in error messages, but it must be unique within the process.
+
+
+Documentation Section
+
+- This field is used to provide any notes related to the element.
+
+
+SpiffWorkflow Scripts
+
+- Pre-Script: Updates Task Data using Python prior to execution of the Activity.
+
+- Post-Script: Updates Task Data using Python immediately after execution of the Activity.
+
+
+Instructions
+
+During the execution of this task, the following instructions will be displayed to the end user.
+This section serves as a means to format and present information to the user.
+The formatting is achieved through a combination of Markdown and Jinja.
+To view and edit the instructions, click on the editor, and a window will open displaying the instructions in the specified format.
+
+
+
+
+3. ##### End Task
+
+
+The next process in the workflow is an end task.
+A BPMN diagram should contain an end event for every distinct end state of a process.
+
+Now explore the properties panel when you click on the last end event process:
+
+
+
+General
+
+- The Name for a Start Event is often left blank unless it needs to be named to provide more clarity on the flow or to be able to view this name in Process Instance logs.
+
+- ID is automatically populated by the system (default behavior) however the user can update it, but it must remain unique within the process.
+
+
+Documentation
+
+- This field is used to provide any notes related to the element.
+
+
+Instructions
+
+- These are the Instructions for the End User, which will be displayed when this task is executed.You can click on launch editor to see the markdown file.
+
+
+
diff --git a/docs/UsingSpiffdemo/Images/End_Task_Properties.png b/docs/UsingSpiffdemo/Images/End_Task_Properties.png
new file mode 100644
index 000000000..b96f69ab9
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/End_Task_Properties.png differ
diff --git a/docs/UsingSpiffdemo/Images/Instructions_panel.png b/docs/UsingSpiffdemo/Images/Instructions_panel.png
new file mode 100644
index 000000000..bef047618
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Instructions_panel.png differ
diff --git a/docs/UsingSpiffdemo/Images/Login.png b/docs/UsingSpiffdemo/Images/Login.png
new file mode 100644
index 000000000..2dcb2eba0
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Login.png differ
diff --git a/docs/UsingSpiffdemo/Images/Manual_task.png b/docs/UsingSpiffdemo/Images/Manual_task.png
new file mode 100644
index 000000000..bb7b3e5f0
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Manual_task.png differ
diff --git a/docs/UsingSpiffdemo/Images/Manual_task_Properties.png b/docs/UsingSpiffdemo/Images/Manual_task_Properties.png
new file mode 100644
index 000000000..70915aee7
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Manual_task_Properties.png differ
diff --git a/docs/UsingSpiffdemo/Images/Manual_task_Properties1.png b/docs/UsingSpiffdemo/Images/Manual_task_Properties1.png
new file mode 100644
index 000000000..fca10a948
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Manual_task_Properties1.png differ
diff --git a/docs/UsingSpiffdemo/Images/Manual_task_Properties12.png b/docs/UsingSpiffdemo/Images/Manual_task_Properties12.png
new file mode 100644
index 000000000..fca10a948
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Manual_task_Properties12.png differ
diff --git a/docs/UsingSpiffdemo/Images/Manual_task_instructions_panel.png b/docs/UsingSpiffdemo/Images/Manual_task_instructions_panel.png
new file mode 100644
index 000000000..b80bc9f2e
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Manual_task_instructions_panel.png differ
diff --git a/docs/UsingSpiffdemo/Images/Navigating_Process.png b/docs/UsingSpiffdemo/Images/Navigating_Process.png
new file mode 100644
index 000000000..dc329cf76
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Navigating_Process.png differ
diff --git a/docs/UsingSpiffdemo/Images/Propertise_panel.png b/docs/UsingSpiffdemo/Images/Propertise_panel.png
new file mode 100644
index 000000000..969cd9776
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Propertise_panel.png differ
diff --git a/docs/UsingSpiffdemo/Images/Start_Event_Properties.png b/docs/UsingSpiffdemo/Images/Start_Event_Properties.png
new file mode 100644
index 000000000..9de466686
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Start_Event_Properties.png differ
diff --git a/docs/UsingSpiffdemo/Images/Start_Event_Properties1.png b/docs/UsingSpiffdemo/Images/Start_Event_Properties1.png
new file mode 100644
index 000000000..9de466686
Binary files /dev/null and b/docs/UsingSpiffdemo/Images/Start_Event_Properties1.png differ
diff --git a/spiffworkflow-backend/migrations/versions/881cdb50a567_.py b/spiffworkflow-backend/migrations/versions/881cdb50a567_.py
new file mode 100644
index 000000000..de453cd0c
--- /dev/null
+++ b/spiffworkflow-backend/migrations/versions/881cdb50a567_.py
@@ -0,0 +1,34 @@
+"""empty message
+
+Revision ID: 881cdb50a567
+Revises: 377be1608b45
+Create Date: 2023-06-20 15:26:28.087551
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '881cdb50a567'
+down_revision = '377be1608b45'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('task', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('saved_form_data_hash', sa.String(length=255), nullable=True))
+ batch_op.create_index(batch_op.f('ix_task_saved_form_data_hash'), ['saved_form_data_hash'], unique=False)
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('task', schema=None) as batch_op:
+ batch_op.drop_index(batch_op.f('ix_task_saved_form_data_hash'))
+ batch_op.drop_column('saved_form_data_hash')
+
+ # ### end Alembic commands ###
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
index ab9457268..b56b1c72d 100755
--- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml
@@ -1846,12 +1846,6 @@ paths:
description: The unique id of an existing process instance.
schema:
type: integer
- - name: save_as_draft
- in: query
- required: false
- description: Save the data to task but do not complete it.
- schema:
- type: boolean
get:
tags:
- Tasks
@@ -1888,6 +1882,44 @@ paths:
schema:
$ref: "#/components/schemas/OkTrue"
+ /tasks/{process_instance_id}/{task_guid}/save-draft:
+ parameters:
+ - name: task_guid
+ in: path
+ required: true
+ description: The unique id of an existing process group.
+ schema:
+ type: string
+ - name: process_instance_id
+ in: path
+ required: true
+ description: The unique id of an existing process instance.
+ schema:
+ type: integer
+ post:
+ tags:
+ - Tasks
+ operationId: spiffworkflow_backend.routes.tasks_controller.task_save_draft
+ summary: Update the draft form for this task
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ProcessGroup"
+ responses:
+ "200":
+ description: One task
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Task"
+ "202":
+ description: "ok: true"
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/OkTrue"
+
/tasks/{process_instance_id}/send-user-signal-event:
parameters:
- name: process_instance_id
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py
index 9b4402367..c5805a606 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/json_data.py
@@ -37,6 +37,10 @@ class JsonDataDict(TypedDict):
# for added_key in added_keys:
# added[added_key] = b[added_key]
# final_tuple = [added, removed, changed]
+
+
+# to find the users of this model run:
+# grep -R '_data_hash: ' src/spiffworkflow_backend/models/
class JsonDataModel(SpiffworkflowBaseDBModel):
__tablename__ = "json_data"
id: int = db.Column(db.Integer, primary_key=True)
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py
index 359b20d3a..8e9d6359f 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/task.py
@@ -63,11 +63,13 @@ class TaskModel(SpiffworkflowBaseDBModel):
json_data_hash: str = db.Column(db.String(255), nullable=False, index=True)
python_env_data_hash: str = db.Column(db.String(255), nullable=False, index=True)
+ saved_form_data_hash: str | None = db.Column(db.String(255), nullable=True, index=True)
start_in_seconds: float | None = db.Column(db.DECIMAL(17, 6))
end_in_seconds: float | None = db.Column(db.DECIMAL(17, 6))
data: dict | None = None
+ saved_form_data: dict | None = None
# these are here to be compatible with task api
form_schema: dict | None = None
@@ -89,6 +91,11 @@ class TaskModel(SpiffworkflowBaseDBModel):
def json_data(self) -> dict:
return JsonDataModel.find_data_dict_by_hash(self.json_data_hash)
+ def get_saved_form_data(self) -> dict | None:
+ if self.saved_form_data_hash is not None:
+ return JsonDataModel.find_data_dict_by_hash(self.saved_form_data_hash)
+ return None
+
class Task:
HUMAN_TASK_TYPES = ["User Task", "Manual Task"]
diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py
index 7f1f12c61..2427c10b5 100644
--- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py
+++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/tasks_controller.py
@@ -286,6 +286,7 @@ def task_show(process_instance_id: int, task_guid: str = "next") -> flask.wrappe
can_complete = False
task_model.data = task_model.get_data()
+ task_model.saved_form_data = task_model.get_saved_form_data()
task_model.process_model_display_name = process_model.display_name
task_model.process_model_identifier = process_model.id
task_model.typename = task_definition.typename
@@ -463,11 +464,49 @@ def interstitial(process_instance_id: int) -> Response:
)
+def task_save_draft(
+ process_instance_id: int,
+ task_guid: str,
+ body: dict[str, Any],
+) -> flask.wrappers.Response:
+ principal = _find_principal_or_raise()
+ process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
+ if not process_instance.can_submit_task():
+ raise ApiError(
+ error_code="process_instance_not_runnable",
+ message=(
+ f"Process Instance ({process_instance.id}) has status "
+ f"{process_instance.status} which does not allow tasks to be submitted."
+ ),
+ status_code=400,
+ )
+ AuthorizationService.assert_user_can_complete_task(process_instance.id, task_guid, principal.user)
+ task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
+ json_data_dict = TaskService.update_task_data_on_task_model_and_return_dict_if_updated(
+ task_model, body, "saved_form_data_hash"
+ )
+ if json_data_dict is not None:
+ JsonDataModel.insert_or_update_json_data_dict(json_data_dict)
+ db.session.add(task_model)
+ db.session.commit()
+
+ return Response(
+ json.dumps(
+ {
+ "ok": True,
+ "process_model_identifier": process_instance.process_model_identifier,
+ "process_instance_id": process_instance_id,
+ }
+ ),
+ status=200,
+ mimetype="application/json",
+ )
+
+
def _task_submit_shared(
process_instance_id: int,
task_guid: str,
body: dict[str, Any],
- save_as_draft: bool = False,
) -> flask.wrappers.Response:
principal = _find_principal_or_raise()
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
@@ -494,59 +533,34 @@ def _task_submit_shared(
)
)
- # multi-instance code from crconnect - we may need it or may not
- # if terminate_loop and spiff_task.is_looping():
- # spiff_task.terminate_loop()
- #
- # If we need to update all tasks, then get the next ready task and if it a multi-instance with the same
- # task spec, complete that form as well.
- # if update_all:
- # last_index = spiff_task.task_info()["mi_index"]
- # next_task = processor.next_task()
- # while next_task and next_task.task_info()["mi_index"] > last_index:
- # __update_task(processor, next_task, form_data, user)
- # last_index = next_task.task_info()["mi_index"]
- # next_task = processor.next_task()
+ human_task = _find_human_task_or_raise(
+ process_instance_id=process_instance_id,
+ task_guid=task_guid,
+ only_tasks_that_can_be_completed=True,
+ )
- if save_as_draft:
- task_model = _get_task_model_from_guid_or_raise(task_guid, process_instance_id)
- ProcessInstanceService.update_form_task_data(process_instance, spiff_task, body, g.user)
- json_data_dict = TaskService.update_task_data_on_task_model_and_return_dict_if_updated(
- task_model, spiff_task.data, "json_data_hash"
- )
- if json_data_dict is not None:
- JsonDataModel.insert_or_update_json_data_dict(json_data_dict)
- db.session.add(task_model)
- db.session.commit()
- else:
- human_task = _find_human_task_or_raise(
- process_instance_id=process_instance_id,
- task_guid=task_guid,
- only_tasks_that_can_be_completed=True,
- )
+ with sentry_sdk.start_span(op="task", description="complete_form_task"):
+ with ProcessInstanceQueueService.dequeued(process_instance):
+ ProcessInstanceService.complete_form_task(
+ processor=processor,
+ spiff_task=spiff_task,
+ data=body,
+ user=g.user,
+ human_task=human_task,
+ )
- with sentry_sdk.start_span(op="task", description="complete_form_task"):
- with ProcessInstanceQueueService.dequeued(process_instance):
- ProcessInstanceService.complete_form_task(
- processor=processor,
- spiff_task=spiff_task,
- data=body,
- user=g.user,
- human_task=human_task,
- )
-
- next_human_task_assigned_to_me = (
- HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, completed=False)
- .order_by(asc(HumanTaskModel.id)) # type: ignore
- .join(HumanTaskUserModel)
- .filter_by(user_id=principal.user_id)
- .first()
- )
- if next_human_task_assigned_to_me:
- return make_response(jsonify(HumanTaskModel.to_task(next_human_task_assigned_to_me)), 200)
- elif processor.next_task():
- task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
- return make_response(jsonify(task), 200)
+ next_human_task_assigned_to_me = (
+ HumanTaskModel.query.filter_by(process_instance_id=process_instance_id, completed=False)
+ .order_by(asc(HumanTaskModel.id)) # type: ignore
+ .join(HumanTaskUserModel)
+ .filter_by(user_id=principal.user_id)
+ .first()
+ )
+ if next_human_task_assigned_to_me:
+ return make_response(jsonify(HumanTaskModel.to_task(next_human_task_assigned_to_me)), 200)
+ elif processor.next_task():
+ task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
+ return make_response(jsonify(task), 200)
return Response(
json.dumps(
@@ -565,10 +579,9 @@ def task_submit(
process_instance_id: int,
task_guid: str,
body: dict[str, Any],
- save_as_draft: bool = False,
) -> flask.wrappers.Response:
with sentry_sdk.start_span(op="controller_action", description="tasks_controller.task_submit"):
- return _task_submit_shared(process_instance_id, task_guid, body, save_as_draft)
+ return _task_submit_shared(process_instance_id, task_guid, body)
def _get_tasks(
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py
index cf4728aaa..624595847 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py
@@ -61,7 +61,7 @@ class BaseTest:
process_model_id: str | None = "random_fact",
bpmn_file_name: str | None = None,
bpmn_file_location: str | None = None,
- ) -> str:
+ ) -> ProcessModelInfo:
"""Creates a process group.
Creates a process model
@@ -83,13 +83,13 @@ class BaseTest:
user=user,
)
- load_test_spec(
+ process_model = load_test_spec(
process_model_id=process_model_identifier,
bpmn_file_name=bpmn_file_name,
process_model_source_directory=bpmn_file_location,
)
- return process_model_identifier
+ return process_model
def create_process_group(
self,
@@ -188,7 +188,7 @@ class BaseTest:
process_model_id: str,
process_model_location: str | None = None,
process_model: ProcessModelInfo | None = None,
- file_name: str = "random_fact.svg",
+ file_name: str = "random_fact.bpmn",
file_data: bytes = b"abcdef",
user: UserModel | None = None,
) -> Any:
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/example_data.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/example_data.py
index 8241c0fcc..78320ebf3 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/example_data.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/example_data.py
@@ -61,6 +61,9 @@ class ExampleDataLoader:
)
files = sorted(glob.glob(file_glob))
+
+ if len(files) == 0:
+ raise Exception(f"Could not find any files with file_glob: {file_glob}")
for file_path in files:
if os.path.isdir(file_path):
continue # Don't try to process sub directories
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py
index 51f35e521..6993e3dc0 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_nested_groups.py
@@ -24,7 +24,7 @@ class TestNestedGroups(BaseTest):
process_model_id = "manual_task"
bpmn_file_name = "manual_task.bpmn"
bpmn_file_location = "manual_task"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -34,13 +34,13 @@ class TestNestedGroups(BaseTest):
)
response = self.create_process_instance_from_process_model_id_with_api(
client,
- process_model_identifier,
+ process_model.id,
self.logged_in_headers(with_super_admin_user),
)
process_instance_id = response.json["id"]
client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
process_instance = ProcessInstanceService().get_process_instance(process_instance_id)
@@ -79,7 +79,7 @@ class TestNestedGroups(BaseTest):
process_model_id = "manual_task"
bpmn_file_name = "manual_task.bpmn"
bpmn_file_location = "manual_task"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -89,7 +89,7 @@ class TestNestedGroups(BaseTest):
)
response = self.create_process_instance_from_process_model_id_with_api(
client,
- process_model_identifier,
+ process_model.id,
self.logged_in_headers(with_super_admin_user),
)
process_instance_id = response.json["id"]
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
index 18b313bc1..a32bb75fe 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py
@@ -12,8 +12,6 @@ from flask.testing import FlaskClient
from SpiffWorkflow.task import TaskState # type: ignore
from spiffworkflow_backend.exceptions.process_entity_not_found_error import ProcessEntityNotFoundError
from spiffworkflow_backend.models.db import db
-from spiffworkflow_backend.models.group import GroupModel
-from spiffworkflow_backend.models.human_task import HumanTaskModel
from spiffworkflow_backend.models.process_group import ProcessGroup
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
@@ -25,8 +23,6 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfoSchema
from spiffworkflow_backend.models.spec_reference import SpecReferenceCache
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.user import UserModel
-from spiffworkflow_backend.routes.tasks_controller import _dequeued_interstitial_stream
-from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_caller_service import ProcessCallerService
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
@@ -242,9 +238,9 @@ class TestProcessApi(BaseTest):
data = {"file": (io.BytesIO(updated_bpmn_file_data_bytes), bpmn_file_name)}
file_contents_hash = sha256(bpmn_file_data_bytes).hexdigest()
- modified_process_model_id = process_model_identifier.replace("/", ":")
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.put(
- f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}",
data=data,
follow_redirects=True,
content_type="multipart/form-data",
@@ -276,7 +272,7 @@ class TestProcessApi(BaseTest):
assert process_model.id == process_model_identifier
# delete the model
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.delete(
f"/v1.0/process-models/{modified_process_model_identifier}",
headers=self.logged_in_headers(with_super_admin_user),
@@ -304,7 +300,7 @@ class TestProcessApi(BaseTest):
self.create_spec_file(
client=client,
process_model_id=process_model_identifier,
- process_model_location=test_process_model_id,
+ process_model_location=bpmn_file_location,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
@@ -360,7 +356,7 @@ class TestProcessApi(BaseTest):
process_model.primary_process_id = "superduper"
process_model.metadata_extraction_paths = [{"key": "extraction1", "path": "path1"}]
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.put(
f"/v1.0/process-models/{modified_process_model_identifier}",
headers=self.logged_in_headers(with_super_admin_user),
@@ -752,12 +748,12 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- modified_process_model_id = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
data = {"key1": "THIS DATA"}
response = client.put(
- f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg?file_contents_hash=does_not_matter",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact.svg?file_contents_hash=does_not_matter",
data=data,
follow_redirects=True,
content_type="multipart/form-data",
@@ -774,12 +770,12 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- modified_process_model_id = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
data = {"file": (io.BytesIO(b""), "random_fact.svg")}
response = client.put(
- f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg?file_contents_hash=does_not_matter",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact.svg?file_contents_hash=does_not_matter",
data=data,
follow_redirects=True,
content_type="multipart/form-data",
@@ -808,11 +804,11 @@ class TestProcessApi(BaseTest):
bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, process_model_id)
file_contents_hash = sha256(bpmn_file_data_bytes).hexdigest()
- modified_process_model_id = process_model_identifier.replace("/", ":")
+ modified_process_model_identifier = process_model_identifier.replace("/", ":")
new_file_contents = b"THIS_IS_NEW_DATA"
data = {"file": (io.BytesIO(new_file_contents), bpmn_file_name)}
response = client.put(
- f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}",
data=data,
follow_redirects=True,
content_type="multipart/form-data",
@@ -824,7 +820,7 @@ class TestProcessApi(BaseTest):
assert response.json["file_contents"] is not None
response = client.get(
- f"/v1.0/process-models/{modified_process_model_id}/files/simple_form.json",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/simple_form.json",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@@ -838,8 +834,8 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- bad_process_model_identifier = f"x{process_model_identifier}"
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ bad_process_model_identifier = f"x{process_model.id}"
modified_bad_process_model_identifier = bad_process_model_identifier.replace("/", ":")
response = client.delete(
f"/v1.0/process-models/{modified_bad_process_model_identifier}/files/random_fact.svg",
@@ -858,8 +854,8 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.delete(
f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact_DOES_NOT_EXIST.svg",
@@ -878,9 +874,9 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- process_model = ProcessModelService.get_process_model(process_model_id=process_model_identifier)
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ process_model = ProcessModelService.get_process_model(process_model_id=process_model.id)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.delete(
f"/v1.0/process-models/{modified_process_model_identifier}/files/{process_model.primary_file_name}",
@@ -899,19 +895,20 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
self.create_spec_file(
client,
- process_model_id=process_model_identifier,
- file_name="second_file.json",
+ process_model_id=process_model.id,
+ process_model=process_model,
+ file_name="second_file.bpmn",
file_data=b"
HEY
",
user=with_super_admin_user,
)
response = client.delete(
- f"/v1.0/process-models/{modified_process_model_identifier}/files/second_file.json",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/second_file.bpmn",
follow_redirects=True,
headers=self.logged_in_headers(with_super_admin_user),
)
@@ -921,7 +918,7 @@ class TestProcessApi(BaseTest):
assert response.json["ok"]
response = client.get(
- f"/v1.0/process-models/{modified_process_model_identifier}/files/second_file.json",
+ f"/v1.0/process-models/{modified_process_model_identifier}/files/second_file.bpmn",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 404
@@ -933,8 +930,8 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.get(
f"/v1.0/process-models/{modified_process_model_identifier}/files/random_fact.bpmn",
@@ -952,8 +949,8 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.post(
f"/v1.0/process-instances/{modified_process_model_identifier}",
@@ -1082,8 +1079,8 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
- process_group_id, process_model_id = os.path.split(process_model_identifier)
+ process_model = self.create_group_and_model_with_bpmn(client, with_super_admin_user)
+ process_group_id, process_model_id = os.path.split(process_model.id)
response = client.get(
f"/v1.0/process-groups/{process_group_id}",
@@ -1093,7 +1090,7 @@ class TestProcessApi(BaseTest):
assert response.status_code == 200
assert response.json is not None
assert response.json["id"] == process_group_id
- assert response.json["process_models"][0]["id"] == process_model_identifier
+ assert response.json["process_models"][0]["id"] == process_model.id
assert response.json["parent_groups"] == []
def test_get_process_group_show_when_nested(
@@ -1136,10 +1133,10 @@ class TestProcessApi(BaseTest):
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client, with_super_admin_user, bpmn_file_name="random_fact.bpmn"
)
- modified_process_model_identifier = process_model_identifier.replace("/", ":")
+ modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
response = client.get(
f"/v1.0/process-models/{modified_process_model_identifier}",
@@ -1148,7 +1145,7 @@ class TestProcessApi(BaseTest):
assert response.status_code == 200
assert response.json is not None
- assert response.json["id"] == process_model_identifier
+ assert response.json["id"] == process_model.id
assert len(response.json["files"]) == 1
assert response.json["files"][0]["name"] == "random_fact.bpmn"
assert response.json["parent_groups"] == [{"display_name": "test_group", "id": "test_group"}]
@@ -1198,7 +1195,7 @@ class TestProcessApi(BaseTest):
with_super_admin_user: UserModel,
) -> None:
# process_model_id = "runs_without_input/sample"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id="runs_without_input",
@@ -1208,13 +1205,11 @@ class TestProcessApi(BaseTest):
)
headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@@ -1222,7 +1217,7 @@ class TestProcessApi(BaseTest):
assert type(response.json["updated_at_in_seconds"]) is int
assert response.json["updated_at_in_seconds"] > 0
assert response.json["status"] == "complete"
- assert response.json["process_model_identifier"] == process_model_identifier
+ assert response.json["process_model_identifier"] == process_model.id
assert response.json["data"]["Mike"] == "Awesome"
assert response.json["data"]["person"] == "Kevin"
@@ -1235,16 +1230,16 @@ class TestProcessApi(BaseTest):
) -> None:
process_group_id = "simple_script"
process_model_id = "simple_script"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
process_model_id=process_model_id,
)
- modified_process_model_identifier = self.modify_process_identifier_for_path_param(process_model_identifier)
+ modified_process_model_identifier = self.modify_process_identifier_for_path_param(process_model.id)
headers = self.logged_in_headers(with_super_admin_user)
create_response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
+ client, process_model.id, headers
)
assert create_response.json is not None
process_instance_id = create_response.json["id"]
@@ -1259,7 +1254,7 @@ class TestProcessApi(BaseTest):
assert show_response.json is not None
assert show_response.status_code == 200
file_system_root = FileSystemService.root_path()
- file_path = f"{file_system_root}/{process_model_identifier}/{process_model_id}.bpmn"
+ file_path = f"{file_system_root}/{process_model.id}/{process_model_id}.bpmn"
with open(file_path) as f_open:
xml_file_contents = f_open.read()
assert show_response.json["bpmn_xml_file_contents"] == xml_file_contents
@@ -1272,7 +1267,7 @@ class TestProcessApi(BaseTest):
with_super_admin_user: UserModel,
) -> None:
process_model_id = "call_activity_nested"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id="test_group_two",
@@ -1281,10 +1276,10 @@ class TestProcessApi(BaseTest):
)
spec_reference = SpecReferenceCache.query.filter_by(identifier="Level2b").first()
assert spec_reference
- modified_process_model_identifier = self.modify_process_identifier_for_path_param(process_model_identifier)
+ modified_process_model_identifier = self.modify_process_identifier_for_path_param(process_model.id)
headers = self.logged_in_headers(with_super_admin_user)
create_response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
+ client, process_model.id, headers
)
assert create_response.json is not None
assert create_response.status_code == 201
@@ -1301,7 +1296,7 @@ class TestProcessApi(BaseTest):
assert show_response.json is not None
assert show_response.status_code == 200
file_system_root = FileSystemService.root_path()
- process_instance_file_path = f"{file_system_root}/{process_model_identifier}/{process_model_id}.bpmn"
+ process_instance_file_path = f"{file_system_root}/{process_model.id}/{process_model_id}.bpmn"
with open(process_instance_file_path) as f_open:
xml_file_contents = f_open.read()
assert show_response.json["bpmn_xml_file_contents"] != xml_file_contents
@@ -1368,7 +1363,7 @@ class TestProcessApi(BaseTest):
process_model_id = "message_sender"
bpmn_file_name = "message_sender.bpmn"
bpmn_file_location = "message_send_one_conversation"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1386,14 +1381,14 @@ class TestProcessApi(BaseTest):
}
response = self.create_process_instance_from_process_model_id_with_api(
client,
- process_model_identifier,
+ process_model.id,
self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
@@ -1443,7 +1438,7 @@ class TestProcessApi(BaseTest):
process_model_id = "message_sender"
bpmn_file_name = "message_sender.bpmn"
bpmn_file_location = "message_send_one_conversation"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1462,7 +1457,7 @@ class TestProcessApi(BaseTest):
response = self.create_process_instance_from_process_model_id_with_api(
client,
- process_model_identifier,
+ process_model.id,
self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
@@ -1538,7 +1533,7 @@ class TestProcessApi(BaseTest):
process_model_id = "message_sender"
bpmn_file_name = "message_sender.bpmn"
bpmn_file_location = "message_send_one_conversation"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1549,14 +1544,14 @@ class TestProcessApi(BaseTest):
response = self.create_process_instance_from_process_model_id_with_api(
client,
- process_model_identifier,
+ process_model.id,
self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@@ -1571,7 +1566,7 @@ class TestProcessApi(BaseTest):
assert len(all_tasks) == 8
response = client.post(
- f"/v1.0/process-instance-terminate/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
+ f"/v1.0/process-instance-terminate/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@@ -1598,7 +1593,7 @@ class TestProcessApi(BaseTest):
process_group_id = "my_process_group"
process_model_id = "sample"
bpmn_file_location = "sample"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1607,181 +1602,24 @@ class TestProcessApi(BaseTest):
)
headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
assert response.json is not None
process_instance_id = response.json["id"]
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.json is not None
assert response.status_code == 200
delete_response = client.delete(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert delete_response.json["ok"] is True
assert delete_response.status_code == 200
- def test_task_show(
- self,
- app: Flask,
- client: FlaskClient,
- with_db_and_bpmn_file_cleanup: None,
- with_super_admin_user: UserModel,
- ) -> None:
- process_group_id = "my_process_group"
- process_model_id = "dynamic_enum_select_fields"
- bpmn_file_location = "dynamic_enum_select_fields"
- process_model_identifier = self.create_group_and_model_with_bpmn(
- client,
- with_super_admin_user,
- process_group_id=process_group_id,
- process_model_id=process_model_id,
- # bpmn_file_name=bpmn_file_name,
- bpmn_file_location=bpmn_file_location,
- )
-
- headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
- assert response.json is not None
- process_instance_id = response.json["id"]
-
- response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
- headers=self.logged_in_headers(with_super_admin_user),
- )
- # Call this to assure all engine-steps are fully processed.
- _dequeued_interstitial_stream(process_instance_id)
- assert response.json is not None
- assert response.json["next_task"] is not None
-
- human_tasks = (
- db.session.query(HumanTaskModel).filter(HumanTaskModel.process_instance_id == process_instance_id).all()
- )
- assert len(human_tasks) == 1
- human_task = human_tasks[0]
- response = client.get(
- f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
- headers=self.logged_in_headers(with_super_admin_user),
- )
- assert response.status_code == 200
- assert response.json is not None
- assert response.json["form_schema"]["definitions"]["Color"]["anyOf"][1]["title"] == "Green"
-
- # if you set this in task data:
- # form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes", "building.floor"]
- # you will get this ui schema:
- assert response.json["form_ui_schema"] == {
- "building": {"floor": {"ui:widget": "hidden"}},
- "veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"},
- }
-
- def test_interstitial_page(
- self,
- app: Flask,
- client: FlaskClient,
- with_db_and_bpmn_file_cleanup: None,
- with_super_admin_user: UserModel,
- ) -> None:
- process_group_id = "my_process_group"
- process_model_id = "interstitial"
- bpmn_file_location = "interstitial"
- # Assure we have someone in the finance team
- finance_user = self.find_or_create_user("testuser2")
- AuthorizationService.import_permissions_from_yaml_file()
- process_model_identifier = self.create_group_and_model_with_bpmn(
- client,
- with_super_admin_user,
- process_group_id=process_group_id,
- process_model_id=process_model_id,
- bpmn_file_location=bpmn_file_location,
- )
- headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
- assert response.json is not None
- process_instance_id = response.json["id"]
-
- response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
- headers=headers,
- )
-
- assert response.json is not None
- assert response.json["next_task"] is not None
- assert response.json["next_task"]["state"] == "READY"
- assert response.json["next_task"]["title"] == "Script Task #2"
-
- # Rather that call the API and deal with the Server Side Events, call the loop directly and covert it to
- # a list. It tests all of our code. No reason to test Flasks SSE support.
- stream_results = _dequeued_interstitial_stream(process_instance_id)
- results = list(stream_results)
- # strip the "data:" prefix and convert remaining string to dict.
- json_results = [json.loads(x[5:]) for x in results] # type: ignore
- # There should be 2 results back -
- # the first script task should not be returned (it contains no end user instructions)
- # The second script task should produce rendered jinja text
- # The Manual Task should then return a message as well.
- assert len(results) == 2
- assert json_results[0]["task"]["state"] == "READY"
- assert json_results[0]["task"]["title"] == "Script Task #2"
- assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am Script Task 2"
- assert json_results[1]["task"]["state"] == "READY"
- assert json_results[1]["task"]["title"] == "Manual Task"
-
- response = client.put(
- f"/v1.0/tasks/{process_instance_id}/{json_results[1]['task']['id']}",
- headers=headers,
- )
-
- assert response.json is not None
-
- # we should now be on a task that does not belong to the original user, and the interstitial page should know this.
- results = list(_dequeued_interstitial_stream(process_instance_id))
- json_results = [json.loads(x[5:]) for x in results] # type: ignore
- assert len(results) == 1
- assert json_results[0]["task"]["state"] == "READY"
- assert json_results[0]["task"]["can_complete"] is False
- assert json_results[0]["task"]["title"] == "Please Approve"
- assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane"
-
- # Suspending the task should still report that the user can not complete the task.
- process_instance = ProcessInstanceModel.query.filter_by(id=process_instance_id).first()
- processor = ProcessInstanceProcessor(process_instance)
- processor.suspend()
- processor.save()
-
- results = list(_dequeued_interstitial_stream(process_instance_id))
- json_results = [json.loads(x[5:]) for x in results] # type: ignore
- assert len(results) == 1
- assert json_results[0]["task"]["state"] == "READY"
- assert json_results[0]["task"]["can_complete"] is False
- assert json_results[0]["task"]["title"] == "Please Approve"
- assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane"
-
- # Complete task as the finance user.
- response = client.put(
- f"/v1.0/tasks/{process_instance_id}/{json_results[0]['task']['id']}",
- headers=self.logged_in_headers(finance_user),
- )
-
- # We should now be on the end task with a valid message, even after loading it many times.
- list(_dequeued_interstitial_stream(process_instance_id))
- list(_dequeued_interstitial_stream(process_instance_id))
- results = list(_dequeued_interstitial_stream(process_instance_id))
- json_results = [json.loads(x[5:]) for x in results] # type: ignore
- assert len(json_results) == 1
- assert json_results[0]["task"]["state"] == "COMPLETED"
- assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am the end task"
-
def test_process_instance_list_with_default_list(
self,
app: Flask,
@@ -1792,7 +1630,7 @@ class TestProcessApi(BaseTest):
process_group_id = "runs_without_input"
process_model_id = "sample"
bpmn_file_location = "sample"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1801,7 +1639,7 @@ class TestProcessApi(BaseTest):
)
headers = self.logged_in_headers(with_super_admin_user)
- self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers)
+ self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
response = self.post_to_process_instance_list(client, with_super_admin_user)
assert len(response.json["results"]) == 1
@@ -1811,7 +1649,7 @@ class TestProcessApi(BaseTest):
process_instance_dict = response.json["results"][0]
assert type(process_instance_dict["id"]) is int
- assert process_instance_dict["process_model_identifier"] == process_model_identifier
+ assert process_instance_dict["process_model_identifier"] == process_model.id
assert type(process_instance_dict["start_in_seconds"]) is int
assert process_instance_dict["start_in_seconds"] > 0
assert process_instance_dict["end_in_seconds"] is None
@@ -1828,7 +1666,7 @@ class TestProcessApi(BaseTest):
process_model_id = "sample"
bpmn_file_name = "sample.bpmn"
bpmn_file_location = "sample"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1837,11 +1675,11 @@ class TestProcessApi(BaseTest):
bpmn_file_location=bpmn_file_location,
)
headers = self.logged_in_headers(with_super_admin_user)
- self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers)
- self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers)
- self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers)
- self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers)
- self.create_process_instance_from_process_model_id_with_api(client, process_model_identifier, headers)
+ self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
response = self.post_to_process_instance_list(client, with_super_admin_user, param_string="?per_page=2&page=3")
assert len(response.json["results"]) == 1
@@ -1866,7 +1704,7 @@ class TestProcessApi(BaseTest):
process_model_id = "sample"
bpmn_file_name = "sample.bpmn"
bpmn_file_location = "sample"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -1881,8 +1719,8 @@ class TestProcessApi(BaseTest):
process_instance = ProcessInstanceModel(
status=ProcessInstanceStatus[statuses[i]].value,
process_initiator=with_super_admin_user,
- process_model_identifier=process_model_identifier,
- process_model_display_name=process_model_identifier,
+ process_model_identifier=process_model.id,
+ process_model_display_name=process_model.id,
updated_at_in_seconds=round(time.time()),
start_in_seconds=(1000 * i) + 1000,
end_in_seconds=(1000 * i) + 2000,
@@ -1896,7 +1734,7 @@ class TestProcessApi(BaseTest):
"filter_by": [
{
"field_name": "process_model_identifier",
- "field_value": process_model_identifier,
+ "field_value": process_model.id,
"operator": "equals",
}
],
@@ -1916,7 +1754,7 @@ class TestProcessApi(BaseTest):
"filter_by": [
{
"field_name": "process_model_identifier",
- "field_value": process_model_identifier,
+ "field_value": process_model.id,
"operator": "equals",
},
{
@@ -1939,7 +1777,7 @@ class TestProcessApi(BaseTest):
"filter_by": [
{
"field_name": "process_model_identifier",
- "field_value": process_model_identifier,
+ "field_value": process_model.id,
"operator": "equals",
},
{"field_name": "process_status", "field_value": "not_started,complete", "operator": "equals"},
@@ -2041,7 +1879,7 @@ class TestProcessApi(BaseTest):
process_model_id = "sample"
bpmn_file_name = "sample.bpmn"
bpmn_file_location = "sample"
- process_model_identifier = self.create_group_and_model_with_bpmn( # noqa: F841
+ process_model = self.create_group_and_model_with_bpmn( # noqa: F841
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -2079,7 +1917,7 @@ class TestProcessApi(BaseTest):
process_model_id = "error"
bpmn_file_name = "error.bpmn"
bpmn_file_location = "error"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -2088,14 +1926,14 @@ class TestProcessApi(BaseTest):
bpmn_file_location=bpmn_file_location,
)
- process_instance_id = self._setup_testing_instance(client, process_model_identifier, with_super_admin_user)
+ process_instance_id = self._setup_testing_instance(client, process_model.id, with_super_admin_user)
process = db.session.query(ProcessInstanceModel).filter(ProcessInstanceModel.id == process_instance_id).first()
assert process is not None
assert process.status == "not_started"
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 400
@@ -2119,7 +1957,7 @@ class TestProcessApi(BaseTest):
process_model_id = "error"
bpmn_file_name = "error.bpmn"
bpmn_file_location = "error"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -2128,8 +1966,8 @@ class TestProcessApi(BaseTest):
bpmn_file_location=bpmn_file_location,
)
- process_instance_id = self._setup_testing_instance(client, process_model_identifier, with_super_admin_user)
- process_model = ProcessModelService.get_process_model(process_model_identifier)
+ process_instance_id = self._setup_testing_instance(client, process_model.id, with_super_admin_user)
+ process_model = ProcessModelService.get_process_model(process_model.id)
ProcessModelService.update_process_model(
process_model,
{"fault_or_suspend_on_exception": NotificationType.suspend.value},
@@ -2140,7 +1978,7 @@ class TestProcessApi(BaseTest):
assert process.status == "not_started"
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 400
@@ -2194,7 +2032,7 @@ class TestProcessApi(BaseTest):
file_data = b"abc123"
bpmn_file_name = "hello_world.bpmn"
bpmn_file_location = "hello_world"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client,
with_super_admin_user,
process_group_id=process_group_id,
@@ -2205,13 +2043,15 @@ class TestProcessApi(BaseTest):
result = self.create_spec_file(
client,
- process_model_id=process_model_identifier,
+ process_model_id=process_model.id,
+ process_model=process_model,
+ process_model_location=bpmn_file_location,
file_name=file_name,
file_data=file_data,
user=with_super_admin_user,
)
- assert result["process_model_id"] == process_model_identifier
+ assert result["process_model_id"] == process_model.id
assert result["name"] == file_name
assert bytes(str(result["file_contents"]), "utf-8") == file_data
@@ -2295,103 +2135,6 @@ class TestProcessApi(BaseTest):
# + 2 -Two messages logged for the API Calls used to create the processes.
assert len(response.json["results"]) == 6
- # @pytest.mark.skipif(
- # os.environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres",
- # reason="look at comment in tasks_controller method task_list_my_tasks",
- # )
- def test_correct_user_can_get_and_update_a_task(
- self,
- app: Flask,
- client: FlaskClient,
- with_db_and_bpmn_file_cleanup: None,
- with_super_admin_user: UserModel,
- ) -> None:
- initiator_user = self.find_or_create_user("testuser4")
- finance_user = self.find_or_create_user("testuser2")
- assert initiator_user.principal is not None
- assert finance_user.principal is not None
- AuthorizationService.import_permissions_from_yaml_file()
-
- finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
- assert finance_group is not None
-
- process_group_id = "finance"
- process_model_id = "model_with_lanes"
- bpmn_file_name = "lanes.bpmn"
- bpmn_file_location = "model_with_lanes"
- process_model_identifier = self.create_group_and_model_with_bpmn(
- client,
- with_super_admin_user,
- process_group_id=process_group_id,
- process_model_id=process_model_id,
- bpmn_file_name=bpmn_file_name,
- bpmn_file_location=bpmn_file_location,
- )
-
- response = self.create_process_instance_from_process_model_id_with_api(
- client,
- process_model_identifier,
- headers=self.logged_in_headers(initiator_user),
- )
- assert response.status_code == 201
-
- assert response.json is not None
- process_instance_id = response.json["id"]
- response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
- headers=self.logged_in_headers(initiator_user),
- )
- assert response.status_code == 200
-
- response = client.get(
- "/v1.0/tasks",
- headers=self.logged_in_headers(finance_user),
- )
- assert response.status_code == 200
- assert response.json is not None
- assert len(response.json["results"]) == 0
-
- response = client.get(
- "/v1.0/tasks",
- headers=self.logged_in_headers(initiator_user),
- )
- assert response.status_code == 200
- assert response.json is not None
- assert len(response.json["results"]) == 1
-
- task_id = response.json["results"][0]["id"]
- assert task_id is not None
-
- response = client.put(
- f"/v1.0/tasks/{process_instance_id}/{task_id}",
- headers=self.logged_in_headers(finance_user),
- )
- assert response.status_code == 500
- assert response.json
- assert "UserDoesNotHaveAccessToTaskError" in response.json["message"]
-
- response = client.put(
- f"/v1.0/tasks/{process_instance_id}/{task_id}",
- headers=self.logged_in_headers(initiator_user),
- )
- assert response.status_code == 200
-
- response = client.get(
- "/v1.0/tasks",
- headers=self.logged_in_headers(initiator_user),
- )
- assert response.status_code == 200
- assert response.json is not None
- assert len(response.json["results"]) == 0
-
- response = client.get(
- "/v1.0/tasks",
- headers=self.logged_in_headers(finance_user),
- )
- assert response.status_code == 200
- assert response.json is not None
- assert len(response.json["results"]) == 1
-
# TODO: test the auth callback endpoint
# def test_can_store_authentication_secret(
# self, app: Flask, client: FlaskClient, with_db_and_bpmn_file_cleanup: None
@@ -2514,7 +2257,7 @@ class TestProcessApi(BaseTest):
) -> None:
bpmn_file_name = "manual_task.bpmn"
bpmn_file_location = "manual_task"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_model_id="manual_task",
@@ -2525,22 +2268,20 @@ class TestProcessApi(BaseTest):
bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, bpmn_file_location)
self.create_spec_file(
client=client,
- process_model_id=process_model_identifier,
- process_model_location=process_model_identifier,
+ process_model_id=process_model.id,
+ process_model_location=bpmn_file_location,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
)
headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
assert response.json is not None
process_instance_id = response.json["id"]
client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@@ -2548,14 +2289,14 @@ class TestProcessApi(BaseTest):
assert process_instance.status == "user_input_required"
client.post(
- f"/v1.0/process-instance-suspend/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
+ f"/v1.0/process-instance-suspend/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
process_instance = ProcessInstanceService().get_process_instance(process_instance_id)
assert process_instance.status == "suspended"
response = client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
process_instance = ProcessInstanceService().get_process_instance(process_instance_id)
@@ -2563,7 +2304,7 @@ class TestProcessApi(BaseTest):
assert response.status_code == 400
response = client.post(
- f"/v1.0/process-instance-resume/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
+ f"/v1.0/process-instance-resume/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@@ -2581,7 +2322,7 @@ class TestProcessApi(BaseTest):
process_model_id = "simple_script"
bpmn_file_name = "simple_script.bpmn"
bpmn_file_location = "simple_script"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id=process_group_id,
@@ -2593,8 +2334,8 @@ class TestProcessApi(BaseTest):
bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, bpmn_file_location)
self.create_spec_file(
client=client,
- process_model_id=process_model_identifier,
- process_model_location=process_model_identifier,
+ process_model_id=process_model.id,
+ process_model_location=bpmn_file_location,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
@@ -2637,7 +2378,7 @@ class TestProcessApi(BaseTest):
process_model_id = "process_navigation"
bpmn_file_name = "process_navigation.bpmn"
bpmn_file_location = "process_navigation"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id=process_group_id,
@@ -2649,21 +2390,19 @@ class TestProcessApi(BaseTest):
bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, bpmn_file_location)
self.create_spec_file(
client=client,
- process_model_id=process_model_identifier,
- process_model_location=process_model_identifier,
+ process_model_id=process_model.id,
+ process_model_location=bpmn_file_location,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
)
headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
process_instance_id = response.json["id"]
client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
@@ -2678,7 +2417,7 @@ class TestProcessApi(BaseTest):
"typename": "MessageEventDefinition",
}
response = client.post(
- f"/v1.0/send-event/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}",
+ f"/v1.0/send-event/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
data=json.dumps(data),
@@ -2689,13 +2428,13 @@ class TestProcessApi(BaseTest):
assert response.json["state"] == "COMPLETED"
response = client.get(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info?all_tasks=true",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/task-info?all_tasks=true",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
end_task = next(task for task in response.json if task["bpmn_identifier"] == "Event_174a838")
response = client.get(
- f"/v1.0/task-data/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{end_task['guid']}",
+ f"/v1.0/task-data/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/{end_task['guid']}",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@@ -2713,7 +2452,7 @@ class TestProcessApi(BaseTest):
process_model_id = "manual_task"
bpmn_file_name = "manual_task.bpmn"
bpmn_file_location = "manual_task"
- process_model_identifier = self.create_group_and_model_with_bpmn(
+ process_model = self.create_group_and_model_with_bpmn(
client=client,
user=with_super_admin_user,
process_group_id=process_group_id,
@@ -2725,33 +2464,31 @@ class TestProcessApi(BaseTest):
bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, bpmn_file_location)
self.create_spec_file(
client=client,
- process_model_id=process_model_identifier,
- process_model_location=process_model_identifier,
+ process_model_id=process_model.id,
+ process_model_location=bpmn_file_location,
file_name=bpmn_file_name,
file_data=bpmn_file_data_bytes,
user=with_super_admin_user,
)
headers = self.logged_in_headers(with_super_admin_user)
- response = self.create_process_instance_from_process_model_id_with_api(
- client, process_model_identifier, headers
- )
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
process_instance_id = response.json["id"]
client.post(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/run",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
headers=self.logged_in_headers(with_super_admin_user),
)
response = client.get(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/task-info",
headers=self.logged_in_headers(with_super_admin_user),
)
assert len(response.json) == 7
human_task = next(task for task in response.json if task["bpmn_identifier"] == "manual_task_one")
response = client.post(
- f"/v1.0/task-complete/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/{human_task['guid']}",
+ f"/v1.0/task-complete/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/{human_task['guid']}",
headers=self.logged_in_headers(with_super_admin_user),
content_type="application/json",
data=json.dumps({"execute": False}),
@@ -2762,7 +2499,7 @@ class TestProcessApi(BaseTest):
assert task_model.state == "COMPLETED"
response = client.get(
- f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model_identifier)}/{process_instance_id}/task-info",
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/task-info",
headers=self.logged_in_headers(with_super_admin_user),
)
assert response.status_code == 200
@@ -2899,7 +2636,7 @@ class TestProcessApi(BaseTest):
# process_model_id = "hello_world"
# bpmn_file_name = "hello_world.bpmn"
# bpmn_file_location = "hello_world"
- # process_model_identifier = self.create_group_and_model_with_bpmn(
+ # process_model = self.create_group_and_model_with_bpmn(
# client=client,
# user=with_super_admin_user,
# process_group_id=sub_process_group_id,
@@ -2908,7 +2645,7 @@ class TestProcessApi(BaseTest):
# bpmn_file_location=bpmn_file_location,
# )
# process_model_absolute_dir = os.path.join(
- # bpmn_root, process_model_identifier
+ # bpmn_root, process_model.id
# )
#
# output = GitService.run_shell_command_to_get_stdout(["git", "status"])
@@ -2942,7 +2679,7 @@ class TestProcessApi(BaseTest):
# assert "nothing to commit" in output
# assert "working tree clean" in output
#
- # # process_model = ProcessModelService.get_process_model(process_model_identifier)
+ # # process_model = ProcessModelService.get_process_model(process_model.id)
#
# listing = os.listdir(process_model_absolute_dir)
# assert len(listing) == 2
@@ -2981,9 +2718,9 @@ class TestProcessApi(BaseTest):
# assert "process_model.json" in listing
# assert "new_file.txt" in listing
#
- # # modified_process_model_id = process_model_identifier.replace("/", ":")
+ # # modified_process_model_identifier = process_model.modify_process_identifier_for_path_param(process_model.id)
# # response = client.post(
- # # f"/v1.0/process-model-publish/{modified_process_model_id}?branch_to_update=staging",
+ # # f"/v1.0/process-model-publish/{modified_process_model_identifier}?branch_to_update=staging",
# # headers=self.logged_in_headers(with_super_admin_user),
# # )
#
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py
new file mode 100644
index 000000000..5d390f186
--- /dev/null
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_tasks_controller.py
@@ -0,0 +1,320 @@
+import json
+
+from flask.app import Flask
+from flask.testing import FlaskClient
+from spiffworkflow_backend.models.db import db
+from spiffworkflow_backend.models.group import GroupModel
+from spiffworkflow_backend.models.human_task import HumanTaskModel
+from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
+from spiffworkflow_backend.models.user import UserModel
+from spiffworkflow_backend.routes.tasks_controller import _dequeued_interstitial_stream
+from spiffworkflow_backend.services.authorization_service import AuthorizationService
+from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
+
+from tests.spiffworkflow_backend.helpers.base_test import BaseTest
+
+
+class TestTasksController(BaseTest):
+ def test_task_show(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ process_group_id = "my_process_group"
+ process_model_id = "dynamic_enum_select_fields"
+ bpmn_file_location = "dynamic_enum_select_fields"
+ process_model = self.create_group_and_model_with_bpmn(
+ client,
+ with_super_admin_user,
+ process_group_id=process_group_id,
+ process_model_id=process_model_id,
+ # bpmn_file_name=bpmn_file_name,
+ bpmn_file_location=bpmn_file_location,
+ )
+
+ headers = self.logged_in_headers(with_super_admin_user)
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ assert response.json is not None
+ process_instance_id = response.json["id"]
+
+ response = client.post(
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ # Call this to assure all engine-steps are fully processed.
+ _dequeued_interstitial_stream(process_instance_id)
+ assert response.json is not None
+ assert response.json["next_task"] is not None
+
+ human_tasks = (
+ db.session.query(HumanTaskModel).filter(HumanTaskModel.process_instance_id == process_instance_id).all()
+ )
+ assert len(human_tasks) == 1
+ human_task = human_tasks[0]
+ response = client.get(
+ f"/v1.0/tasks/{process_instance_id}/{human_task.task_id}",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert response.json["form_schema"]["definitions"]["Color"]["anyOf"][1]["title"] == "Green"
+
+ # if you set this in task data:
+ # form_ui_hidden_fields = ["veryImportantFieldButOnlySometimes", "building.floor"]
+ # you will get this ui schema:
+ assert response.json["form_ui_schema"] == {
+ "building": {"floor": {"ui:widget": "hidden"}},
+ "veryImportantFieldButOnlySometimes": {"ui:widget": "hidden"},
+ }
+
+ def test_interstitial_page(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ process_group_id = "my_process_group"
+ process_model_id = "interstitial"
+ bpmn_file_location = "interstitial"
+ # Assure we have someone in the finance team
+ finance_user = self.find_or_create_user("testuser2")
+ AuthorizationService.import_permissions_from_yaml_file()
+ process_model = self.create_group_and_model_with_bpmn(
+ client,
+ with_super_admin_user,
+ process_group_id=process_group_id,
+ process_model_id=process_model_id,
+ bpmn_file_location=bpmn_file_location,
+ )
+ headers = self.logged_in_headers(with_super_admin_user)
+ response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
+ assert response.json is not None
+ process_instance_id = response.json["id"]
+
+ response = client.post(
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
+ headers=headers,
+ )
+
+ assert response.json is not None
+ assert response.json["next_task"] is not None
+ assert response.json["next_task"]["state"] == "READY"
+ assert response.json["next_task"]["title"] == "Script Task #2"
+
+ # Rather that call the API and deal with the Server Side Events, call the loop directly and covert it to
+ # a list. It tests all of our code. No reason to test Flasks SSE support.
+ stream_results = _dequeued_interstitial_stream(process_instance_id)
+ results = list(stream_results)
+ # strip the "data:" prefix and convert remaining string to dict.
+ json_results = [json.loads(x[5:]) for x in results] # type: ignore
+ # There should be 2 results back -
+ # the first script task should not be returned (it contains no end user instructions)
+ # The second script task should produce rendered jinja text
+ # The Manual Task should then return a message as well.
+ assert len(results) == 2
+ assert json_results[0]["task"]["state"] == "READY"
+ assert json_results[0]["task"]["title"] == "Script Task #2"
+ assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am Script Task 2"
+ assert json_results[1]["task"]["state"] == "READY"
+ assert json_results[1]["task"]["title"] == "Manual Task"
+
+ response = client.put(
+ f"/v1.0/tasks/{process_instance_id}/{json_results[1]['task']['id']}",
+ headers=headers,
+ )
+
+ assert response.json is not None
+
+ # we should now be on a task that does not belong to the original user, and the interstitial page should know this.
+ results = list(_dequeued_interstitial_stream(process_instance_id))
+ json_results = [json.loads(x[5:]) for x in results] # type: ignore
+ assert len(results) == 1
+ assert json_results[0]["task"]["state"] == "READY"
+ assert json_results[0]["task"]["can_complete"] is False
+ assert json_results[0]["task"]["title"] == "Please Approve"
+ assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane"
+
+ # Suspending the task should still report that the user can not complete the task.
+ process_instance = ProcessInstanceModel.query.filter_by(id=process_instance_id).first()
+ processor = ProcessInstanceProcessor(process_instance)
+ processor.suspend()
+ processor.save()
+
+ results = list(_dequeued_interstitial_stream(process_instance_id))
+ json_results = [json.loads(x[5:]) for x in results] # type: ignore
+ assert len(results) == 1
+ assert json_results[0]["task"]["state"] == "READY"
+ assert json_results[0]["task"]["can_complete"] is False
+ assert json_results[0]["task"]["title"] == "Please Approve"
+ assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am a manual task in another lane"
+
+ # Complete task as the finance user.
+ response = client.put(
+ f"/v1.0/tasks/{process_instance_id}/{json_results[0]['task']['id']}",
+ headers=self.logged_in_headers(finance_user),
+ )
+
+ # We should now be on the end task with a valid message, even after loading it many times.
+ list(_dequeued_interstitial_stream(process_instance_id))
+ list(_dequeued_interstitial_stream(process_instance_id))
+ results = list(_dequeued_interstitial_stream(process_instance_id))
+ json_results = [json.loads(x[5:]) for x in results] # type: ignore
+ assert len(json_results) == 1
+ assert json_results[0]["task"]["state"] == "COMPLETED"
+ assert json_results[0]["task"]["properties"]["instructionsForEndUser"] == "I am the end task"
+
+ def test_correct_user_can_get_and_update_a_task(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ initiator_user = self.find_or_create_user("testuser4")
+ finance_user = self.find_or_create_user("testuser2")
+ assert initiator_user.principal is not None
+ assert finance_user.principal is not None
+ AuthorizationService.import_permissions_from_yaml_file()
+
+ finance_group = GroupModel.query.filter_by(identifier="Finance Team").first()
+ assert finance_group is not None
+
+ process_group_id = "finance"
+ process_model_id = "model_with_lanes"
+ bpmn_file_name = "lanes.bpmn"
+ bpmn_file_location = "model_with_lanes"
+ process_model = self.create_group_and_model_with_bpmn(
+ client,
+ with_super_admin_user,
+ process_group_id=process_group_id,
+ process_model_id=process_model_id,
+ bpmn_file_name=bpmn_file_name,
+ bpmn_file_location=bpmn_file_location,
+ )
+
+ response = self.create_process_instance_from_process_model_id_with_api(
+ client,
+ process_model.id,
+ headers=self.logged_in_headers(initiator_user),
+ )
+ assert response.status_code == 201
+
+ assert response.json is not None
+ process_instance_id = response.json["id"]
+ response = client.post(
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
+ headers=self.logged_in_headers(initiator_user),
+ )
+ assert response.status_code == 200
+
+ response = client.get(
+ "/v1.0/tasks",
+ headers=self.logged_in_headers(finance_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert len(response.json["results"]) == 0
+
+ response = client.get(
+ "/v1.0/tasks",
+ headers=self.logged_in_headers(initiator_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert len(response.json["results"]) == 1
+
+ task_id = response.json["results"][0]["id"]
+ assert task_id is not None
+
+ response = client.put(
+ f"/v1.0/tasks/{process_instance_id}/{task_id}",
+ headers=self.logged_in_headers(finance_user),
+ )
+ assert response.status_code == 500
+ assert response.json
+ assert "UserDoesNotHaveAccessToTaskError" in response.json["message"]
+
+ response = client.put(
+ f"/v1.0/tasks/{process_instance_id}/{task_id}",
+ headers=self.logged_in_headers(initiator_user),
+ )
+ assert response.status_code == 200
+
+ response = client.get(
+ "/v1.0/tasks",
+ headers=self.logged_in_headers(initiator_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert len(response.json["results"]) == 0
+
+ response = client.get(
+ "/v1.0/tasks",
+ headers=self.logged_in_headers(finance_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert len(response.json["results"]) == 1
+
+ def test_task_save_draft(
+ self,
+ app: Flask,
+ client: FlaskClient,
+ with_db_and_bpmn_file_cleanup: None,
+ with_super_admin_user: UserModel,
+ ) -> None:
+ process_group_id = "test_group"
+ process_model_id = "simple_form"
+ process_model = self.create_group_and_model_with_bpmn(
+ client,
+ with_super_admin_user,
+ process_group_id=process_group_id,
+ process_model_id=process_model_id,
+ )
+
+ response = self.create_process_instance_from_process_model_id_with_api(
+ client,
+ process_model.id,
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ assert response.status_code == 201
+
+ assert response.json is not None
+ process_instance_id = response.json["id"]
+ response = client.post(
+ f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ assert response.status_code == 200
+
+ response = client.get(
+ "/v1.0/tasks",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert len(response.json["results"]) == 1
+
+ task_id = response.json["results"][0]["id"]
+ assert task_id is not None
+
+ draft_data = {"HEY": "I'm draft"}
+
+ response = client.post(
+ f"/v1.0/tasks/{process_instance_id}/{task_id}/save-draft",
+ headers=self.logged_in_headers(with_super_admin_user),
+ content_type="application/json",
+ data=json.dumps(draft_data),
+ )
+ assert response.status_code == 200
+
+ response = client.get(
+ f"/v1.0/tasks/{process_instance_id}/{task_id}",
+ headers=self.logged_in_headers(with_super_admin_user),
+ )
+ assert response.status_code == 200
+ assert response.json is not None
+ assert response.json["saved_form_data"] == draft_data
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py
index f7becef36..fd49fb864 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_permissions.py
@@ -27,9 +27,9 @@ class TestPermissions(BaseTest):
) -> None:
process_group_id = "group-a"
load_test_spec(
- "group-a/timers_intermediate_catch_event",
- bpmn_file_name="timers_intermediate_catch_event.bpmn",
- process_model_source_directory="timers_intermediate_catch_event",
+ "group-a/timer_intermediate_catch_event",
+ bpmn_file_name="timer_intermediate_catch_event.bpmn",
+ process_model_source_directory="timer_intermediate_catch_event",
)
dan = self.find_or_create_user()
principal = dan.principal
@@ -53,12 +53,15 @@ class TestPermissions(BaseTest):
process_group_ids = ["group-a", "group-b"]
process_group_a_id = process_group_ids[0]
process_group_b_id = process_group_ids[1]
- for process_group_id in process_group_ids:
- load_test_spec(
- f"{process_group_id}/timers_intermediate_catch_event",
- bpmn_file_name="timers_intermediate_catch_event",
- process_model_source_directory="timers_intermediate_catch_event",
- )
+ load_test_spec(
+ f"{process_group_a_id}/timer_intermediate_catch_event",
+ bpmn_file_name="timer_intermediate_catch_event",
+ process_model_source_directory="timer_intermediate_catch_event",
+ )
+ load_test_spec(
+ f"{process_group_b_id}/hello_world",
+ process_model_source_directory="hello_world",
+ )
group_a_admin = self.find_or_create_user()
permission_target = PermissionTargetModel(uri=f"/{process_group_a_id}")
@@ -80,12 +83,15 @@ class TestPermissions(BaseTest):
def test_user_can_be_granted_access_through_a_group(self, app: Flask, with_db_and_bpmn_file_cleanup: None) -> None:
process_group_ids = ["group-a", "group-b"]
process_group_a_id = process_group_ids[0]
- for process_group_id in process_group_ids:
- load_test_spec(
- f"{process_group_id}/timers_intermediate_catch_event",
- bpmn_file_name="timers_intermediate_catch_event.bpmn",
- process_model_source_directory="timers_intermediate_catch_event",
- )
+ load_test_spec(
+ f"{process_group_a_id}/timer_intermediate_catch_event",
+ bpmn_file_name="timer_intermediate_catch_event",
+ process_model_source_directory="timer_intermediate_catch_event",
+ )
+ load_test_spec(
+ f"{process_group_ids[1]}/hello_world",
+ process_model_source_directory="hello_world",
+ )
user = self.find_or_create_user()
group = GroupModel(identifier="groupA")
db.session.add(group)
@@ -118,12 +124,15 @@ class TestPermissions(BaseTest):
process_group_ids = ["group-a", "group-b"]
process_group_a_id = process_group_ids[0]
process_group_b_id = process_group_ids[1]
- for process_group_id in process_group_ids:
- load_test_spec(
- f"{process_group_id}/timers_intermediate_catch_event",
- bpmn_file_name="timers_intermediate_catch_event.bpmn",
- process_model_source_directory="timers_intermediate_catch_event",
- )
+ load_test_spec(
+ f"{process_group_a_id}/timer_intermediate_catch_event",
+ bpmn_file_name="timer_intermediate_catch_event",
+ process_model_source_directory="timer_intermediate_catch_event",
+ )
+ load_test_spec(
+ f"{process_group_b_id}/hello_world",
+ process_model_source_directory="hello_world",
+ )
group_a_admin = self.find_or_create_user()
permission_target = PermissionTargetModel(uri="/%")
diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py
index 25db5fa73..2a409fd3a 100644
--- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py
+++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_task_service.py
@@ -167,11 +167,6 @@ class TestTaskService(BaseTest):
process_model_source_directory="signal_event_extensions",
bpmn_file_name="signal_event_extensions",
)
- load_test_spec(
- "test_group/SpiffCatchEventExtensions",
- process_model_source_directory="call_activity_nested",
- bpmn_file_name="SpiffCatchEventExtensions",
- )
process_instance = self.create_process_instance_from_process_model(process_model)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True, execution_strategy_name="greedy")
diff --git a/spiffworkflow-frontend/src/helpers.test.tsx b/spiffworkflow-frontend/src/helpers.test.tsx
index 5a7889a65..2efb107d2 100644
--- a/spiffworkflow-frontend/src/helpers.test.tsx
+++ b/spiffworkflow-frontend/src/helpers.test.tsx
@@ -3,6 +3,7 @@ import {
isInteger,
slugifyString,
underscorizeString,
+ recursivelyChangeNullAndUndefined,
} from './helpers';
test('it can slugify a string', () => {
@@ -29,3 +30,72 @@ test('it can validate numeric values', () => {
expect(isInteger('1 2')).toEqual(false);
expect(isInteger(2)).toEqual(true);
});
+
+test('it can replace undefined values in object with null', () => {
+ const sampleData = {
+ talentType: 'foo',
+ rating: 'bar',
+ contacts: {
+ gmeets: undefined,
+ zoom: undefined,
+ phone: 'baz',
+ awesome: false,
+ info: '',
+ },
+ items: [
+ undefined,
+ {
+ contacts: {
+ gmeets: undefined,
+ zoom: undefined,
+ phone: 'baz',
+ },
+ },
+ 'HEY',
+ ],
+ undefined,
+ };
+
+ expect((sampleData.items[1] as any).contacts.zoom).toEqual(undefined);
+
+ const result = recursivelyChangeNullAndUndefined(sampleData, null);
+ expect(result).toEqual(sampleData);
+ expect(result.items[1].contacts.zoom).toEqual(null);
+ expect(result.items[2]).toEqual('HEY');
+ expect(result.contacts.awesome).toEqual(false);
+ expect(result.contacts.info).toEqual('');
+});
+
+test('it can replace null values in object with undefined', () => {
+ const sampleData = {
+ talentType: 'foo',
+ rating: 'bar',
+ contacts: {
+ gmeets: null,
+ zoom: null,
+ phone: 'baz',
+ awesome: false,
+ info: '',
+ },
+ items: [
+ null,
+ {
+ contacts: {
+ gmeets: null,
+ zoom: null,
+ phone: 'baz',
+ },
+ },
+ 'HEY',
+ ],
+ };
+
+ expect((sampleData.items[1] as any).contacts.zoom).toEqual(null);
+
+ const result = recursivelyChangeNullAndUndefined(sampleData, undefined);
+ expect(result).toEqual(sampleData);
+ expect(result.items[1].contacts.zoom).toEqual(undefined);
+ expect(result.items[2]).toEqual('HEY');
+ expect(result.contacts.awesome).toEqual(false);
+ expect(result.contacts.info).toEqual('');
+});
diff --git a/spiffworkflow-frontend/src/helpers.tsx b/spiffworkflow-frontend/src/helpers.tsx
index c816302c6..150e31991 100644
--- a/spiffworkflow-frontend/src/helpers.tsx
+++ b/spiffworkflow-frontend/src/helpers.tsx
@@ -11,6 +11,10 @@ import {
DEFAULT_PAGE,
} from './components/PaginationForTable';
+export const doNothing = () => {
+ return undefined;
+};
+
// https://www.30secondsofcode.org/js/s/slugify
export const slugifyString = (str: any) => {
return str
@@ -26,6 +30,24 @@ export const underscorizeString = (inputString: string) => {
return slugifyString(inputString).replace(/-/g, '_');
};
+export const recursivelyChangeNullAndUndefined = (obj: any, newValue: any) => {
+ if (obj === null || obj === undefined) {
+ return newValue;
+ }
+ if (Array.isArray(obj)) {
+ obj.forEach((value: any, index: number) => {
+ // eslint-disable-next-line no-param-reassign
+ obj[index] = recursivelyChangeNullAndUndefined(value, newValue);
+ });
+ } else if (typeof obj === 'object') {
+ Object.entries(obj).forEach(([key, value]) => {
+ // eslint-disable-next-line no-param-reassign
+ obj[key] = recursivelyChangeNullAndUndefined(value, newValue);
+ });
+ }
+ return obj;
+};
+
export const selectKeysFromSearchParams = (obj: any, keys: string[]) => {
const newSearchParams: { [key: string]: string } = {};
keys.forEach((key: string) => {
diff --git a/spiffworkflow-frontend/src/interfaces.ts b/spiffworkflow-frontend/src/interfaces.ts
index 2084c6cc0..13bf9e7c6 100644
--- a/spiffworkflow-frontend/src/interfaces.ts
+++ b/spiffworkflow-frontend/src/interfaces.ts
@@ -67,6 +67,8 @@ export interface Task {
form_schema: any;
form_ui_schema: any;
signal_buttons: SignalButton[];
+
+ saved_form_data?: any;
}
export interface ProcessInstanceTask {
diff --git a/spiffworkflow-frontend/src/routes/TaskShow.tsx b/spiffworkflow-frontend/src/routes/TaskShow.tsx
index 75ffa9bfe..83cf919d5 100644
--- a/spiffworkflow-frontend/src/routes/TaskShow.tsx
+++ b/spiffworkflow-frontend/src/routes/TaskShow.tsx
@@ -12,10 +12,15 @@ import {
ButtonSet,
} from '@carbon/react';
+import { useDebouncedCallback } from 'use-debounce';
import { Form } from '../rjsf/carbon_theme';
import HttpService from '../services/HttpService';
import useAPIError from '../hooks/UseApiError';
-import { modifyProcessIdentifierForPathParam } from '../helpers';
+import {
+ doNothing,
+ modifyProcessIdentifierForPathParam,
+ recursivelyChangeNullAndUndefined,
+} from '../helpers';
import { EventDefinition, Task } from '../interfaces';
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
import InstructionsForEndUser from '../components/InstructionsForEndUser';
@@ -27,7 +32,6 @@ export default function TaskShow() {
const params = useParams();
const navigate = useNavigate();
const [disabled, setDisabled] = useState(false);
- const [noValidate, setNoValidate] = useState(false);
const [taskData, setTaskData] = useState(null);
@@ -46,7 +50,10 @@ export default function TaskShow() {
useEffect(() => {
const processResult = (result: Task) => {
setTask(result);
- setTaskData(result.data);
+
+ // convert null back to undefined so rjsf doesn't attempt to incorrectly validate them
+ const taskDataToUse = result.saved_form_data || result.data;
+ setTaskData(recursivelyChangeNullAndUndefined(taskDataToUse, undefined));
setDisabled(false);
if (!result.can_complete) {
navigateToInterstitial(result);
@@ -83,6 +90,28 @@ export default function TaskShow() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [params]);
+ // Before we auto-saved form data, we remembered what data was in the form, and then created a synthetic submit event
+ // in order to implement a "Save and close" button. That button no longer saves (since we have auto-save), but the crazy
+ // frontend code to support that Save and close button is here, in case we need to reference that someday:
+ // https://github.com/sartography/spiff-arena/blob/182f56a1ad23ce780e8f5b0ed00efac3e6ad117b/spiffworkflow-frontend/src/routes/TaskShow.tsx#L329
+ const autoSaveTaskData = (formData: any) => {
+ HttpService.makeCallToBackend({
+ path: `/tasks/${params.process_instance_id}/${params.task_id}/save-draft`,
+ postBody: formData,
+ httpMethod: 'POST',
+ successCallback: doNothing,
+ failureCallback: addError,
+ });
+ };
+
+ const addDebouncedTaskDataAutoSave = useDebouncedCallback(
+ (value: string) => {
+ autoSaveTaskData(value);
+ },
+ // delay in ms
+ 1000
+ );
+
const processSubmitResult = (result: any) => {
removeError();
if (result.ok) {
@@ -108,20 +137,16 @@ export default function TaskShow() {
navigate(`/tasks`);
return;
}
- let queryParams = '';
+ const queryParams = '';
- // if validations are turned off then save as draft
- if (noValidate) {
- queryParams = '?save_as_draft=true';
- }
setDisabled(true);
removeError();
delete dataToSubmit.isManualTask;
// NOTE: rjsf sets blanks values to undefined and JSON.stringify removes keys with undefined values
- // so there is no way to clear out a field that previously had a value.
- // To resolve this, we could potentially go through the object that we are posting (either in here or in
- // HttpService) and translate all undefined values to null.
+ // so we convert undefined values to null recursively so that we can unset values in form fields
+ recursivelyChangeNullAndUndefined(dataToSubmit, null);
+
HttpService.makeCallToBackend({
path: `/tasks/${params.process_instance_id}/${params.task_id}${queryParams}`,
successCallback: processSubmitResult,
@@ -323,16 +348,8 @@ export default function TaskShow() {
return errors;
};
- // This turns off validations and then dispatches the click event after
- // waiting a second to give the state time to update.
- // This is to allow saving the form without validations causing issues.
- const handleSaveAndCloseButton = () => {
- setNoValidate(true);
- setTimeout(() => {
- (document.getElementById('our-very-own-form') as any).dispatchEvent(
- new Event('submit', { cancelable: true, bubbles: true })
- );
- }, 1000);
+ const handleCloseButton = () => {
+ navigate(`/tasks`);
};
const formElement = () => {
@@ -384,12 +401,12 @@ export default function TaskShow() {
closeButton = (
);
}
@@ -427,14 +444,16 @@ export default function TaskShow() {
id="our-very-own-form"
disabled={disabled}
formData={taskData}
- onChange={(obj: any) => setTaskData(obj.formData)}
+ onChange={(obj: any) => {
+ setTaskData(obj.formData);
+ addDebouncedTaskDataAutoSave(obj.formData);
+ }}
onSubmit={handleFormSubmit}
schema={jsonSchema}
uiSchema={formUiSchema}
widgets={widgets}
validator={validator}
customValidate={customValidate}
- noValidate={noValidate}
omitExtraData
>
{reactFragmentToHideSubmitButton}