From 100f6454f860bc464d59fb76a1927f8b4db73cdc Mon Sep 17 00:00:00 2001 From: burnettk Date: Wed, 13 Jul 2022 22:52:00 -0400 Subject: [PATCH] switch to spiff_properties branch of spiff, remove api blueprint, camunda to spiff parser/serializer --- poetry.lock | 21 +- pyproject.toml | 8 +- src/spiffworkflow_backend/__init__.py | 2 - .../models/process_instance.py | 7 - .../routes/api_blueprint.py | 59 ----- .../services/process_instance_processor.py | 19 +- .../services/process_instance_service.py | 4 +- .../spiff_workflow_connector.py | 214 ------------------ 8 files changed, 26 insertions(+), 308 deletions(-) delete mode 100644 src/spiffworkflow_backend/routes/api_blueprint.py delete mode 100755 src/spiffworkflow_backend/spiff_workflow_connector.py diff --git a/poetry.lock b/poetry.lock index 5d5a1d5a..91e1bcb5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -618,23 +618,18 @@ develop = false [package.dependencies] click = "^8.0.1" flask = "*" -flask-admin = "*" -flask-bcrypt = "*" -flask-cors = "*" -flask-mail = "*" flask-marshmallow = "*" flask-migrate = "*" -flask-restful = "*" -sentry-sdk = "1.7.0" +sentry-sdk = "1.7.1" sphinx-autoapi = "^1.8.4" -spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/parse_spiffworkflow_extensions"} +spiffworkflow = "*" werkzeug = "*" [package.source] type = "git" url = "https://github.com/sartography/flask-bpmn" -reference = "main" -resolved_reference = "c454e729c634c7c86ff30cf4d388480647d18d7b" +reference = "feature/with-spiff-properties" +resolved_reference = "c7497aabb039420d9e7c68a50d7a4b3e74100e81" [[package]] name = "flask-cors" @@ -1561,7 +1556,7 @@ requests = "*" [[package]] name = "sentry-sdk" -version = "1.7.0" +version = "1.7.1" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -1806,8 +1801,8 @@ pytz = "*" [package.source] type = "git" url = "https://github.com/sartography/SpiffWorkflow" -reference = "feature/parse_spiffworkflow_extensions" -resolved_reference = "67054883d4040d6755bf0555f072ff85aa42093c" +reference = "feature/spiff_properties" +resolved_reference = "e108aa12da008bdd8d0319e182d28fbd3afb4c67" [[package]] name = "sqlalchemy" @@ -2098,7 +2093,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "c20a647c5a3e12bb5e1e9280316566acaa548e6722b7e4e13b610f25877bd4d0" +content-hash = "83f09337ac9218c85a6095a8dcf82eff5153dda76453f77ca97dadfb9ed58b8a" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 73343be6..98f0581c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,13 +27,13 @@ flask-marshmallow = "*" flask-migrate = "*" flask-restful = "*" werkzeug = "*" -spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/parse_spiffworkflow_extensions"} -# spiffworkflow = {develop = true, path = "/home/jason/projects/github/sartography/SpiffWorkflow"} -sentry-sdk = "1.7.0" +spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "feature/spiff_properties"} +# spiffworkflow = {develop = true, path = "/Users/kevin/projects/github/sartography/SpiffWorkflow"} +sentry-sdk = "1.7.1" sphinx-autoapi = "^1.8.4" # flask-bpmn = {develop = true, path = "/home/jason/projects/github/sartography/flask-bpmn"} # flask-bpmn = {develop = true, path = "/Users/kevin/projects/github/sartography/flask-bpmn"} -flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "main"} +flask-bpmn = {git = "https://github.com/sartography/flask-bpmn", rev = "feature/with-spiff-properties"} mysql-connector-python = "^8.0.29" pytest-flask = "^1.2.0" pytest-flask-sqlalchemy = "^1.1.0" diff --git a/src/spiffworkflow_backend/__init__.py b/src/spiffworkflow_backend/__init__.py index f3ec1aea..45e4a066 100644 --- a/src/spiffworkflow_backend/__init__.py +++ b/src/spiffworkflow_backend/__init__.py @@ -14,7 +14,6 @@ from flask_mail import Mail # type: ignore import spiffworkflow_backend.load_database_models # noqa: F401 from spiffworkflow_backend.config import setup_config from spiffworkflow_backend.routes.admin_blueprint.admin_blueprint import admin_blueprint -from spiffworkflow_backend.routes.api_blueprint import api_blueprint from spiffworkflow_backend.routes.process_api_blueprint import process_api_blueprint from spiffworkflow_backend.routes.user_blueprint import user_blueprint @@ -55,7 +54,6 @@ def create_app() -> flask.app.Flask: migrate.init_app(app, db) app.register_blueprint(user_blueprint) - app.register_blueprint(api_blueprint) app.register_blueprint(process_api_blueprint) app.register_blueprint(api_error_blueprint) app.register_blueprint(admin_blueprint, url_prefix="/admin") diff --git a/src/spiffworkflow_backend/models/process_instance.py b/src/spiffworkflow_backend/models/process_instance.py index 9c7d5416..c59156b1 100644 --- a/src/spiffworkflow_backend/models/process_instance.py +++ b/src/spiffworkflow_backend/models/process_instance.py @@ -165,7 +165,6 @@ class ProcessInstanceApi: next_task: Task | None, process_model_identifier: str, process_group_identifier: str, - total_tasks: int, completed_tasks: int, updated_at_in_seconds: int, is_review: bool, @@ -178,7 +177,6 @@ class ProcessInstanceApi: # self.navigation = navigation fixme: would be a hotness. self.process_model_identifier = process_model_identifier self.process_group_identifier = process_group_identifier - self.total_tasks = total_tasks self.completed_tasks = completed_tasks self.updated_at_in_seconds = updated_at_in_seconds self.title = title @@ -199,7 +197,6 @@ class ProcessInstanceApiSchema(Schema): "navigation", "process_model_identifier", "process_group_identifier", - "total_tasks", "completed_tasks", "updated_at_in_seconds", "is_review", @@ -228,7 +225,6 @@ class ProcessInstanceApiSchema(Schema): "navigation", "process_model_identifier", "process_group_identifier", - "total_tasks", "completed_tasks", "updated_at_in_seconds", "is_review", @@ -251,7 +247,6 @@ class ProcessInstanceMetadata: spec_version: str | None = None state: str | None = None status: str | None = None - total_tasks: int | None = None completed_tasks: int | None = None is_review: bool | None = None state_message: str | None = None @@ -270,7 +265,6 @@ class ProcessInstanceMetadata: process_group_id=process_model.process_group_id, state_message=process_instance.state_message, status=process_instance.status, - total_tasks=process_instance.total_tasks, completed_tasks=process_instance.completed_tasks, is_review=process_model.is_review, process_model_identifier=process_instance.process_model_identifier, @@ -292,7 +286,6 @@ class ProcessInstanceMetadataSchema(Schema): "display_name", "description", "state", - "total_tasks", "completed_tasks", "process_group_id", "is_review", diff --git a/src/spiffworkflow_backend/routes/api_blueprint.py b/src/spiffworkflow_backend/routes/api_blueprint.py deleted file mode 100644 index 8c383f16..00000000 --- a/src/spiffworkflow_backend/routes/api_blueprint.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Api.""" -import json -import os - -from flask import Blueprint -from flask import current_app -from flask import request -from flask import Response -from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore -from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore -from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore - -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel -from spiffworkflow_backend.spiff_workflow_connector import parse -from spiffworkflow_backend.spiff_workflow_connector import run - - -wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter( - [UserTaskConverter, BusinessRuleTaskConverter] -) -serializer = BpmnWorkflowSerializer(wf_spec_converter) - -api_blueprint = Blueprint("api", __name__) - - -@api_blueprint.route("/run_process", methods=["POST"]) -def run_process() -> Response: - """Run_process.""" - content = request.json - if content is None: - return Response( - json.dumps({"error": "Could not find json request"}), - status=400, - mimetype="application/json", - ) - - bpmn_spec_dir = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"] - process = "order_product" - dmn = [ - os.path.join(bpmn_spec_dir, "product_prices.dmn"), - os.path.join(bpmn_spec_dir, "shipping_costs.dmn"), - ] - bpmn = [ - os.path.join(bpmn_spec_dir, "multiinstance.bpmn"), - os.path.join(bpmn_spec_dir, "call_activity_multi.bpmn"), - ] - - workflow = None - process_instance = ProcessInstanceModel.query.filter().first() - if process_instance is None: - workflow = parse(process, bpmn, dmn) - else: - workflow = serializer.deserialize_json(process_instance.bpmn_json) - - response = run(workflow, content.get("task_identifier"), content.get("answer")) - - return Response( - json.dumps({"response": response}), status=200, mimetype="application/json" - ) diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index 7788229e..c633d519 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -14,6 +14,7 @@ from lxml import etree # type: ignore from SpiffWorkflow import Task as SpiffTask # type: ignore from SpiffWorkflow import TaskState from SpiffWorkflow import WorkflowException +from SpiffWorkflow.bpmn.exceptions import WorkflowTaskExecException # type: ignore from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore from SpiffWorkflow.bpmn.PythonScriptEngine import Box # type: ignore from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine @@ -23,13 +24,12 @@ from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ig from SpiffWorkflow.bpmn.specs.events import CancelEventDefinition # type: ignore from SpiffWorkflow.bpmn.specs.events import EndEvent from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore -from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser # type: ignore -from SpiffWorkflow.camunda.serializer import UserTaskConverter # type: ignore from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter # type: ignore -from SpiffWorkflow.exceptions import WorkflowTaskExecException # type: ignore from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore from SpiffWorkflow.specs import WorkflowSpec # type: ignore +from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore +from SpiffWorkflow.spiff.serializer import UserTaskConverter # type: ignore from SpiffWorkflow.util.deep_merge import DeepMerge # type: ignore from spiffworkflow_backend.models.active_task import ActiveTaskModel @@ -90,10 +90,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore class MyCustomParser(BpmnDmnParser): # type: ignore - """A BPMN and DMN parser that can also parse Camunda forms.""" + """A BPMN and DMN parser that can also parse spiffworkflow-specific extensions.""" OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES - OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES) + OVERRIDE_PARSER_CLASSES.update(SpiffBpmnParser.OVERRIDE_PARSER_CLASSES) class ProcessInstanceProcessor: @@ -355,8 +355,13 @@ class ProcessInstanceProcessor: extensions = ready_or_waiting_task.task_spec.extensions form_file_name = None - if "formKey" in extensions: - form_file_name = extensions["formKey"] + if "properties" in extensions: + properties = extensions["properties"] + if "formJsonSchemaFilename" in properties: + form_file_name = properties["formJsonSchemaFilename"] + # FIXME: + # if "formUiSchemaFilename" in properties: + # form_file_name = properties["formUiSchemaFilename"] active_task = ActiveTaskModel( spiffworkflow_task_id=str(ready_or_waiting_task.id), diff --git a/src/spiffworkflow_backend/services/process_instance_service.py b/src/spiffworkflow_backend/services/process_instance_service.py index 7a1ef6a4..cea76786 100644 --- a/src/spiffworkflow_backend/services/process_instance_service.py +++ b/src/spiffworkflow_backend/services/process_instance_service.py @@ -67,7 +67,7 @@ class ProcessInstanceService: If requested, and possible, next_task is set to the current_task. """ - navigation = processor.bpmn_process_instance.get_deep_nav_list() + # navigation = processor.bpmn_process_instance.get_deep_nav_list() # ProcessInstanceService.update_navigation(navigation, processor) process_model_service = ProcessModelService() process_model = process_model_service.get_process_model( @@ -82,7 +82,7 @@ class ProcessInstanceService: # navigation=navigation, process_model_identifier=processor.process_model_identifier, process_group_identifier=processor.process_group_identifier, - total_tasks=len(navigation), + # total_tasks=len(navigation), completed_tasks=processor.process_instance_model.completed_tasks, updated_at_in_seconds=processor.process_instance_model.updated_at_in_seconds, is_review=is_review_value, diff --git a/src/spiffworkflow_backend/spiff_workflow_connector.py b/src/spiffworkflow_backend/spiff_workflow_connector.py deleted file mode 100755 index c267f6fa..00000000 --- a/src/spiffworkflow_backend/spiff_workflow_connector.py +++ /dev/null @@ -1,214 +0,0 @@ -"""Spiff Workflow Connector.""" -from typing import Any -from typing import Dict -from typing import List -from typing import Optional -from typing import Union - -from flask_bpmn.models.db import db -from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore -from SpiffWorkflow.bpmn.specs.events.event_types import CatchingEvent # type: ignore -from SpiffWorkflow.bpmn.specs.events.event_types import ThrowingEvent -from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask # type: ignore -from SpiffWorkflow.bpmn.specs.ScriptTask import ScriptTask # type: ignore -from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore -from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser # type: ignore -from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore -from SpiffWorkflow.camunda.specs.UserTask import EnumFormField # type: ignore -from SpiffWorkflow.camunda.specs.UserTask import UserTask -from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore -from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore -from SpiffWorkflow.task import Task # type: ignore -from SpiffWorkflow.task import TaskState -from typing_extensions import TypedDict - -from spiffworkflow_backend.models.process_instance import ProcessInstanceModel -from spiffworkflow_backend.models.user import UserModel - - -wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter( - [UserTaskConverter, BusinessRuleTaskConverter] -) -serializer = BpmnWorkflowSerializer(wf_spec_converter) - - -class Parser(BpmnDmnParser): # type: ignore - """Parser.""" - - OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES - OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES) - - -class ProcessStatus(TypedDict, total=False): - """ProcessStatus.""" - - last_task: str - upcoming_tasks: List[str] - next_activity: Dict[str, str] - - -def parse(process: str, bpmn_files: List[str], dmn_files: List[str]) -> BpmnWorkflow: - """Parse.""" - parser = Parser() - parser.add_bpmn_files(bpmn_files) - if dmn_files: - parser.add_dmn_files(dmn_files) - return BpmnWorkflow(parser.get_spec(process)) - - -def format_task(task: Task, include_state: bool = True) -> str: - """Format_task.""" - if hasattr(task.task_spec, "lane") and task.task_spec.lane is not None: - lane = f"[{task.task_spec.lane}]" - else: - lane = "" - state = f"[{task.get_state_name()}]" if include_state else "" - return f"{lane} {task.task_spec.description} ({task.task_spec.name}) {state}" - - -def process_field( - field: Any, answer: Union[dict, None], required_user_input_fields: Dict[str, str] -) -> Union[str, int, None]: - """Handles the complexities of figuring out what to do about each necessary user field.""" - response = None - if isinstance(field, EnumFormField): - option_map = {opt.name: opt.id for opt in field.options} - options = "(" + ", ".join(option_map) + ")" - if answer is None: - required_user_input_fields[field.label] = options - else: - response = option_map[answer[field.label]] - elif field.type == "string": - if answer is None: - required_user_input_fields[field.label] = "STRING" - else: - response = answer[field.label] - else: - if answer is None: - required_user_input_fields[field.label] = "(1..)" - else: - if field.type == "long": - response = int(answer[field.label]) - - return response - - -def complete_user_task( - task: Task, answer: Optional[Dict[str, str]] = None -) -> Dict[Any, Any]: - """Complete_user_task.""" - if task.data is None: - task.data = {} - - required_user_input_fields: Dict[str, str] = {} - for field in task.task_spec.form.fields: - response = process_field(field, answer, required_user_input_fields) - if answer: - task.update_data_var(field.id, response) - return required_user_input_fields - - -def get_state(workflow: BpmnWorkflow) -> ProcessStatus: - """Print_state.""" - task = workflow.last_task - - return_json: ProcessStatus = {"last_task": format_task(task), "upcoming_tasks": []} - - display_types = (UserTask, ManualTask, ScriptTask, ThrowingEvent, CatchingEvent) - all_tasks = [ - task - for task in workflow.get_tasks() - if isinstance(task.task_spec, display_types) - ] - upcoming_tasks = [ - task for task in all_tasks if task.state in [TaskState.READY, TaskState.WAITING] - ] - - for _idx, task in enumerate(upcoming_tasks): - return_json["upcoming_tasks"].append(format_task(task)) - - return return_json - - -def create_user() -> UserModel: - """Create_user.""" - user = UserModel(username="user1") - db.session.add(user) - db.session.commit() - - return user - - -def create_process_instance() -> ProcessInstanceModel: - """Create_process_instance.""" - user = UserModel.query.filter().first() - if user is None: - user = create_user() - - process_instance = ProcessInstanceModel( - process_model_identifier="process_model1", process_initiator_id=user.id - ) - db.session.add(process_instance) - db.session.commit() - - return process_instance - - -def run( - workflow: BpmnWorkflow, - task_identifier: Optional[str] = None, - answer: Optional[Dict[str, str]] = None, -) -> Union[ProcessStatus, Dict[str, str]]: - """Run.""" - workflow.do_engine_steps() - tasks_status = ProcessStatus() - - if workflow.is_completed(): - return tasks_status - - ready_tasks = workflow.get_ready_user_tasks() - options = {} - formatted_options = {} - - for idx, task in enumerate(ready_tasks): - option = format_task(task, False) - options[str(idx + 1)] = task - formatted_options[str(idx + 1)] = option - - if task_identifier is None: - return formatted_options - - next_task = options[task_identifier] - if isinstance(next_task.task_spec, UserTask): - if answer is None: - return complete_user_task(next_task) - else: - complete_user_task(next_task, answer) - next_task.complete() - elif isinstance(next_task.task_spec, ManualTask): - next_task.complete() - else: - next_task.complete() - - workflow.refresh_waiting_tasks() - workflow.do_engine_steps() - tasks_status = get_state(workflow) - - ready_tasks = workflow.get_ready_user_tasks() - formatted_options = {} - for idx, task in enumerate(ready_tasks): - option = format_task(task, False) - formatted_options[str(idx + 1)] = option - - state = serializer.serialize_json(workflow) - process_instance = ProcessInstanceModel.query.filter().first() - - if process_instance is None: - process_instance = create_process_instance() - process_instance.bpmn_json = state - db.session.add(process_instance) - db.session.commit() - - tasks_status["next_activity"] = formatted_options - - return tasks_status