some auto linting fixes w/ burnettk
This commit is contained in:
parent
d63dfe1f21
commit
2c4f2adeeb
|
@ -10,4 +10,4 @@
|
|||
/src/*.egg-info/
|
||||
__pycache__/
|
||||
*.sqlite3
|
||||
node_modules
|
||||
node_modules
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from __future__ import with_statement
|
||||
|
||||
import logging
|
||||
from logging.config import fileConfig
|
||||
|
||||
|
|
|
@ -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}")
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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="")
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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 = []
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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}'",
|
||||
)
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue