some auto linting fixes w/ burnettk

This commit is contained in:
jasquat 2022-05-31 14:10:00 -04:00
parent d63dfe1f21
commit 2c4f2adeeb
33 changed files with 1608 additions and 921 deletions

2
.gitignore vendored
View File

@ -10,4 +10,4 @@
/src/*.egg-info/
__pycache__/
*.sqlite3
node_modules
node_modules

View File

@ -1,5 +1,3 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig

View File

@ -1,14 +1,14 @@
"""__init__."""
import os
import flask.app
import connexion
import flask.app
from flask_bpmn.api.api_error import api_error_blueprint
from flask_bpmn.models.db import db
from flask_bpmn.models.db import migrate
from spiff_workflow_webapp.routes.admin_blueprint.admin_blueprint import admin_blueprint
from spiff_workflow_webapp.config import setup_config
from spiff_workflow_webapp.routes.admin_blueprint.admin_blueprint import admin_blueprint
from spiff_workflow_webapp.routes.api_blueprint import api_blueprint
from spiff_workflow_webapp.routes.process_api_blueprint import process_api_blueprint
from spiff_workflow_webapp.routes.user_blueprint import user_blueprint
@ -21,11 +21,13 @@ def create_app() -> flask.app.Flask:
# variable, it will be one thing when we run flask db upgrade in the
# noxfile and another thing when the tests actually run.
# instance_path is described more at https://flask.palletsprojects.com/en/2.1.x/config/
connexion_app = connexion.FlaskApp(__name__, server_args={"instance_path": os.environ.get("FLASK_INSTANCE_PATH")})
connexion_app = connexion.FlaskApp(
__name__, server_args={"instance_path": os.environ.get("FLASK_INSTANCE_PATH")}
)
app = connexion_app.app
app.config['CONNEXION_APP'] = connexion_app
app.secret_key = 'super secret key'
app.config['SESSION_TYPE'] = 'filesystem'
app.config["CONNEXION_APP"] = connexion_app
app.secret_key = "super secret key"
app.config["SESSION_TYPE"] = "filesystem"
setup_config(app)
db.init_app(app)
@ -36,7 +38,7 @@ def create_app() -> flask.app.Flask:
app.register_blueprint(process_api_blueprint)
app.register_blueprint(api_error_blueprint)
app.register_blueprint(admin_blueprint, url_prefix="/admin")
connexion_app.add_api("api.yml", base_path='/v1.0')
connexion_app.add_api("api.yml", base_path="/v1.0")
for name, value in app.config.items():
print(f"{name} = {value}")

View File

@ -7,8 +7,7 @@ info:
servers:
- url: http://localhost:5000/v1.0
security:
- jwt: ['secret']
- jwt: ["secret"]
paths:
/workflow-specification:
@ -48,9 +47,9 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowSpec'
$ref: "#/components/schemas/WorkflowSpec"
responses:
'200':
"200":
description: Workflow specification created successfully.
content:
application/json:
@ -93,7 +92,7 @@ paths:
type: string
format: binary
responses:
'200':
"200":
description: Metadata about the uploaded file, but not the file content.
content:
application/json:
@ -125,7 +124,7 @@ paths:
tags:
- Workflow Specifications
responses:
'200':
"200":
description: Workflow generated successfully
content:
application/json:
@ -180,7 +179,7 @@ paths:
tags:
- Spec Files
responses:
'200':
"200":
description: Returns the file information requested.
content:
application/json:
@ -255,84 +254,83 @@ components:
properties:
workflow_spec_id:
type: string
example : top_level_workflow
date_created :
example: top_level_workflow
date_created:
type: string
example : 2020-12-09 16:55:12.951500+00:00
location :
type : string
example : remote
new :
type : boolean
example : false
example: 2020-12-09 16:55:12.951500+00:00
location:
type: string
example: remote
new:
type: boolean
example: false
WorkflowSpecFilesList:
properties:
file_model_id:
type : integer
example : 171
workflow_spec_id :
type: integer
example: 171
workflow_spec_id:
type: string
example : top_level_workflow
filename :
example: top_level_workflow
filename:
type: string
example : data_security_plan.dmn
date_created :
example: data_security_plan.dmn
date_created:
type: string
example : 2020-12-01 13:58:12.420333+00:00
example: 2020-12-01 13:58:12.420333+00:00
type:
type : string
example : dmn
primary :
type : boolean
example : false
type: string
example: dmn
primary:
type: boolean
example: false
content_type:
type: string
example : text/xml
example: text/xml
primary_process_id:
type : string
example : null
type: string
example: null
md5_hash:
type: string
example: f12e2bbd-a20c-673b-ccb8-a8a1ea9c5b7b
WorkflowSpecFilesDiff:
properties:
filename :
filename:
type: string
example : data_security_plan.dmn
date_created :
example: data_security_plan.dmn
date_created:
type: string
example : 2020-12-01 13:58:12.420333+00:00
example: 2020-12-01 13:58:12.420333+00:00
type:
type : string
example : dmn
primary :
type : boolean
example : false
type: string
example: dmn
primary:
type: boolean
example: false
content_type:
type: string
example : text/xml
example: text/xml
primary_process_id:
type : string
example : null
type: string
example: null
md5_hash:
type: string
example: f12e2bbd-a20c-673b-ccb8-a8a1ea9c5b7b
location:
type : string
example : remote
type: string
example: remote
new:
type: boolean
example : false
example: false
WorkflowSpecAll:
properties:
workflow_spec_id :
workflow_spec_id:
type: string
example : acaf1258-43b4-437e-8846-f612afa66811
date_created :
example: acaf1258-43b4-437e-8846-f612afa66811
date_created:
type: string
example : 2020-12-01 13:58:12.420333+00:00
example: 2020-12-01 13:58:12.420333+00:00
md5_hash:
type: string
example: c30fd597f21715018eab12f97f9d4956
@ -353,7 +351,7 @@ components:
example: dhf8r
status:
type: string
enum: ['in_progress', 'hold', 'open_for_enrollment', 'abandoned']
enum: ["in_progress", "hold", "open_for_enrollment", "abandoned"]
example: done
sponsor:
type: string
@ -503,7 +501,7 @@ components:
format: int64
status:
type: string
enum: ['new','user_input_required','waiting','complete']
enum: ["new", "user_input_required", "waiting", "complete"]
navigation:
type: array
items:
@ -527,9 +525,9 @@ components:
example:
id: 291234
status: 'user_input_required'
workflow_spec_id: 'random_fact'
spec_version: 'v1.1 [22,23]'
status: "user_input_required"
workflow_spec_id: "random_fact"
spec_version: "v1.1 [22,23]"
is_latest_spec: True
next_task:
id: study_identification
@ -559,7 +557,7 @@ components:
type: object
multi_instance_type:
type: string
enum: ['none', 'looping', 'parallel', 'sequential']
enum: ["none", "looping", "parallel", "sequential"]
multi_instance_count:
type: number
multi_instance_index:
@ -613,91 +611,91 @@ components:
value: "model.my_enum_field_value === 'something'"
PaginatedTaskLog:
properties:
code:
example: "email_sent"
type: string
level:
example: "warning"
type: string
user:
example: "email_sent"
type: string
page:
type: integer
example: 0
per_page:
type: integer
example: 10
sort_column:
type: string
example: "timestamp"
sort_reverse:
type: boolean
example: false
code:
example: "email_sent"
type: string
level:
example: "warning"
type: string
user:
example: "email_sent"
type: string
page:
type: integer
example: 0
per_page:
type: integer
example: 10
sort_column:
type: string
example: "timestamp"
sort_reverse:
type: boolean
example: false
items:
type: array
items:
type: array
items:
$ref: "#/components/schemas/TaskLog"
has_next:
type: boolean
example: true
has_prev:
type: boolean
example: false
$ref: "#/components/schemas/TaskLog"
has_next:
type: boolean
example: true
has_prev:
type: boolean
example: false
TaskLog:
properties:
level:
type: string
example: "info"
code:
example: "email_sent"
type: string
message:
example: "Approval email set to Jake in Accounting"
type: string
workflow_id:
example: 42
type: integer
study_id:
example: 187
type: integer
user_uid:
example: "dhf8r"
type: string
timestamp:
type: string
format: date_time
example: "2021-01-07T11:36:40.001Z"
level:
type: string
example: "info"
code:
example: "email_sent"
type: string
message:
example: "Approval email set to Jake in Accounting"
type: string
workflow_id:
example: 42
type: integer
study_id:
example: 187
type: integer
user_uid:
example: "dhf8r"
type: string
timestamp:
type: string
format: date_time
example: "2021-01-07T11:36:40.001Z"
TaskEvent:
properties:
workflow:
$ref: "#/components/schemas/Workflow"
study:
$ref: "#/components/schemas/Study"
workflow_sec:
$ref: "#/components/schemas/WorkflowSpec"
spec_version:
type: string
action:
type: string
task_id:
type: string
task_type:
type: string
task_lane:
type: string
form_data:
type: object
mi_type:
type: string
mi_count:
type: integer
mi_index:
type: integer
process_name:
type: string
date:
type: string
workflow:
$ref: "#/components/schemas/Workflow"
study:
$ref: "#/components/schemas/Study"
workflow_sec:
$ref: "#/components/schemas/WorkflowSpec"
spec_version:
type: string
action:
type: string
task_id:
type: string
task_type:
type: string
task_lane:
type: string
form_data:
type: object
mi_type:
type: string
mi_count:
type: integer
mi_index:
type: integer
process_name:
type: string
date:
type: string
Form:
properties:
key:
@ -749,7 +747,7 @@ components:
readOnly: true
type:
type: string
enum: ['string', 'long', 'boolean', 'date', 'enum']
enum: ["string", "long", "boolean", "date", "enum"]
readOnly: true
label:
type: string
@ -855,7 +853,16 @@ components:
example: 4
state:
type: string
enum: ['FUTURE', 'WAITING', 'READY', 'CANCELLED', 'COMPLETED','LIKELY','MAYBE']
enum:
[
"FUTURE",
"WAITING",
"READY",
"CANCELLED",
"COMPLETED",
"LIKELY",
"MAYBE",
]
readOnly: true
is_decision:
type: boolean
@ -893,9 +900,9 @@ components:
example: 5
GitRepo:
properties:
# remote:
# type: string
# example: sartography/crconnect-workflow-specs
# remote:
# type: string
# example: sartography/crconnect-workflow-specs
directory:
type: string
example: /home/cr-connect/sync_files
@ -907,8 +914,7 @@ components:
example: staging
changes:
type: array
example: ['file_1.txt', 'file_2.txt']
example: ["file_1.txt", "file_2.txt"]
untracked:
type: array
example: ['a_file.txt', 'b_file.txt']
example: ["a_file.txt", "b_file.txt"]

View File

@ -1,4 +1,4 @@
"""Default."""
from os import environ
BPMN_SPEC_ABSOLUTE_DIR = environ.get('BPMN_SPEC_ABSOLUTE_DIR', default="")
BPMN_SPEC_ABSOLUTE_DIR = environ.get("BPMN_SPEC_ABSOLUTE_DIR", default="")

View File

@ -1,9 +1,8 @@
"""Data_store."""
from flask_bpmn.models.db import db
from flask_marshmallow.sqla import SQLAlchemyAutoSchema
from sqlalchemy import func
from flask_bpmn.models.db import db
class DataStoreModel(db.Model):
"""DataStoreModel."""

View File

@ -1,57 +1,64 @@
"""File."""
import enum
from marshmallow import Schema, INCLUDE
from flask_bpmn.models.db import db
from marshmallow import INCLUDE
from marshmallow import Schema
from sqlalchemy import func
from sqlalchemy.orm import deferred # type: ignore
from sqlalchemy.orm import relationship
from flask_bpmn.models.db import db
from spiff_workflow_webapp.models.data_store import DataStoreModel
class FileModel(db.Model):
"""FileModel."""
__tablename__ = 'file'
__tablename__ = "file"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
type = db.Column(db.String(50), nullable=False)
content_type = db.Column(db.String(50), nullable=False)
process_instance_id = db.Column(db.Integer, db.ForeignKey('process_instance.id'), nullable=True)
process_instance_id = db.Column(
db.Integer, db.ForeignKey("process_instance.id"), nullable=True
)
task_spec = db.Column(db.String(50), nullable=True)
irb_doc_code = db.Column(db.String(50), nullable=False) # Code reference to the documents.xlsx reference file.
irb_doc_code = db.Column(
db.String(50), nullable=False
) # Code reference to the documents.xlsx reference file.
data_stores = relationship(DataStoreModel, cascade="all,delete", backref="file")
md5_hash = db.Column(db.String(50), unique=False, nullable=False)
data = deferred(db.Column(db.LargeBinary)) # Don't load it unless you have to.
size = db.Column(db.Integer, default=0)
date_modified = db.Column(db.DateTime(timezone=True), onupdate=func.now())
date_created = db.Column(db.DateTime(timezone=True), server_default=func.now())
user_uid = db.Column(db.String(50), db.ForeignKey('user.uid'), nullable=True)
user_uid = db.Column(db.String(50), db.ForeignKey("user.uid"), nullable=True)
archived = db.Column(db.Boolean, default=False)
class FileType(enum.Enum):
"""FileType."""
bpmn = "bpmn"
csv = 'csv'
csv = "csv"
dmn = "dmn"
doc = "doc"
docx = "docx"
gif = 'gif'
jpg = 'jpg'
md = 'md'
pdf = 'pdf'
png = 'png'
ppt = 'ppt'
pptx = 'pptx'
rtf = 'rtf'
gif = "gif"
jpg = "jpg"
md = "md"
pdf = "pdf"
png = "png"
ppt = "ppt"
pptx = "pptx"
rtf = "rtf"
svg = "svg"
svg_xml = "svg+xml"
txt = 'txt'
xls = 'xls'
xlsx = 'xlsx'
xml = 'xml'
zip = 'zip'
txt = "txt"
xls = "xls"
xlsx = "xlsx"
xml = "xml"
zip = "zip"
CONTENT_TYPES = {
@ -74,11 +81,11 @@ CONTENT_TYPES = {
"xls": "application/vnd.ms-excel",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xml": "application/xml",
"zip": "application/zip"
"zip": "application/zip",
}
class File(object):
class File:
"""File."""
def __init__(self):
@ -97,8 +104,9 @@ class File(object):
self.archived = None
@classmethod
def from_file_system(cls, file_name, file_type, content_type,
last_modified, file_size):
def from_file_system(
cls, file_name, file_type, content_type, last_modified, file_size
):
"""From_file_system."""
instance = cls()
instance.name = file_name
@ -114,13 +122,28 @@ class File(object):
class FileSchema(Schema):
"""FileSchema."""
class Meta:
"""Meta."""
model = File
fields = ["id", "name", "content_type", "process_instance_id",
"irb_doc_code", "last_modified", "type", "archived",
"size", "data_store", "document", "user_uid", "url"]
fields = [
"id",
"name",
"content_type",
"process_instance_id",
"irb_doc_code",
"last_modified",
"type",
"archived",
"size",
"data_store",
"document",
"user_uid",
"url",
]
unknown = INCLUDE
# url = Method("get_url")
#
# def get_url(self, obj):

View File

@ -1,5 +1,6 @@
"""Process_group."""
from marshmallow import Schema, post_load
from marshmallow import post_load
from marshmallow import Schema
# class ProcessGroupModel(db.Model): # type: ignore
@ -15,9 +16,7 @@ class ProcessGroup:
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_group')
)
self.id = id # A unique string name, lower case, under scores (ie, 'my_group')
self.display_name = display_name
self.display_order = display_order
self.admin = admin

View File

@ -1,44 +1,58 @@
"""Process_instance."""
import enum
import marshmallow
from marshmallow import Schema, INCLUDE
from marshmallow_enum import EnumField
import marshmallow
from flask_bpmn.models.db import db
from marshmallow import INCLUDE
from marshmallow import Schema
from marshmallow_enum import EnumField
from SpiffWorkflow.navigation import NavItem
from sqlalchemy import ForeignKey # type: ignore
from sqlalchemy import func
from sqlalchemy.orm import deferred # type: ignore
from sqlalchemy.orm import relationship
from sqlalchemy import func
from SpiffWorkflow.navigation import NavItem
from spiff_workflow_webapp.models.user import UserModel
from spiff_workflow_webapp.models.task import TaskSchema
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.models.task import TaskSchema
from spiff_workflow_webapp.models.user import UserModel
class NavigationItemSchema(Schema):
"""NavigationItemSchema."""
class Meta:
"""Meta."""
fields = ["spec_id", "name", "spec_type", "task_id", "description", "backtracks", "indent",
"lane", "state", "children"]
fields = [
"spec_id",
"name",
"spec_type",
"task_id",
"description",
"backtracks",
"indent",
"lane",
"state",
"children",
]
unknown = INCLUDE
state = marshmallow.fields.String(required=False, allow_none=True)
description = marshmallow.fields.String(required=False, allow_none=True)
backtracks = marshmallow.fields.String(required=False, allow_none=True)
lane = marshmallow.fields.String(required=False, allow_none=True)
task_id = marshmallow.fields.String(required=False, allow_none=True)
children = marshmallow.fields.List(marshmallow.fields.Nested(lambda: NavigationItemSchema()))
children = marshmallow.fields.List(
marshmallow.fields.Nested(lambda: NavigationItemSchema())
)
@marshmallow.post_load
def make_nav(self, data, **kwargs):
"""Make_nav."""
state = data.pop('state', None)
task_id = data.pop('task_id', None)
children = data.pop('children', [])
spec_type = data.pop('spec_type', None)
state = data.pop("state", None)
task_id = data.pop("task_id", None)
children = data.pop("children", [])
spec_type = data.pop("spec_type", None)
item = NavItem(**data)
item.state = state
item.task_id = task_id
@ -49,6 +63,7 @@ class NavigationItemSchema(Schema):
class ProcessInstanceStatus(enum.Enum):
"""ProcessInstanceStatus."""
not_started = "not_started"
user_input_required = "user_input_required"
waiting = "waiting"
@ -71,17 +86,26 @@ class ProcessInstanceModel(db.Model): # type: ignore
status = db.Column(db.Enum(ProcessInstanceStatus))
class ProcessInstanceApi(object):
class ProcessInstanceApi:
"""ProcessInstanceApi."""
def __init__(self, id, status, next_task,
process_model_identifier, total_tasks, completed_tasks,
last_updated, is_review, title):
def __init__(
self,
id,
status,
next_task,
process_model_identifier,
total_tasks,
completed_tasks,
last_updated,
is_review,
title,
):
"""__init__."""
self.id = id
self.status = status
self.next_task = next_task # The next task that requires user input.
# self.navigation = navigation fixme: would be a hotness.
# self.navigation = navigation fixme: would be a hotness.
self.process_model_identifier = process_model_identifier
self.total_tasks = total_tasks
self.completed_tasks = completed_tasks
@ -92,37 +116,76 @@ class ProcessInstanceApi(object):
class ProcessInstanceApiSchema(Schema):
"""ProcessInstanceApiSchema."""
class Meta:
"""Meta."""
model = ProcessInstanceApi
fields = ["id", "status", "next_task", "navigation",
"process_model_identifier", "total_tasks", "completed_tasks",
"last_updated", "is_review", "title", "study_id", "state"]
fields = [
"id",
"status",
"next_task",
"navigation",
"process_model_identifier",
"total_tasks",
"completed_tasks",
"last_updated",
"is_review",
"title",
"study_id",
"state",
]
unknown = INCLUDE
status = EnumField(ProcessInstanceStatus)
next_task = marshmallow.fields.Nested(TaskSchema, dump_only=True, required=False)
navigation = marshmallow.fields.List(marshmallow.fields.Nested(NavigationItemSchema, dump_only=True))
navigation = marshmallow.fields.List(
marshmallow.fields.Nested(NavigationItemSchema, dump_only=True)
)
state = marshmallow.fields.String(allow_none=True)
@marshmallow.post_load
def make_process_instance(self, data, **kwargs):
"""Make_process_instance."""
keys = ['id', 'status', 'next_task', 'navigation',
'process_model_identifier', "total_tasks", "completed_tasks",
"last_updated", "is_review", "title", "study_id", "state"]
keys = [
"id",
"status",
"next_task",
"navigation",
"process_model_identifier",
"total_tasks",
"completed_tasks",
"last_updated",
"is_review",
"title",
"study_id",
"state",
]
filtered_fields = {key: data[key] for key in keys}
filtered_fields['next_task'] = TaskSchema().make_task(data['next_task'])
filtered_fields["next_task"] = TaskSchema().make_task(data["next_task"])
return ProcessInstanceApi(**filtered_fields)
class ProcessInstanceMetadata(object):
class ProcessInstanceMetadata:
"""ProcessInstanceMetadata."""
def __init__(self, id, display_name=None, description=None, spec_version=None,
category_id=None, category_display_name=None, state=None,
status: ProcessInstanceStatus = None, total_tasks=None, completed_tasks=None,
is_review=None, display_order=None, state_message=None, process_model_identifier=None):
def __init__(
self,
id,
display_name=None,
description=None,
spec_version=None,
category_id=None,
category_display_name=None,
state=None,
status: ProcessInstanceStatus = None,
total_tasks=None,
completed_tasks=None,
is_review=None,
display_order=None,
state_message=None,
process_model_identifier=None,
):
"""__init__."""
self.id = id
self.display_name = display_name
@ -140,7 +203,9 @@ class ProcessInstanceMetadata(object):
self.process_model_identifier = process_model_identifier
@classmethod
def from_process_instance(cls, process_instance: ProcessInstanceModel, spec: ProcessModelInfo):
def from_process_instance(
cls, process_instance: ProcessInstanceModel, spec: ProcessModelInfo
):
"""From_process_instance."""
instance = cls(
id=process_instance.id,
@ -154,19 +219,31 @@ class ProcessInstanceMetadata(object):
completed_tasks=process_instance.completed_tasks,
is_review=spec.is_review,
display_order=spec.display_order,
process_model_identifier=process_instance.process_model_identifier
process_model_identifier=process_instance.process_model_identifier,
)
return instance
class ProcessInstanceMetadataSchema(Schema):
"""ProcessInstanceMetadataSchema."""
status = EnumField(ProcessInstanceStatus)
class Meta:
"""Meta."""
model = ProcessInstanceMetadata
additional = ["id", "display_name", "description", "state",
"total_tasks", "completed_tasks", "display_order",
"category_id", "is_review", "category_display_name", "state_message"]
additional = [
"id",
"display_name",
"description",
"state",
"total_tasks",
"completed_tasks",
"display_order",
"category_id",
"is_review",
"category_display_name",
"state_message",
]
unknown = INCLUDE

View File

@ -1,6 +1,7 @@
"""Process_model."""
from marshmallow import Schema, post_load
import marshmallow
from marshmallow import post_load
from marshmallow import Schema
from sqlalchemy import ForeignKey # type: ignore
# from spiff_workflow_webapp.models.process_group import ProcessGroupModel
@ -16,7 +17,7 @@ from sqlalchemy import ForeignKey # type: ignore
# name = db.Column(db.String(50))
class ProcessModelInfo(object):
class ProcessModelInfo:
"""ProcessModelInfo."""
def __init__(
@ -63,9 +64,12 @@ class ProcessModelInfo(object):
class ProcessModelInfoSchema(Schema):
"""ProcessModelInfoSchema."""
class Meta:
"""Meta."""
model = ProcessModelInfo
id = marshmallow.fields.String(required=True)
display_name = marshmallow.fields.String(required=True)
description = marshmallow.fields.String()

View File

@ -1,11 +1,12 @@
"""Task."""
import enum
import marshmallow
from marshmallow import Schema
from marshmallow_enum import EnumField
class Task(object):
class Task:
"""Task."""
##########################################################################
@ -22,15 +23,17 @@ class Task(object):
FIELD_TYPE_BOOLEAN = "boolean"
FIELD_TYPE_DATE = "date"
FIELD_TYPE_ENUM = "enum"
FIELD_TYPE_TEXTAREA = "textarea" # textarea: Multiple lines of text
FIELD_TYPE_TEXTAREA = "textarea" # textarea: Multiple lines of text
FIELD_TYPE_AUTO_COMPLETE = "autocomplete"
FIELD_TYPE_FILE = "file"
FIELD_TYPE_FILES = "files" # files: Multiple files
FIELD_TYPE_TEL = "tel" # tel: Phone number
FIELD_TYPE_EMAIL = "email" # email: Email address
FIELD_TYPE_URL = "url" # url: Website address
FIELD_TYPE_URL = "url" # url: Website address
FIELD_PROP_AUTO_COMPLETE_MAX = "autocomplete_num" # Not used directly, passed in from the front end.
FIELD_PROP_AUTO_COMPLETE_MAX = (
"autocomplete_num" # Not used directly, passed in from the front end.
)
# Required field
FIELD_CONSTRAINT_REQUIRED = "required"
@ -62,7 +65,9 @@ class Task(object):
# File specific field properties
FIELD_PROP_DOC_CODE = "doc_code" # to associate a file upload field with a doc code
FIELD_PROP_FILE_DATA = "file_data" # to associate a bit of data with a specific file upload file.
FIELD_PROP_FILE_DATA = (
"file_data" # to associate a bit of data with a specific file upload file.
)
# Additional properties
FIELD_PROP_ENUM_TYPE = "enum_type"
@ -77,9 +82,23 @@ class Task(object):
##########################################################################
def __init__(self, id, name, title, type, state, lane, form, documentation, data,
multi_instance_type, multi_instance_count, multi_instance_index,
process_name, properties):
def __init__(
self,
id,
name,
title,
type,
state,
lane,
form,
documentation,
data,
multi_instance_type,
multi_instance_count,
multi_instance_index,
process_name,
properties,
):
"""__init__."""
self.id = id
self.name = name
@ -90,25 +109,36 @@ class Task(object):
self.documentation = documentation
self.data = data
self.lane = lane
self.multi_instance_type = multi_instance_type # Some tasks have a repeat behavior.
self.multi_instance_count = multi_instance_count # This is the number of times the task could repeat.
self.multi_instance_index = multi_instance_index # And the index of the currently repeating task.
self.multi_instance_type = (
multi_instance_type # Some tasks have a repeat behavior.
)
self.multi_instance_count = (
multi_instance_count # This is the number of times the task could repeat.
)
self.multi_instance_index = (
multi_instance_index # And the index of the currently repeating task.
)
self.process_name = process_name
self.properties = properties # Arbitrary extension properties from BPMN editor.
@classmethod
def valid_property_names(cls):
"""Valid_property_names."""
return [value for name, value in vars(cls).items() if name.startswith('FIELD_PROP')]
return [
value for name, value in vars(cls).items() if name.startswith("FIELD_PROP")
]
@classmethod
def valid_field_types(cls):
"""Valid_field_types."""
return [value for name, value in vars(cls).items() if name.startswith('FIELD_TYPE')]
return [
value for name, value in vars(cls).items() if name.startswith("FIELD_TYPE")
]
class MultiInstanceType(enum.Enum):
"""MultiInstanceType."""
none = "none"
looping = "looping"
parallel = "parallel"
@ -117,49 +147,85 @@ class MultiInstanceType(enum.Enum):
class OptionSchema(Schema):
"""OptionSchema."""
class Meta:
"""Meta."""
fields = ["id", "name", "data"]
class ValidationSchema(Schema):
"""ValidationSchema."""
class Meta:
"""Meta."""
fields = ["name", "config"]
class FormFieldPropertySchema(Schema):
"""FormFieldPropertySchema."""
class Meta:
"""Meta."""
fields = ["id", "value"]
class FormFieldSchema(Schema):
"""FormFieldSchema."""
class Meta:
"""Meta."""
fields = ["id", "type", "label", "default_value", "options", "validation", "properties", "value"]
fields = [
"id",
"type",
"label",
"default_value",
"options",
"validation",
"properties",
"value",
]
default_value = marshmallow.fields.String(required=False, allow_none=True)
options = marshmallow.fields.List(marshmallow.fields.Nested(OptionSchema))
validation = marshmallow.fields.List(marshmallow.fields.Nested(ValidationSchema))
properties = marshmallow.fields.List(marshmallow.fields.Nested(FormFieldPropertySchema))
properties = marshmallow.fields.List(
marshmallow.fields.Nested(FormFieldPropertySchema)
)
class FormSchema(Schema):
"""FormSchema."""
key = marshmallow.fields.String(required=True, allow_none=False)
fields = marshmallow.fields.List(marshmallow.fields.Nested(FormFieldSchema))
class TaskSchema(Schema):
"""TaskSchema."""
class Meta:
"""Meta."""
fields = ["id", "name", "title", "type", "state", "lane", "form", "documentation", "data", "multi_instance_type",
"multi_instance_count", "multi_instance_index", "process_name", "properties"]
fields = [
"id",
"name",
"title",
"type",
"state",
"lane",
"form",
"documentation",
"data",
"multi_instance_type",
"multi_instance_count",
"multi_instance_index",
"process_name",
"properties",
]
multi_instance_type = EnumField(MultiInstanceType)
documentation = marshmallow.fields.String(required=False, allow_none=True)

View File

@ -1,18 +1,19 @@
"""Task_event."""
from __future__ import annotations
import enum
from marshmallow import INCLUDE, fields, Schema
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from flask_bpmn.models.db import db
from marshmallow import fields
from marshmallow import INCLUDE
from marshmallow import Schema
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from sqlalchemy import func
class TaskAction(enum.Enum):
"""TaskAction."""
COMPLETE = "COMPLETE"
TOKEN_RESET = "TOKEN_RESET"
HARD_RESET = "HARD_RESET"
@ -22,10 +23,15 @@ class TaskAction(enum.Enum):
class TaskEventModel(db.Model):
"""TaskEventModel."""
__tablename__ = 'task_event'
__tablename__ = "task_event"
id = db.Column(db.Integer, primary_key=True)
user_uid = db.Column(db.String(50), nullable=False) # In some cases the unique user id may not exist in the db yet.
process_instance_id = db.Column(db.Integer, db.ForeignKey('process_instance.id'), nullable=False)
user_uid = db.Column(
db.String(50), nullable=False
) # In some cases the unique user id may not exist in the db yet.
process_instance_id = db.Column(
db.Integer, db.ForeignKey("process_instance.id"), nullable=False
)
spec_version = db.Column(db.String(50))
action = db.Column(db.String(50))
task_id = db.Column(db.String(50))
@ -34,7 +40,9 @@ class TaskEventModel(db.Model):
task_type = db.Column(db.String(50))
task_state = db.Column(db.String(50))
task_lane = db.Column(db.String(50))
form_data = db.Column(db.JSON) # And form data submitted when the task was completed.
form_data = db.Column(
db.JSON
) # And form data submitted when the task was completed.
mi_type = db.Column(db.String(50))
mi_count = db.Column(db.Integer)
mi_index = db.Column(db.Integer)
@ -44,18 +52,20 @@ class TaskEventModel(db.Model):
class TaskEventModelSchema(SQLAlchemyAutoSchema):
"""TaskEventModelSchema."""
class Meta:
"""Meta."""
model = TaskEventModel
load_instance = True
include_relationships = True
include_fk = True # Includes foreign keys
class TaskEvent(object):
class TaskEvent:
"""TaskEvent."""
def __init__(self, model: TaskEventModel, process_instance: "ProcessInstanceModel"):
def __init__(self, model: TaskEventModel, process_instance: ProcessInstanceModel):
"""__init__."""
self.id = model.id
self.process_instance = process_instance
@ -78,7 +88,18 @@ class TaskEventSchema(Schema):
class Meta:
"""Meta."""
model = TaskEvent
additional = ["id", "user_uid", "action", "task_id", "task_title",
"task_name", "task_type", "task_state", "task_lane", "date"]
additional = [
"id",
"user_uid",
"action",
"task_id",
"task_title",
"task_name",
"task_type",
"task_state",
"task_lane",
"date",
]
unknown = INCLUDE

View File

@ -1,14 +1,14 @@
"""User."""
import jwt
from marshmallow import Schema
import marshmallow
from flask import current_app
from flask_bpmn.models.db import db
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
from marshmallow import Schema
from sqlalchemy.orm import relationship # type: ignore
from spiff_workflow_webapp.models.user_group_assignment import UserGroupAssignmentModel
from spiff_workflow_webapp.models.group import GroupModel
from spiff_workflow_webapp.models.user_group_assignment import UserGroupAssignmentModel
class UserModel(db.Model): # type: ignore
@ -37,12 +37,12 @@ class UserModel(db.Model): # type: ignore
payload = {
# 'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=hours, minutes=0, seconds=0),
# 'iat': datetime.datetime.utcnow(),
'sub': self.uid
"sub": self.uid
}
return jwt.encode(
payload,
current_app.config.get('SECRET_KEY'),
algorithm='HS256',
current_app.config.get("SECRET_KEY"),
algorithm="HS256",
)
def is_admin(self):
@ -57,29 +57,41 @@ class UserModel(db.Model): # type: ignore
:return: integer|string
"""
try:
payload = jwt.decode(auth_token, current_app.config.get('SECRET_KEY'), algorithms='HS256')
payload = jwt.decode(
auth_token, current_app.config.get("SECRET_KEY"), algorithms="HS256"
)
return payload
except jwt.ExpiredSignatureError:
raise ApiError('token_expired', 'The Authentication token you provided expired and must be renewed.')
raise ApiError(
"token_expired",
"The Authentication token you provided expired and must be renewed.",
)
except jwt.InvalidTokenError:
raise ApiError('token_invalid', 'The Authentication token you provided is invalid. You need a new token. ')
raise ApiError(
"token_invalid",
"The Authentication token you provided is invalid. You need a new token. ",
)
class UserModelSchema(Schema):
"""UserModelSchema."""
class Meta:
"""Meta."""
model = UserModel
# load_instance = True
# include_relationships = False
# exclude = ("UserGroupAssignment",)
id = marshmallow.fields.String(required=True)
username = marshmallow.fields.String(required=True)
class AdminSessionModel(db.Model):
"""AdminSessionModel."""
__tablename__ = 'admin_session'
__tablename__ = "admin_session"
id = db.Column(db.Integer, primary_key=True)
token = db.Column(db.String(50), unique=True)
admin_impersonate_uid = db.Column(db.String(50))

View File

@ -1,36 +1,43 @@
"""APIs for dealing with process groups, process models, and process instances."""
import os
from typing import Any
from flask import Blueprint, render_template, redirect, url_for, flash
from flask import Blueprint
from flask import flash
from flask import redirect
from flask import render_template
from flask import request
from flask import url_for
from flask_bpmn.models.db import db
from flask import request, current_app
from werkzeug.utils import secure_filename
from spiff_workflow_webapp.models.user import UserModel
from spiff_workflow_webapp.services.process_instance_processor import ProcessInstanceProcessor
from spiff_workflow_webapp.services.process_instance_service import ProcessInstanceService
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
from spiff_workflow_webapp.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiff_workflow_webapp.services.process_instance_service import (
ProcessInstanceService,
)
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
admin_blueprint = Blueprint("admin", __name__, template_folder='templates', static_folder='static')
admin_blueprint = Blueprint(
"admin", __name__, template_folder="templates", static_folder="static"
)
ALLOWED_BPMN_EXTENSIONS = {'bpmn', 'dmn'}
ALLOWED_BPMN_EXTENSIONS = {"bpmn", "dmn"}
@admin_blueprint.route("/process-groups", methods=["GET"])
def process_groups_list():
"""Process_groups_list."""
process_groups = ProcessModelService().get_process_groups()
return render_template('process_groups_list.html', process_groups=process_groups)
return render_template("process_groups_list.html", process_groups=process_groups)
@admin_blueprint.route("/process-groups/<process_group_id>", methods=["GET"])
def process_group_show(process_group_id):
"""Show_process_group."""
process_group = ProcessModelService().get_process_group(process_group_id)
return render_template('process_group_show.html', process_group=process_group)
return render_template("process_group_show.html", process_group=process_group)
@admin_blueprint.route("/process-models/<process_model_id>", methods=["GET"])
@ -40,63 +47,96 @@ def process_model_show(process_model_id):
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
current_file_name = process_model.primary_file_name
bpmn_xml = SpecFileService.get_data(process_model, current_file_name)
return render_template('process_model_show.html', process_model=process_model, bpmn_xml=bpmn_xml, files=files, current_file_name=current_file_name)
return render_template(
"process_model_show.html",
process_model=process_model,
bpmn_xml=bpmn_xml,
files=files,
current_file_name=current_file_name,
)
@admin_blueprint.route("/process-models/<process_model_id>/<file_name>", methods=["GET"])
@admin_blueprint.route(
"/process-models/<process_model_id>/<file_name>", methods=["GET"]
)
def process_model_show_file(process_model_id, file_name):
"""Process_model_show_file."""
process_model = ProcessModelService().get_spec(process_model_id)
bpmn_xml = SpecFileService.get_data(process_model, file_name)
files = SpecFileService.get_files(process_model, extension_filter="bpmn")
return render_template('process_model_show.html', process_model=process_model, bpmn_xml=bpmn_xml, files=files, current_file_name=file_name)
return render_template(
"process_model_show.html",
process_model=process_model,
bpmn_xml=bpmn_xml,
files=files,
current_file_name=file_name,
)
@admin_blueprint.route("/process-models/<process_model_id>/upload-file", methods=["POST"])
@admin_blueprint.route(
"/process-models/<process_model_id>/upload-file", methods=["POST"]
)
def process_model_upload_file(process_model_id):
"""Process_model_upload_file."""
process_model_service = ProcessModelService()
process_model = process_model_service.get_spec(process_model_id)
if 'file' not in request.files:
flash('No file part', "error")
request_file = request.files['file']
if "file" not in request.files:
flash("No file part", "error")
request_file = request.files["file"]
# If the user does not select a file, the browser submits an
# empty file without a filename.
if request_file.filename == '':
flash('No selected file', "error")
if request_file.filename == "":
flash("No selected file", "error")
if request_file and _allowed_file(request_file.filename):
SpecFileService.add_file(process_model, request_file.filename, request_file.stream.read())
SpecFileService.add_file(
process_model, request_file.filename, request_file.stream.read()
)
process_model_service.update_spec(process_model)
return redirect(url_for('admin.process_model_show', process_model_id=process_model.id))
return redirect(
url_for("admin.process_model_show", process_model_id=process_model.id)
)
@admin_blueprint.route("/process_models/<process_model_id>/edit/<file_name>", methods=["GET"])
@admin_blueprint.route(
"/process_models/<process_model_id>/edit/<file_name>", methods=["GET"]
)
def process_model_edit(process_model_id, file_name):
"""Edit_bpmn."""
process_model = ProcessModelService().get_spec(process_model_id)
bpmn_xml = SpecFileService.get_data(process_model, file_name)
return render_template('process_model_edit.html', bpmn_xml=bpmn_xml.decode("utf-8"),
process_model=process_model, file_name=file_name)
return render_template(
"process_model_edit.html",
bpmn_xml=bpmn_xml.decode("utf-8"),
process_model=process_model,
file_name=file_name,
)
@admin_blueprint.route("/process-models/<process_model_id>/save", methods=["POST"])
def process_model_save(process_model_id):
"""Process_model_save."""
process_model = ProcessModelService().get_spec(process_model_id)
SpecFileService.update_file(process_model, process_model.primary_file_name, request.get_data())
SpecFileService.update_file(
process_model, process_model.primary_file_name, request.get_data()
)
bpmn_xml = SpecFileService.get_data(process_model, process_model.primary_file_name)
return render_template('process_model_edit.html', bpmn_xml=bpmn_xml.decode("utf-8"),
process_model=process_model)
return render_template(
"process_model_edit.html",
bpmn_xml=bpmn_xml.decode("utf-8"),
process_model=process_model,
)
@admin_blueprint.route("/process-models/<process_model_id>/run", methods=["GET"])
def process_model_run(process_model_id):
"""Process_model_run."""
user = _find_or_create_user('Mr. Test') # Fixme - sheesh!
process_instance = ProcessInstanceService.create_process_instance(process_model_id, user)
user = _find_or_create_user("Mr. Test") # Fixme - sheesh!
process_instance = ProcessInstanceService.create_process_instance(
process_model_id, user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps()
result = processor.get_data()
@ -106,7 +146,14 @@ def process_model_run(process_model_id):
current_file_name = process_model.primary_file_name
bpmn_xml = SpecFileService.get_data(process_model, process_model.primary_file_name)
return render_template('process_model_show.html', process_model=process_model, bpmn_xml=bpmn_xml, result=result, files=files, current_file_name=current_file_name)
return render_template(
"process_model_show.html",
process_model=process_model,
bpmn_xml=bpmn_xml,
result=result,
files=files,
current_file_name=current_file_name,
)
def _find_or_create_user(username: str = "test_user1") -> Any:
@ -120,5 +167,8 @@ def _find_or_create_user(username: str = "test_user1") -> Any:
def _allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_BPMN_EXTENSIONS
"""_allowed_file."""
return (
"." in filename
and filename.rsplit(".", 1)[1].lower() in ALLOWED_BPMN_EXTENSIONS
)

View File

@ -1,22 +1,23 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
{% block head %}
<link rel="stylesheet" href="{{ url_for('admin.static', filename='style.css') }}">
<link
rel="stylesheet"
href="{{ url_for('admin.static', filename='style.css') }}"
/>
<title>{% block title %}{% endblock %}</title>
{% endblock %}
</head>
<body>
<h1>{{ self.title() }}</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<ul class=flashes>
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
{% with messages = get_flashed_messages(with_categories=true) %} {% if
messages %}
<ul class="flashes">
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %} {% endwith %} {% block content %}{% endblock %}
</body>
</html>

View File

@ -1,12 +1,23 @@
{% extends "layout.html" %}
{% block title %}Process Group: {{ process_group.id }}{% endblock %}
{% block content %}
<button type="button" onclick="window.location.href='{{ url_for( 'admin.process_groups_list') }}';">Back</button>
{% extends "layout.html" %} {% block title %}Process Group: {{ process_group.id
}}{% endblock %} {% block content %}
<button
type="button"
onclick="window.location.href='{{ url_for( 'admin.process_groups_list') }}';"
>
Back
</button>
<table>
<tbody>
{# here we iterate over every item in our list#}
{% for process_model in process_group.specs %}
<tr><td><a href="{{ url_for('admin.process_model_show', process_model_id=process_model.id) }}">{{ process_model.display_name }}</a></td></tr>
{# here we iterate over every item in our list#} {% for process_model in
process_group.specs %}
<tr>
<td>
<a
href="{{ url_for('admin.process_model_show', process_model_id=process_model.id) }}"
>{{ process_model.display_name }}</a
>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -1,11 +1,17 @@
{% extends "layout.html" %}
{% block title %}Process Groups{% endblock %}
{% block content %}
{% extends "layout.html" %} {% block title %}Process Groups{% endblock %} {%
block content %}
<table>
<tbody>
{# here we iterate over every item in our list#}
{% for process_group in process_groups %}
<tr><td><a href="{{ url_for('admin.process_group_show', process_group_id=process_group.id) }}">{{ process_group.display_name }}</a></td></tr>
{# here we iterate over every item in our list#} {% for process_group in
process_groups %}
<tr>
<td>
<a
href="{{ url_for('admin.process_group_show', process_group_id=process_group.id) }}"
>{{ process_group.display_name }}</a
>
</td>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -1,144 +1,151 @@
{% extends "layout.html" %}
{% block title %}Process Model Edit: {{ process_model.id }}{% endblock %}
{% block head %}
<meta charset="UTF-8" />
{% extends "layout.html" %} {% block title %}Process Model Edit: {{
process_model.id }}{% endblock %} {% block head %}
<meta charset="UTF-8" />
<!-- example styles -->
<!-- required modeler styles -->
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css">
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/diagram-js.css">
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-font/css/bpmn.css">
<!-- example styles -->
<!-- required modeler styles -->
<link
rel="stylesheet"
href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/diagram-js.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-font/css/bpmn.css"
/>
<!-- modeler distro -->
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-modeler.development.js"></script>
<!-- modeler distro -->
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-modeler.development.js"></script>
<!-- needed for this example only -->
<script src="https://unpkg.com/jquery@3.3.1/dist/jquery.js"></script>
<!-- needed for this example only -->
<script src="https://unpkg.com/jquery@3.3.1/dist/jquery.js"></script>
<!-- example styles -->
<style>
html, body, #canvas {
height: 100%;
padding: 0;
margin: 0;
}
<!-- example styles -->
<style>
html,
body,
#canvas {
height: 100%;
padding: 0;
margin: 0;
}
.diagram-note {
background-color: rgba(66, 180, 21, 0.7);
color: White;
border-radius: 5px;
font-family: Arial;
font-size: 12px;
padding: 5px;
min-height: 16px;
width: 50px;
text-align: center;
}
.diagram-note {
background-color: rgba(66, 180, 21, 0.7);
color: White;
border-radius: 5px;
font-family: Arial;
font-size: 12px;
padding: 5px;
min-height: 16px;
width: 50px;
text-align: center;
}
.needs-discussion:not(.djs-connection) .djs-visual > :nth-child(1) {
stroke: rgba(66, 180, 21, 0.7) !important; /* color elements as red */
}
.needs-discussion:not(.djs-connection) .djs-visual > :nth-child(1) {
stroke: rgba(66, 180, 21, 0.7) !important; /* color elements as red */
}
#save-button {
position: fixed;
bottom: 20px;
left: 20px;
}
</style>
{% endblock %}
{% block content %}
<div id="result">{{ result }}</div>
<button type="button" onclick="window.location.href='{{ url_for( 'admin.process_model_show_file', process_model_id=process_model.id, file_name=file_name ) }}';">Back</button>
<button type="button" onclick="exportDiagram()" >Save</button>
<div id="canvas"></div>
#save-button {
position: fixed;
bottom: 20px;
left: 20px;
}
</style>
{% endblock %} {% block content %}
<div id="result">{{ result }}</div>
<button
type="button"
onclick="window.location.href='{{ url_for( 'admin.process_model_show_file', process_model_id=process_model.id, file_name=file_name ) }}';"
>
Back
</button>
<button type="button" onclick="exportDiagram()">Save</button>
<div id="canvas"></div>
<meta id="bpmn_xml" data-name="{{bpmn_xml}}">
<meta id="process_model_id" data-name="{{process_model.id}}">
<script>
<meta id="bpmn_xml" data-name="{{bpmn_xml}}" />
<meta id="process_model_id" data-name="{{process_model.id}}" />
<script>
// modeler instance
var bpmnModeler = new BpmnJS({
container: "#canvas",
keyboard: {
bindTo: window,
},
});
// modeler instance
var bpmnModeler = new BpmnJS({
container: '#canvas',
keyboard: {
bindTo: window
}
/**
* Save diagram contents and print them to the console.
*/
async function exportDiagram() {
try {
var data = await bpmnModeler.saveXML({ format: true });
var process_model_id = $("#process_model_id").data().name;
console.log("The data is", data);
//POST request with body equal on data in JSON format
fetch("/admin/process-models/" + process_model_id + "/save", {
method: "POST",
headers: {
"Content-Type": "text/xml",
},
body: data.xml,
})
.then((response) => response.json())
//Then with the data from the response in JSON...
.then((data) => {
console.log("Success:", data);
})
//Then with the error genereted...
.catch((error) => {
console.error("Error:", error);
});
/**
* Save diagram contents and print them to the console.
*/
async function exportDiagram() {
alert("Diagram exported. Check the developer tools!");
} catch (err) {
console.error("could not save BPMN 2.0 diagram", err);
}
}
try {
/**
* Open diagram in our modeler instance.
*
* @param {String} bpmnXML diagram to display
*/
async function openDiagram(bpmnXML) {
// import diagram
try {
await bpmnModeler.importXML(bpmnXML);
var data = await bpmnModeler.saveXML({ format: true });
var process_model_id = $('#process_model_id').data().name;
console.log("The data is", data)
//POST request with body equal on data in JSON format
fetch('/admin/process-models/' + process_model_id + '/save' , {
method: 'POST',
headers: {
'Content-Type': 'text/xml',
},
body: data.xml
})
.then((response) => response.json())
//Then with the data from the response in JSON...
.then((data) => {
console.log('Success:', data);
})
//Then with the error genereted...
.catch((error) => {
console.error('Error:', error);
});
// access modeler components
var canvas = bpmnModeler.get("canvas");
var overlays = bpmnModeler.get("overlays");
alert('Diagram exported. Check the developer tools!');
} catch (err) {
console.error('could not save BPMN 2.0 diagram', err);
}
}
// zoom to fit full viewport
canvas.zoom("fit-viewport");
/**
* Open diagram in our modeler instance.
*
* @param {String} bpmnXML diagram to display
*/
async function openDiagram(bpmnXML) {
// attach an overlay to a node
overlays.add("SCAN_OK", "note", {
position: {
bottom: 0,
right: 0,
},
html: '<div class="diagram-note">Mixed up the labels?</div>',
});
// import diagram
try {
// add marker
canvas.addMarker("SCAN_OK", "needs-discussion");
} catch (err) {
console.error("could not import BPMN 2.0 diagram", err);
}
}
await bpmnModeler.importXML(bpmnXML);
var bpmn_xml = $("#bpmn_xml").data();
openDiagram(bpmn_xml.name);
// access modeler components
var canvas = bpmnModeler.get('canvas');
var overlays = bpmnModeler.get('overlays');
// zoom to fit full viewport
canvas.zoom('fit-viewport');
// attach an overlay to a node
overlays.add('SCAN_OK', 'note', {
position: {
bottom: 0,
right: 0
},
html: '<div class="diagram-note">Mixed up the labels?</div>'
});
// add marker
canvas.addMarker('SCAN_OK', 'needs-discussion');
} catch (err) {
console.error('could not import BPMN 2.0 diagram', err);
}
}
var bpmn_xml = $('#bpmn_xml').data();
openDiagram(bpmn_xml.name)
// wire save button
$('#save-button').click(exportDiagram);
</script>
// wire save button
$("#save-button").click(exportDiagram);
</script>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "layout.html" %}
{% block title %}Process Model: {{ process_model.id }}{% endblock %}
{% block head %}
{{ super() }}
{% extends "layout.html" %} {% block title %}Process Model: {{ process_model.id
}}{% endblock %} {% block head %} {{ super() }}
<meta charset="UTF-8" />
<script src="{{ url_for('admin.static', filename='node_modules/bpmn-js/dist/bpmn-viewer.development.js') }}"></script>
@ -11,122 +9,140 @@
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-viewer.development.js"></script>
-->
<!-- required viewer styles -->
<link rel="stylesheet" href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css">
<!-- required viewer styles -->
<link
rel="stylesheet"
href="https://unpkg.com/bpmn-js@9.1.0/dist/assets/bpmn-js.css"
/>
<!-- viewer distro (with pan and zoom) -->
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-navigated-viewer.development.js"></script>
<!-- viewer distro (with pan and zoom) -->
<script src="https://unpkg.com/bpmn-js@9.1.0/dist/bpmn-navigated-viewer.development.js"></script>
<!-- needed for this example only -->
<script src="https://unpkg.com/jquery@3.3.1/dist/jquery.js"></script>
<!-- needed for this example only -->
<script src="https://unpkg.com/jquery@3.3.1/dist/jquery.js"></script>
<!-- example styles -->
<style>
html, body, #canvas {
height: 90%;
padding: 0;
margin: 0;
}
<!-- example styles -->
<style>
html,
body,
#canvas {
height: 90%;
padding: 0;
margin: 0;
}
.diagram-note {
background-color: rgba(66, 180, 21, 0.7);
color: White;
border-radius: 5px;
font-family: Arial;
font-size: 12px;
padding: 5px;
min-height: 16px;
width: 50px;
text-align: center;
}
.diagram-note {
background-color: rgba(66, 180, 21, 0.7);
color: White;
border-radius: 5px;
font-family: Arial;
font-size: 12px;
padding: 5px;
min-height: 16px;
width: 50px;
text-align: center;
}
.needs-discussion:not(.djs-connection) .djs-visual > :nth-child(1) {
stroke: rgba(66, 180, 21, 0.7) !important; /* color elements as red */
}
</style>
{% endblock %}
{% block content %}
<div id="result">{{ result }}</div>
<button type="button" onclick="window.location.href='{{ url_for( 'admin.process_group_show', process_group_id=process_model.process_group_id ) }}';">Back</button>
<button type="button" onclick="window.location.href='{{ url_for( 'admin.process_model_run' , process_model_id=process_model.id ) }}';">Run</button>
<button type="button" onclick="window.location.href='{{ url_for( 'admin.process_model_edit' , process_model_id=process_model.id, file_name=current_file_name ) }}';">Edit</button>
.needs-discussion:not(.djs-connection) .djs-visual > :nth-child(1) {
stroke: rgba(66, 180, 21, 0.7) !important; /* color elements as red */
}
</style>
{% endblock %} {% block content %}
<div id="result">{{ result }}</div>
<button
type="button"
onclick="window.location.href='{{ url_for( 'admin.process_group_show', process_group_id=process_model.process_group_id ) }}';"
>
Back
</button>
<button
type="button"
onclick="window.location.href='{{ url_for( 'admin.process_model_run' , process_model_id=process_model.id ) }}';"
>
Run
</button>
<button
type="button"
onclick="window.location.href='{{ url_for( 'admin.process_model_edit' , process_model_id=process_model.id, file_name=current_file_name ) }}';"
>
Edit
</button>
{% if files %}
<h3>BPMN Files</h3>
<ul>
{% if files %}
<h3>BPMN Files</h3>
<ul>
{% for file in files %}
<li>
<a href="{{ url_for('admin.process_model_show_file', process_model_id=process_model.id, file_name=file.name) }}">{{ file.name }}</a>
{% if file.name == current_file_name %}
(current)
{% endif %}
<a
href="{{ url_for('admin.process_model_show_file', process_model_id=process_model.id, file_name=file.name) }}"
>{{ file.name }}</a
>
{% if file.name == current_file_name %} (current) {% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
</ul>
{% endif %}
<form method=post action="/admin/process-models/{{process_model.id}}/upload-file" enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>
</form>
<form
method="post"
action="/admin/process-models/{{process_model.id}}/upload-file"
enctype="multipart/form-data"
>
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
<div id="canvas"></div>
<div id="canvas"></div>
<meta id="bpmn_xml" data-name="{{bpmn_xml}}">
<script>
<meta id="bpmn_xml" data-name="{{bpmn_xml}}" />
<script>
var diagramUrl =
"https://cdn.staticaly.com/gh/bpmn-io/bpmn-js-examples/dfceecba/starter/diagram.bpmn";
var diagramUrl = 'https://cdn.staticaly.com/gh/bpmn-io/bpmn-js-examples/dfceecba/starter/diagram.bpmn';
// viewer instance
var bpmnViewer = new BpmnJS({
container: "#canvas",
});
// viewer instance
var bpmnViewer = new BpmnJS({
container: '#canvas'
});
/**
* Open diagram in our viewer instance.
*
* @param {String} bpmnXML diagram to display
*/
async function openDiagram(bpmnXML) {
// import diagram
try {
await bpmnViewer.importXML(bpmnXML);
// access viewer components
var canvas = bpmnViewer.get("canvas");
var overlays = bpmnViewer.get("overlays");
/**
* Open diagram in our viewer instance.
*
* @param {String} bpmnXML diagram to display
*/
async function openDiagram(bpmnXML) {
// zoom to fit full viewport
canvas.zoom("fit-viewport");
// import diagram
try {
// attach an overlay to a node
overlays.add("SCAN_OK", "note", {
position: {
bottom: 0,
right: 0,
},
html: '<div class="diagram-note">Mixed up the labels?</div>',
});
await bpmnViewer.importXML(bpmnXML);
// add marker
canvas.addMarker("SCAN_OK", "needs-discussion");
} catch (err) {
console.error("could not import BPMN 2.0 diagram", err);
}
}
var bpmn_xml = $("#bpmn_xml").data();
openDiagram(bpmn_xml.name);
// access viewer components
var canvas = bpmnViewer.get('canvas');
var overlays = bpmnViewer.get('overlays');
// zoom to fit full viewport
canvas.zoom('fit-viewport');
// attach an overlay to a node
overlays.add('SCAN_OK', 'note', {
position: {
bottom: 0,
right: 0
},
html: '<div class="diagram-note">Mixed up the labels?</div>'
});
// add marker
canvas.addMarker('SCAN_OK', 'needs-discussion');
} catch (err) {
console.error('could not import BPMN 2.0 diagram', err);
}
}
var bpmn_xml = $('#bpmn_xml').data();
openDiagram(bpmn_xml.name)
// load external diagram file via AJAX and open it
//$.get(diagramUrl, openDiagram, 'text');
</script>
<!--
// load external diagram file via AJAX and open it
//$.get(diagramUrl, openDiagram, 'text');
</script>
<!--
Thanks for trying out our BPMN toolkit!
If you'd like to learn more about what our library,
continue with some more basic examples:
@ -140,4 +156,4 @@ html, body, #canvas {
Related starters:
* https://raw.githubusercontent.com/bpmn-io/bpmn-js-examples/starter/modeler.html
-->
{% endblock %}
{% endblock %}

View File

@ -1,22 +1,26 @@
"""APIs for dealing with process groups, process models, and process instances."""
import connexion
from flask import Blueprint, g
from flask import Blueprint
from flask import g
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.file import FileSchema
from spiff_workflow_webapp.models.file import FileType
from spiff_workflow_webapp.models.process_instance import ProcessInstanceApiSchema
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema
from spiff_workflow_webapp.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiff_workflow_webapp.services.process_instance_service import (
ProcessInstanceService,
)
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
# from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer # type: ignore
# from SpiffWorkflow.camunda.serializer.task_spec_converters import UserTaskConverter # type: ignore
# from SpiffWorkflow.dmn.serializer.task_spec_converters import BusinessRuleTaskConverter # type: ignore
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema
from spiff_workflow_webapp.models.process_instance import ProcessInstanceApiSchema
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.services.process_instance_service import ProcessInstanceService
from spiff_workflow_webapp.services.process_instance_processor import ProcessInstanceProcessor
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
from spiff_workflow_webapp.models.file import FileSchema, FileType
process_api_blueprint = Blueprint("process_api", __name__)
@ -39,9 +43,12 @@ def get_file(spec_id, file_name):
workflow_spec = workflow_spec_service.get_spec(spec_id)
files = SpecFileService.get_files(workflow_spec, file_name)
if len(files) == 0:
raise ApiError(code='unknown file',
message=f'No information exists for file {file_name}'
f' it does not exist in workflow {spec_id}.', status_code=404)
raise ApiError(
code="unknown file",
message=f"No information exists for file {file_name}"
f" it does not exist in workflow {spec_id}.",
status_code=404,
)
return FileSchema().dump(files[0])
@ -49,8 +56,10 @@ def add_file(spec_id):
"""Add_file."""
workflow_spec_service = ProcessModelService()
workflow_spec = workflow_spec_service.get_spec(spec_id)
request_file = connexion.request.files['file']
file = SpecFileService.add_file(workflow_spec, request_file.filename, request_file.stream.read())
request_file = connexion.request.files["file"]
file = SpecFileService.add_file(
workflow_spec, request_file.filename, request_file.stream.read()
)
if not workflow_spec.primary_process_id and file.type == FileType.bpmn.value:
SpecFileService.set_primary_bpmn(workflow_spec, file.name)
workflow_spec_service.update_spec(workflow_spec)
@ -66,5 +75,7 @@ def create_process_instance(spec_id):
processor.save()
# ProcessInstanceService.update_task_assignments(processor)
workflow_api_model = ProcessInstanceService.processor_to_process_instance_api(processor)
workflow_api_model = ProcessInstanceService.processor_to_process_instance_api(
processor
)
return ProcessInstanceApiSchema().dump(workflow_api_model)

View File

@ -1,7 +1,8 @@
"""User."""
from flask import current_app, g
from flask import current_app
from flask import g
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.user import UserModel
"""
@ -12,26 +13,29 @@ from spiff_workflow_webapp.models.user import UserModel
def verify_token(token=None):
"""
Verifies the token for the user (if provided). If in production environment and token is not provided,
gets user from the SSO headers and returns their token.
Verifies the token for the user (if provided). If in production environment and token is not provided,
gets user from the SSO headers and returns their token.
Args:
token: Optional[str]
Args:
token: Optional[str]
Returns:
token: str
Returns:
token: str
Raises:
ApiError. If not on production and token is not valid, returns an 'invalid_token' 403 error.
If on production and user is not authenticated, returns a 'no_user' 403 error.
"""
failure_error = ApiError("invalid_token", "Unable to decode the token you provided. Please re-authenticate",
status_code=403)
Raises:
ApiError. If not on production and token is not valid, returns an 'invalid_token' 403 error.
If on production and user is not authenticated, returns a 'no_user' 403 error.
"""
failure_error = ApiError(
"invalid_token",
"Unable to decode the token you provided. Please re-authenticate",
status_code=403,
)
if token:
try:
token_info = UserModel.decode_auth_token(token)
g.user = UserModel.query.filter_by(uid=token_info['sub']).first()
g.user = UserModel.query.filter_by(uid=token_info["sub"]).first()
# If the user is valid, store the token for this session
if g.user:
@ -59,16 +63,20 @@ def verify_token(token=None):
return token_info
else:
raise ApiError("no_user",
"User not found. Please login via the frontend app before accessing this feature.",
status_code=403)
raise ApiError(
"no_user",
"User not found. Please login via the frontend app before accessing this feature.",
status_code=403,
)
else:
# Fall back to a default user if this is not production.
g.user = UserModel.query.first()
if not g.user:
raise ApiError(
"no_user", "You are in development mode, but there are no users in the database. Add one, and it will use it.")
"no_user",
"You are in development mode, but there are no users in the database. Add one, and it will use it.",
)
token = g.user.encode_auth_token()
token_info = UserModel.decode_auth_token(token)
return token_info
@ -76,4 +84,4 @@ def verify_token(token=None):
def _is_production():
"""_is_production."""
return 'PRODUCTION' in current_app.config and current_app.config['PRODUCTION']
return "PRODUCTION" in current_app.config and current_app.config["PRODUCTION"]

View File

@ -3,7 +3,6 @@ import importlib
import os
import pkgutil
from flask_bpmn.api.api_error import ApiError
@ -13,26 +12,31 @@ from flask_bpmn.api.api_error import ApiError
SCRIPT_SUB_CLASSES = None
class Script(object):
class Script:
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
def get_description(self):
"""Get_description."""
raise ApiError("invalid_script",
"This script does not supply a description.")
raise ApiError("invalid_script", "This script does not supply a description.")
def do_task(self, task, workflow_id, *args, **kwargs):
"""Do_task."""
raise ApiError("invalid_script",
"This is an internal error. The script you are trying to execute '%s' " % self.__class__.__name__
+ "does not properly implement the do_task function.")
raise ApiError(
"invalid_script",
"This is an internal error. The script you are trying to execute '%s' "
% self.__class__.__name__
+ "does not properly implement the do_task function.",
)
def do_task_validate_only(self, task, workflow_id, *args, **kwargs):
"""Do_task_validate_only."""
raise ApiError("invalid_script",
"This is an internal error. The script you are trying to execute '%s' " % self.__class__.__name__
+ "does must provide a validate_only option that mimics the do_task, "
+ "but does not make external calls or database updates.")
raise ApiError(
"invalid_script",
"This is an internal error. The script you are trying to execute '%s' "
% self.__class__.__name__
+ "does must provide a validate_only option that mimics the do_task, "
+ "but does not make external calls or database updates.",
)
@staticmethod
def generate_augmented_list(task, workflow_id):
@ -45,6 +49,7 @@ class Script(object):
We may be able to remove the task for each of these calls if we are not using it other than potentially
updating the task data.
"""
def make_closure(subclass, task, workflow_id):
"""Yes - this is black magic.
@ -56,13 +61,17 @@ class Script(object):
that we created.
"""
instance = subclass()
return lambda *ar, **kw: subclass.do_task(instance, task, workflow_id, *ar, **kw)
return lambda *ar, **kw: subclass.do_task(
instance, task, workflow_id, *ar, **kw
)
execlist = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):
subclass = subclasses[x]
execlist[subclass.__module__.split('.')[-1]] = make_closure(subclass, task,
workflow_id)
execlist[subclass.__module__.split(".")[-1]] = make_closure(
subclass, task, workflow_id
)
return execlist
@staticmethod
@ -76,16 +85,21 @@ class Script(object):
We may be able to remove the task for each of these calls if we are not using it other than potentially
updating the task data.
"""
def make_closure_validate(subclass, task, workflow_id):
"""Make_closure_validate."""
instance = subclass()
return lambda *a, **b: subclass.do_task_validate_only(instance, task, workflow_id, *a, **b)
return lambda *a, **b: subclass.do_task_validate_only(
instance, task, workflow_id, *a, **b
)
execlist = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):
subclass = subclasses[x]
execlist[subclass.__module__.split('.')[-1]] = make_closure_validate(subclass, task,
workflow_id)
execlist[subclass.__module__.split(".")[-1]] = make_closure_validate(
subclass, task, workflow_id
)
return execlist
@classmethod
@ -103,7 +117,7 @@ class Script(object):
# hackish mess to make sure we have all the modules loaded for the scripts
pkg_dir = os.path.dirname(__file__)
for (_module_loader, name, _ispkg) in pkgutil.iter_modules([pkg_dir]):
importlib.import_module('.' + name, __package__)
importlib.import_module("." + name, __package__)
"""Returns a list of all classes that extend this class."""
all_subclasses = []

View File

@ -4,14 +4,16 @@ import os
from typing import List
import pytz
from flask import current_app
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.file import FileType, CONTENT_TYPES, File
from spiff_workflow_webapp.models.file import CONTENT_TYPES
from spiff_workflow_webapp.models.file import File
from spiff_workflow_webapp.models.file import FileType
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
class FileSystemService(object):
class FileSystemService:
"""FileSystemService."""
""" Simple Service meant for extension that provides some useful
@ -29,9 +31,9 @@ class FileSystemService(object):
def root_path():
"""Root_path."""
# fixme: allow absolute files
dir_name = current_app.config['BPMN_SPEC_ABSOLUTE_DIR']
dir_name = current_app.config["BPMN_SPEC_ABSOLUTE_DIR"]
app_root = current_app.root_path
return os.path.join(app_root, '..', dir_name)
return os.path.join(app_root, "..", dir_name)
@staticmethod
def process_group_path(name: str):
@ -41,7 +43,9 @@ class FileSystemService(object):
@staticmethod
def library_path(name: str):
"""Library_path."""
return os.path.join(FileSystemService.root_path(), FileSystemService.LIBRARY_SPECS, name)
return os.path.join(
FileSystemService.root_path(), FileSystemService.LIBRARY_SPECS, name
)
@staticmethod
def process_group_path_for_spec(spec):
@ -49,18 +53,26 @@ class FileSystemService(object):
if spec.is_master_spec:
return os.path.join(FileSystemService.root_path())
elif spec.library:
process_group_path = FileSystemService.process_group_path(FileSystemService.LIBRARY_SPECS)
process_group_path = FileSystemService.process_group_path(
FileSystemService.LIBRARY_SPECS
)
elif spec.standalone:
process_group_path = FileSystemService.process_group_path(FileSystemService.STAND_ALONE_SPECS)
process_group_path = FileSystemService.process_group_path(
FileSystemService.STAND_ALONE_SPECS
)
else:
process_group_path = FileSystemService.process_group_path(spec.process_group_id)
process_group_path = FileSystemService.process_group_path(
spec.process_group_id
)
return process_group_path
@staticmethod
def workflow_path(spec: ProcessModelInfo):
"""Workflow_path."""
if spec.is_master_spec:
return os.path.join(FileSystemService.root_path(), FileSystemService.MASTER_SPECIFICATION)
return os.path.join(
FileSystemService.root_path(), FileSystemService.MASTER_SPECIFICATION
)
else:
process_group_path = FileSystemService.process_group_path_for_spec(spec)
return os.path.join(process_group_path, spec.id)
@ -77,7 +89,7 @@ class FileSystemService(object):
def write_file_data_to_system(file_path, file_data):
"""Write_file_data_to_system."""
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'wb') as f_handle:
with open(file_path, "wb") as f_handle:
f_handle.write(file_data)
@staticmethod
@ -91,9 +103,12 @@ class FileSystemService(object):
"""Assert_valid_file_name."""
file_extension = FileSystemService.get_extension(file_name)
if file_extension not in FileType._member_names_:
raise ApiError('unknown_extension',
'The file you provided does not have an accepted extension:'
+ file_extension, status_code=404)
raise ApiError(
"unknown_extension",
"The file you provided does not have an accepted extension:"
+ file_extension,
status_code=404,
)
@staticmethod
def _timestamp(file_path: str):
@ -122,7 +137,7 @@ class FileSystemService(object):
items = os.scandir(file_path)
for item in items:
if item.is_file():
if item.name.startswith('.'):
if item.name.startswith("."):
continue # Ignore hidden files
if item.name == FileSystemService.WF_JSON_FILE:
continue # Ignore the json files.
@ -139,7 +154,9 @@ class FileSystemService(object):
content_type = CONTENT_TYPES[file_type.name]
last_modified = FileSystemService._last_modified(file_path)
size = os.path.getsize(file_path)
file = File.from_file_system(file_name, file_type, content_type, last_modified, size)
file = File.from_file_system(
file_name, file_type, content_type, last_modified, size
)
return file
@staticmethod
@ -150,8 +167,13 @@ class FileSystemService(object):
file_type = FileType[extension]
content_type = CONTENT_TYPES[file_type.name]
except KeyError:
raise ApiError("invalid_type", "Invalid File Type: %s, for file %s" % (extension, item.name))
raise ApiError(
"invalid_type",
f"Invalid File Type: {extension}, for file {item.name}",
)
stats = item.stat()
file_size = stats.st_size
last_modified = FileSystemService._last_modified(item.path)
return File.from_file_system(item.name, file_type, content_type, last_modified, file_size)
return File.from_file_system(
item.name, file_type, content_type, last_modified, file_size
)

View File

@ -1,38 +1,43 @@
"""Process_instance_processor."""
import json
from typing import List
from flask import current_app
from lxml import etree
from datetime import datetime
from typing import List
from flask import current_app
from flask_bpmn.api.api_error import ApiError
from flask_bpmn.models.db import db
from lxml import etree
from SpiffWorkflow import Task as SpiffTask
from SpiffWorkflow import TaskState
from SpiffWorkflow import WorkflowException
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from SpiffWorkflow.bpmn.serializer import BpmnWorkflowSerializer
from SpiffWorkflow.bpmn.specs.events import EndEvent, CancelEventDefinition
from SpiffWorkflow.camunda.serializer import UserTaskConverter
from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter
from SpiffWorkflow.serializer.exceptions import MissingSpecError
from SpiffWorkflow import Task as SpiffTask, TaskState, WorkflowException
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from SpiffWorkflow.bpmn.serializer.BpmnSerializer import BpmnSerializer
from SpiffWorkflow.bpmn.specs.events import CancelEventDefinition
from SpiffWorkflow.bpmn.specs.events import EndEvent
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
from SpiffWorkflow.camunda.serializer import UserTaskConverter
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter
from SpiffWorkflow.exceptions import WorkflowTaskExecException
from SpiffWorkflow.serializer.exceptions import MissingSpecError
from SpiffWorkflow.specs import WorkflowSpec
from flask_bpmn.models.db import db
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.file import File, FileType
from spiff_workflow_webapp.models.task_event import TaskEventModel, TaskAction
from spiff_workflow_webapp.models.user import UserModelSchema
from spiff_workflow_webapp.models.file import File
from spiff_workflow_webapp.models.file import FileType
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
from spiff_workflow_webapp.models.process_instance import ProcessInstanceStatus
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel, ProcessInstanceStatus
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
# from crc.services.user_file_service import UserFileService
from spiff_workflow_webapp.services.user_service import UserService
from spiff_workflow_webapp.models.task_event import TaskAction
from spiff_workflow_webapp.models.task_event import TaskEventModel
from spiff_workflow_webapp.models.user import UserModelSchema
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
from spiff_workflow_webapp.services.user_service import UserService
# from crc.services.user_file_service import UserFileService
class CustomBpmnScriptEngine(PythonScriptEngine):
@ -51,9 +56,11 @@ class CustomBpmnScriptEngine(PythonScriptEngine):
try:
return super()._evaluate(expression, context, task, {})
except Exception as exception:
raise WorkflowTaskExecException(task,
"Error evaluating expression "
"'%s', %s" % (expression, str(exception))) from exception
raise WorkflowTaskExecException(
task,
"Error evaluating expression "
"'%s', %s" % (expression, str(exception)),
) from exception
def execute(self, task: SpiffTask, script, data):
"""Execute."""
@ -62,72 +69,101 @@ class CustomBpmnScriptEngine(PythonScriptEngine):
except WorkflowException as e:
raise e
except Exception as e:
raise WorkflowTaskExecException(task, f' {script}, {e}', e) from e
raise WorkflowTaskExecException(task, f" {script}, {e}", e) from e
class MyCustomParser(BpmnDmnParser):
"""A BPMN and DMN parser that can also parse Camunda forms."""
OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES
OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES)
class ProcessInstanceProcessor(object):
class ProcessInstanceProcessor:
"""ProcessInstanceProcessor."""
_script_engine = CustomBpmnScriptEngine()
SERIALIZER_VERSION = "1.0-CRC"
wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter(
[UserTaskConverter, BusinessRuleTaskConverter])
[UserTaskConverter, BusinessRuleTaskConverter]
)
_serializer = BpmnWorkflowSerializer(wf_spec_converter, version=SERIALIZER_VERSION)
_old_serializer = BpmnSerializer()
PROCESS_INSTANCE_ID_KEY = "process_instance_id"
VALIDATION_PROCESS_KEY = "validate_only"
def __init__(self, process_instance_model: ProcessInstanceModel, validate_only=False):
def __init__(
self, process_instance_model: ProcessInstanceModel, validate_only=False
):
"""Create a Workflow Processor based on the serialized information available in the process_instance model."""
self.process_instance_model = process_instance_model
self.process_model_service = ProcessModelService()
spec = None
if process_instance_model.bpmn_json is None:
spec_info = self.process_model_service.get_spec(process_instance_model.process_model_identifier)
spec_info = self.process_model_service.get_spec(
process_instance_model.process_model_identifier
)
if spec_info is None:
raise (ApiError("missing_spec", "The spec this process_instance references does not currently exist."))
self.spec_files = SpecFileService.get_files(spec_info, include_libraries=True)
raise (
ApiError(
"missing_spec",
"The spec this process_instance references does not currently exist.",
)
)
self.spec_files = SpecFileService.get_files(
spec_info, include_libraries=True
)
spec = self.get_spec(self.spec_files, spec_info)
else:
B = len(process_instance_model.bpmn_json.encode('utf-8'))
MB = float(1024 ** 2)
B = len(process_instance_model.bpmn_json.encode("utf-8"))
MB = float(1024**2)
json_size = B / MB
if json_size > 1:
wf_json = json.loads(process_instance_model.bpmn_json)
if 'spec' in wf_json and 'tasks' in wf_json:
task_tree = wf_json['tasks']
test_spec = wf_json['spec']
task_size = "{:.2f}".format(len(json.dumps(task_tree).encode('utf-8')) / MB)
spec_size = "{:.2f}".format(len(json.dumps(test_spec).encode('utf-8')) / MB)
message = 'Workflow ' + process_instance_model.process_model_identifier + \
' JSON Size is over 1MB:{0:.2f} MB'.format(json_size)
if "spec" in wf_json and "tasks" in wf_json:
task_tree = wf_json["tasks"]
test_spec = wf_json["spec"]
task_size = "{:.2f}".format(
len(json.dumps(task_tree).encode("utf-8")) / MB
)
spec_size = "{:.2f}".format(
len(json.dumps(test_spec).encode("utf-8")) / MB
)
message = (
"Workflow "
+ process_instance_model.process_model_identifier
+ f" JSON Size is over 1MB:{json_size:.2f} MB"
)
message += f"\n Task Size: {task_size}"
message += f"\n Spec Size: {spec_size}"
current_app.logger.warning(message)
def check_sub_specs(test_spec, indent=0, show_all=False):
"""Check_sub_specs."""
for my_spec_name in test_spec['task_specs']:
my_spec = test_spec['task_specs'][my_spec_name]
my_spec_size = len(json.dumps(my_spec).encode('utf-8')) / MB
for my_spec_name in test_spec["task_specs"]:
my_spec = test_spec["task_specs"][my_spec_name]
my_spec_size = len(json.dumps(my_spec).encode("utf-8")) / MB
if my_spec_size > 0.1 or show_all:
current_app.logger.warning((' ' * indent) + 'Sub-Spec '
+ my_spec['name'] + ' :' + "{:.2f}".format(my_spec_size))
if 'spec' in my_spec:
if my_spec['name'] == 'Call_Emails_Process_Email':
current_app.logger.warning(
(" " * indent)
+ "Sub-Spec "
+ my_spec["name"]
+ " :"
+ f"{my_spec_size:.2f}"
)
if "spec" in my_spec:
if my_spec["name"] == "Call_Emails_Process_Email":
pass
check_sub_specs(my_spec['spec'], indent + 5)
check_sub_specs(my_spec["spec"], indent + 5)
check_sub_specs(test_spec, 5)
self.process_model_identifier = process_instance_model.process_model_identifier
try:
self.bpmn_process_instance = self.__get_bpmn_process_instance(process_instance_model, spec, validate_only)
self.bpmn_process_instance = self.__get_bpmn_process_instance(
process_instance_model, spec, validate_only
)
self.bpmn_process_instance.script_engine = self._script_engine
self.add_user_info_to_process_instance(self.bpmn_process_instance)
@ -139,17 +175,24 @@ class ProcessInstanceProcessor(object):
# and save it again. In this way, the workflow process is always aware of the
# database model to which it is associated, and scripts running within the model
# can then load data as needed.
self.bpmn_process_instance.data[ProcessInstanceProcessor.PROCESS_INSTANCE_ID_KEY] = process_instance_model.id
process_instance_model.bpmn_json = ProcessInstanceProcessor._serializer.serialize_json(
self.bpmn_process_instance)
self.bpmn_process_instance.data[
ProcessInstanceProcessor.PROCESS_INSTANCE_ID_KEY
] = process_instance_model.id
process_instance_model.bpmn_json = (
ProcessInstanceProcessor._serializer.serialize_json(
self.bpmn_process_instance
)
)
self.save()
except MissingSpecError as ke:
raise ApiError(code="unexpected_process_instance_structure",
message="Failed to deserialize process_instance"
" '%s' due to a mis-placed or missing task '%s'" %
(self.process_model_identifier, str(ke))) from ke
raise ApiError(
code="unexpected_process_instance_structure",
message="Failed to deserialize process_instance"
" '%s' due to a mis-placed or missing task '%s'"
% (self.process_model_identifier, str(ke)),
) from ke
@staticmethod
def add_user_info_to_process_instance(bpmn_process_instance):
@ -159,7 +202,7 @@ class ProcessInstanceProcessor(object):
current_user_data = UserModelSchema().dump(current_user)
tasks = bpmn_process_instance.get_tasks(TaskState.READY)
for task in tasks:
task.data['current_user'] = current_user_data
task.data["current_user"] = current_user_data
@staticmethod
def reset(process_instance_model, clear_data=False):
@ -172,26 +215,35 @@ class ProcessInstanceProcessor(object):
"""
# Try to execute a cancel notify
try:
bpmn_process_instance = ProcessInstanceProcessor.__get_bpmn_process_instance(process_instance_model)
bpmn_process_instance = (
ProcessInstanceProcessor.__get_bpmn_process_instance(
process_instance_model
)
)
ProcessInstanceProcessor.__cancel_notify(bpmn_process_instance)
except Exception as e:
db.session.rollback() # in case the above left the database with a bad transaction
current_app.logger.error("Unable to send a cancel notify for process_instance %s during a reset."
" Continuing with the reset anyway so we don't get in an unresolvable"
" state. An %s error occured with the following information: %s" %
(process_instance_model.id, e.__class__.__name__, str(e)))
current_app.logger.error(
"Unable to send a cancel notify for process_instance %s during a reset."
" Continuing with the reset anyway so we don't get in an unresolvable"
" state. An %s error occured with the following information: %s"
% (process_instance_model.id, e.__class__.__name__, str(e))
)
process_instance_model.bpmn_json = None
process_instance_model.status = ProcessInstanceStatus.not_started
# clear out any task assignments
db.session.query(TaskEventModel). \
filter(TaskEventModel.process_instance_id == process_instance_model.id). \
filter(TaskEventModel.action == TaskAction.ASSIGNMENT.value).delete()
db.session.query(TaskEventModel).filter(
TaskEventModel.process_instance_id == process_instance_model.id
).filter(TaskEventModel.action == TaskAction.ASSIGNMENT.value).delete()
if clear_data:
# Clear out data in previous task events
task_events = db.session.query(TaskEventModel). \
filter(TaskEventModel.process_instance_id == process_instance_model.id).all()
task_events = (
db.session.query(TaskEventModel)
.filter(TaskEventModel.process_instance_id == process_instance_model.id)
.all()
)
for task_event in task_events:
task_event.form_data = {}
db.session.add(task_event)
@ -204,40 +256,70 @@ class ProcessInstanceProcessor(object):
db.session.commit()
@staticmethod
def __get_bpmn_workflow(process_instance_model: ProcessInstanceModel, spec: WorkflowSpec = None, validate_only=False):
def __get_bpmn_workflow(
process_instance_model: ProcessInstanceModel,
spec: WorkflowSpec = None,
validate_only=False,
):
"""__get_bpmn_workflow."""
if process_instance_model.bpmn_workflow_json:
version = ProcessInstanceProcessor._serializer.get_version(process_instance_model.bpmn_workflow_json)
if(version == ProcessInstanceProcessor.SERIALIZER_VERSION):
version = ProcessInstanceProcessor._serializer.get_version(
process_instance_model.bpmn_workflow_json
)
if version == ProcessInstanceProcessor.SERIALIZER_VERSION:
bpmn_workflow = ProcessInstanceProcessor._serializer.deserialize_json(
process_instance_model.bpmn_workflow_json)
process_instance_model.bpmn_workflow_json
)
else:
bpmn_workflow = ProcessInstanceProcessor.\
_old_serializer.deserialize_workflow(process_instance_model.bpmn_workflow_json,
workflow_spec=spec)
bpmn_workflow = (
ProcessInstanceProcessor._old_serializer.deserialize_workflow(
process_instance_model.bpmn_workflow_json, workflow_spec=spec
)
)
bpmn_workflow.script_engine = ProcessInstanceProcessor._script_engine
else:
bpmn_workflow = BpmnWorkflow(spec, script_engine=ProcessInstanceProcessor._script_engine)
bpmn_workflow.data[ProcessInstanceProcessor.PROCESS_INSTANCE_ID_KEY] = process_instance_model.study_id
bpmn_workflow.data[ProcessInstanceProcessor.VALIDATION_PROCESS_KEY] = validate_only
bpmn_workflow = BpmnWorkflow(
spec, script_engine=ProcessInstanceProcessor._script_engine
)
bpmn_workflow.data[
ProcessInstanceProcessor.PROCESS_INSTANCE_ID_KEY
] = process_instance_model.study_id
bpmn_workflow.data[
ProcessInstanceProcessor.VALIDATION_PROCESS_KEY
] = validate_only
return bpmn_workflow
@staticmethod
def __get_bpmn_process_instance(process_instance_model: ProcessInstanceModel, spec: WorkflowSpec = None, validate_only=False):
def __get_bpmn_process_instance(
process_instance_model: ProcessInstanceModel,
spec: WorkflowSpec = None,
validate_only=False,
):
"""__get_bpmn_process_instance."""
if process_instance_model.bpmn_json:
version = ProcessInstanceProcessor._serializer.get_version(process_instance_model.bpmn_json)
if(version == ProcessInstanceProcessor.SERIALIZER_VERSION):
bpmn_process_instance = ProcessInstanceProcessor._serializer.deserialize_json(
process_instance_model.bpmn_json)
version = ProcessInstanceProcessor._serializer.get_version(
process_instance_model.bpmn_json
)
if version == ProcessInstanceProcessor.SERIALIZER_VERSION:
bpmn_process_instance = (
ProcessInstanceProcessor._serializer.deserialize_json(
process_instance_model.bpmn_json
)
)
else:
bpmn_process_instance = ProcessInstanceProcessor.\
_old_serializer.deserialize_process_instance(process_instance_model.bpmn_json,
process_model=spec)
bpmn_process_instance.script_engine = ProcessInstanceProcessor._script_engine
bpmn_process_instance = ProcessInstanceProcessor._old_serializer.deserialize_process_instance(
process_instance_model.bpmn_json, process_model=spec
)
bpmn_process_instance.script_engine = (
ProcessInstanceProcessor._script_engine
)
else:
bpmn_process_instance = BpmnWorkflow(spec, script_engine=ProcessInstanceProcessor._script_engine)
bpmn_process_instance.data[ProcessInstanceProcessor.VALIDATION_PROCESS_KEY] = validate_only
bpmn_process_instance = BpmnWorkflow(
spec, script_engine=ProcessInstanceProcessor._script_engine
)
bpmn_process_instance.data[
ProcessInstanceProcessor.VALIDATION_PROCESS_KEY
] = validate_only
return bpmn_process_instance
def save(self):
@ -247,7 +329,9 @@ class ProcessInstanceProcessor(object):
tasks = list(self.get_all_user_tasks())
self.process_instance_model.status = self.get_status()
self.process_instance_model.total_tasks = len(tasks)
self.process_instance_model.completed_tasks = sum(1 for t in tasks if t.state in complete_states)
self.process_instance_model.completed_tasks = sum(
1 for t in tasks if t.state in complete_states
)
self.process_instance_model.last_updated = datetime.utcnow()
db.session.add(self.process_instance_model)
db.session.commit()
@ -261,16 +345,26 @@ class ProcessInstanceProcessor(object):
spec_files = SpecFileService().get_files(process_model, include_libraries=True)
spec = ProcessInstanceProcessor.get_spec(spec_files, process_model)
try:
bpmn_process_instance = BpmnWorkflow(spec, script_engine=ProcessInstanceProcessor._script_engine)
bpmn_process_instance.data[ProcessInstanceProcessor.VALIDATION_PROCESS_KEY] = False
ProcessInstanceProcessor.add_user_info_to_process_instance(bpmn_process_instance)
bpmn_process_instance = BpmnWorkflow(
spec, script_engine=ProcessInstanceProcessor._script_engine
)
bpmn_process_instance.data[
ProcessInstanceProcessor.VALIDATION_PROCESS_KEY
] = False
ProcessInstanceProcessor.add_user_info_to_process_instance(
bpmn_process_instance
)
bpmn_process_instance.do_engine_steps()
except WorkflowException as we:
raise ApiError.from_task_spec("error_running_master_spec", str(we), we.sender) from we
raise ApiError.from_task_spec(
"error_running_master_spec", str(we), we.sender
) from we
if not bpmn_process_instance.is_completed():
raise ApiError("master_spec_not_automatic",
"The master spec should only contain fully automated tasks, it failed to complete.")
raise ApiError(
"master_spec_not_automatic",
"The master spec should only contain fully automated tasks, it failed to complete.",
)
return bpmn_process_instance.last_task.data
@ -293,18 +387,28 @@ class ProcessInstanceProcessor(object):
elif file.type == FileType.dmn.value:
dmn: etree.Element = etree.fromstring(data)
parser.add_dmn_xml(dmn, filename=file.name)
if process_model_info.primary_process_id is None or process_model_info.primary_process_id == "":
raise (ApiError(code="no_primary_bpmn_error",
message="There is no primary BPMN model defined for process_instance %s" % process_model_info.id))
if (
process_model_info.primary_process_id is None
or process_model_info.primary_process_id == ""
):
raise (
ApiError(
code="no_primary_bpmn_error",
message="There is no primary BPMN model defined for process_instance %s"
% process_model_info.id,
)
)
try:
spec = parser.get_spec(process_model_info.primary_process_id)
except ValidationException as ve:
raise ApiError(code="process_instance_validation_error",
message="Failed to parse the Workflow Specification. "
+ "Error is '%s.'" % str(ve),
file_name=ve.filename,
task_id=ve.id,
tag=ve.tag) from ve
raise ApiError(
code="process_instance_validation_error",
message="Failed to parse the Workflow Specification. "
+ "Error is '%s.'" % str(ve),
file_name=ve.filename,
task_id=ve.id,
tag=ve.tag,
) from ve
return spec
@staticmethod
@ -342,7 +446,7 @@ class ProcessInstanceProcessor(object):
"""__cancel_notify."""
try:
# A little hackly, but make the bpmn_process_instance catch a cancel event.
bpmn_process_instance.signal('cancel') # generate a cancel signal.
bpmn_process_instance.signal("cancel") # generate a cancel signal.
bpmn_process_instance.catch(CancelEventDefinition())
bpmn_process_instance.do_engine_steps()
except WorkflowTaskExecException as we:
@ -368,9 +472,14 @@ class ProcessInstanceProcessor(object):
endtasks = []
if self.bpmn_process_instance.is_completed():
for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.ANY_MASK):
for task in SpiffTask.Iterator(
self.bpmn_process_instance.task_tree, TaskState.ANY_MASK
):
# Assure that we find the end event for this process_instance, and not for any sub-process_instances.
if isinstance(task.task_spec, EndEvent) and task.process_instance == self.bpmn_process_instance:
if (
isinstance(task.task_spec, EndEvent)
and task.process_instance == self.bpmn_process_instance
):
endtasks.append(task)
return endtasks[-1]
@ -403,7 +512,10 @@ class ProcessInstanceProcessor(object):
if task._is_descendant_of(last_user_task):
return task
for task in ready_tasks:
if self.bpmn_process_instance.last_task and task.parent == last_user_task.parent:
if (
self.bpmn_process_instance.last_task
and task.parent == last_user_task.parent
):
return task
return ready_tasks[0]
@ -411,7 +523,9 @@ class ProcessInstanceProcessor(object):
# If there are no ready tasks, but the thing isn't complete yet, find the first non-complete task
# and return that
next_task = None
for task in SpiffTask.Iterator(self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK):
for task in SpiffTask.Iterator(
self.bpmn_process_instance.task_tree, TaskState.NOT_FINISHED_MASK
):
next_task = task
return next_task
@ -420,7 +534,13 @@ class ProcessInstanceProcessor(object):
completed_user_tasks = self.bpmn_process_instance.get_tasks(TaskState.COMPLETED)
completed_user_tasks.reverse()
completed_user_tasks = list(
filter(lambda task: not self.bpmn_process_instance._is_engine_task(task.task_spec), completed_user_tasks))
filter(
lambda task: not self.bpmn_process_instance._is_engine_task(
task.task_spec
),
completed_user_tasks,
)
)
return completed_user_tasks
def previous_task(self):
@ -465,19 +585,26 @@ class ProcessInstanceProcessor(object):
def get_all_user_tasks(self):
"""Get_all_user_tasks."""
all_tasks = self.bpmn_process_instance.get_tasks(TaskState.ANY_MASK)
return [t for t in all_tasks if not self.bpmn_process_instance._is_engine_task(t.task_spec)]
return [
t
for t in all_tasks
if not self.bpmn_process_instance._is_engine_task(t.task_spec)
]
def get_all_completed_tasks(self):
"""Get_all_completed_tasks."""
all_tasks = self.bpmn_process_instance.get_tasks(TaskState.ANY_MASK)
return [t for t in all_tasks
if not self.bpmn_process_instance._is_engine_task(t.task_spec)
and t.state in [TaskState.COMPLETED, TaskState.CANCELLED]]
return [
t
for t in all_tasks
if not self.bpmn_process_instance._is_engine_task(t.task_spec)
and t.state in [TaskState.COMPLETED, TaskState.CANCELLED]
]
def get_nav_item(self, task):
"""Get_nav_item."""
for nav_item in self.bpmn_process_instance.get_nav_list():
if nav_item['task_id'] == task.id:
if nav_item["task_id"] == task.id:
return nav_item
def find_spec_and_field(self, spec_name, field_id):
@ -490,15 +617,21 @@ class ProcessInstanceProcessor(object):
for spec in process_instance.spec.task_specs.values():
if spec.name == spec_name:
if not hasattr(spec, "form"):
raise ApiError("invalid_spec",
"The spec name you provided does not contain a form.")
raise ApiError(
"invalid_spec",
"The spec name you provided does not contain a form.",
)
for field in spec.form.fields:
if field.id == field_id:
return spec, field
raise ApiError("invalid_field",
f"The task '{spec_name}' has no field named '{field_id}'")
raise ApiError(
"invalid_field",
f"The task '{spec_name}' has no field named '{field_id}'",
)
raise ApiError("invalid_spec",
f"Unable to find a task in the process_instance called '{spec_name}'")
raise ApiError(
"invalid_spec",
f"Unable to find a task in the process_instance called '{spec_name}'",
)

View File

@ -1,25 +1,27 @@
"""Process_instance_service."""
from datetime import datetime
from typing import List
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from flask import current_app
from flask_bpmn.models.db import db
from SpiffWorkflow import NavItem
from typing import List
from SpiffWorkflow.bpmn.specs.ManualTask import ManualTask
from SpiffWorkflow.bpmn.specs.UserTask import UserTask
from SpiffWorkflow.util.deep_merge import DeepMerge
from spiff_workflow_webapp.services.user_service import UserService
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel, ProcessInstanceStatus, ProcessInstanceApi
from spiff_workflow_webapp.services.process_instance_processor import ProcessInstanceProcessor
from spiff_workflow_webapp.models.process_instance import ProcessInstanceApi
from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
from spiff_workflow_webapp.models.process_instance import ProcessInstanceStatus
from spiff_workflow_webapp.models.task_event import TaskAction
from spiff_workflow_webapp.models.task_event import TaskEventModel
from spiff_workflow_webapp.services.process_instance_processor import (
ProcessInstanceProcessor,
)
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.models.task_event import TaskEventModel, TaskAction
from spiff_workflow_webapp.services.user_service import UserService
class ProcessInstanceService():
class ProcessInstanceService:
"""ProcessInstanceService."""
TASK_STATE_LOCKED = "locked"
@ -27,16 +29,20 @@ class ProcessInstanceService():
@staticmethod
def create_process_instance(process_model_identifier, user):
"""Get_process_instance_from_spec."""
process_instance_model = ProcessInstanceModel(status=ProcessInstanceStatus.not_started,
process_initiator=user,
process_model_identifier=process_model_identifier,
last_updated=datetime.now())
process_instance_model = ProcessInstanceModel(
status=ProcessInstanceStatus.not_started,
process_initiator=user,
process_model_identifier=process_model_identifier,
last_updated=datetime.now(),
)
db.session.add(process_instance_model)
db.session.commit()
return process_instance_model
@staticmethod
def processor_to_process_instance_api(processor: ProcessInstanceProcessor, next_task=None):
def processor_to_process_instance_api(
processor: ProcessInstanceProcessor, next_task=None
):
"""Returns an API model representing the state of the current process_instance, if requested, and
possible, next_task is set to the current_task."""
navigation = processor.bpmn_process_instance.get_deep_nav_list()
@ -55,12 +61,15 @@ class ProcessInstanceService():
is_review=spec.is_review,
title=spec.display_name,
)
if not next_task: # The Next Task can be requested to be a certain task, useful for parallel tasks.
if (
not next_task
): # The Next Task can be requested to be a certain task, useful for parallel tasks.
# This may or may not work, sometimes there is no next task to complete.
next_task = processor.next_task()
if next_task:
previous_form_data = ProcessInstanceService.get_previously_submitted_data(
processor.process_instance_model.id, next_task)
processor.process_instance_model.id, next_task
)
# DeepMerge.merge(next_task.data, previous_form_data)
next_task.data = DeepMerge.merge(previous_form_data, next_task.data)
@ -73,36 +82,49 @@ class ProcessInstanceService():
return process_instance_api
@staticmethod
def update_navigation(navigation: List[NavItem], processor: ProcessInstanceProcessor):
def update_navigation(
navigation: List[NavItem], processor: ProcessInstanceProcessor
):
"""Update_navigation."""
# Recursive function to walk down through children, and clean up descriptions, and statuses
for nav_item in navigation:
spiff_task = processor.bpmn_workflow.get_task(nav_item.task_id)
if spiff_task:
nav_item.description = ProcessInstanceService.__calculate_title(spiff_task)
user_uids = ProcessInstanceService.get_users_assigned_to_task(processor, spiff_task)
if (isinstance(spiff_task.task_spec, UserTask) or isinstance(spiff_task.task_spec, ManualTask)) \
and not UserService.in_list(user_uids, allow_admin_impersonate=True):
nav_item.description = ProcessInstanceService.__calculate_title(
spiff_task
)
user_uids = ProcessInstanceService.get_users_assigned_to_task(
processor, spiff_task
)
if (
isinstance(spiff_task.task_spec, UserTask)
or isinstance(spiff_task.task_spec, ManualTask)
) and not UserService.in_list(user_uids, allow_admin_impersonate=True):
nav_item.state = ProcessInstanceService.TASK_STATE_LOCKED
else:
# Strip off the first word in the description, to meet guidlines for BPMN.
if nav_item.description:
if nav_item.description is not None and ' ' in nav_item.description:
nav_item.description = nav_item.description.partition(' ')[2]
if nav_item.description is not None and " " in nav_item.description:
nav_item.description = nav_item.description.partition(" ")[2]
# Recurse here
ProcessInstanceService.update_navigation(nav_item.children, processor)
@staticmethod
def get_previously_submitted_data(process_instance_id, spiff_task):
""" If the user has completed this task previously, find the form data for the last submission."""
query = db.session.query(TaskEventModel) \
.filter_by(process_instance_id=process_instance_id) \
.filter_by(task_name=spiff_task.task_spec.name) \
"""If the user has completed this task previously, find the form data for the last submission."""
query = (
db.session.query(TaskEventModel)
.filter_by(process_instance_id=process_instance_id)
.filter_by(task_name=spiff_task.task_spec.name)
.filter_by(action=TaskAction.COMPLETE.value)
)
if hasattr(spiff_task, 'internal_data') and 'runtimes' in spiff_task.internal_data:
query = query.filter_by(mi_index=spiff_task.internal_data['runtimes'])
if (
hasattr(spiff_task, "internal_data")
and "runtimes" in spiff_task.internal_data
):
query = query.filter_by(mi_index=spiff_task.internal_data["runtimes"])
latest_event = query.order_by(TaskEventModel.date.desc()).first()
if latest_event:
@ -110,11 +132,13 @@ class ProcessInstanceService():
return latest_event.form_data
else:
missing_form_error = (
f'We have lost data for workflow {workflow_id}, '
f'task {spiff_task.task_spec.name}, it is not in the task event model, '
f'and it should be.'
f"We have lost data for workflow {workflow_id}, "
f"task {spiff_task.task_spec.name}, it is not in the task event model, "
f"and it should be."
)
current_app.logger.error(
"missing_form_data", missing_form_error, exc_info=True
)
current_app.logger.error("missing_form_data", missing_form_error, exc_info=True)
return {}
else:
return {}

View File

@ -4,8 +4,10 @@ import os
import shutil
from typing import List
from spiff_workflow_webapp.models.process_model import ProcessModelInfo, ProcessModelInfoSchema
from spiff_workflow_webapp.models.process_group import ProcessGroup, ProcessGroupSchema
from spiff_workflow_webapp.models.process_group import ProcessGroup
from spiff_workflow_webapp.models.process_group import ProcessGroupSchema
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema
from spiff_workflow_webapp.services.file_system_service import FileSystemService
@ -61,7 +63,9 @@ class ProcessModelService(FileSystemService):
def get_master_spec(self):
"""Get_master_spec."""
path = os.path.join(FileSystemService.root_path(), FileSystemService.MASTER_SPECIFICATION)
path = os.path.join(
FileSystemService.root_path(), FileSystemService.MASTER_SPECIFICATION
)
if os.path.exists(path):
return self.__scan_spec(path, FileSystemService.MASTER_SPECIFICATION)
@ -81,7 +85,9 @@ class ProcessModelService(FileSystemService):
for sd in spec_dirs:
if sd.name == spec_id:
# Now we have the process_group direcotry, and spec directory
process_group = self.__scan_process_group(process_group_dir)
process_group = self.__scan_process_group(
process_group_dir
)
return self.__scan_spec(sd.path, sd.name, process_group)
def get_specs(self):
@ -97,9 +103,9 @@ class ProcessModelService(FileSystemService):
specs = spec.process_group.specs
specs.sort(key=lambda w: w.display_order)
index = specs.index(spec)
if direction == 'up' and index > 0:
if direction == "up" and index > 0:
specs[index - 1], specs[index] = specs[index], specs[index - 1]
if direction == 'down' and index < len(specs) - 1:
if direction == "down" and index < len(specs) - 1:
specs[index + 1], specs[index] = specs[index], specs[index + 1]
return self.cleanup_workflow_spec_display_order(spec.process_group)
@ -169,9 +175,9 @@ class ProcessModelService(FileSystemService):
"""Reorder_workflow_spec_process_group."""
cats = self.get_process_groups() # Returns an ordered list
index = cats.index(cat)
if direction == 'up' and index > 0:
if direction == "up" and index > 0:
cats[index - 1], cats[index] = cats[index], cats[index - 1]
if direction == 'down' and index < len(cats) - 1:
if direction == "down" and index < len(cats) - 1:
cats[index + 1], cats[index] = cats[index], cats[index + 1]
index = 0
for process_group in cats:
@ -198,7 +204,7 @@ class ProcessModelService(FileSystemService):
with os.scandir(FileSystemService.root_path()) as directory_items:
process_groups = []
for item in directory_items:
if item.is_dir() and not item.name[0] == '.':
if item.is_dir() and not item.name[0] == ".":
if item.name == self.REFERENCE_FILES:
continue
elif item.name == self.MASTER_SPECIFICATION:
@ -218,14 +224,21 @@ class ProcessModelService(FileSystemService):
data = json.load(cat_json)
cat = self.GROUP_SCHEMA.load(data)
else:
cat = ProcessGroup(id=dir_item.name, display_name=dir_item.name, display_order=10000, admin=False)
cat = ProcessGroup(
id=dir_item.name,
display_name=dir_item.name,
display_order=10000,
admin=False,
)
with open(cat_path, "w") as wf_json:
json.dump(self.GROUP_SCHEMA.dump(cat), wf_json, indent=4)
with os.scandir(dir_item.path) as workflow_dirs:
cat.specs = []
for item in workflow_dirs:
if item.is_dir():
cat.specs.append(self.__scan_spec(item.path, item.name, process_group=cat))
cat.specs.append(
self.__scan_spec(item.path, item.name, process_group=cat)
)
cat.specs.sort(key=lambda w: w.display_order)
return cat
@ -235,8 +248,8 @@ class ProcessModelService(FileSystemService):
# Add in the Workflows for each process_group
# Fixme: moved fro the Study Service
workflow_metas = []
# for workflow in workflow_models:
# workflow_metas.append(WorkflowMetadata.from_workflow(workflow))
# for workflow in workflow_models:
# workflow_metas.append(WorkflowMetadata.from_workflow(workflow))
return workflow_metas
def __scan_spec(self, path, name, process_group=None):
@ -249,10 +262,19 @@ class ProcessModelService(FileSystemService):
data = json.load(wf_json)
spec = self.WF_SCHEMA.load(data)
else:
spec = ProcessModelInfo(id=name, library=False, standalone=False, is_master_spec=is_master,
display_name=name, description="", primary_process_id="",
primary_file_name="", display_order=0, is_review=False,
libraries=[])
spec = ProcessModelInfo(
id=name,
library=False,
standalone=False,
is_master_spec=is_master,
display_name=name,
description="",
primary_process_id="",
primary_file_name="",
display_order=0,
is_review=False,
libraries=[],
)
with open(spec_path, "w") as wf_json:
json.dump(self.WF_SCHEMA.dump(spec), wf_json, indent=4)
if process_group:

View File

@ -4,12 +4,11 @@ import shutil
from typing import List
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.file import File, FileType
from lxml import etree
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
from lxml import etree
from spiff_workflow_webapp.models.file import File
from spiff_workflow_webapp.models.file import FileType
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.services.file_system_service import FileSystemService
@ -23,8 +22,13 @@ class SpecFileService(FileSystemService):
"""
@staticmethod
def get_files(workflow_spec: ProcessModelInfo, file_name=None, include_libraries=False, extension_filter="") -> List[File]:
""" Returns all files associated with a workflow specification."""
def get_files(
workflow_spec: ProcessModelInfo,
file_name=None,
include_libraries=False,
extension_filter="",
) -> List[File]:
"""Returns all files associated with a workflow specification."""
path = SpecFileService.workflow_path(workflow_spec)
files = SpecFileService._get_files(path, file_name)
if include_libraries:
@ -38,13 +42,17 @@ class SpecFileService(FileSystemService):
return files
@staticmethod
def add_file(workflow_spec: ProcessModelInfo, file_name: str, binary_data: bytearray) -> File:
def add_file(
workflow_spec: ProcessModelInfo, file_name: str, binary_data: bytearray
) -> File:
"""Add_file."""
# Same as update
return SpecFileService.update_file(workflow_spec, file_name, binary_data)
@staticmethod
def update_file(workflow_spec: ProcessModelInfo, file_name: str, binary_data) -> File:
def update_file(
workflow_spec: ProcessModelInfo, file_name: str, binary_data
) -> File:
"""Update_file."""
SpecFileService.assert_valid_file_name(file_name)
file_path = SpecFileService.file_path(workflow_spec, file_name)
@ -69,8 +77,11 @@ class SpecFileService(FileSystemService):
if os.path.exists(file_path):
break
if not os.path.exists(file_path):
raise ApiError("unknown_file", f"No file found with name {file_name} in {workflow_spec.display_name}")
with open(file_path, 'rb') as f_handle:
raise ApiError(
"unknown_file",
f"No file found with name {file_name} in {workflow_spec.display_name}",
)
with open(file_path, "rb") as f_handle:
spec_file_data = f_handle.read()
return spec_file_data
@ -110,7 +121,9 @@ class SpecFileService(FileSystemService):
shutil.rmtree(dir_path)
@staticmethod
def set_primary_bpmn(workflow_spec: ProcessModelInfo, file_name: str, binary_data=None):
def set_primary_bpmn(
workflow_spec: ProcessModelInfo, file_name: str, binary_data=None
):
"""Set_primary_bpmn."""
# If this is a BPMN, extract the process id, and determine if it is contains swim lanes.
extension = SpecFileService.get_extension(file_name)
@ -125,27 +138,41 @@ class SpecFileService(FileSystemService):
workflow_spec.is_review = SpecFileService.has_swimlane(bpmn)
except etree.XMLSyntaxError as xse:
raise ApiError("invalid_xml", "Failed to parse xml: " + str(xse), file_name=file_name)
raise ApiError(
"invalid_xml",
"Failed to parse xml: " + str(xse),
file_name=file_name,
)
except ValidationException as ve:
if ve.args[0].find('No executable process tag found') >= 0:
raise ApiError(code='missing_executable_option',
message='No executable process tag found. Please make sure the Executable option is set in the workflow.')
if ve.args[0].find("No executable process tag found") >= 0:
raise ApiError(
code="missing_executable_option",
message="No executable process tag found. Please make sure the Executable option is set in the workflow.",
)
else:
raise ApiError(code='validation_error',
message=f'There was an error validating your workflow. Original message is: {ve}')
raise ApiError(
code="validation_error",
message=f"There was an error validating your workflow. Original message is: {ve}",
)
else:
raise ApiError("invalid_xml", "Only a BPMN can be the primary file.", file_name=file_name)
raise ApiError(
"invalid_xml",
"Only a BPMN can be the primary file.",
file_name=file_name,
)
@staticmethod
def has_swimlane(et_root: etree.Element):
"""
Look through XML and determine if there are any lanes present that have a label.
"""
elements = et_root.xpath('//bpmn:lane',
namespaces={'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL'})
elements = et_root.xpath(
"//bpmn:lane",
namespaces={"bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL"},
)
retval = False
for el in elements:
if el.get('name'):
if el.get("name"):
retval = True
return retval
@ -154,11 +181,13 @@ class SpecFileService(FileSystemService):
"""Get_process_id."""
process_elements = []
for child in et_root:
if child.tag.endswith('process') and child.attrib.get('isExecutable', False):
if child.tag.endswith("process") and child.attrib.get(
"isExecutable", False
):
process_elements.append(child)
if len(process_elements) == 0:
raise ValidationException('No executable process tag found')
raise ValidationException("No executable process tag found")
# There are multiple root elements
if len(process_elements) > 1:
@ -167,9 +196,11 @@ class SpecFileService(FileSystemService):
for e in process_elements:
this_element: etree.Element = e
for child_element in list(this_element):
if child_element.tag.endswith('startEvent'):
return this_element.attrib['id']
if child_element.tag.endswith("startEvent"):
return this_element.attrib["id"]
raise ValidationException('No start event found in %s' % et_root.attrib['id'])
raise ValidationException(
"No start event found in %s" % et_root.attrib["id"]
)
return process_elements[0].attrib['id']
return process_elements[0].attrib["id"]

View File

@ -1,22 +1,20 @@
"""User_service."""
from flask import g
from flask_bpmn.models.db import db
from flask_bpmn.api.api_error import ApiError
from spiff_workflow_webapp.models.user import UserModel, AdminSessionModel
from flask_bpmn.models.db import db
from spiff_workflow_webapp.models.user import AdminSessionModel
from spiff_workflow_webapp.models.user import UserModel
class UserService(object):
class UserService:
"""Provides common tools for working with users."""
# Returns true if the current user is logged in.
@staticmethod
def has_user():
"""Has_user."""
return 'token' in g and \
bool(g.token) and \
'user' in g and \
bool(g.user)
return "token" in g and bool(g.token) and "user" in g and bool(g.user)
# Returns true if the current user is an admin.
@staticmethod
@ -33,7 +31,11 @@ class UserService(object):
return adminSession is not None
else:
raise ApiError("unauthorized", "You do not have permissions to do this.", status_code=403)
raise ApiError(
"unauthorized",
"You do not have permissions to do this.",
status_code=403,
)
# Returns true if the given user uid is different from the current user's uid.
@staticmethod
@ -45,11 +47,17 @@ class UserService(object):
def current_user(allow_admin_impersonate=False) -> UserModel:
"""Current_user."""
if not UserService.has_user():
raise ApiError("logged_out", "You are no longer logged in.", status_code=401)
raise ApiError(
"logged_out", "You are no longer logged in.", status_code=401
)
# Admins can pretend to be different users and act on a user's behalf in
# some circumstances.
if UserService.user_is_admin() and allow_admin_impersonate and UserService.admin_is_impersonating():
if (
UserService.user_is_admin()
and allow_admin_impersonate
and UserService.admin_is_impersonating()
):
return UserService.get_admin_session_user()
else:
return g.user
@ -61,24 +69,36 @@ class UserService(object):
def start_impersonating(uid=None):
"""Start_impersonating."""
if not UserService.has_user():
raise ApiError("logged_out", "You are no longer logged in.", status_code=401)
raise ApiError(
"logged_out", "You are no longer logged in.", status_code=401
)
if not UserService.user_is_admin():
raise ApiError("unauthorized", "You do not have permissions to do this.", status_code=403)
raise ApiError(
"unauthorized",
"You do not have permissions to do this.",
status_code=403,
)
if uid is None:
raise ApiError("invalid_uid", "Please provide a valid user uid.")
if UserService.is_different_user(uid):
# Impersonate the user if the given uid is valid.
impersonate_user = db.session.query(UserModel).filter(UserModel.uid == uid).first()
impersonate_user = (
db.session.query(UserModel).filter(UserModel.uid == uid).first()
)
if impersonate_user is not None:
g.impersonate_user = impersonate_user
# Store the uid and user session token.
db.session.query(AdminSessionModel).filter(AdminSessionModel.token == g.token).delete()
db.session.add(AdminSessionModel(token=g.token, admin_impersonate_uid=uid))
db.session.query(AdminSessionModel).filter(
AdminSessionModel.token == g.token
).delete()
db.session.add(
AdminSessionModel(token=g.token, admin_impersonate_uid=uid)
)
db.session.commit()
else:
raise ApiError("invalid_uid", "The uid provided is not valid.")
@ -87,10 +107,12 @@ class UserService(object):
def stop_impersonating():
"""Stop_impersonating."""
if not UserService.has_user():
raise ApiError("logged_out", "You are no longer logged in.", status_code=401)
raise ApiError(
"logged_out", "You are no longer logged in.", status_code=401
)
# Clear out the current impersonating user.
if 'impersonate_user' in g:
if "impersonate_user" in g:
del g.impersonate_user
admin_session: AdminSessionModel = UserService.get_admin_session()
@ -104,7 +126,9 @@ class UserService(object):
False if there is no user, or the user is not in the list.
"""
if UserService.has_user(): # If someone is logged in, lock tasks that don't belong to them.
if (
UserService.has_user()
): # If someone is logged in, lock tasks that don't belong to them.
user = UserService.current_user(allow_admin_impersonate)
if user.uid in uids:
return True
@ -114,9 +138,17 @@ class UserService(object):
def get_admin_session() -> AdminSessionModel:
"""Get_admin_session."""
if UserService.user_is_admin():
return db.session.query(AdminSessionModel).filter(AdminSessionModel.token == g.token).first()
return (
db.session.query(AdminSessionModel)
.filter(AdminSessionModel.token == g.token)
.first()
)
else:
raise ApiError("unauthorized", "You do not have permissions to do this.", status_code=403)
raise ApiError(
"unauthorized",
"You do not have permissions to do this.",
status_code=403,
)
@staticmethod
def get_admin_session_user() -> UserModel:
@ -125,6 +157,14 @@ class UserService(object):
admin_session = UserService.get_admin_session()
if admin_session is not None:
return db.session.query(UserModel).filter(UserModel.uid == admin_session.admin_impersonate_uid).first()
return (
db.session.query(UserModel)
.filter(UserModel.uid == admin_session.admin_impersonate_uid)
.first()
)
else:
raise ApiError("unauthorized", "You do not have permissions to do this.", status_code=403)
raise ApiError(
"unauthorized",
"You do not have permissions to do this.",
status_code=403,
)

View File

@ -1,43 +1,59 @@
"""example_data."""
"""Example_data."""
import glob
import os
from flask import current_app
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.services.spec_file_service import SpecFileService
class ExampleDataLoader:
"""ExampleDataLoader."""
def create_spec(self, id, display_name="", description="", filepath=None, master_spec=False,
process_group_id='', display_order=0, from_tests=False, standalone=False, library=False):
def create_spec(
self,
id,
display_name="",
description="",
filepath=None,
master_spec=False,
process_group_id="",
display_order=0,
from_tests=False,
standalone=False,
library=False,
):
"""Assumes that a directory exists in static/bpmn with the same name as the given id.
further assumes that the [id].bpmn is the primary file for the process model.
returns an array of data models to be added to the database.
"""
global file
spec = ProcessModelInfo(id=id,
display_name=display_name,
description=description,
process_group_id=process_group_id,
display_order=display_order,
is_master_spec=master_spec,
standalone=standalone,
library=library,
primary_file_name="",
primary_process_id="",
is_review=False,
libraries=[])
spec = ProcessModelInfo(
id=id,
display_name=display_name,
description=description,
process_group_id=process_group_id,
display_order=display_order,
is_master_spec=master_spec,
standalone=standalone,
library=library,
primary_file_name="",
primary_process_id="",
is_review=False,
libraries=[],
)
workflow_spec_service = ProcessModelService()
workflow_spec_service.add_spec(spec)
if not filepath and not from_tests:
filepath = os.path.join(current_app.root_path, 'static', 'bpmn', id, "*.*")
filepath = os.path.join(current_app.root_path, "static", "bpmn", id, "*.*")
if not filepath and from_tests:
filepath = os.path.join(current_app.root_path, '..', '..', 'tests', 'data', id, "*.*")
filepath = os.path.join(
current_app.root_path, "..", "..", "tests", "data", id, "*.*"
)
files = glob.glob(filepath)
for file_path in files:
@ -46,12 +62,14 @@ class ExampleDataLoader:
noise, file_extension = os.path.splitext(file_path)
filename = os.path.basename(file_path)
is_primary = filename.lower() == id + '.bpmn'
is_primary = filename.lower() == id + ".bpmn"
file = None
try:
file = open(file_path, 'rb')
file = open(file_path, "rb")
data = file.read()
SpecFileService.add_file(workflow_spec=spec, file_name=filename, binary_data=data)
SpecFileService.add_file(
workflow_spec=spec, file_name=filename, binary_data=data
)
if is_primary:
SpecFileService.set_primary_bpmn(spec, filename, data)
workflow_spec_service = ProcessModelService()

View File

@ -1,17 +1,16 @@
"""User."""
import os
from typing import Any
from flask_bpmn.models.db import db
from flask_bpmn.models.db import db
from tests.spiff_workflow_webapp.helpers.example_data import ExampleDataLoader
from spiff_workflow_webapp.models.process_group import ProcessGroup
from spiff_workflow_webapp.models.user import UserModel
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from tests.spiff_workflow_webapp.helpers.example_data import ExampleDataLoader
def find_or_create_user(username: str = "test_user1") -> Any:
"""Find_or_create_user."""
user = UserModel.query.filter_by(username=username).first()
if user is None:
user = UserModel(username=username)
@ -19,6 +18,8 @@ def find_or_create_user(username: str = "test_user1") -> Any:
db.session.commit()
return user
#
#
# def find_or_create_process_group(name: str = "test_group1") -> Any:
@ -38,12 +39,24 @@ def assure_process_group_exists(process_group_id=None):
if process_group_id is not None:
process_group = workflow_spec_service.get_process_group(process_group_id)
if process_group is None:
process_group = ProcessGroup(id="test_process_group", display_name="Test Workflows", admin=False, display_order=0)
process_group = ProcessGroup(
id="test_process_group",
display_name="Test Workflows",
admin=False,
display_order=0,
)
workflow_spec_service.add_process_group(process_group)
return process_group
def load_test_spec(app, dir_name, display_name=None, master_spec=False, process_group_id=None, library=False):
def load_test_spec(
app,
dir_name,
display_name=None,
master_spec=False,
process_group_id=None,
library=False,
):
"""Loads a spec into the database based on a directory in /tests/data."""
process_group = None
workflow_spec_service = ProcessModelService()
@ -56,8 +69,14 @@ def load_test_spec(app, dir_name, display_name=None, master_spec=False, process_
else:
if display_name is None:
display_name = dir_name
spec = ExampleDataLoader().create_spec(id=dir_name, master_spec=master_spec, from_tests=True,
display_name=display_name, process_group_id=process_group_id, library=library)
spec = ExampleDataLoader().create_spec(
id=dir_name,
master_spec=master_spec,
from_tests=True,
display_name=display_name,
process_group_id=process_group_id,
library=library,
)
return spec
@ -72,7 +91,8 @@ def load_test_spec(app, dir_name, display_name=None, master_spec=False, process_
# return '?%s' % '&'.join(query_string_list)
def logged_in_headers(user=None, redirect_url='http://some/frontend/url'):
def logged_in_headers(user=None, redirect_url="http://some/frontend/url"):
"""Logged_in_headers."""
# if user is None:
# uid = 'test_user'
# user_info = {'uid': 'test_user'}
@ -92,4 +112,4 @@ def logged_in_headers(user=None, redirect_url='http://some/frontend/url'):
# user = UserService.current_user(allow_admin_impersonate=True)
# self.assertEqual(uid, user.uid, 'Logged in user should match given user uid')
return dict(Authorization='Bearer ' + user.encode_auth_token())
return dict(Authorization="Bearer " + user.encode_auth_token())

View File

@ -9,6 +9,7 @@ from spiff_workflow_webapp.models.process_instance import ProcessInstanceModel
def test_user_can_be_created_and_deleted(client: FlaskClient) -> None:
"""Test_user_can_be_created_and_deleted."""
process_instance = ProcessInstanceModel.query.filter().first()
if process_instance is not None:
db.session.delete(process_instance)

View File

@ -1,19 +1,20 @@
"""Test Process Api Blueprint."""
import json
import pytest
import os
import io
import json
import os
import shutil
import pytest
from flask.testing import FlaskClient
from tests.spiff_workflow_webapp.helpers.test_data import find_or_create_user
from tests.spiff_workflow_webapp.helpers.test_data import load_test_spec
from tests.spiff_workflow_webapp.helpers.test_data import logged_in_headers
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema, ProcessModelInfo
from spiff_workflow_webapp.models.process_group import ProcessGroup
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
from spiff_workflow_webapp.models.file import FileType
from tests.spiff_workflow_webapp.helpers.test_data import load_test_spec, find_or_create_user, logged_in_headers
from spiff_workflow_webapp.models.process_group import ProcessGroup
from spiff_workflow_webapp.models.process_model import ProcessModelInfo
from spiff_workflow_webapp.models.process_model import ProcessModelInfoSchema
from spiff_workflow_webapp.services.process_model_service import ProcessModelService
@pytest.fixture()
@ -33,6 +34,7 @@ def test_add_new_process_model(app, client: FlaskClient, with_bpmn_file_cleanup)
create_process_model(app, client)
create_spec_file(app, client)
# def test_get_process_model(self):
#
# load_test_spec('random_fact')
@ -46,52 +48,80 @@ def test_add_new_process_model(app, client: FlaskClient, with_bpmn_file_cleanup)
#
def test_get_workflow_from_workflow_spec(app, client: FlaskClient, with_bpmn_file_cleanup):
def test_get_workflow_from_workflow_spec(
app, client: FlaskClient, with_bpmn_file_cleanup
):
"""Test_get_workflow_from_workflow_spec."""
user = find_or_create_user()
spec = load_test_spec(app, 'hello_world')
rv = client.post(f'/v1.0/workflow-specification/{spec.id}', headers=logged_in_headers(user))
spec = load_test_spec(app, "hello_world")
rv = client.post(
f"/v1.0/workflow-specification/{spec.id}", headers=logged_in_headers(user)
)
assert rv.status_code == 200
assert('hello_world' == rv.json['process_model_identifier'])
#assert('Task_GetName' == rv.json['next_task']['name'])
assert "hello_world" == rv.json["process_model_identifier"]
# assert('Task_GetName' == rv.json['next_task']['name'])
def create_process_model(app, client: FlaskClient):
"""Create_process_model."""
process_model_service = ProcessModelService()
assert(0 == len(process_model_service.get_specs()))
assert(0 == len(process_model_service.get_process_groups()))
cat = ProcessGroup(id="test_cat", display_name="Test Category", display_order=0, admin=False)
assert 0 == len(process_model_service.get_specs())
assert 0 == len(process_model_service.get_process_groups())
cat = ProcessGroup(
id="test_cat", display_name="Test Category", display_order=0, admin=False
)
process_model_service.add_process_group(cat)
spec = ProcessModelInfo(id='make_cookies', display_name='Cooooookies',
description='Om nom nom delicious cookies', process_group_id=cat.id,
standalone=False, is_review=False, is_master_spec=False, libraries=[], library=False,
primary_process_id='', primary_file_name='')
spec = ProcessModelInfo(
id="make_cookies",
display_name="Cooooookies",
description="Om nom nom delicious cookies",
process_group_id=cat.id,
standalone=False,
is_review=False,
is_master_spec=False,
libraries=[],
library=False,
primary_process_id="",
primary_file_name="",
)
user = find_or_create_user()
rv = client.post('/v1.0/workflow-specification',
content_type="application/json",
data=json.dumps(ProcessModelInfoSchema().dump(spec)),
headers=logged_in_headers(user))
rv = client.post(
"/v1.0/workflow-specification",
content_type="application/json",
data=json.dumps(ProcessModelInfoSchema().dump(spec)),
headers=logged_in_headers(user),
)
assert rv.status_code == 200
fs_spec = process_model_service.get_spec('make_cookies')
assert(spec.display_name == fs_spec.display_name)
assert(0 == fs_spec.display_order)
assert(1 == len(process_model_service.get_process_groups()))
fs_spec = process_model_service.get_spec("make_cookies")
assert spec.display_name == fs_spec.display_name
assert 0 == fs_spec.display_order
assert 1 == len(process_model_service.get_process_groups())
def create_spec_file(app, client: FlaskClient):
"""Test_create_spec_file."""
spec = load_test_spec(app, 'random_fact')
data = {'file': (io.BytesIO(b"abcdef"), 'random_fact.svg')}
spec = load_test_spec(app, "random_fact")
data = {"file": (io.BytesIO(b"abcdef"), "random_fact.svg")}
user = find_or_create_user()
rv = client.post('/v1.0/workflow-specification/%s/file' % spec.id, data=data, follow_redirects=True,
content_type='multipart/form-data', headers=logged_in_headers(user))
rv = client.post(
"/v1.0/workflow-specification/%s/file" % spec.id,
data=data,
follow_redirects=True,
content_type="multipart/form-data",
headers=logged_in_headers(user),
)
assert rv.status_code == 200
assert(rv.get_data() is not None)
assert rv.get_data() is not None
file = json.loads(rv.get_data(as_text=True))
assert(FileType.svg.value == file['type'])
assert("image/svg+xml" == file['content_type'])
assert FileType.svg.value == file["type"]
assert "image/svg+xml" == file["content_type"]
rv = client.get(f'/v1.0/workflow-specification/{spec.id}/file/random_fact.svg', headers=logged_in_headers(user))
rv = client.get(
f"/v1.0/workflow-specification/{spec.id}/file/random_fact.svg",
headers=logged_in_headers(user),
)
assert rv.status_code == 200
file2 = json.loads(rv.get_data(as_text=True))
assert(file == file2)
assert file == file2

View File

@ -9,6 +9,7 @@ from spiff_workflow_webapp.models.user import UserModel
def test_acceptance(client: FlaskClient) -> None:
"""Test_acceptance."""
# Create a user U
user = create_user(client, "U")
# Create a group G
@ -30,6 +31,7 @@ def test_acceptance(client: FlaskClient) -> None:
def test_user_can_be_created_and_deleted(client: FlaskClient) -> None:
"""Test_user_can_be_created_and_deleted."""
username = "joe"
response = client.get(f"/user/{username}")
assert response.status_code == 201
@ -43,12 +45,14 @@ def test_user_can_be_created_and_deleted(client: FlaskClient) -> None:
def test_delete_returns_an_error_if_user_is_not_found(client: FlaskClient) -> None:
"""Test_delete_returns_an_error_if_user_is_not_found."""
username = "joe"
response = client.delete(f"/user/{username}")
assert response.status_code == 400
def test_create_returns_an_error_if_user_exists(client: FlaskClient) -> None:
"""Test_create_returns_an_error_if_user_exists."""
username = "joe"
response = client.get(f"/user/{username}")
assert response.status_code == 201
@ -65,6 +69,7 @@ def test_create_returns_an_error_if_user_exists(client: FlaskClient) -> None:
def test_group_can_be_created_and_deleted(client: FlaskClient) -> None:
"""Test_group_can_be_created_and_deleted."""
group_name = "administrators"
response = client.get(f"/group/{group_name}")
assert response.status_code == 201
@ -78,12 +83,14 @@ def test_group_can_be_created_and_deleted(client: FlaskClient) -> None:
def test_delete_returns_an_error_if_group_is_not_found(client: FlaskClient) -> None:
"""Test_delete_returns_an_error_if_group_is_not_found."""
group_name = "administrators"
response = client.delete(f"/group/{group_name}")
assert response.status_code == 400
def test_create_returns_an_error_if_group_exists(client: FlaskClient) -> None:
"""Test_create_returns_an_error_if_group_exists."""
group_name = "administrators"
response = client.get(f"/group/{group_name}")
assert response.status_code == 201
@ -100,6 +107,7 @@ def test_create_returns_an_error_if_group_exists(client: FlaskClient) -> None:
def test_user_can_be_assigned_to_a_group(client: FlaskClient) -> None:
"""Test_user_can_be_assigned_to_a_group."""
user = create_user(client, "joe")
group = create_group(client, "administrators")
assign_user_to_group(client, user, group)
@ -108,6 +116,7 @@ def test_user_can_be_assigned_to_a_group(client: FlaskClient) -> None:
def test_user_can_be_removed_from_a_group(client: FlaskClient) -> None:
"""Test_user_can_be_removed_from_a_group."""
user = create_user(client, "joe")
group = create_group(client, "administrators")
assign_user_to_group(client, user, group)
@ -117,6 +126,7 @@ def test_user_can_be_removed_from_a_group(client: FlaskClient) -> None:
def create_user(client: FlaskClient, username: str) -> Any:
"""Create_user."""
response = client.get(f"/user/{username}")
assert response.status_code == 201
user = UserModel.query.filter_by(username=username).first()
@ -125,6 +135,7 @@ def create_user(client: FlaskClient, username: str) -> Any:
def delete_user(client: FlaskClient, username: str) -> None:
"""Delete_user."""
response = client.delete(f"/user/{username}")
assert response.status_code == 204
user = UserModel.query.filter_by(username=username).first()
@ -132,6 +143,7 @@ def delete_user(client: FlaskClient, username: str) -> None:
def create_group(client: FlaskClient, group_name: str) -> Any:
"""Create_group."""
response = client.get(f"/group/{group_name}")
assert response.status_code == 201
group = GroupModel.query.filter_by(name=group_name).first()
@ -140,6 +152,7 @@ def create_group(client: FlaskClient, group_name: str) -> Any:
def delete_group(client: FlaskClient, group_name: str) -> None:
"""Delete_group."""
response = client.delete(f"/group/{group_name}")
assert response.status_code == 204
group = GroupModel.query.filter_by(name=group_name).first()
@ -149,6 +162,7 @@ def delete_group(client: FlaskClient, group_name: str) -> None:
def assign_user_to_group(
client: FlaskClient, user: UserModel, group: GroupModel
) -> None:
"""Assign_user_to_group."""
response = client.post(
"/assign_user_to_group",
content_type="application/json",
@ -163,6 +177,7 @@ def assign_user_to_group(
def remove_user_from_group(
client: FlaskClient, user: UserModel, group: GroupModel
) -> None:
"""Remove_user_from_group."""
response = client.post(
"remove_user_from_group",
content_type="application/json",