From 4b3a91c13c34b69f03f88c31b55916204b58e952 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 16 May 2022 15:21:35 -0400 Subject: [PATCH] this is now working and stores state in a sqlite3 db w/ burnettk --- bin/test_with_curl | 26 +++ migrations/README | 1 + migrations/alembic.ini | 50 ++++ migrations/env.py | 91 ++++++++ migrations/script.py.mako | 24 ++ migrations/versions/c73669985652_.py | 38 +++ src/spiff_workflow_webapp/api/data_store.py | 122 ---------- src/spiff_workflow_webapp/db.sqlite3 | Bin 20480 -> 20480 bytes .../models/data_store.py | 32 --- .../models/process_model.py | 8 + src/spiff_workflow_webapp/routes/api.py | 34 ++- .../services/data_store_service.py | 219 ------------------ .../spiff_workflow_connector.py | 85 ++++--- 13 files changed, 306 insertions(+), 424 deletions(-) create mode 100755 bin/test_with_curl create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/c73669985652_.py delete mode 100644 src/spiff_workflow_webapp/api/data_store.py delete mode 100644 src/spiff_workflow_webapp/models/data_store.py create mode 100644 src/spiff_workflow_webapp/models/process_model.py delete mode 100644 src/spiff_workflow_webapp/services/data_store_service.py diff --git a/bin/test_with_curl b/bin/test_with_curl new file mode 100755 index 00000000..196e2c78 --- /dev/null +++ b/bin/test_with_curl @@ -0,0 +1,26 @@ +#!/usr/bin/env bash + +function error_handler() { + >&2 echo "Exited with BAD EXIT CODE '${2}' in ${0} script at line: ${1}." + exit "$2" +} +trap 'error_handler ${LINENO} $?' ERR +set -o errtrace -o errexit -o nounset -o pipefail + +if [[ "${1:-}" == "c" ]]; then + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{}' +elif grep -qE '^[0-9]$' <<<"${1:-}" ; then + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d "{ \"task_identifier\": \"${1}\"}" +else + rm -rf currentstate.json + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Product Name": "G", "Quantity": "2"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Sleeve Type": "Short"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Continue shopping?": "N"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Shipping Method": "Overnight"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Shipping Address": "Somewhere"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Place Order": "Y"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Card Number": "MY_CARD"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "2", "answer": {"Was the customer charged?": "Y"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Was the product available?": "Y"}}' + curl --fail localhost:5000/run_process -H "Content-type: application/json" -X POST -d '{ "task_identifier": "1", "answer": {"Was the order shipped?": "Y"}}' +fi diff --git a/migrations/README b/migrations/README new file mode 100644 index 00000000..0e048441 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 00000000..ec9d45c2 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 00000000..68feded2 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,91 @@ +from __future__ import with_statement + +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option( + 'sqlalchemy.url', + str(current_app.extensions['migrate'].db.get_engine().url).replace( + '%', '%%')) +target_metadata = current_app.extensions['migrate'].db.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + connectable = current_app.extensions['migrate'].db.get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + process_revision_directives=process_revision_directives, + **current_app.extensions['migrate'].configure_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/c73669985652_.py b/migrations/versions/c73669985652_.py new file mode 100644 index 00000000..28ce4024 --- /dev/null +++ b/migrations/versions/c73669985652_.py @@ -0,0 +1,38 @@ +"""empty message + +Revision ID: c73669985652 +Revises: +Create Date: 2022-05-16 15:19:43.112086 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c73669985652' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('process_models', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('bpmn_json', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=50), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user') + op.drop_table('process_models') + # ### end Alembic commands ### diff --git a/src/spiff_workflow_webapp/api/data_store.py b/src/spiff_workflow_webapp/api/data_store.py deleted file mode 100644 index 0810a714..00000000 --- a/src/spiff_workflow_webapp/api/data_store.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Methods to talk to the database.""" -import json -from datetime import datetime -from typing import Any - -from spiff_workflow_webapp.models.data_store import DataStoreModel -from spiff_workflow_webapp.models.data_store import DataStoreSchema -from spiff_workflow_webapp.services.data_store_service import DataStoreBase -from flask import Blueprint -from sqlalchemy.orm import Session # type: ignore - -from spiff_workflow_webapp.api.api_error import ApiError - -# from crc import session - - -def construct_blueprint(database_session: Session) -> Blueprint: - """Construct_blueprint.""" - data_store_blueprint = Blueprint("data_store", __name__) - database_session = database_session - - def study_multi_get(study_id: str) -> Any: - """Get all data_store values for a given study_id study.""" - if study_id is None: - raise ApiError("unknown_study", "Please provide a valid Study ID.") - - dsb = DataStoreBase() - retval = dsb.get_multi_common(study_id, None) - results = DataStoreSchema(many=True).dump(retval) - return results - - def user_multi_get(user_id: str) -> Any: - """Get all data values in the data_store for a userid.""" - if user_id is None: - raise ApiError("unknown_study", "Please provide a valid UserID.") - - dsb = DataStoreBase() - retval = dsb.get_multi_common(None, user_id) - results = DataStoreSchema(many=True).dump(retval) - return results - - def file_multi_get(file_id: str) -> Any: - """Get all data values in the data store for a file_id.""" - if file_id is None: - raise ApiError( - code="unknown_file", message="Please provide a valid file id." - ) - dsb = DataStoreBase() - retval = dsb.get_multi_common(None, None, file_id=file_id) - results = DataStoreSchema(many=True).dump(retval) - return results - - def datastore_del(id: str) -> Any: - """Delete a data store item for a key.""" - database_session.query(DataStoreModel).filter_by(id=id).delete() - database_session.commit() - json_value = json.dumps("deleted", ensure_ascii=False, indent=2) - return json_value - - def datastore_get(id: str) -> Any: - """Retrieve a data store item by a key.""" - item = database_session.query(DataStoreModel).filter_by(id=id).first() - results = DataStoreSchema(many=False).dump(item) - return results - - def update_datastore(id: str, body: dict) -> Any: - """Allow a modification to a datastore item.""" - if id is None: - raise ApiError("unknown_id", "Please provide a valid ID.") - - item = database_session.query(DataStoreModel).filter_by(id=id).first() - if item is None: - raise ApiError("unknown_item", 'The item "' + id + '" is not recognized.') - - DataStoreSchema().load(body, instance=item, database_session=database_session) - item.last_updated = datetime.utcnow() - database_session.add(item) - database_session.commit() - return DataStoreSchema().dump(item) - - def add_datastore(body: dict) -> Any: - """Add a new datastore item.""" - if body.get(id, None): - raise ApiError( - "id_specified", "You may not specify an id for a new datastore item" - ) - - if "key" not in body: - raise ApiError( - "no_key", "You need to specify a key to add a datastore item" - ) - - if "value" not in body: - raise ApiError( - "no_value", "You need to specify a value to add a datastore item" - ) - - if ( - ("user_id" not in body) - and ("study_id" not in body) - and ("file_id" not in body) - ): - raise ApiError( - "conflicting_values", - "A datastore item should have either a study_id, user_id or file_id ", - ) - - present = 0 - for field in ["user_id", "study_id", "file_id"]: - if field in body: - present = present + 1 - if present > 1: - message = "A datastore item should have one of a study_id, user_id or a file_id but not more than one of these" - raise ApiError("conflicting_values", message) - - item = DataStoreSchema().load(body) - # item.last_updated = datetime.utcnow() # Do this in the database - database_session.add(item) - database_session.commit() - return DataStoreSchema().dump(item) - - return data_store_blueprint diff --git a/src/spiff_workflow_webapp/db.sqlite3 b/src/spiff_workflow_webapp/db.sqlite3 index 6183880de5f146c4e10808928ec6f263814e7dbc..22312af85573a2acca9fae9923cc268b171fb359 100644 GIT binary patch delta 203 zcmZozz}T>Wae_1>%S0JxQ5FWha4%l|9}Fyfat!>u{5$#D`Q$bW3asK4PiJ8l7Zqh} zG%QI>%1JFPPAvjq*2%m1g58*z*~K+A851(tJq;xq0zf#rb&(Ucvr;ll%C@HuLg77vM=YH#Re~w6riaGd0?z O@P!{mV3C6W5CH&=L_Grl delta 278 zcmZozz}T>Wae_1>??f4AQCwkaPlu?;Qzw^n*TcgIsT*k zd-yl=ui{?_bV@saJUb^dgJ_wNo*_F2gkiwW4r8#v7_2Y`3zQ+m%*>!&l95>)pO=`M g8lRLJpIO4r1e64Exfq!lq#g50GV=2(7a0fu0H%jU<^TWy diff --git a/src/spiff_workflow_webapp/models/data_store.py b/src/spiff_workflow_webapp/models/data_store.py deleted file mode 100644 index 39c6370b..00000000 --- a/src/spiff_workflow_webapp/models/data_store.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Data_store.""" -from crc import db -from flask_marshmallow.sqla import SQLAlchemyAutoSchema -from sqlalchemy import func - - -class DataStoreModel(db.Model): - """DataStoreModel.""" - - __tablename__ = "data_store" - id = db.Column(db.Integer, primary_key=True) - last_updated = db.Column(db.DateTime(timezone=True), server_default=func.now()) - key = db.Column(db.String, nullable=False) - workflow_id = db.Column(db.Integer) - study_id = db.Column(db.Integer, nullable=True) - task_spec = db.Column(db.String) - spec_id = db.Column(db.String) - user_id = db.Column(db.String, nullable=True) - file_id = db.Column(db.Integer, db.ForeignKey("file.id"), nullable=True) - value = db.Column(db.String) - - -class DataStoreSchema(SQLAlchemyAutoSchema): - """DataStoreSchema.""" - - class Meta: - """Meta.""" - - model = DataStoreModel - load_instance = True - include_fk = True - sqla_session = db.session diff --git a/src/spiff_workflow_webapp/models/process_model.py b/src/spiff_workflow_webapp/models/process_model.py new file mode 100644 index 00000000..4f77d1ef --- /dev/null +++ b/src/spiff_workflow_webapp/models/process_model.py @@ -0,0 +1,8 @@ +from ..extensions import db +from sqlalchemy.orm import deferred + + +class ProcessModel(db.Model): + __tablename__ = 'process_models' + id = db.Column(db.Integer, primary_key=True) + bpmn_json = deferred(db.Column(db.JSON)) diff --git a/src/spiff_workflow_webapp/routes/api.py b/src/spiff_workflow_webapp/routes/api.py index b7214303..fa128ff0 100644 --- a/src/spiff_workflow_webapp/routes/api.py +++ b/src/spiff_workflow_webapp/routes/api.py @@ -2,26 +2,37 @@ import os from flask import Blueprint +from flask import request from ..models.user import User from spiff_workflow_webapp.spiff_workflow_connector import parse from spiff_workflow_webapp.spiff_workflow_connector import run +from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer +from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter +from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter +from spiff_workflow_webapp.models.process_model import ProcessModel + +wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter([ UserTaskConverter, BusinessRuleTaskConverter ]) +serializer = BpmnWorkflowSerializer(wf_spec_converter) + api = Blueprint("api", __name__) @api.route("/user/") def create_user(name): """Create_user.""" - user = User.query.filter_by(name="Anthony").first() + user = User.query.filter_by(name=name).first() return {"user": user.name} -@api.route("/run_process", defaults={"answer": None, "task_identifier": None}) -@api.route("/run_process/", defaults={"answer": None}) -@api.route("/run_process//") -def run_process(task_identifier, answer): +# @api.route("/run_process", defaults={"answer": None, "task_identifier": None}) +# @api.route("/run_process/", defaults={"answer": None}) +# @api.route("/run_process//") +# def run_process(task_identifier, answer): +@api.route("/run_process", methods=['POST']) +def run_process(): """Run_process.""" # parser = argparse.ArgumentParser("Simple BPMN runner") # parser.add_argument( @@ -47,7 +58,9 @@ def run_process(task_identifier, answer): # ) # args = parser.parse_args() - # content = request.json + content = request.json + # if 'task_identifier' in content: + homedir = os.environ.get("HOME") process = "order_product" dmn = [ @@ -64,8 +77,13 @@ def run_process(task_identifier, answer): # with open(args.restore) as state: # wf = serializer.deserialize_json(state.read()) # else: - workflow = parse(process, bpmn, dmn) - response = run(workflow, task_identifier, answer) + workflow = None + process_model = ProcessModel.query.filter().first() + if process_model is None: + workflow = parse(process, bpmn, dmn) + else: + workflow = serializer.deserialize_json(process_model.bpmn_json) + response = run(workflow, content.get("task_identifier"), content.get("answer")) # except Exception: # sys.stderr.write(traceback.format_exc()) # sys.exit(1) diff --git a/src/spiff_workflow_webapp/services/data_store_service.py b/src/spiff_workflow_webapp/services/data_store_service.py deleted file mode 100644 index 99716b7d..00000000 --- a/src/spiff_workflow_webapp/services/data_store_service.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Data_store_service.""" -from crc import session -from crc.models.data_store import DataStoreModel -from crc.models.workflow import WorkflowModel -from flask import g -import sqlalchemy -from sqlalchemy import desc -from spiff_workflow_webapp.api.api_error import ApiError -from typing import Any - - -class DataStoreBase: - """DataStoreBase.""" - - def set_validate_common( - self, task_id, study_id, workflow_id, script_name, user_id, file_id, *args - ): - """Set_validate_common.""" - self.check_args_2(args, script_name) - key = args[0] - value = args[1] - if script_name == "study_data_set": - record = { - "task_id": task_id, - "study_id": study_id, - "workflow_id": workflow_id, - key: value, - } - elif script_name == "file_data_set": - record = { - "task_id": task_id, - "study_id": study_id, - "workflow_id": workflow_id, - "file_id": file_id, - key: value, - } - elif script_name == "user_data_set": - record = { - "task_id": task_id, - "study_id": study_id, - "workflow_id": workflow_id, - "user_id": user_id, - key: value, - } - g.validation_data_store.append(record) - return record - - def get_validate_common( - self, script_name, study_id=None, user_id=None, file_id=None, *args - ): - """This method uses a temporary validation_data_store that is only available for the current validation request. - - This allows us to set data_store values during validation that don't affect the real data_store. - For data_store `gets`, we first look in the temporary validation_data_store. - If we don't find an entry in validation_data_store, we look in the real data_store. - """ - key = args[0] - if script_name == "study_data_get": - # If it's in the validation data store, return it - for record in g.validation_data_store: - if ( - "study_id" in record - and record["study_id"] == study_id - and key in record - ): - return record[key] - # If not in validation_data_store, look in the actual data_store - return self.get_data_common( - study_id, user_id, "study_data_get", file_id, *args - ) - elif script_name == "file_data_get": - for record in g.validation_data_store: - if ( - "file_id" in record - and record["file_id"] == file_id - and key in record - ): - return record[key] - return self.get_data_common( - study_id, user_id, "file_data_get", file_id, *args - ) - elif script_name == "user_data_get": - for record in g.validation_data_store: - if ( - "user_id" in record - and record["user_id"] == user_id - and key in record - ): - return record[key] - return self.get_data_common( - study_id, user_id, "user_data_get", file_id, *args - ) - - @staticmethod - def check_args(args: Any, maxlen: int = 1, script_name: str = "study_data_get") -> None: - """Check_args.""" - if len(args) < 1 or len(args) > maxlen: - raise ApiError( - code="missing_argument", - message=f"The {script_name} script takes either one or two arguments, " - f"starting with the key and an optional default", - ) - - @staticmethod - def check_args_2(args: Tuple[str, str], script_name: str = "study_data_set"): - """Check_args_2.""" - if len(args) != 2: - raise ApiError( - code="missing_argument", - message=f"The {script_name} script takes two arguments, key and value, in that order.", - ) - - def set_data_common( - self, task_spec, study_id, user_id, workflow_id, script_name, file_id, *args - ): - """Set_data_common.""" - self.check_args_2(args, script_name=script_name) - key = args[0] - value = args[1] - if value == "" or value is None: - # We delete the data store if the value is empty - return self.delete_data_store(study_id, user_id, file_id, *args) - workflow_spec_id = None - if workflow_id is not None: - workflow = ( - session.query(WorkflowModel) - .filter(WorkflowModel.id == workflow_id) - .first() - ) - workflow_spec_id = workflow.workflow_spec_id - - # Check if this data store is previously set - query = session.query(DataStoreModel).filter(DataStoreModel.key == key) - if study_id: - query = query.filter(DataStoreModel.study_id == study_id) - elif file_id: - query = query.filter(DataStoreModel.file_id == file_id) - elif user_id: - query = query.filter(DataStoreModel.user_id == user_id) - result = query.order_by(desc(DataStoreModel.last_updated)).all() - if result: - dsm = result[0] - dsm.value = value - if task_spec: - dsm.task_spec = task_spec - if workflow_id: - dsm.workflow_id = workflow_id - if workflow_spec_id: - dsm.spec_id = workflow_spec_id - if len(result) > 1: - # We had a bug where we had created new records instead of updating values of existing records - # This just gets rid of all the old unused records - self.delete_extra_data_stores(result[1:]) - else: - dsm = DataStoreModel( - key=key, - value=value, - study_id=study_id, - task_spec=task_spec, - user_id=user_id, # Make this available to any User - file_id=file_id, - workflow_id=workflow_id, - spec_id=workflow_spec_id, - ) - session.add(dsm) - session.commit() - - return dsm.value - - def get_data_common(self, study_id: str, user_id: str, script_name: str, file_id: (str | None) = None, *args: Any) -> Any: - """Get_data_common.""" - self.check_args(args, 2, script_name) - record = ( - session.query(DataStoreModel) - .filter_by(study_id=study_id, user_id=user_id, file_id=file_id, key=args[0]) - .first() - ) - if record: - return record.value - else: - # This is a possible default value passed in from the data_store get methods - if len(args) == 2: - return args[1] - - @staticmethod - def get_multi_common(study_id: str, user_id: str, file_id: (str | None) = None) -> sqlalchemy.orm.Query: - """Get_multi_common.""" - results = session.query(DataStoreModel).filter_by( - study_id=study_id, user_id=user_id, file_id=file_id - ) - return results - - @staticmethod - def delete_data_store(study_id: str, user_id: str, file_id: str, *args: Any) -> None: - """Delete_data_store.""" - query = session.query(DataStoreModel).filter(DataStoreModel.key == args[0]) - if user_id: - query = query.filter(DataStoreModel.user_id == user_id) - elif file_id: - query = query.filter(DataStoreModel.file_id == file_id) - elif study_id: - query = query.filter(DataStoreModel.study_id == study_id) - record = query.first() - if record is not None: - session.delete(record) - session.commit() - - @staticmethod - def delete_extra_data_stores(records: list[DataStoreModel]) -> None: - """We had a bug where we created new records instead of updating existing records. - - We use this to clean up all the extra records. - We may remove this method in the future. - """ - for record in records: - session.query(DataStoreModel).filter( - DataStoreModel.id == record.id - ).delete() - session.commit() diff --git a/src/spiff_workflow_webapp/spiff_workflow_connector.py b/src/spiff_workflow_webapp/spiff_workflow_connector.py index b18a3fe0..6487e069 100755 --- a/src/spiff_workflow_webapp/spiff_workflow_connector.py +++ b/src/spiff_workflow_webapp/spiff_workflow_connector.py @@ -19,6 +19,9 @@ from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter from SpiffWorkflow.task import Task +from spiff_workflow_webapp.extensions import db +from spiff_workflow_webapp.models.process_model import ProcessModel + # from custom_script_engine import CustomScriptEngine wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter( @@ -43,15 +46,6 @@ def parse(process, bpmn_files, dmn_files): return BpmnWorkflow(parser.get_spec(process)) -def select_option(prompt, options): - """Select_option.""" - option = input(prompt) - while option not in options: - print("Invalid selection") - option = input(prompt) - return option - - def display_task(task): """Display_task.""" print(f"\n{task.task_spec.description}") @@ -84,29 +78,30 @@ def complete_user_task(task, answer=None): if answer is None: required_user_input_fields[field.label] = options else: - response = option_map[answer] + 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) - task.update_data_var(field.id, response) + response = int(answer[field.label]) + if answer: + task.update_data_var(field.id, response) return required_user_input_fields -def complete_manual_task(task): - """Complete_manual_task.""" - display_task(task) - input("Press any key to mark task complete") - - def print_state(workflow): """Print_state.""" task = workflow.last_task - print("\nLast Task") - print(format_task(task)) - print(json.dumps(task.data, indent=2, separators=[", ", ": "])) + # print("\nLast Task") + # print(format_task(task)) + # print(json.dumps(task.data, indent=2, separators=[", ", ": "])) + return_json = {"last_task": format_task(task)} display_types = (UserTask, ManualTask, ScriptTask, ThrowingEvent, CatchingEvent) all_tasks = [ @@ -118,13 +113,14 @@ def print_state(workflow): task for task in all_tasks if task.state in [Task.READY, Task.WAITING] ] - print("\nUpcoming Tasks") + return_json['upcoming_tasks'] = [] for _idx, task in enumerate(upcoming_tasks): - print(format_task(task)) + return_json['upcoming_tasks'].append(format_task(task)) - if input("\nShow all tasks? ").lower() == "y": - for _idx, task in enumerate(all_tasks): - print(format_task(task)) + # if input("\nShow all tasks? ").lower() == "y": + # for _idx, task in enumerate(all_tasks): + # print(format_task(task)) + return return_json def run(workflow, task_identifier=None, answer=None): @@ -146,19 +142,6 @@ def run(workflow, task_identifier=None, answer=None): if task_identifier is None: return formatted_options - # selected = None - # while selected not in options and selected not in ["", "D", "d", "exit"]: - # selected = input( - # "Select task to complete, enter to wait, or D to dump the workflow state: " - # ) - - # if selected.lower() == "d": - # filename = input("Enter filename: ") - # state = serializer.serialize_json(workflow) - # with open(filename, "w") as dump: - # dump.write(state) - # elif selected == "exit": - # exit() next_task = options[task_identifier] if isinstance(next_task.task_spec, UserTask): if answer is None: @@ -167,18 +150,34 @@ def run(workflow, task_identifier=None, answer=None): complete_user_task(next_task, answer) next_task.complete() elif isinstance(next_task.task_spec, ManualTask): - complete_manual_task(next_task) next_task.complete() else: next_task.complete() workflow.refresh_waiting_tasks() workflow.do_engine_steps() + tasks_status = {} if step: - print_state(workflow) + tasks_status = print_state(workflow) - print("\nWorkflow Data") - print(json.dumps(workflow.data, indent=2, separators=[", ", ": "])) + 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_model = ProcessModel.query.filter().first() + if process_model is None: + process_model = ProcessModel() + process_model.bpmn_json = state + db.session.add(process_model) + db.session.commit() + + # with open("currentstate.json", "w") as dump: + # dump.write(state) + tasks_status["next_activity"] = formatted_options + return tasks_status if __name__ == "__main__":