added process models and cleaned up some of the code w/ burnettk
This commit is contained in:
parent
69af11076e
commit
e3d90a6044
1
.flake8
1
.flake8
|
@ -10,6 +10,7 @@ rst-directives = deprecated
|
|||
per-file-ignores =
|
||||
# prefer naming tests descriptively rather than forcing comments
|
||||
tests/*:S101,D103
|
||||
bin/keycloak_test_server.py:B950,D
|
||||
|
||||
# the exclude=./migrations option doesn't seem to work with pre-commit
|
||||
# migrations are autogenerated from "flask db migration" so ignore them
|
||||
|
|
|
@ -67,7 +67,7 @@ jobs:
|
|||
|
||||
env:
|
||||
NOXSESSION: ${{ matrix.session }}
|
||||
TEST_DATABASE_TYPE: ${{ matrix.database }}
|
||||
SPIFF_DATABASE_TYPE: ${{ matrix.database }}
|
||||
FORCE_COLOR: "1"
|
||||
PRE_COMMIT_COLOR: "always"
|
||||
MYSQL_PASSWORD: password
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,12 @@
|
|||
{
|
||||
"web": {
|
||||
"issuer": "http://localhost:8080/realms/finance",
|
||||
"auth_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/auth",
|
||||
"client_id": "myclient",
|
||||
"client_secret": "OAh6rkjXIiPJDtPOz4459i3VtdlxGcce",
|
||||
"redirect_uris": [
|
||||
"http://localhost:5000/*"
|
||||
],
|
||||
"userinfo_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/userinfo",
|
||||
"token_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
"web": {
|
||||
"issuer": "http://localhost:8080/realms/finance",
|
||||
"auth_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/auth",
|
||||
"client_id": "myclient",
|
||||
"client_secret": "OAh6rkjXIiPJDtPOz4459i3VtdlxGcce",
|
||||
"redirect_uris": ["http://localhost:5000/*"],
|
||||
"userinfo_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/userinfo",
|
||||
"token_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/token",
|
||||
"token_introspection_uri": "http://localhost:8080/realms/finance/protocol/openid-connect/token/introspect"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,87 +1,100 @@
|
|||
# type: ignore
|
||||
"""keycloak_test_server."""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask import Flask, g
|
||||
from flask_oidc import OpenIDConnect
|
||||
import requests
|
||||
from flask import Flask
|
||||
from flask import g
|
||||
from flask_oidc import OpenIDConnect
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.update({
|
||||
'SECRET_KEY': 'SomethingNotEntirelySecret',
|
||||
'TESTING': True,
|
||||
'DEBUG': True,
|
||||
'OIDC_CLIENT_SECRETS': 'bin/keycloak_test_secrets.json',
|
||||
'OIDC_ID_TOKEN_COOKIE_SECURE': False,
|
||||
'OIDC_REQUIRE_VERIFIED_EMAIL': False,
|
||||
'OIDC_USER_INFO_ENABLED': True,
|
||||
'OIDC_OPENID_REALM': 'flask-demo',
|
||||
'OIDC_SCOPES': ['openid', 'email', 'profile'],
|
||||
'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post'
|
||||
})
|
||||
app.config.update(
|
||||
{
|
||||
"SECRET_KEY": "SomethingNotEntirelySecret",
|
||||
"TESTING": True,
|
||||
"DEBUG": True,
|
||||
"OIDC_CLIENT_SECRETS": "bin/keycloak_test_secrets.json",
|
||||
"OIDC_ID_TOKEN_COOKIE_SECURE": False,
|
||||
"OIDC_REQUIRE_VERIFIED_EMAIL": False,
|
||||
"OIDC_USER_INFO_ENABLED": True,
|
||||
"OIDC_OPENID_REALM": "flask-demo",
|
||||
"OIDC_SCOPES": ["openid", "email", "profile"],
|
||||
"OIDC_INTROSPECTION_AUTH_METHOD": "client_secret_post",
|
||||
}
|
||||
)
|
||||
|
||||
oidc = OpenIDConnect(app)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@app.route("/")
|
||||
def hello_world():
|
||||
"""Hello_world."""
|
||||
if oidc.user_loggedin:
|
||||
return ('Hello, %s, <a href="/private">See private</a> '
|
||||
'<a href="/logout">Log out</a>') % \
|
||||
oidc.user_getfield('preferred_username')
|
||||
return (
|
||||
'Hello, %s, <a href="/private">See private</a> '
|
||||
'<a href="/logout">Log out</a>'
|
||||
) % oidc.user_getfield("preferred_username")
|
||||
else:
|
||||
return 'Welcome anonymous, <a href="/private">Log in</a>'
|
||||
|
||||
|
||||
@app.route('/private')
|
||||
@app.route("/private")
|
||||
@oidc.require_login
|
||||
def hello_me():
|
||||
"""Example for protected endpoint that extracts private information from the OpenID Connect id_token.
|
||||
Uses the accompanied access_token to access a backend service.
|
||||
|
||||
Uses the accompanied access_token to access a backend service.
|
||||
"""
|
||||
info = oidc.user_getinfo(["preferred_username", "email", "sub"])
|
||||
|
||||
info = oidc.user_getinfo(['preferred_username', 'email', 'sub'])
|
||||
|
||||
username = info.get('preferred_username')
|
||||
email = info.get('email')
|
||||
user_id = info.get('sub')
|
||||
username = info.get("preferred_username")
|
||||
email = info.get("email")
|
||||
user_id = info.get("sub")
|
||||
|
||||
if user_id in oidc.credentials_store:
|
||||
try:
|
||||
from oauth2client.client import OAuth2Credentials
|
||||
access_token = OAuth2Credentials.from_json(oidc.credentials_store[user_id]).access_token
|
||||
print('access_token=<%s>' % access_token)
|
||||
headers = {'Authorization': 'Bearer %s' % (access_token)}
|
||||
|
||||
access_token = OAuth2Credentials.from_json(
|
||||
oidc.credentials_store[user_id]
|
||||
).access_token
|
||||
print("access_token=<%s>" % access_token)
|
||||
headers = {"Authorization": "Bearer %s" % (access_token)}
|
||||
# YOLO
|
||||
# greeting = requests.get('http://localhost:8080/greeting', headers=headers).text
|
||||
except:
|
||||
greeting = requests.get(
|
||||
"http://localhost:8080/greeting", headers=headers
|
||||
).text
|
||||
except BaseException:
|
||||
print("Could not access greeting-service")
|
||||
greeting = "Hello %s" % username
|
||||
|
||||
return ("""%s your email is %s and your user_id is %s!
|
||||
|
||||
return """{} your email is {} and your user_id is {}!
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="//localhost:8080/auth/realms/finance/account?referrer=flask-app&referrer_uri=http://localhost:5000/private&">Account</a></li>
|
||||
</ul>""" %
|
||||
(greeting, email, user_id))
|
||||
</ul>""".format(
|
||||
greeting,
|
||||
email,
|
||||
user_id,
|
||||
)
|
||||
|
||||
|
||||
@app.route('/api', methods=['POST'])
|
||||
@oidc.accept_token(require_token=True, scopes_required=['openid'])
|
||||
@app.route("/api", methods=["POST"])
|
||||
@oidc.accept_token(require_token=True, scopes_required=["openid"])
|
||||
def hello_api():
|
||||
"""OAuth 2.0 protected API endpoint accessible via AccessToken"""
|
||||
|
||||
return json.dumps({'hello': 'Welcome %s' % g.oidc_token_info['sub']})
|
||||
"""OAuth 2.0 protected API endpoint accessible via AccessToken."""
|
||||
return json.dumps({"hello": "Welcome %s" % g.oidc_token_info["sub"]})
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
"""Performs local logout by removing the session cookie."""
|
||||
|
||||
oidc.logout()
|
||||
return 'Hi, you have been logged out! <a href="/">Return</a>'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
|
|
@ -10,4 +10,5 @@ set -o errtrace -o errexit -o nounset -o pipefail
|
|||
if [[ -z "${FLASK_ENV:-}" ]]; then
|
||||
export FLASK_ENV=development
|
||||
fi
|
||||
|
||||
FLASK_APP=src/spiff_workflow_webapp poetry run flask run
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
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
|
||||
|
@ -11,17 +14,17 @@ 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")
|
||||
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
|
||||
'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:
|
||||
|
@ -42,7 +45,9 @@ def run_migrations_offline():
|
|||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(url=url, target_metadata=target_metadata, literal_binds=True)
|
||||
context.configure(
|
||||
url=url, target_metadata=target_metadata, literal_binds=True
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
@ -60,20 +65,20 @@ def run_migrations_online():
|
|||
# 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):
|
||||
if getattr(config.cmd_opts, 'autogenerate', False):
|
||||
script = directives[0]
|
||||
if script.upgrade_ops.is_empty():
|
||||
directives[:] = []
|
||||
logger.info("No changes in schema detected.")
|
||||
logger.info('No changes in schema detected.')
|
||||
|
||||
connectable = current_app.extensions["migrate"].db.get_engine()
|
||||
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
|
||||
**current_app.extensions['migrate'].configure_args
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 0c67e34e932b
|
||||
Revises:
|
||||
Create Date: 2022-05-23 16:01:00.321518
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0c67e34e932b'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('group',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=255), nullable=True),
|
||||
sa.Column('new_name_two', sa.String(length=255), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('process_group',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('user',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('username', sa.String(length=50), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=True),
|
||||
sa.Column('email', sa.String(length=50), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_table('process_model',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('process_group_id', sa.Integer(), nullable=False),
|
||||
sa.Column('version', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=50), nullable=True),
|
||||
sa.ForeignKeyConstraint(['process_group_id'], ['process_group.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('user_group_assignment',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['group_id'], ['group.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id', 'group_id', name='user_group_assignment_unique')
|
||||
)
|
||||
op.create_table('process_instance',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('process_model_id', sa.Integer(), nullable=False),
|
||||
sa.Column('bpmn_json', sa.JSON(), nullable=True),
|
||||
sa.Column('start_in_seconds', sa.Integer(), nullable=True),
|
||||
sa.Column('end_in_seconds', sa.Integer(), nullable=True),
|
||||
sa.Column('process_initiator_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['process_initiator_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['process_model_id'], ['process_model.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('process_instance')
|
||||
op.drop_table('user_group_assignment')
|
||||
op.drop_table('process_model')
|
||||
op.drop_table('user')
|
||||
op.drop_table('process_group')
|
||||
op.drop_table('group')
|
||||
# ### end Alembic commands ###
|
|
@ -1,68 +0,0 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 1f7b1ad256dc
|
||||
Revises:
|
||||
Create Date: 2022-05-20 14:21:53.581395
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1f7b1ad256dc"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"group",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("name", sa.String(length=255), nullable=True),
|
||||
sa.Column("new_name_two", sa.String(length=255), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"process_model",
|
||||
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("username", sa.String(length=50), nullable=False),
|
||||
sa.Column("name", sa.String(length=50), nullable=True),
|
||||
sa.Column("email", sa.String(length=50), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("username"),
|
||||
)
|
||||
op.create_table(
|
||||
"user_group_assignment",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("group_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["group_id"],
|
||||
["group.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["user.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("user_id", "group_id", name="user_group_assignment_unique"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("user_group_assignment")
|
||||
op.drop_table("user")
|
||||
op.drop_table("process_model")
|
||||
op.drop_table("group")
|
||||
# ### end Alembic commands ###
|
|
@ -36,7 +36,9 @@ nox.options.sessions = (
|
|||
|
||||
def setup_database(session: Session) -> None:
|
||||
"""Run database migrations against the database."""
|
||||
session.env["FLASK_INSTANCE_PATH"] = os.path.join(os.getcwd(), "instance")
|
||||
session.env["FLASK_INSTANCE_PATH"] = os.path.join(
|
||||
os.getcwd(), "instance", "testing"
|
||||
)
|
||||
session.env["FLASK_APP"] = "src/spiff_workflow_webapp"
|
||||
session.env["FLASK_ENV"] = "testing"
|
||||
session.run("flask", "db", "upgrade")
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
"""__init__."""
|
|
@ -1,229 +0,0 @@
|
|||
"""API Error functionality."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import sentry_sdk
|
||||
from flask import Blueprint
|
||||
from flask import current_app
|
||||
from flask import g
|
||||
from marshmallow import Schema
|
||||
from SpiffWorkflow import WorkflowException # type: ignore
|
||||
from SpiffWorkflow.exceptions import WorkflowTaskExecException # type: ignore
|
||||
from SpiffWorkflow.specs.base import TaskSpec # type: ignore
|
||||
from SpiffWorkflow.task import Task # type: ignore
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
|
||||
api_error_blueprint = Blueprint("api_error_blueprint", __name__)
|
||||
|
||||
|
||||
class ApiError(Exception):
|
||||
"""ApiError Class to help handle exceptions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: str,
|
||||
message: str,
|
||||
status_code: int = 400,
|
||||
file_name: str = "",
|
||||
task_id: str = "",
|
||||
task_name: str = "",
|
||||
tag: str = "",
|
||||
task_data: dict | None | str = None,
|
||||
error_type: str = "",
|
||||
error_line: str = "",
|
||||
line_number: int = 0,
|
||||
offset: int = 0,
|
||||
task_trace: dict | None = None,
|
||||
) -> None:
|
||||
"""The Init Method."""
|
||||
if task_data is None:
|
||||
task_data = {}
|
||||
if task_trace is None:
|
||||
task_trace = {}
|
||||
self.status_code = status_code
|
||||
self.code = code # a short consistent string describing the error.
|
||||
self.message = message # A detailed message that provides more information.
|
||||
|
||||
# OPTIONAL: The id of the task in the BPMN Diagram.
|
||||
self.task_id = task_id or ""
|
||||
|
||||
# OPTIONAL: The name of the task in the BPMN Diagram.
|
||||
|
||||
# OPTIONAL: The file that caused the error.
|
||||
self.task_name = task_name or ""
|
||||
self.file_name = file_name or ""
|
||||
|
||||
# OPTIONAL: The XML Tag that caused the issue.
|
||||
self.tag = tag or ""
|
||||
|
||||
# OPTIONAL: A snapshot of data connected to the task when error occurred.
|
||||
self.task_data = task_data or ""
|
||||
self.line_number = line_number
|
||||
self.offset = offset
|
||||
self.error_type = error_type
|
||||
self.error_line = error_line
|
||||
self.task_trace = task_trace
|
||||
|
||||
try:
|
||||
user = g.user.uid
|
||||
except Exception:
|
||||
user = "Unknown"
|
||||
self.task_user = user
|
||||
# This is for sentry logging into Slack
|
||||
sentry_sdk.set_context("User", {"user": user})
|
||||
Exception.__init__(self, self.message)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Instructions to print instance as a string."""
|
||||
msg = "ApiError: % s. " % self.message
|
||||
if self.task_name:
|
||||
msg += f"Error in task '{self.task_name}' ({self.task_id}). "
|
||||
if self.line_number:
|
||||
msg += "Error is on line %i. " % self.line_number
|
||||
if self.file_name:
|
||||
msg += "In file %s. " % self.file_name
|
||||
return msg
|
||||
|
||||
@classmethod
|
||||
def from_task(
|
||||
cls,
|
||||
code: str,
|
||||
message: str,
|
||||
task: Task,
|
||||
status_code: int = 400,
|
||||
line_number: int = 0,
|
||||
offset: int = 0,
|
||||
error_type: str = "",
|
||||
error_line: str = "",
|
||||
task_trace: dict | None = None,
|
||||
) -> ApiError:
|
||||
"""Constructs an API Error with details pulled from the current task."""
|
||||
instance = cls(code, message, status_code=status_code)
|
||||
instance.task_id = task.task_spec.name or ""
|
||||
instance.task_name = task.task_spec.description or ""
|
||||
instance.file_name = task.workflow.spec.file or ""
|
||||
instance.line_number = line_number
|
||||
instance.offset = offset
|
||||
instance.error_type = error_type
|
||||
instance.error_line = error_line
|
||||
if task_trace:
|
||||
instance.task_trace = task_trace
|
||||
else:
|
||||
instance.task_trace = WorkflowTaskExecException.get_task_trace(task)
|
||||
|
||||
# Fixme: spiffworkflow is doing something weird where task ends up referenced in the data in some cases.
|
||||
if "task" in task.data:
|
||||
task.data.pop("task")
|
||||
|
||||
# Assure that there is nothing in the json data that can't be serialized.
|
||||
instance.task_data = ApiError.remove_unserializeable_from_dict(task.data)
|
||||
|
||||
current_app.logger.error(message, exc_info=True)
|
||||
return instance
|
||||
|
||||
@staticmethod
|
||||
def remove_unserializeable_from_dict(my_dict: dict) -> dict:
|
||||
"""Removes unserializeable from dict."""
|
||||
keys_to_delete = []
|
||||
for key, value in my_dict.items():
|
||||
if not ApiError.is_jsonable(value):
|
||||
keys_to_delete.append(key)
|
||||
for key in keys_to_delete:
|
||||
del my_dict[key]
|
||||
return my_dict
|
||||
|
||||
@staticmethod
|
||||
def is_jsonable(x: Any) -> bool:
|
||||
"""Attempts a json.dump on given input and returns false if it cannot."""
|
||||
try:
|
||||
json.dumps(x)
|
||||
return True
|
||||
except (TypeError, OverflowError, ValueError):
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_task_spec(
|
||||
cls,
|
||||
code: str,
|
||||
message: str,
|
||||
task_spec: TaskSpec,
|
||||
status_code: int = 400,
|
||||
) -> ApiError:
|
||||
"""Constructs an API Error with details pulled from the current task."""
|
||||
instance = cls(code, message, status_code=status_code)
|
||||
instance.task_id = task_spec.name or ""
|
||||
instance.task_name = task_spec.description or ""
|
||||
if task_spec._wf_spec:
|
||||
instance.file_name = task_spec._wf_spec.file
|
||||
current_app.logger.error(message, exc_info=True)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_workflow_exception(
|
||||
cls,
|
||||
code: str,
|
||||
message: str,
|
||||
exp: WorkflowException,
|
||||
) -> ApiError:
|
||||
"""Deals with workflow exceptions.
|
||||
|
||||
We catch a lot of workflow exception errors,
|
||||
so consolidating the code, and doing the best things
|
||||
we can with the data we have.
|
||||
"""
|
||||
if isinstance(exp, WorkflowTaskExecException):
|
||||
return ApiError.from_task(
|
||||
code,
|
||||
message,
|
||||
exp.task,
|
||||
line_number=exp.line_number,
|
||||
offset=exp.offset,
|
||||
error_type=exp.exception.__class__.__name__,
|
||||
error_line=exp.error_line,
|
||||
task_trace=exp.task_trace,
|
||||
)
|
||||
|
||||
else:
|
||||
return ApiError.from_task_spec(code, message, exp.sender)
|
||||
|
||||
|
||||
class ApiErrorSchema(Schema):
|
||||
"""ApiErrorSchema Class."""
|
||||
|
||||
class Meta:
|
||||
"""Sets the fields to search the error schema for."""
|
||||
|
||||
fields = (
|
||||
"code",
|
||||
"message",
|
||||
"workflow_name",
|
||||
"file_name",
|
||||
"task_name",
|
||||
"task_id",
|
||||
"task_data",
|
||||
"task_user",
|
||||
"hint",
|
||||
"line_number",
|
||||
"offset",
|
||||
"error_type",
|
||||
"error_line",
|
||||
"task_trace",
|
||||
)
|
||||
|
||||
|
||||
@api_error_blueprint.app_errorhandler(ApiError)
|
||||
def handle_invalid_usage(error: ApiError) -> tuple[str, int]:
|
||||
"""Handles invalid usage error."""
|
||||
response = ApiErrorSchema().dump(error)
|
||||
return response, error.status_code
|
||||
|
||||
|
||||
@api_error_blueprint.app_errorhandler(InternalServerError)
|
||||
def handle_internal_server_error(error: ApiError) -> tuple[str, int]:
|
||||
"""Handles internal server error."""
|
||||
original = getattr(error, "original_exception", None)
|
||||
api_error = ApiError(code="Internal Server Error (500)", message=str(original))
|
||||
response = ApiErrorSchema().dump(api_error)
|
||||
return response, 500
|
|
@ -16,7 +16,7 @@ def setup_config(app: Flask) -> None:
|
|||
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
|
||||
app.config.from_object("spiff_workflow_webapp.config.default")
|
||||
|
||||
if os.environ.get("TEST_DATABASE_TYPE") == "sqlite":
|
||||
if os.environ.get("SPIFF_DATABASE_TYPE") == "sqlite":
|
||||
app.config[
|
||||
"SQLALCHEMY_DATABASE_URI"
|
||||
] = f"sqlite:///{app.instance_path}/db_{app.env}.sqlite3"
|
||||
|
@ -29,9 +29,10 @@ def setup_config(app: Flask) -> None:
|
|||
"SQLALCHEMY_DATABASE_URI"
|
||||
] = f"mysql+mysqlconnector://root:{mysql_pswd}@localhost/spiff_workflow_webapp_{app.env}"
|
||||
|
||||
env_config_module = "spiff_workflow_webapp.config." + app.env
|
||||
try:
|
||||
app.config.from_object("spiff_workflow_webapp.config." + app.env)
|
||||
app.config.from_object(env_config_module)
|
||||
except ImportStringError as exception:
|
||||
raise Exception(
|
||||
"Cannot find config file for FLASK_ENV: " + app.env
|
||||
raise ModuleNotFoundError(
|
||||
f"Cannot find config module: {env_config_module}"
|
||||
) from exception
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
"""Permission."""
|
||||
from flask_bpmn.models.db import db
|
||||
|
||||
|
||||
class PermissionModel(db.Model): # type: ignore
|
||||
"""PermissionModel."""
|
||||
|
||||
__tablename__ = "permission"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50))
|
|
@ -5,7 +5,6 @@ from flask_bpmn.models.db import db
|
|||
from sqlalchemy import Enum # type: ignore
|
||||
from sqlalchemy import ForeignKey
|
||||
|
||||
from spiff_workflow_webapp.models.permission import PermissionModel
|
||||
from spiff_workflow_webapp.models.permission_target import PermissionTargetModel
|
||||
from spiff_workflow_webapp.models.principal import PrincipalModel
|
||||
|
||||
|
@ -17,14 +16,22 @@ class GrantDeny(enum.Enum):
|
|||
deny = 2
|
||||
|
||||
|
||||
class Permission(enum.Enum):
|
||||
"""Permission."""
|
||||
|
||||
instantiate = 1
|
||||
administer = 2
|
||||
view_instance = 3
|
||||
|
||||
|
||||
class PermissionAssignmentModel(db.Model): # type: ignore
|
||||
"""PermissionAssignmentModel."""
|
||||
|
||||
__tablename__ = "permission_assignment"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
principal_id = db.Column(ForeignKey(PrincipalModel.id), nullable=False)
|
||||
permission_id = db.Column(ForeignKey(PermissionModel.id), nullable=False)
|
||||
permission_target_id = db.Column(
|
||||
ForeignKey(PermissionTargetModel.id), nullable=False
|
||||
)
|
||||
grant_type = db.Column(Enum(GrantDeny))
|
||||
permission = db.Column(Enum(Permission))
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
"""PermissionTarget."""
|
||||
from flask_bpmn.models.db import db
|
||||
from sqlalchemy import ForeignKey # type: ignore
|
||||
from sqlalchemy.schema import CheckConstraint # type: ignore
|
||||
|
||||
# from sqlalchemy import ForeignKey # type: ignore
|
||||
# from sqlalchemy.orm import relationship # type: ignore
|
||||
|
||||
# from spiff_workflow_webapp.models.principal import PrincipalModel
|
||||
# from spiff_workflow_webapp.models.permission import PermissionModel
|
||||
from spiff_workflow_webapp.models.process_group import ProcessGroupModel
|
||||
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
|
||||
from spiff_workflow_webapp.models.process_model import ProcessModel
|
||||
|
||||
|
||||
class PermissionTargetModel(db.Model): # type: ignore
|
||||
"""PermissionTargetModel."""
|
||||
|
||||
__tablename__ = "permission_target"
|
||||
__table_args__ = (
|
||||
CheckConstraint(
|
||||
"NOT(process_group_id IS NULL AND process_model_id IS NULL AND process_instance_id IS NULL)"
|
||||
),
|
||||
)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# user_id = db.Column(ForeignKey(UserModel.id), nullable=False)
|
||||
# group_id = db.Column(ForeignKey(GroupModel.id), nullable=False)
|
||||
process_group_id = db.Column(ForeignKey(ProcessGroupModel.id), nullable=True)
|
||||
process_model_id = db.Column(ForeignKey(ProcessModel.id), nullable=True)
|
||||
process_instance_id = db.Column(ForeignKey(ProcessInstanceModel.id), nullable=True)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Principal."""
|
||||
from flask_bpmn.models.db import db
|
||||
from sqlalchemy import ForeignKey # type: ignore
|
||||
from sqlalchemy.schema import CheckConstraint # type: ignore
|
||||
|
||||
from spiff_workflow_webapp.models.group import GroupModel
|
||||
from spiff_workflow_webapp.models.user import UserModel
|
||||
|
@ -10,6 +11,8 @@ class PrincipalModel(db.Model): # type: ignore
|
|||
"""PrincipalModel."""
|
||||
|
||||
__tablename__ = "principal"
|
||||
__table_args__ = (CheckConstraint("NOT(user_id IS NULL AND group_id IS NULL)"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(ForeignKey(UserModel.id), nullable=False)
|
||||
group_id = db.Column(ForeignKey(GroupModel.id), nullable=False)
|
||||
user_id = db.Column(ForeignKey(UserModel.id), nullable=True)
|
||||
group_id = db.Column(ForeignKey(GroupModel.id), nullable=True)
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
"""Process_group."""
|
||||
from flask_bpmn.models.db import db
|
||||
from sqlalchemy.orm import deferred # type: ignore
|
||||
|
||||
|
||||
class ProcessGroupModel(db.Model): # type: ignore
|
||||
"""ProcessGroupMode."""
|
||||
|
||||
__tablename__ = "process_group"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(50))
|
|
@ -0,0 +1,21 @@
|
|||
"""Process_instance."""
|
||||
from flask_bpmn.models.db import db
|
||||
from sqlalchemy.orm import deferred # type: ignore
|
||||
from sqlalchemy import ForeignKey # type: ignore
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from spiff_workflow_webapp.models.user import UserModel
|
||||
from spiff_workflow_webapp.models.process_model import ProcessModel
|
||||
|
||||
|
||||
class ProcessInstanceModel(db.Model): # type: ignore
|
||||
"""ProcessInstanceModel."""
|
||||
|
||||
__tablename__ = "process_instance"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
process_model_id = db.Column(ForeignKey(ProcessModel.id), nullable=False)
|
||||
bpmn_json = deferred(db.Column(db.JSON))
|
||||
start_in_seconds = db.Column(db.Integer)
|
||||
end_in_seconds = db.Column(db.Integer)
|
||||
process_initiator_id = db.Column(ForeignKey(UserModel.id), nullable=False)
|
||||
process_initiator = relationship("UserModel")
|
|
@ -1,6 +1,8 @@
|
|||
"""Process_model."""
|
||||
from flask_bpmn.models.db import db
|
||||
from sqlalchemy.orm import deferred # type: ignore
|
||||
from sqlalchemy import ForeignKey # type: ignore
|
||||
|
||||
from spiff_workflow_webapp.models.process_group import ProcessGroupModel
|
||||
|
||||
|
||||
class ProcessModel(db.Model): # type: ignore
|
||||
|
@ -8,4 +10,6 @@ class ProcessModel(db.Model): # type: ignore
|
|||
|
||||
__tablename__ = "process_model"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
bpmn_json = deferred(db.Column(db.JSON))
|
||||
process_group_id = db.Column(ForeignKey(ProcessGroupModel.id), nullable=False)
|
||||
version = db.Column(db.Integer, nullable=False, default=1)
|
||||
name = db.Column(db.String(50))
|
||||
|
|
|
@ -1,167 +0,0 @@
|
|||
"""Workflow."""
|
||||
# import enum
|
||||
#
|
||||
# import marshmallow
|
||||
# from crc import db
|
||||
# from crc import ma
|
||||
# from marshmallow import post_load
|
||||
# from sqlalchemy import func
|
||||
# from sqlalchemy.orm import deferred
|
||||
#
|
||||
#
|
||||
# class WorkflowSpecCategory:
|
||||
# """WorkflowSpecCategory."""
|
||||
#
|
||||
# def __init__(self, id, display_name, display_order=0, admin=False):
|
||||
# """__init__."""
|
||||
# self.id = (
|
||||
# id # A unique string name, lower case, under scores (ie, 'my_category')
|
||||
# )
|
||||
# self.display_name = display_name
|
||||
# self.display_order = display_order
|
||||
# self.admin = admin
|
||||
# self.workflows = [] # For storing Workflow Metadata
|
||||
# self.specs = [] # For the list of specifications associated with a category
|
||||
# self.meta = None # For storing category metadata
|
||||
#
|
||||
# def __eq__(self, other):
|
||||
# """__eq__."""
|
||||
# if not isinstance(other, WorkflowSpecCategory):
|
||||
# return False
|
||||
# if other.id == self.id:
|
||||
# return True
|
||||
# return False
|
||||
#
|
||||
#
|
||||
# class WorkflowSpecCategorySchema(ma.Schema):
|
||||
# """WorkflowSpecCategorySchema."""
|
||||
#
|
||||
# class Meta:
|
||||
# """Meta."""
|
||||
#
|
||||
# model = WorkflowSpecCategory
|
||||
# fields = ["id", "display_name", "display_order", "admin"]
|
||||
#
|
||||
# @post_load
|
||||
# def make_cat(self, data, **kwargs):
|
||||
# """Make_cat."""
|
||||
# return WorkflowSpecCategory(**data)
|
||||
#
|
||||
#
|
||||
# class WorkflowSpecInfo:
|
||||
# """WorkflowSpecInfo."""
|
||||
#
|
||||
# def __init__(
|
||||
# self,
|
||||
# id,
|
||||
# display_name,
|
||||
# description,
|
||||
# is_master_spec=False,
|
||||
# standalone=False,
|
||||
# library=False,
|
||||
# primary_file_name="",
|
||||
# primary_process_id="",
|
||||
# libraries=None,
|
||||
# category_id="",
|
||||
# display_order=0,
|
||||
# is_review=False,
|
||||
# ):
|
||||
# """__init__."""
|
||||
# self.id = id # Sting unique id
|
||||
# self.display_name = display_name
|
||||
# self.description = description
|
||||
# self.display_order = display_order
|
||||
# self.is_master_spec = is_master_spec
|
||||
# self.standalone = standalone
|
||||
# self.library = library
|
||||
# self.primary_file_name = primary_file_name
|
||||
# self.primary_process_id = primary_process_id
|
||||
# self.is_review = is_review
|
||||
# self.category_id = category_id
|
||||
#
|
||||
# if libraries is None:
|
||||
# libraries = []
|
||||
# self.libraries = libraries
|
||||
#
|
||||
# def __eq__(self, other):
|
||||
# """__eq__."""
|
||||
# if not isinstance(other, WorkflowSpecInfo):
|
||||
# return False
|
||||
# if other.id == self.id:
|
||||
# return True
|
||||
# return False
|
||||
#
|
||||
#
|
||||
# class WorkflowSpecInfoSchema(ma.Schema):
|
||||
# """WorkflowSpecInfoSchema."""
|
||||
#
|
||||
# class Meta:
|
||||
# """Meta."""
|
||||
#
|
||||
# model = WorkflowSpecInfo
|
||||
#
|
||||
# id = marshmallow.fields.String(required=True)
|
||||
# display_name = marshmallow.fields.String(required=True)
|
||||
# description = marshmallow.fields.String()
|
||||
# is_master_spec = marshmallow.fields.Boolean(required=True)
|
||||
# standalone = marshmallow.fields.Boolean(required=True)
|
||||
# library = marshmallow.fields.Boolean(required=True)
|
||||
# display_order = marshmallow.fields.Integer(allow_none=True)
|
||||
# primary_file_name = marshmallow.fields.String(allow_none=True)
|
||||
# primary_process_id = marshmallow.fields.String(allow_none=True)
|
||||
# is_review = marshmallow.fields.Boolean(allow_none=True)
|
||||
# category_id = marshmallow.fields.String(allow_none=True)
|
||||
# libraries = marshmallow.fields.List(marshmallow.fields.String(), allow_none=True)
|
||||
#
|
||||
# @post_load
|
||||
# def make_spec(self, data, **kwargs):
|
||||
# """Make_spec."""
|
||||
# return WorkflowSpecInfo(**data)
|
||||
#
|
||||
#
|
||||
# class WorkflowState(enum.Enum):
|
||||
# """WorkflowState."""
|
||||
#
|
||||
# hidden = "hidden"
|
||||
# disabled = "disabled"
|
||||
# required = "required"
|
||||
# optional = "optional"
|
||||
# locked = "locked"
|
||||
#
|
||||
# @classmethod
|
||||
# def has_value(cls, value):
|
||||
# """Has_value."""
|
||||
# return value in cls._value2member_map_
|
||||
#
|
||||
# @staticmethod
|
||||
# def list():
|
||||
# """List."""
|
||||
# return list(map(lambda c: c.value, WorkflowState))
|
||||
#
|
||||
#
|
||||
# class WorkflowStatus(enum.Enum):
|
||||
# """WorkflowStatus."""
|
||||
#
|
||||
# not_started = "not_started"
|
||||
# user_input_required = "user_input_required"
|
||||
# waiting = "waiting"
|
||||
# complete = "complete"
|
||||
# erroring = "erroring"
|
||||
#
|
||||
#
|
||||
# class WorkflowModel(db.Model):
|
||||
# """WorkflowModel."""
|
||||
#
|
||||
# __tablename__ = "workflow"
|
||||
# id = db.Column(db.Integer, primary_key=True)
|
||||
# bpmn_workflow_json = deferred(db.Column(db.JSON))
|
||||
# status = db.Column(db.Enum(WorkflowStatus))
|
||||
# study_id = db.Column(db.Integer, db.ForeignKey("study.id"))
|
||||
# study = db.relationship("StudyModel", backref="workflow", lazy="select")
|
||||
# workflow_spec_id = db.Column(db.String)
|
||||
# total_tasks = db.Column(db.Integer, default=0)
|
||||
# completed_tasks = db.Column(db.Integer, default=0)
|
||||
# last_updated = db.Column(db.DateTime(timezone=True), server_default=func.now())
|
||||
# user_id = db.Column(db.String, default=None)
|
||||
# state = db.Column(db.String, nullable=True)
|
||||
# state_message = db.Column(db.String, nullable=True)
|
|
@ -10,7 +10,7 @@ from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # typ
|
|||
from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore
|
||||
from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
|
||||
|
||||
from spiff_workflow_webapp.models.process_model import ProcessModel
|
||||
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
|
||||
from spiff_workflow_webapp.spiff_workflow_connector import parse
|
||||
from spiff_workflow_webapp.spiff_workflow_connector import run
|
||||
|
||||
|
@ -46,11 +46,11 @@ def run_process() -> Response:
|
|||
]
|
||||
|
||||
workflow = None
|
||||
process_model = ProcessModel.query.filter().first()
|
||||
if process_model is None:
|
||||
process_instance = ProcessInstanceModel.query.filter().first()
|
||||
if process_instance is None:
|
||||
workflow = parse(process, bpmn, dmn)
|
||||
else:
|
||||
workflow = serializer.deserialize_json(process_model.bpmn_json)
|
||||
workflow = serializer.deserialize_json(process_instance.bpmn_json)
|
||||
|
||||
response = run(workflow, content.get("task_identifier"), content.get("answer"))
|
||||
|
||||
|
|
|
@ -22,9 +22,11 @@ from SpiffWorkflow.task import Task # type: ignore
|
|||
from SpiffWorkflow.task import TaskState
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from spiff_workflow_webapp.models.process_group import ProcessGroupModel
|
||||
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
|
||||
from spiff_workflow_webapp.models.process_model import ProcessModel
|
||||
from spiff_workflow_webapp.models.user import UserModel
|
||||
|
||||
# from custom_script_engine import CustomScriptEngine
|
||||
|
||||
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(
|
||||
[UserTaskConverter, BusinessRuleTaskConverter]
|
||||
|
@ -120,6 +122,47 @@ def get_state(workflow: BpmnWorkflow) -> ProcessStatus:
|
|||
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_model() -> ProcessModel:
|
||||
"""Create_process_model."""
|
||||
process_group = ProcessGroupModel.query.filter().first()
|
||||
if process_group is None:
|
||||
process_group = ProcessGroupModel(name="group1")
|
||||
db.session.add(process_group)
|
||||
db.session.commit()
|
||||
|
||||
process_model = ProcessModel(process_group_id=process_group.id)
|
||||
db.session.add(process_model)
|
||||
db.session.commit()
|
||||
|
||||
return process_model
|
||||
|
||||
|
||||
def create_process_instance() -> ProcessInstanceModel:
|
||||
"""Create_process_instance."""
|
||||
process_model = ProcessModel.query.filter().first()
|
||||
if process_model is None:
|
||||
process_model = create_process_model()
|
||||
|
||||
user = UserModel.query.filter().first()
|
||||
if user is None:
|
||||
user = create_user()
|
||||
|
||||
process_instance = ProcessInstanceModel(process_model_id=process_model.id, 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,
|
||||
|
@ -168,11 +211,12 @@ def run(
|
|||
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)
|
||||
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
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
"""__init__."""
|
|
@ -1,26 +0,0 @@
|
|||
"""Test cases for the __main__ module."""
|
||||
import io
|
||||
|
||||
from spiff_workflow_webapp.api.api_error import ApiError
|
||||
|
||||
|
||||
def test_is_jsonable_can_succeed() -> None:
|
||||
result = ApiError.is_jsonable("This is a string and should pass json.dumps")
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_jsonable_can_fail() -> None:
|
||||
result = ApiError.is_jsonable(io.StringIO("BAD JSON OBJECT"))
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_remove_unserializeable_from_dict_succeeds() -> None:
|
||||
initial_dict_object = {
|
||||
"valid_key": "valid_value",
|
||||
"invalid_key_value": io.StringIO("BAD JSON OBJECT"),
|
||||
}
|
||||
final_dict_object = {
|
||||
"valid_key": "valid_value",
|
||||
}
|
||||
result = ApiError.remove_unserializeable_from_dict(initial_dict_object)
|
||||
assert result == final_dict_object
|
|
@ -5,13 +5,13 @@ from typing import Union
|
|||
from flask.testing import FlaskClient
|
||||
from flask_bpmn.models.db import db
|
||||
|
||||
from spiff_workflow_webapp.models.process_model import ProcessModel
|
||||
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
|
||||
|
||||
|
||||
def test_user_can_be_created_and_deleted(client: FlaskClient) -> None:
|
||||
process_model = ProcessModel.query.filter().first()
|
||||
if process_model is not None:
|
||||
db.session.delete(process_model)
|
||||
process_instance = ProcessInstanceModel.query.filter().first()
|
||||
if process_instance is not None:
|
||||
db.session.delete(process_instance)
|
||||
db.session.commit()
|
||||
|
||||
last_response = None
|
||||
|
@ -30,9 +30,9 @@ def test_user_can_be_created_and_deleted(client: FlaskClient) -> None:
|
|||
for task in tasks:
|
||||
run_task(client, task, last_response)
|
||||
|
||||
process_model = ProcessModel.query.filter().first()
|
||||
if process_model is not None:
|
||||
db.session.delete(process_model)
|
||||
process_instance = ProcessInstanceModel.query.filter().first()
|
||||
if process_instance is not None:
|
||||
db.session.delete(process_instance)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue