added process models and cleaned up some of the code w/ burnettk

This commit is contained in:
jasquat 2022-05-23 16:36:23 -04:00
parent 69af11076e
commit e3d90a6044
26 changed files with 2452 additions and 2409 deletions

View File

@ -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

View File

@ -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

View File

@ -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"
}
}

View File

@ -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()

View File

@ -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

View File

@ -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():

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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")

View File

@ -1 +0,0 @@
"""__init__."""

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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")

View File

@ -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))

View File

@ -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)

View File

@ -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"))

View File

@ -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

View File

@ -1 +0,0 @@
"""__init__."""

View File

@ -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

View File

@ -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()