Merge branch 'dev' into feature/approval_request_script
This commit is contained in:
commit
0ba2819867
37
crc/api.yml
37
crc/api.yml
|
@ -82,7 +82,7 @@ paths:
|
|||
# /v1.0/study
|
||||
/study:
|
||||
get:
|
||||
operationId: crc.api.study.all_studies
|
||||
operationId: crc.api.study.user_studies
|
||||
summary: Provides a list of studies related to the current user.
|
||||
tags:
|
||||
- Studies
|
||||
|
@ -109,11 +109,13 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Study"
|
||||
/study-files:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Study"
|
||||
/study/all:
|
||||
get:
|
||||
operationId: crc.api.study.all_studies_and_files
|
||||
summary: Provides a list of studies with submitted files
|
||||
operationId: crc.api.study.all_studies
|
||||
summary: Provides a list of studies
|
||||
tags:
|
||||
- Studies
|
||||
responses:
|
||||
|
@ -122,7 +124,9 @@ paths:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Study"
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Study"
|
||||
/study/{study_id}:
|
||||
parameters:
|
||||
- name: study_id
|
||||
|
@ -353,24 +357,12 @@ paths:
|
|||
description: The unique id of a workflow specification
|
||||
schema:
|
||||
type: string
|
||||
- name: study_id
|
||||
in: query
|
||||
required: false
|
||||
description: The unique id of a study
|
||||
schema:
|
||||
type: integer
|
||||
- name: workflow_id
|
||||
in: query
|
||||
required: false
|
||||
description: The unique id of a workflow instance
|
||||
schema:
|
||||
type: integer
|
||||
- name: task_id
|
||||
in: query
|
||||
required: false
|
||||
description: The unique id of a workflow task
|
||||
schema:
|
||||
type: string
|
||||
- name: form_field_key
|
||||
in: query
|
||||
required: false
|
||||
|
@ -680,7 +672,7 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Workflow"
|
||||
/workflow/{workflow_id}/task/{task_id}/lookup/{field_id}:
|
||||
/workflow/{workflow_id}/lookup/{field_id}:
|
||||
parameters:
|
||||
- name: workflow_id
|
||||
in: path
|
||||
|
@ -689,13 +681,6 @@ paths:
|
|||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
- name: task_id
|
||||
in: path
|
||||
required: true
|
||||
description: The id of the task
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
- name: field_id
|
||||
in: path
|
||||
required: true
|
||||
|
|
|
@ -1,51 +1,58 @@
|
|||
import io
|
||||
from typing import List
|
||||
|
||||
import connexion
|
||||
from flask import send_file
|
||||
|
||||
from crc import session
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileModelSchema, FileModel, FileDataModel
|
||||
from crc.models.file import FileSchema, FileModel, File, FileModelSchema
|
||||
from crc.models.workflow import WorkflowSpecModel
|
||||
from crc.services.file_service import FileService
|
||||
|
||||
|
||||
def get_files(workflow_spec_id=None, study_id=None, workflow_id=None, task_id=None, form_field_key=None):
|
||||
if all(v is None for v in [workflow_spec_id, study_id, workflow_id, task_id, form_field_key]):
|
||||
raise ApiError('missing_parameter',
|
||||
'Please specify at least one of workflow_spec_id, study_id, '
|
||||
'workflow_id, and task_id for this file in the HTTP parameters')
|
||||
def to_file_api(file_model):
|
||||
"""Converts a FileModel object to something we can return via the aip"""
|
||||
return File.from_models(file_model, FileService.get_file_data(file_model.id))
|
||||
|
||||
results = FileService.get_files(workflow_spec_id, study_id, workflow_id, task_id, form_field_key)
|
||||
return FileModelSchema(many=True).dump(results)
|
||||
|
||||
def get_files(workflow_spec_id=None, workflow_id=None, form_field_key=None):
|
||||
if all(v is None for v in [workflow_spec_id, workflow_id, form_field_key]):
|
||||
raise ApiError('missing_parameter',
|
||||
'Please specify either a workflow_spec_id or a '
|
||||
'workflow_id with an optional form_field_key')
|
||||
|
||||
file_models = FileService.get_files(workflow_spec_id=workflow_spec_id,
|
||||
workflow_id=workflow_id,
|
||||
irb_doc_code=form_field_key)
|
||||
|
||||
files = (to_file_api(model) for model in file_models)
|
||||
return FileSchema(many=True).dump(files)
|
||||
|
||||
|
||||
def get_reference_files():
|
||||
results = FileService.get_files(is_reference=True)
|
||||
return FileModelSchema(many=True).dump(results)
|
||||
files = (to_file_api(model) for model in results)
|
||||
return FileSchema(many=True).dump(files)
|
||||
|
||||
|
||||
def add_file(workflow_spec_id=None, study_id=None, workflow_id=None, task_id=None, form_field_key=None):
|
||||
all_none = all(v is None for v in [workflow_spec_id, study_id, workflow_id, task_id, form_field_key])
|
||||
missing_some = (workflow_spec_id is None) and (None in [study_id, workflow_id, form_field_key])
|
||||
if all_none or missing_some:
|
||||
raise ApiError('missing_parameter',
|
||||
'Please specify either a workflow_spec_id or all 3 of study_id, '
|
||||
'workflow_id, and field_id for this file in the HTTP parameters')
|
||||
if 'file' not in connexion.request.files:
|
||||
raise ApiError('invalid_file',
|
||||
'Expected a file named "file" in the multipart form request')
|
||||
|
||||
def add_file(workflow_spec_id=None, workflow_id=None, form_field_key=None):
|
||||
file = connexion.request.files['file']
|
||||
if workflow_spec_id:
|
||||
if workflow_id:
|
||||
if form_field_key is None:
|
||||
raise ApiError('invalid_workflow_file',
|
||||
'When adding a workflow related file, you must specify a form_field_key')
|
||||
file_model = FileService.add_workflow_file(workflow_id=workflow_id, irb_doc_code=form_field_key,
|
||||
name=file.filename, content_type=file.content_type,
|
||||
binary_data=file.stream.read())
|
||||
elif workflow_spec_id:
|
||||
workflow_spec = session.query(WorkflowSpecModel).filter_by(id=workflow_spec_id).first()
|
||||
file_model = FileService.add_workflow_spec_file(workflow_spec, file.filename, file.content_type,
|
||||
file.stream.read())
|
||||
else:
|
||||
file_model = FileService.add_form_field_file(study_id, workflow_id, task_id, form_field_key, file.filename,
|
||||
file.content_type, file.stream.read())
|
||||
raise ApiError("invalid_file", "You must supply either a workflow spec id or a workflow_id and form_field_key.")
|
||||
|
||||
return FileModelSchema().dump(file_model)
|
||||
return FileSchema().dump(to_file_api(file_model))
|
||||
|
||||
|
||||
def get_reference_file(name):
|
||||
|
@ -80,7 +87,7 @@ def set_reference_file(name):
|
|||
file_model = file_models[0]
|
||||
FileService.update_file(file_models[0], file.stream.read(), file.content_type)
|
||||
|
||||
return FileModelSchema().dump(file_model)
|
||||
return FileSchema().dump(to_file_api(file_model))
|
||||
|
||||
|
||||
def update_file_data(file_id):
|
||||
|
@ -89,7 +96,7 @@ def update_file_data(file_id):
|
|||
if file_model is None:
|
||||
raise ApiError('no_such_file', 'The file id you provided does not exist')
|
||||
file_model = FileService.update_file(file_model, file.stream.read(), file.content_type)
|
||||
return FileModelSchema().dump(file_model)
|
||||
return FileSchema().dump(to_file_api(file_model))
|
||||
|
||||
|
||||
def get_file_data(file_id, version=None):
|
||||
|
@ -101,7 +108,7 @@ def get_file_data(file_id, version=None):
|
|||
attachment_filename=file_data.file_model.name,
|
||||
mimetype=file_data.file_model.content_type,
|
||||
cache_timeout=-1, # Don't cache these files on the browser.
|
||||
last_modified=file_data.last_updated
|
||||
last_modified=file_data.date_created
|
||||
)
|
||||
|
||||
|
||||
|
@ -109,7 +116,7 @@ def get_file_info(file_id):
|
|||
file_model = session.query(FileModel).filter_by(id=file_id).with_for_update().first()
|
||||
if file_model is None:
|
||||
raise ApiError('no_such_file', 'The file id you provided does not exist', status_code=404)
|
||||
return FileModelSchema().dump(file_model)
|
||||
return FileSchema().dump(to_file_api(file_model))
|
||||
|
||||
|
||||
def update_file_info(file_id, body):
|
||||
|
@ -124,7 +131,7 @@ def update_file_info(file_id, body):
|
|||
file_model = FileModelSchema().load(body, session=session)
|
||||
session.add(file_model)
|
||||
session.commit()
|
||||
return FileModelSchema().dump(file_model)
|
||||
return FileSchema().dump(to_file_api(file_model))
|
||||
|
||||
|
||||
def delete_file(file_id):
|
||||
|
|
|
@ -6,7 +6,7 @@ from sqlalchemy.exc import IntegrityError
|
|||
from crc import session
|
||||
from crc.api.common import ApiError, ApiErrorSchema
|
||||
from crc.models.protocol_builder import ProtocolBuilderStatus
|
||||
from crc.models.study import StudySchema, StudyFilesSchema, StudyModel, Study
|
||||
from crc.models.study import StudySchema, StudyModel, Study
|
||||
from crc.services.study_service import StudyService
|
||||
|
||||
|
||||
|
@ -65,7 +65,7 @@ def delete_study(study_id):
|
|||
raise ApiError(code="study_integrity_error", message=message)
|
||||
|
||||
|
||||
def all_studies():
|
||||
def user_studies():
|
||||
"""Returns all the studies associated with the current user. """
|
||||
StudyService.synch_with_protocol_builder_if_enabled(g.user)
|
||||
studies = StudyService.get_studies_for_user(g.user)
|
||||
|
@ -73,8 +73,8 @@ def all_studies():
|
|||
return results
|
||||
|
||||
|
||||
def all_studies_and_files():
|
||||
"""Returns all studies with submitted files"""
|
||||
studies = StudyService.get_studies_with_files()
|
||||
results = StudyFilesSchema(many=True).dump(studies)
|
||||
def all_studies():
|
||||
"""Returns all studies (regardless of user) with submitted files"""
|
||||
studies = StudyService.get_all_studies_with_files()
|
||||
results = StudySchema(many=True).dump(studies)
|
||||
return results
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import json
|
||||
|
||||
import connexion
|
||||
import flask
|
||||
from flask import redirect, g, request
|
||||
|
||||
from crc import app, db
|
||||
|
@ -109,8 +110,11 @@ def _handle_login(user_info: LdapUserInfo, redirect_url=app.config['FRONTEND_AUT
|
|||
# Return the frontend auth callback URL, with auth token appended.
|
||||
auth_token = user.encode_auth_token().decode()
|
||||
if redirect_url is not None:
|
||||
app.logger.info("SSO_LOGIN: REDIRECTING TO: " + redirect_url)
|
||||
return redirect('%s/%s' % (redirect_url, auth_token))
|
||||
if redirect_url.find("http://") != 0 and redirect_url.find("https://") != 0:
|
||||
redirect_url = "http://" + redirect_url
|
||||
url = '%s?token=%s' % (redirect_url, auth_token)
|
||||
app.logger.info("SSO_LOGIN: REDIRECTING TO: " + url)
|
||||
return flask.redirect(url, code=302)
|
||||
else:
|
||||
app.logger.info("SSO_LOGIN: NO REDIRECT, JUST RETURNING AUTH TOKEN.")
|
||||
return auth_token
|
||||
|
|
|
@ -118,8 +118,8 @@ def __get_workflow_api_model(processor: WorkflowProcessor, next_task = None):
|
|||
next_task=None,
|
||||
navigation=navigation,
|
||||
workflow_spec_id=processor.workflow_spec_id,
|
||||
spec_version=processor.get_spec_version(),
|
||||
is_latest_spec=processor.get_spec_version() == processor.get_latest_version_string(processor.workflow_spec_id),
|
||||
spec_version=processor.get_version_string(),
|
||||
is_latest_spec=processor.is_latest_spec,
|
||||
total_tasks=processor.workflow_model.total_tasks,
|
||||
completed_tasks=processor.workflow_model.completed_tasks,
|
||||
last_updated=processor.workflow_model.last_updated
|
||||
|
@ -219,26 +219,13 @@ def delete_workflow_spec_category(cat_id):
|
|||
session.commit()
|
||||
|
||||
|
||||
def lookup(workflow_id, task_id, field_id, query, limit):
|
||||
def lookup(workflow_id, field_id, query, limit):
|
||||
"""
|
||||
given a field in a task, attempts to find the lookup table or function associated
|
||||
with that field and runs a full-text query against it to locate the values and
|
||||
labels that would be returned to a type-ahead box.
|
||||
Tries to be fast, but first runs will be very slow.
|
||||
"""
|
||||
workflow_model = session.query(WorkflowModel).filter_by(id=workflow_id).first()
|
||||
if not workflow_model:
|
||||
raise ApiError("unknown_workflow", "No workflow found with id: %i" % workflow_id)
|
||||
processor = WorkflowProcessor(workflow_model)
|
||||
task_id = uuid.UUID(task_id)
|
||||
spiff_task = processor.bpmn_workflow.get_task(task_id)
|
||||
if not spiff_task:
|
||||
raise ApiError("unknown_task", "No task with %s found in workflow: %i" % (task_id, workflow_id))
|
||||
field = None
|
||||
for f in spiff_task.task_spec.form.fields:
|
||||
if f.id == field_id:
|
||||
field = f
|
||||
if not field:
|
||||
raise ApiError("unknown_field", "No field named %s in task %s" % (task_id, spiff_task.task_spec.name))
|
||||
|
||||
lookup_data = LookupService.lookup(spiff_task, field, query, limit)
|
||||
workflow = session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
|
||||
lookup_data = LookupService.lookup(workflow, field_id, query, limit)
|
||||
return LookupDataSchema(many=True).dump(lookup_data)
|
|
@ -1,14 +1,13 @@
|
|||
import enum
|
||||
import marshmallow
|
||||
|
||||
import marshmallow
|
||||
from ldap3.core.exceptions import LDAPSocketOpenError
|
||||
from marshmallow import INCLUDE
|
||||
from sqlalchemy import func
|
||||
|
||||
from ldap3.core.exceptions import LDAPSocketOpenError
|
||||
|
||||
from crc import db, ma
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileModel
|
||||
from crc.models.file import FileDataModel
|
||||
from crc.models.study import StudyModel
|
||||
from crc.models.workflow import WorkflowModel
|
||||
from crc.services.ldap_service import LdapService
|
||||
|
@ -22,13 +21,11 @@ class ApprovalStatus(enum.Enum):
|
|||
|
||||
|
||||
class ApprovalFile(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
file_id = db.Column(db.Integer, db.ForeignKey(FileModel.id), nullable=False)
|
||||
approval_id = db.Column(db.Integer, db.ForeignKey("approval.id"), nullable=False)
|
||||
file_version = db.Column(db.Integer, nullable=False)
|
||||
file_data_id = db.Column(db.Integer, db.ForeignKey(FileDataModel.id), primary_key=True)
|
||||
approval_id = db.Column(db.Integer, db.ForeignKey("approval.id"), primary_key=True)
|
||||
|
||||
approval = db.relationship("ApprovalModel")
|
||||
file = db.relationship(FileModel)
|
||||
file_data = db.relationship(FileDataModel)
|
||||
|
||||
|
||||
class ApprovalModel(db.Model):
|
||||
|
@ -43,9 +40,9 @@ class ApprovalModel(db.Model):
|
|||
message = db.Column(db.String, default='')
|
||||
date_created = db.Column(db.DateTime(timezone=True), default=func.now())
|
||||
version = db.Column(db.Integer) # Incremented integer, so 1,2,3 as requests are made.
|
||||
workflow_hash = db.Column(db.String) # A hash of the workflow at the moment the approval was created.
|
||||
|
||||
approval_files = db.relationship(ApprovalFile, back_populates="approval")
|
||||
approval_files = db.relationship(ApprovalFile, back_populates="approval",
|
||||
cascade="all, delete, delete-orphan",
|
||||
order_by=ApprovalFile.file_data_id)
|
||||
|
||||
|
||||
class Approval(object):
|
||||
|
@ -66,7 +63,6 @@ class Approval(object):
|
|||
instance.message = model.message
|
||||
instance.date_created = model.date_created
|
||||
instance.version = model.version
|
||||
instance.workflow_hash = model.workflow_hash
|
||||
instance.title = ''
|
||||
if model.study:
|
||||
instance.title = model.study.title
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import enum
|
||||
from typing import cast
|
||||
|
||||
from marshmallow import INCLUDE, EXCLUDE
|
||||
from marshmallow_enum import EnumField
|
||||
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
||||
from sqlalchemy import func, Index
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import deferred
|
||||
|
||||
from crc import db, ma
|
||||
|
||||
|
||||
class FileType(enum.Enum):
|
||||
bpmn = "bpmm"
|
||||
bpmn = "bpmn"
|
||||
csv = 'csv'
|
||||
dmn = "dmn"
|
||||
doc = "doc"
|
||||
|
@ -55,15 +57,16 @@ CONTENT_TYPES = {
|
|||
"zip": "application/zip"
|
||||
}
|
||||
|
||||
|
||||
class FileDataModel(db.Model):
|
||||
__tablename__ = 'file_data'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
md5_hash = db.Column(UUID(as_uuid=True), unique=False, nullable=False)
|
||||
data = db.Column(db.LargeBinary)
|
||||
data = deferred(db.Column(db.LargeBinary)) # Don't load it unless you have to.
|
||||
version = db.Column(db.Integer, default=0)
|
||||
last_updated = db.Column(db.DateTime(timezone=True), default=func.now())
|
||||
date_created = db.Column(db.DateTime(timezone=True), default=func.now())
|
||||
file_model_id = db.Column(db.Integer, db.ForeignKey('file.id'))
|
||||
file_model = db.relationship("FileModel")
|
||||
file_model = db.relationship("FileModel", foreign_keys=[file_model_id])
|
||||
|
||||
|
||||
class FileModel(db.Model):
|
||||
|
@ -78,13 +81,31 @@ class FileModel(db.Model):
|
|||
primary_process_id = db.Column(db.String, nullable=True) # An id in the xml of BPMN documents, critical for primary BPMN.
|
||||
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'), nullable=True)
|
||||
workflow_id = db.Column(db.Integer, db.ForeignKey('workflow.id'), nullable=True)
|
||||
study_id = db.Column(db.Integer, db.ForeignKey('study.id'), nullable=True)
|
||||
task_id = db.Column(db.String, nullable=True)
|
||||
irb_doc_code = db.Column(db.String, nullable=True) # Code reference to the irb_documents.xlsx reference file.
|
||||
form_field_key = db.Column(db.String, nullable=True)
|
||||
latest_version = db.Column(db.Integer, default=0)
|
||||
|
||||
|
||||
class File(object):
|
||||
@classmethod
|
||||
def from_models(cls, model: FileModel, data_model: FileDataModel):
|
||||
instance = cls()
|
||||
instance.id = model.id
|
||||
instance.name = model.name
|
||||
instance.is_status = model.is_status
|
||||
instance.is_reference = model.is_reference
|
||||
instance.content_type = model.content_type
|
||||
instance.primary = model.primary
|
||||
instance.primary_process_id = model.primary_process_id
|
||||
instance.workflow_spec_id = model.workflow_spec_id
|
||||
instance.workflow_id = model.workflow_id
|
||||
instance.irb_doc_code = model.irb_doc_code
|
||||
instance.type = model.type
|
||||
if data_model:
|
||||
instance.last_modified = data_model.date_created
|
||||
instance.latest_version = data_model.version
|
||||
else:
|
||||
instance.last_modified = None
|
||||
instance.latest_version = None
|
||||
return instance
|
||||
|
||||
class FileModelSchema(SQLAlchemyAutoSchema):
|
||||
class Meta:
|
||||
|
@ -92,29 +113,37 @@ class FileModelSchema(SQLAlchemyAutoSchema):
|
|||
load_instance = True
|
||||
include_relationships = True
|
||||
include_fk = True # Includes foreign keys
|
||||
unknown = EXCLUDE
|
||||
type = EnumField(FileType)
|
||||
|
||||
|
||||
class FileSchema(ma.Schema):
|
||||
class Meta:
|
||||
model = File
|
||||
fields = ["id", "name", "is_status", "is_reference", "content_type",
|
||||
"primary", "primary_process_id", "workflow_spec_id", "workflow_id",
|
||||
"irb_doc_code", "last_modified", "latest_version", "type"]
|
||||
unknown = INCLUDE
|
||||
type = EnumField(FileType)
|
||||
|
||||
|
||||
class LookupFileModel(db.Model):
|
||||
"""Takes the content of a file (like a xlsx, or csv file) and creates a key/value
|
||||
store that can be used for lookups and searches. This table contains the metadata,
|
||||
so we know the version of the file that was used, and what key column, and value column
|
||||
were used to generate this lookup table. ie, the same xls file might have multiple
|
||||
lookup file models, if different keys and labels are used - or someone decides to
|
||||
make a change. We need to handle full text search over the label and value columns,
|
||||
and not every column, because we don't know how much information will be in there. """
|
||||
"""Gives us a quick way to tell what kind of lookup is set on a form field.
|
||||
Connected to the file data model, so that if a new version of the same file is
|
||||
created, we can update the listing."""
|
||||
#fixme: What happens if they change the file associated with a lookup field?
|
||||
__tablename__ = 'lookup_file'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
label_column = db.Column(db.String)
|
||||
value_column = db.Column(db.String)
|
||||
workflow_spec_id = db.Column(db.String)
|
||||
field_id = db.Column(db.String)
|
||||
is_ldap = db.Column(db.Boolean) # Allows us to run an ldap query instead of a db lookup.
|
||||
file_data_model_id = db.Column(db.Integer, db.ForeignKey('file_data.id'))
|
||||
|
||||
dependencies = db.relationship("LookupDataModel", lazy="select", backref="lookup_file_model", cascade="all, delete, delete-orphan")
|
||||
|
||||
class LookupDataModel(db.Model):
|
||||
__tablename__ = 'lookup_data'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
lookup_file_model_id = db.Column(db.Integer, db.ForeignKey('lookup_file.id'))
|
||||
lookup_file_model = db.relationship(LookupFileModel)
|
||||
value = db.Column(db.String)
|
||||
label = db.Column(db.String)
|
||||
# In the future, we might allow adding an additional "search" column if we want to search things not in label.
|
||||
|
|
|
@ -40,10 +40,6 @@ class StudyModel(db.Model):
|
|||
if self.on_hold:
|
||||
self.protocol_builder_status = ProtocolBuilderStatus.HOLD
|
||||
|
||||
def files(self):
|
||||
_files = FileModel.query.filter_by(workflow_id=self.workflow[0].id)
|
||||
return _files
|
||||
|
||||
|
||||
class WorkflowMetadata(object):
|
||||
def __init__(self, id, name, display_name, description, spec_version, category_id, state: WorkflowState, status: WorkflowStatus,
|
||||
|
@ -68,7 +64,7 @@ class WorkflowMetadata(object):
|
|||
name=workflow.workflow_spec.name,
|
||||
display_name=workflow.workflow_spec.display_name,
|
||||
description=workflow.workflow_spec.description,
|
||||
spec_version=workflow.spec_version,
|
||||
spec_version=workflow.spec_version(),
|
||||
category_id=workflow.workflow_spec.category_id,
|
||||
state=WorkflowState.optional,
|
||||
status=workflow.status,
|
||||
|
@ -122,7 +118,7 @@ class Study(object):
|
|||
self.ind_number = ind_number
|
||||
self.categories = categories
|
||||
self.warnings = []
|
||||
|
||||
self.files = []
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, study_model: StudyModel):
|
||||
|
@ -153,6 +149,7 @@ class StudySchema(ma.Schema):
|
|||
hsr_number = fields.String(allow_none=True)
|
||||
sponsor = fields.String(allow_none=True)
|
||||
ind_number = fields.String(allow_none=True)
|
||||
files = fields.List(fields.Nested(SimpleFileSchema), dump_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Study
|
||||
|
@ -165,14 +162,3 @@ class StudySchema(ma.Schema):
|
|||
"""Can load the basic study data for updates to the database, but categories are write only"""
|
||||
return Study(**data)
|
||||
|
||||
|
||||
class StudyFilesSchema(ma.Schema):
|
||||
|
||||
files = fields.Method('_files')
|
||||
|
||||
class Meta:
|
||||
model = Study
|
||||
additional = ["id", "title", "last_updated", "primary_investigator_id"]
|
||||
|
||||
def _files(self, obj):
|
||||
return [file.name for file in obj.files()]
|
||||
|
|
|
@ -5,6 +5,7 @@ from marshmallow import EXCLUDE
|
|||
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
|
||||
|
||||
from crc import db
|
||||
from crc.models.file import FileModel, FileDataModel
|
||||
|
||||
|
||||
class WorkflowSpecCategoryModel(db.Model):
|
||||
|
@ -67,6 +68,14 @@ class WorkflowStatus(enum.Enum):
|
|||
complete = "complete"
|
||||
|
||||
|
||||
class WorkflowSpecDependencyFile(db.Model):
|
||||
"""Connects a workflow to the version of the specification files it depends on to execute"""
|
||||
file_data_id = db.Column(db.Integer, db.ForeignKey(FileDataModel.id), primary_key=True)
|
||||
workflow_id = db.Column(db.Integer, db.ForeignKey("workflow.id"), primary_key=True)
|
||||
|
||||
file_data = db.relationship(FileDataModel)
|
||||
|
||||
|
||||
class WorkflowModel(db.Model):
|
||||
__tablename__ = 'workflow'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
@ -76,7 +85,13 @@ class WorkflowModel(db.Model):
|
|||
study = db.relationship("StudyModel", backref='workflow')
|
||||
workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id'))
|
||||
workflow_spec = db.relationship("WorkflowSpecModel")
|
||||
spec_version = db.Column(db.String)
|
||||
total_tasks = db.Column(db.Integer, default=0)
|
||||
completed_tasks = db.Column(db.Integer, default=0)
|
||||
last_updated = db.Column(db.DateTime)
|
||||
# Order By is important or generating hashes on reviews.
|
||||
dependencies = db.relationship(WorkflowSpecDependencyFile, cascade="all, delete, delete-orphan",
|
||||
order_by="WorkflowSpecDependencyFile.file_data_id")
|
||||
|
||||
def spec_version(self):
|
||||
dep_ids = list(dep.file_data_id for dep in self.dependencies)
|
||||
return "-".join(str(dep_ids))
|
||||
|
|
|
@ -36,14 +36,11 @@ Takes two arguments:
|
|||
final_document_stream = self.process_template(task, study_id, workflow, *args, **kwargs)
|
||||
file_name = args[0]
|
||||
irb_doc_code = args[1]
|
||||
FileService.add_task_file(study_id=study_id,
|
||||
workflow_id=workflow_id,
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
task_id=task.id,
|
||||
name=file_name,
|
||||
content_type=CONTENT_TYPES['docx'],
|
||||
binary_data=final_document_stream.read(),
|
||||
irb_doc_code=irb_doc_code)
|
||||
FileService.add_workflow_file(workflow_id=workflow_id,
|
||||
name=file_name,
|
||||
content_type=CONTENT_TYPES['docx'],
|
||||
binary_data=final_document_stream.read(),
|
||||
irb_doc_code=irb_doc_code)
|
||||
|
||||
def process_template(self, task, study_id, workflow=None, *args, **kwargs):
|
||||
"""Entry point, mostly worried about wiring it all up."""
|
||||
|
@ -62,13 +59,13 @@ Takes two arguments:
|
|||
|
||||
file_data_model = None
|
||||
if workflow is not None:
|
||||
# Get the workflow's latest files
|
||||
joined_file_data_models = WorkflowProcessor\
|
||||
.get_file_models_for_version(workflow.workflow_spec_id, workflow.spec_version)
|
||||
|
||||
for joined_file_data in joined_file_data_models:
|
||||
if joined_file_data.file_model.name == file_name:
|
||||
file_data_model = session.query(FileDataModel).filter_by(id=joined_file_data.id).first()
|
||||
# Get the workflow specification file with the given name.
|
||||
file_data_models = FileService.get_spec_data_files(
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
workflow_id=workflow.id)
|
||||
for file_data in file_data_models:
|
||||
if file_data.file_model.name == file_name:
|
||||
file_data_model = file_data
|
||||
|
||||
if workflow is None or file_data_model is None:
|
||||
file_data_model = FileService.get_workflow_file_data(task.workflow, file_name)
|
||||
|
|
|
@ -3,8 +3,10 @@ from datetime import datetime
|
|||
from sqlalchemy import desc
|
||||
|
||||
from crc import db, session
|
||||
from crc.api.common import ApiError
|
||||
|
||||
from crc.models.approval import ApprovalModel, ApprovalStatus, ApprovalFile
|
||||
from crc.models.workflow import WorkflowModel
|
||||
from crc.services.file_service import FileService
|
||||
|
||||
|
||||
|
@ -51,15 +53,22 @@ class ApprovalService(object):
|
|||
|
||||
# Construct as hash of the latest files to see if things have changed since
|
||||
# the last approval.
|
||||
latest_files = FileService.get_workflow_files(workflow_id)
|
||||
current_workflow_hash = ApprovalService._generate_workflow_hash(latest_files)
|
||||
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
|
||||
workflow_data_files = FileService.get_workflow_data_files(workflow_id)
|
||||
current_data_file_ids = list(data_file.id for data_file in workflow_data_files)
|
||||
|
||||
if len(current_data_file_ids) == 0:
|
||||
raise ApiError("invalid_workflow_approval", "You can't create an approval for a workflow that has"
|
||||
"no files to approve in it.")
|
||||
|
||||
# If an existing approval request exists and no changes were made, do nothing.
|
||||
# If there is an existing approval request for a previous version of the workflow
|
||||
# then add a new request, and cancel any waiting/pending requests.
|
||||
if latest_approval_request:
|
||||
# We could just compare the ApprovalFile lists here and do away with this hash.
|
||||
if latest_approval_request.workflow_hash == current_workflow_hash:
|
||||
request_file_ids = list(file.file_data_id for file in latest_approval_request.approval_files)
|
||||
current_data_file_ids.sort()
|
||||
request_file_ids.sort()
|
||||
if current_data_file_ids == request_file_ids:
|
||||
return # This approval already exists.
|
||||
else:
|
||||
latest_approval_request.status = ApprovalStatus.CANCELED.value
|
||||
|
@ -71,27 +80,18 @@ class ApprovalService(object):
|
|||
model = ApprovalModel(study_id=study_id, workflow_id=workflow_id,
|
||||
approver_uid=approver_uid, status=ApprovalStatus.WAITING.value,
|
||||
message="", date_created=datetime.now(),
|
||||
version=version, workflow_hash=current_workflow_hash)
|
||||
approval_files = ApprovalService._create_approval_files(latest_files, model)
|
||||
version=version)
|
||||
approval_files = ApprovalService._create_approval_files(workflow_data_files, model)
|
||||
db.session.add(model)
|
||||
db.session.add_all(approval_files)
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def _create_approval_files(files, approval):
|
||||
def _create_approval_files(workflow_data_files, approval):
|
||||
"""Currently based exclusively on the status of files associated with a workflow."""
|
||||
file_approval_models = []
|
||||
for file in files:
|
||||
file_approval_models.append(ApprovalFile(file_id=file.id,
|
||||
approval=approval,
|
||||
file_version=file.latest_version))
|
||||
for file_data in workflow_data_files:
|
||||
file_approval_models.append(ApprovalFile(file_data_id=file_data.id,
|
||||
approval=approval))
|
||||
return file_approval_models
|
||||
|
||||
@staticmethod
|
||||
def _generate_workflow_hash(files):
|
||||
"""Currently based exclusively on the status of files associated with a workflow."""
|
||||
version_array = []
|
||||
for file in files:
|
||||
version_array.append(str(file.id) + "[" + str(file.latest_version) + "]")
|
||||
full_version = "-".join(version_array)
|
||||
return full_version
|
||||
|
|
|
@ -5,13 +5,14 @@ from datetime import datetime
|
|||
from uuid import UUID
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException
|
||||
from pandas import ExcelFile
|
||||
from sqlalchemy import desc
|
||||
|
||||
from crc import session
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileType, FileDataModel, FileModel, LookupFileModel, LookupDataModel
|
||||
from crc.models.workflow import WorkflowSpecModel
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecDependencyFile
|
||||
|
||||
|
||||
class FileService(object):
|
||||
|
@ -40,31 +41,27 @@ class FileService(object):
|
|||
return code in df['code'].values
|
||||
|
||||
@staticmethod
|
||||
def add_form_field_file(study_id, workflow_id, task_id, form_field_key, name, content_type, binary_data):
|
||||
"""Create a new file and associate it with a user task form field within a workflow.
|
||||
Please note that the form_field_key MUST be a known file in the irb_documents.xslx reference document."""
|
||||
if not FileService.is_allowed_document(form_field_key):
|
||||
def add_workflow_file(workflow_id, irb_doc_code, name, content_type, binary_data):
|
||||
"""Create a new file and associate it with the workflow
|
||||
Please note that the irb_doc_code MUST be a known file in the irb_documents.xslx reference document."""
|
||||
if not FileService.is_allowed_document(irb_doc_code):
|
||||
raise ApiError("invalid_form_field_key",
|
||||
"When uploading files, the form field id must match a known document in the "
|
||||
"irb_docunents.xslx reference file. This code is not found in that file '%s'" % form_field_key)
|
||||
"irb_docunents.xslx reference file. This code is not found in that file '%s'" % irb_doc_code)
|
||||
|
||||
"""Assure this is unique to the workflow, task, and document code AND the Name
|
||||
Because we will allow users to upload multiple files for the same form field
|
||||
in some cases """
|
||||
file_model = session.query(FileModel)\
|
||||
.filter(FileModel.workflow_id == workflow_id)\
|
||||
.filter(FileModel.task_id == str(task_id))\
|
||||
.filter(FileModel.name == name)\
|
||||
.filter(FileModel.irb_doc_code == form_field_key).first()
|
||||
.filter(FileModel.irb_doc_code == irb_doc_code).first()
|
||||
|
||||
if not file_model:
|
||||
file_model = FileModel(
|
||||
study_id=study_id,
|
||||
workflow_id=workflow_id,
|
||||
task_id=task_id,
|
||||
name=name,
|
||||
form_field_key=form_field_key,
|
||||
irb_doc_code=form_field_key
|
||||
irb_doc_code=irb_doc_code
|
||||
)
|
||||
return FileService.update_file(file_model, binary_data, content_type)
|
||||
|
||||
|
@ -85,28 +82,6 @@ class FileService(object):
|
|||
df = df.set_index(index_column)
|
||||
return json.loads(df.to_json(orient='index'))
|
||||
|
||||
@staticmethod
|
||||
def add_task_file(study_id, workflow_id, workflow_spec_id, task_id, name, content_type, binary_data,
|
||||
irb_doc_code=None):
|
||||
|
||||
"""Assure this is unique to the workflow, task, and document code. Disregard name."""
|
||||
file_model = session.query(FileModel)\
|
||||
.filter(FileModel.workflow_id == workflow_id)\
|
||||
.filter(FileModel.task_id == str(task_id))\
|
||||
.filter(FileModel.irb_doc_code == irb_doc_code).first()
|
||||
|
||||
if not file_model:
|
||||
"""Create a new file and associate it with an executing task within a workflow."""
|
||||
file_model = FileModel(
|
||||
study_id=study_id,
|
||||
workflow_id=workflow_id,
|
||||
workflow_spec_id=workflow_spec_id,
|
||||
task_id=task_id,
|
||||
name=name,
|
||||
irb_doc_code=irb_doc_code
|
||||
)
|
||||
return FileService.update_file(file_model, binary_data, content_type)
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_files(workflow_id):
|
||||
"""Returns all the file models associated with a running workflow."""
|
||||
|
@ -136,12 +111,12 @@ class FileService(object):
|
|||
def update_file(file_model, binary_data, content_type):
|
||||
session.flush() # Assure the database is up-to-date before running this.
|
||||
|
||||
file_data_model = session.query(FileDataModel). \
|
||||
filter_by(file_model_id=file_model.id,
|
||||
version=file_model.latest_version
|
||||
).with_for_update().first()
|
||||
latest_data_model = session.query(FileDataModel). \
|
||||
filter(FileDataModel.file_model_id == file_model.id).\
|
||||
order_by(desc(FileDataModel.date_created)).first()
|
||||
|
||||
md5_checksum = UUID(hashlib.md5(binary_data).hexdigest())
|
||||
if (file_data_model is not None) and (md5_checksum == file_data_model.md5_hash):
|
||||
if (latest_data_model is not None) and (md5_checksum == latest_data_model.md5_hash):
|
||||
# This file does not need to be updated, it's the same file.
|
||||
return file_model
|
||||
|
||||
|
@ -155,22 +130,20 @@ class FileService(object):
|
|||
file_model.type = FileType[file_extension]
|
||||
file_model.content_type = content_type
|
||||
|
||||
if file_data_model is None:
|
||||
if latest_data_model is None:
|
||||
version = 1
|
||||
else:
|
||||
version = file_data_model.version + 1
|
||||
version = latest_data_model.version + 1
|
||||
|
||||
# If this is a BPMN, extract the process id.
|
||||
if file_model.type == FileType.bpmn:
|
||||
bpmn: ElementTree.Element = ElementTree.fromstring(binary_data)
|
||||
file_model.primary_process_id = WorkflowProcessor.get_process_id(bpmn)
|
||||
file_model.primary_process_id = FileService.get_process_id(bpmn)
|
||||
|
||||
file_model.latest_version = version
|
||||
new_file_data_model = FileDataModel(
|
||||
data=binary_data, file_model_id=file_model.id, file_model=file_model,
|
||||
version=version, md5_hash=md5_checksum, last_updated=datetime.now()
|
||||
version=version, md5_hash=md5_checksum, date_created=datetime.now()
|
||||
)
|
||||
|
||||
session.add_all([file_model, new_file_data_model])
|
||||
session.commit()
|
||||
session.flush() # Assure the id is set on the model before returning it.
|
||||
|
@ -178,49 +151,103 @@ class FileService(object):
|
|||
return file_model
|
||||
|
||||
@staticmethod
|
||||
def get_files(workflow_spec_id=None,
|
||||
study_id=None, workflow_id=None, task_id=None, form_field_key=None,
|
||||
def get_process_id(et_root: ElementTree.Element):
|
||||
process_elements = []
|
||||
for child in et_root:
|
||||
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')
|
||||
|
||||
# There are multiple root elements
|
||||
if len(process_elements) > 1:
|
||||
|
||||
# Look for the element that has the startEvent in it
|
||||
for e in process_elements:
|
||||
this_element: ElementTree.Element = e
|
||||
for child_element in list(this_element):
|
||||
if child_element.tag.endswith('startEvent'):
|
||||
return this_element.attrib['id']
|
||||
|
||||
raise ValidationException('No start event found in %s' % et_root.attrib['id'])
|
||||
|
||||
return process_elements[0].attrib['id']
|
||||
|
||||
@staticmethod
|
||||
def get_files_for_study(study_id, irb_doc_code=None):
|
||||
query = session.query(FileModel).\
|
||||
join(WorkflowModel).\
|
||||
filter(WorkflowModel.study_id == study_id)
|
||||
if irb_doc_code:
|
||||
query = query.filter(FileModel.irb_doc_code == irb_doc_code)
|
||||
return query.all()
|
||||
|
||||
@staticmethod
|
||||
def get_files(workflow_spec_id=None, workflow_id=None,
|
||||
name=None, is_reference=False, irb_doc_code=None):
|
||||
query = session.query(FileModel).filter_by(is_reference=is_reference)
|
||||
if workflow_spec_id:
|
||||
query = query.filter_by(workflow_spec_id=workflow_spec_id)
|
||||
if all(v is None for v in [study_id, workflow_id, task_id, form_field_key]):
|
||||
query = query.filter_by(
|
||||
study_id=None,
|
||||
workflow_id=None,
|
||||
task_id=None,
|
||||
form_field_key=None,
|
||||
)
|
||||
else:
|
||||
if study_id:
|
||||
query = query.filter_by(study_id=study_id)
|
||||
if workflow_id:
|
||||
query = query.filter_by(workflow_id=workflow_id)
|
||||
if task_id:
|
||||
query = query.filter_by(task_id=str(task_id))
|
||||
if form_field_key:
|
||||
query = query.filter_by(form_field_key=form_field_key)
|
||||
if name:
|
||||
query = query.filter_by(name=name)
|
||||
elif workflow_id:
|
||||
query = query.filter_by(workflow_id=workflow_id)
|
||||
if irb_doc_code:
|
||||
query = query.filter_by(irb_doc_code=irb_doc_code)
|
||||
elif is_reference:
|
||||
query = query.filter_by(is_reference=True)
|
||||
|
||||
if name:
|
||||
query = query.filter_by(name=name)
|
||||
query = query.order_by(FileModel.id)
|
||||
|
||||
results = query.all()
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def get_file_data(file_id, file_model=None, version=None):
|
||||
def get_spec_data_files(workflow_spec_id, workflow_id=None, name=None):
|
||||
"""Returns all the FileDataModels related to a workflow specification.
|
||||
If a workflow is specified, returns the version of the spec relatted
|
||||
to that workflow, otherwise, returns the lastes files."""
|
||||
if workflow_id:
|
||||
query = session.query(FileDataModel) \
|
||||
.join(WorkflowSpecDependencyFile) \
|
||||
.filter(WorkflowSpecDependencyFile.workflow_id == workflow_id) \
|
||||
.order_by(FileDataModel.id)
|
||||
if name:
|
||||
query = query.join(FileModel).filter(FileModel.name == name)
|
||||
return query.all()
|
||||
else:
|
||||
"""Returns all the latest files related to a workflow specification"""
|
||||
file_models = FileService.get_files(workflow_spec_id=workflow_spec_id)
|
||||
latest_data_files = []
|
||||
for file_model in file_models:
|
||||
if name and file_model.name == name:
|
||||
latest_data_files.append(FileService.get_file_data(file_model.id))
|
||||
elif not name:
|
||||
latest_data_files.append(FileService.get_file_data(file_model.id))
|
||||
return latest_data_files
|
||||
|
||||
"""Returns the file_data that is associated with the file model id, if an actual file_model
|
||||
is provided, uses that rather than looking it up again."""
|
||||
if file_model is None:
|
||||
file_model = session.query(FileModel).filter(FileModel.id == file_id).first()
|
||||
if version is None:
|
||||
version = file_model.latest_version
|
||||
return session.query(FileDataModel) \
|
||||
.filter(FileDataModel.file_model_id == file_id) \
|
||||
.filter(FileDataModel.version == version) \
|
||||
.first()
|
||||
@staticmethod
|
||||
def get_workflow_data_files(workflow_id=None):
|
||||
"""Returns all the FileDataModels related to a running workflow -
|
||||
So these are the latest data files that were uploaded or generated
|
||||
that go along with this workflow. Not related to the spec in any way"""
|
||||
file_models = FileService.get_files(workflow_id=workflow_id)
|
||||
latest_data_files = []
|
||||
for file_model in file_models:
|
||||
latest_data_files.append(FileService.get_file_data(file_model.id))
|
||||
return latest_data_files
|
||||
|
||||
@staticmethod
|
||||
def get_file_data(file_id: int, version: int = None):
|
||||
"""Returns the file data with the given version, or the lastest file, if version isn't provided."""
|
||||
query = session.query(FileDataModel) \
|
||||
.filter(FileDataModel.file_model_id == file_id)
|
||||
if version:
|
||||
query = query.filter(FileDataModel.version == version)
|
||||
else:
|
||||
query = query.order_by(desc(FileDataModel.date_created))
|
||||
return query.first()
|
||||
|
||||
@staticmethod
|
||||
def get_reference_file_data(file_name):
|
||||
|
@ -229,7 +256,7 @@ class FileService(object):
|
|||
filter(FileModel.name == file_name).first()
|
||||
if not file_model:
|
||||
raise ApiError("file_not_found", "There is no reference file with the name '%s'" % file_name)
|
||||
return FileService.get_file_data(file_model.id, file_model)
|
||||
return FileService.get_file_data(file_model.id)
|
||||
|
||||
@staticmethod
|
||||
def get_workflow_file_data(workflow, file_name):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from pandas import ExcelFile
|
||||
from sqlalchemy import func, desc
|
||||
|
@ -8,8 +9,11 @@ from crc import db
|
|||
from crc.api.common import ApiError
|
||||
from crc.models.api_models import Task
|
||||
from crc.models.file import FileDataModel, LookupFileModel, LookupDataModel
|
||||
from crc.models.workflow import WorkflowModel, WorkflowSpecDependencyFile
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.ldap_service import LdapService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
||||
class TSRank(GenericFunction):
|
||||
package = 'full_text'
|
||||
|
@ -31,33 +35,56 @@ class LookupService(object):
|
|||
"""
|
||||
|
||||
@staticmethod
|
||||
def lookup(spiff_task, field, query, limit):
|
||||
"""Executes the lookup for the given field."""
|
||||
if field.type != Task.FIELD_TYPE_AUTO_COMPLETE:
|
||||
raise ApiError.from_task("invalid_field_type",
|
||||
"Field '%s' must be an autocomplete field to use lookups." % field.label,
|
||||
task=spiff_task)
|
||||
|
||||
# If this field has an associated options file, then do the lookup against that field.
|
||||
if field.has_property(Task.PROP_OPTIONS_FILE):
|
||||
lookup_table = LookupService.get_lookup_table(spiff_task, field)
|
||||
return LookupService._run_lookup_query(lookup_table, query, limit)
|
||||
# If this is a ldap lookup, use the ldap service to provide the fields to return.
|
||||
elif field.has_property(Task.PROP_LDAP_LOOKUP):
|
||||
return LookupService._run_ldap_query(query, limit)
|
||||
else:
|
||||
raise ApiError.from_task("unknown_lookup_option",
|
||||
"Lookup supports using spreadsheet options or ldap options, and neither was"
|
||||
"provided.")
|
||||
def get_lookup_model(spiff_task, field):
|
||||
workflow_id = spiff_task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
|
||||
workflow = db.session.query(WorkflowModel).filter(WorkflowModel.id == workflow_id).first()
|
||||
return LookupService.__get_lookup_model(workflow, field.id)
|
||||
|
||||
@staticmethod
|
||||
def get_lookup_table(spiff_task, field):
|
||||
""" Checks to see if the options are provided in a separate lookup table associated with the
|
||||
def __get_lookup_model(workflow, field_id):
|
||||
lookup_model = db.session.query(LookupFileModel) \
|
||||
.filter(LookupFileModel.workflow_spec_id == workflow.workflow_spec_id) \
|
||||
.filter(LookupFileModel.field_id == field_id).first()
|
||||
|
||||
# one more quick query, to see if the lookup file is still related to this workflow.
|
||||
# if not, we need to rebuild the lookup table.
|
||||
is_current = False
|
||||
if lookup_model:
|
||||
is_current = db.session.query(WorkflowSpecDependencyFile).\
|
||||
filter(WorkflowSpecDependencyFile.file_data_id == lookup_model.file_data_model_id).count()
|
||||
|
||||
if not is_current:
|
||||
if lookup_model:
|
||||
db.session.delete(lookup_model)
|
||||
# Very very very expensive, but we don't know need this till we do.
|
||||
lookup_model = LookupService.create_lookup_model(workflow, field_id)
|
||||
|
||||
return lookup_model
|
||||
|
||||
@staticmethod
|
||||
def lookup(workflow, field_id, query, limit):
|
||||
|
||||
lookup_model = LookupService.__get_lookup_model(workflow, field_id)
|
||||
|
||||
if lookup_model.is_ldap:
|
||||
return LookupService._run_ldap_query(query, limit)
|
||||
else:
|
||||
return LookupService._run_lookup_query(lookup_model, query, limit)
|
||||
|
||||
|
||||
|
||||
@staticmethod
|
||||
def create_lookup_model(workflow_model, field_id):
|
||||
"""
|
||||
This is all really expensive, but should happen just once (per file change).
|
||||
Checks to see if the options are provided in a separate lookup table associated with the
|
||||
workflow, and if so, assures that data exists in the database, and return a model than can be used
|
||||
to locate that data.
|
||||
|
||||
Returns: an array of LookupData, suitable for returning to the api.
|
||||
"""
|
||||
processor = WorkflowProcessor(workflow_model) # VERY expensive, Ludicrous for lookup / type ahead
|
||||
spiff_task, field = processor.find_task_and_field_by_field_id(field_id)
|
||||
|
||||
if field.has_property(Task.PROP_OPTIONS_FILE):
|
||||
if not field.has_property(Task.PROP_OPTIONS_VALUE_COLUMN) or \
|
||||
not field.has_property(Task.PROP_OPTIONS_LABEL_COL):
|
||||
|
@ -72,52 +99,67 @@ class LookupService(object):
|
|||
file_name = field.get_property(Task.PROP_OPTIONS_FILE)
|
||||
value_column = field.get_property(Task.PROP_OPTIONS_VALUE_COLUMN)
|
||||
label_column = field.get_property(Task.PROP_OPTIONS_LABEL_COL)
|
||||
data_model = FileService.get_workflow_file_data(spiff_task.workflow, file_name)
|
||||
lookup_model = LookupService.get_lookup_table_from_data_model(data_model, value_column, label_column)
|
||||
return lookup_model
|
||||
latest_files = FileService.get_spec_data_files(workflow_spec_id=workflow_model.workflow_spec_id,
|
||||
workflow_id=workflow_model.id,
|
||||
name=file_name)
|
||||
if len(latest_files) < 1:
|
||||
raise ApiError("missing_file", "Unable to locate the lookup data file '%s'" % file_name)
|
||||
else:
|
||||
data_model = latest_files[0]
|
||||
|
||||
lookup_model = LookupService.build_lookup_table(data_model, value_column, label_column,
|
||||
workflow_model.workflow_spec_id, field_id)
|
||||
|
||||
elif field.has_property(Task.PROP_LDAP_LOOKUP):
|
||||
lookup_model = LookupFileModel(workflow_spec_id=workflow_model.workflow_spec_id,
|
||||
field_id=field_id,
|
||||
is_ldap=True)
|
||||
else:
|
||||
raise ApiError("unknown_lookup_option",
|
||||
"Lookup supports using spreadsheet options or ldap options, and neither "
|
||||
"was provided.")
|
||||
db.session.add(lookup_model)
|
||||
db.session.commit()
|
||||
return lookup_model
|
||||
|
||||
@staticmethod
|
||||
def get_lookup_table_from_data_model(data_model: FileDataModel, value_column, label_column):
|
||||
def build_lookup_table(data_model: FileDataModel, value_column, label_column, workflow_spec_id, field_id):
|
||||
""" In some cases the lookup table can be very large. This method will add all values to the database
|
||||
in a way that can be searched and returned via an api call - rather than sending the full set of
|
||||
options along with the form. It will only open the file and process the options if something has
|
||||
changed. """
|
||||
xls = ExcelFile(data_model.data)
|
||||
df = xls.parse(xls.sheet_names[0]) # Currently we only look at the fist sheet.
|
||||
if value_column not in df:
|
||||
raise ApiError("invalid_emum",
|
||||
"The file %s does not contain a column named % s" % (data_model.file_model.name,
|
||||
value_column))
|
||||
if label_column not in df:
|
||||
raise ApiError("invalid_emum",
|
||||
"The file %s does not contain a column named % s" % (data_model.file_model.name,
|
||||
label_column))
|
||||
|
||||
lookup_model = db.session.query(LookupFileModel) \
|
||||
.filter(LookupFileModel.file_data_model_id == data_model.id) \
|
||||
.filter(LookupFileModel.value_column == value_column) \
|
||||
.filter(LookupFileModel.label_column == label_column).first()
|
||||
|
||||
if not lookup_model:
|
||||
xls = ExcelFile(data_model.data)
|
||||
df = xls.parse(xls.sheet_names[0]) # Currently we only look at the fist sheet.
|
||||
if value_column not in df:
|
||||
raise ApiError("invalid_emum",
|
||||
"The file %s does not contain a column named % s" % (data_model.file_model.name,
|
||||
value_column))
|
||||
if label_column not in df:
|
||||
raise ApiError("invalid_emum",
|
||||
"The file %s does not contain a column named % s" % (data_model.file_model.name,
|
||||
label_column))
|
||||
|
||||
lookup_model = LookupFileModel(label_column=label_column, value_column=value_column,
|
||||
file_data_model_id=data_model.id)
|
||||
|
||||
db.session.add(lookup_model)
|
||||
for index, row in df.iterrows():
|
||||
lookup_data = LookupDataModel(lookup_file_model=lookup_model,
|
||||
value=row[value_column],
|
||||
label=row[label_column],
|
||||
data=row.to_json())
|
||||
db.session.add(lookup_data)
|
||||
db.session.commit()
|
||||
lookup_model = LookupFileModel(workflow_spec_id=workflow_spec_id,
|
||||
field_id=field_id,
|
||||
file_data_model_id=data_model.id,
|
||||
is_ldap=False)
|
||||
|
||||
db.session.add(lookup_model)
|
||||
for index, row in df.iterrows():
|
||||
lookup_data = LookupDataModel(lookup_file_model=lookup_model,
|
||||
value=row[value_column],
|
||||
label=row[label_column],
|
||||
data=row.to_json())
|
||||
db.session.add(lookup_data)
|
||||
db.session.commit()
|
||||
return lookup_model
|
||||
|
||||
@staticmethod
|
||||
def _run_lookup_query(lookup_file_model, query, limit):
|
||||
db_query = LookupDataModel.query.filter(LookupDataModel.lookup_file_model == lookup_file_model)
|
||||
|
||||
query = re.sub('[^A-Za-z0-9 ]+', '', query)
|
||||
print("Query: " + query)
|
||||
query = query.strip()
|
||||
if len(query) > 0:
|
||||
if ' ' in query:
|
||||
|
|
|
@ -33,10 +33,15 @@ class StudyService(object):
|
|||
return studies
|
||||
|
||||
@staticmethod
|
||||
def get_studies_with_files():
|
||||
def get_all_studies_with_files():
|
||||
"""Returns a list of all studies"""
|
||||
db_studies = session.query(StudyModel).all()
|
||||
return db_studies
|
||||
studies = []
|
||||
for s in db_studies:
|
||||
study = Study.from_model(s)
|
||||
study.files = FileService.get_files_for_study(study.id)
|
||||
studies.append(study)
|
||||
return studies
|
||||
|
||||
@staticmethod
|
||||
def get_study(study_id, study_model: StudyModel = None):
|
||||
|
@ -48,6 +53,7 @@ class StudyService(object):
|
|||
study = Study.from_model(study_model)
|
||||
study.categories = StudyService.get_categories()
|
||||
workflow_metas = StudyService.__get_workflow_metas(study_id)
|
||||
study.files = FileService.get_files_for_study(study.id)
|
||||
|
||||
# Calling this line repeatedly is very very slow. It creates the
|
||||
# master spec and runs it.
|
||||
|
@ -72,6 +78,8 @@ class StudyService(object):
|
|||
def delete_workflow(workflow):
|
||||
for file in session.query(FileModel).filter_by(workflow_id=workflow.id).all():
|
||||
FileService.delete_file(file.id)
|
||||
for deb in workflow.dependencies:
|
||||
session.delete(deb)
|
||||
session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete()
|
||||
session.query(WorkflowModel).filter_by(id=workflow.id).delete()
|
||||
|
||||
|
@ -150,17 +158,15 @@ class StudyService(object):
|
|||
doc['display_name'] = ' / '.join(name_list)
|
||||
|
||||
# For each file, get associated workflow status
|
||||
doc_files = FileService.get_files(study_id=study_id, irb_doc_code=code)
|
||||
doc_files = FileService.get_files_for_study(study_id=study_id, irb_doc_code=code)
|
||||
doc['count'] = len(doc_files)
|
||||
doc['files'] = []
|
||||
for file in doc_files:
|
||||
doc['files'].append({'file_id': file.id,
|
||||
'task_id': file.task_id,
|
||||
'workflow_id': file.workflow_id,
|
||||
'workflow_spec_id': file.workflow_spec_id})
|
||||
'workflow_id': file.workflow_id})
|
||||
|
||||
# update the document status to match the status of the workflow it is in.
|
||||
if not 'status' in doc or doc['status'] is None:
|
||||
if 'status' not in doc or doc['status'] is None:
|
||||
workflow: WorkflowModel = session.query(WorkflowModel).filter_by(id=file.workflow_id).first()
|
||||
doc['status'] = workflow.status.value
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import random
|
||||
import re
|
||||
import string
|
||||
import xml.etree.ElementTree as ElementTree
|
||||
from datetime import datetime
|
||||
from typing import List
|
||||
|
||||
from SpiffWorkflow import Task as SpiffTask, WorkflowException
|
||||
from SpiffWorkflow.bpmn.BpmnScriptEngine import BpmnScriptEngine
|
||||
|
@ -13,14 +12,15 @@ from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
|
|||
from SpiffWorkflow.camunda.parser.CamundaParser import CamundaParser
|
||||
from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser
|
||||
from SpiffWorkflow.exceptions import WorkflowTaskExecException
|
||||
from SpiffWorkflow.operators import Operator
|
||||
from SpiffWorkflow.specs import WorkflowSpec
|
||||
from sqlalchemy import desc
|
||||
|
||||
from crc import session
|
||||
from crc.api.common import ApiError
|
||||
from crc.models.file import FileDataModel, FileModel, FileType
|
||||
from crc.models.workflow import WorkflowStatus, WorkflowModel
|
||||
from crc.models.workflow import WorkflowStatus, WorkflowModel, WorkflowSpecDependencyFile
|
||||
from crc.scripts.script import Script
|
||||
from crc.services.file_service import FileService
|
||||
|
||||
|
||||
class CustomBpmnScriptEngine(BpmnScriptEngine):
|
||||
|
@ -48,7 +48,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
|
|||
mod = __import__(module_name, fromlist=[class_name])
|
||||
klass = getattr(mod, class_name)
|
||||
study_id = task.workflow.data[WorkflowProcessor.STUDY_ID_KEY]
|
||||
if(WorkflowProcessor.WORKFLOW_ID_KEY in task.workflow.data):
|
||||
if WorkflowProcessor.WORKFLOW_ID_KEY in task.workflow.data:
|
||||
workflow_id = task.workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY]
|
||||
else:
|
||||
workflow_id = None
|
||||
|
@ -75,7 +75,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
|
|||
Evaluate the given expression, within the context of the given task and
|
||||
return the result.
|
||||
"""
|
||||
exp,valid = self.validateExpression(expression)
|
||||
exp, valid = self.validateExpression(expression)
|
||||
return self._eval(exp, **task.data)
|
||||
|
||||
@staticmethod
|
||||
|
@ -100,7 +100,7 @@ class WorkflowProcessor(object):
|
|||
STUDY_ID_KEY = "study_id"
|
||||
VALIDATION_PROCESS_KEY = "validate_only"
|
||||
|
||||
def __init__(self, workflow_model: WorkflowModel, soft_reset=False, hard_reset=False):
|
||||
def __init__(self, workflow_model: WorkflowModel, soft_reset=False, hard_reset=False, validate_only=False):
|
||||
"""Create a Workflow Processor based on the serialized information available in the workflow model.
|
||||
If soft_reset is set to true, it will try to use the latest version of the workflow specification.
|
||||
If hard_reset is set to true, it will create a new Workflow, but embed the data from the last
|
||||
|
@ -108,18 +108,22 @@ class WorkflowProcessor(object):
|
|||
If neither flag is set, it will use the same version of the specification that was used to originally
|
||||
create the workflow model. """
|
||||
self.workflow_model = workflow_model
|
||||
orig_version = workflow_model.spec_version
|
||||
if soft_reset or workflow_model.spec_version is None:
|
||||
self.workflow_model.spec_version = WorkflowProcessor.get_latest_version_string(
|
||||
workflow_model.workflow_spec_id)
|
||||
|
||||
spec = self.get_spec(workflow_model.workflow_spec_id, workflow_model.spec_version)
|
||||
if soft_reset or len(workflow_model.dependencies) == 0:
|
||||
self.spec_data_files = FileService.get_spec_data_files(
|
||||
workflow_spec_id=workflow_model.workflow_spec_id)
|
||||
else:
|
||||
self.spec_data_files = FileService.get_spec_data_files(
|
||||
workflow_spec_id=workflow_model.workflow_spec_id,
|
||||
workflow_id=workflow_model.id)
|
||||
|
||||
spec = self.get_spec(self.spec_data_files, workflow_model.workflow_spec_id)
|
||||
self.workflow_spec_id = workflow_model.workflow_spec_id
|
||||
try:
|
||||
self.bpmn_workflow = self.__get_bpmn_workflow(workflow_model, spec)
|
||||
self.bpmn_workflow = self.__get_bpmn_workflow(workflow_model, spec, validate_only)
|
||||
self.bpmn_workflow.script_engine = self._script_engine
|
||||
|
||||
if not self.WORKFLOW_ID_KEY in self.bpmn_workflow.data:
|
||||
if self.WORKFLOW_ID_KEY not in self.bpmn_workflow.data:
|
||||
if not workflow_model.id:
|
||||
session.add(workflow_model)
|
||||
session.commit()
|
||||
|
@ -132,71 +136,63 @@ class WorkflowProcessor(object):
|
|||
self.save()
|
||||
|
||||
except KeyError as ke:
|
||||
if soft_reset:
|
||||
# Undo the soft-reset.
|
||||
workflow_model.spec_version = orig_version
|
||||
raise ApiError(code="unexpected_workflow_structure",
|
||||
message="Failed to deserialize workflow"
|
||||
" '%s' version %s, due to a mis-placed or missing task '%s'" %
|
||||
(self.workflow_spec_id, workflow_model.spec_version, str(ke)) +
|
||||
" This is very likely due to a soft reset where there was a structural change.")
|
||||
(self.workflow_spec_id, self.get_version_string(), str(ke)) +
|
||||
" This is very likely due to a soft reset where there was a structural change.")
|
||||
if hard_reset:
|
||||
# Now that the spec is loaded, get the data and rebuild the bpmn with the new details
|
||||
workflow_model.spec_version = self.hard_reset()
|
||||
self.hard_reset()
|
||||
workflow_model.bpmn_workflow_json = WorkflowProcessor._serializer.serialize_workflow(self.bpmn_workflow)
|
||||
self.save()
|
||||
if soft_reset:
|
||||
self.save()
|
||||
|
||||
def __get_bpmn_workflow(self, workflow_model: WorkflowModel, spec: WorkflowSpec):
|
||||
# set whether this is the latest spec file.
|
||||
if self.spec_data_files == FileService.get_spec_data_files(workflow_spec_id=workflow_model.workflow_spec_id):
|
||||
self.is_latest_spec = True
|
||||
else:
|
||||
self.is_latest_spec = False
|
||||
|
||||
def __get_bpmn_workflow(self, workflow_model: WorkflowModel, spec: WorkflowSpec, validate_only=False):
|
||||
if workflow_model.bpmn_workflow_json:
|
||||
bpmn_workflow = self._serializer.deserialize_workflow(workflow_model.bpmn_workflow_json, workflow_spec=spec)
|
||||
else:
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine)
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = workflow_model.study_id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = validate_only
|
||||
bpmn_workflow.do_engine_steps()
|
||||
return bpmn_workflow
|
||||
|
||||
def save(self):
|
||||
"""Saves the current state of this processor to the database """
|
||||
workflow_model = self.workflow_model
|
||||
workflow_model.bpmn_workflow_json = self.serialize()
|
||||
self.workflow_model.bpmn_workflow_json = self.serialize()
|
||||
complete_states = [SpiffTask.CANCELLED, SpiffTask.COMPLETED]
|
||||
tasks = list(self.get_all_user_tasks())
|
||||
workflow_model.status = self.get_status()
|
||||
workflow_model.total_tasks = len(tasks)
|
||||
workflow_model.completed_tasks = sum(1 for t in tasks if t.state in complete_states)
|
||||
workflow_model.last_updated = datetime.now()
|
||||
session.add(workflow_model)
|
||||
self.workflow_model.status = self.get_status()
|
||||
self.workflow_model.total_tasks = len(tasks)
|
||||
self.workflow_model.completed_tasks = sum(1 for t in tasks if t.state in complete_states)
|
||||
self.workflow_model.last_updated = datetime.now()
|
||||
self.update_dependencies(self.spec_data_files)
|
||||
session.add(self.workflow_model)
|
||||
session.commit()
|
||||
|
||||
@staticmethod
|
||||
def run_master_spec(spec_model, study):
|
||||
"""Executes a BPMN specification for the given study, without recording any information to the database
|
||||
Useful for running the master specification, which should not persist. """
|
||||
version = WorkflowProcessor.get_latest_version_string(spec_model.id)
|
||||
spec = WorkflowProcessor.get_spec(spec_model.id, version)
|
||||
try:
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine)
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study.id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False
|
||||
bpmn_workflow.do_engine_steps()
|
||||
except WorkflowException as we:
|
||||
raise ApiError.from_task_spec("error_running_master_spec", str(we), we.sender)
|
||||
|
||||
if not bpmn_workflow.is_completed():
|
||||
raise ApiError("master_spec_not_automatic",
|
||||
"The master spec should only contain fully automated tasks, it failed to complete.")
|
||||
|
||||
return bpmn_workflow.last_task.data
|
||||
def get_version_string(self):
|
||||
# this could potentially become expensive to load all the data in the data models.
|
||||
# in which case we might consider using a deferred loader for the actual data, but
|
||||
# trying not to pre-optimize.
|
||||
file_data_models = FileService.get_spec_data_files(self.workflow_model.workflow_spec_id,
|
||||
self.workflow_model.id)
|
||||
return WorkflowProcessor.__get_version_string_for_data_models(file_data_models)
|
||||
|
||||
@staticmethod
|
||||
def get_parser():
|
||||
parser = MyCustomParser()
|
||||
return parser
|
||||
def get_latest_version_string_for_spec(spec_id):
|
||||
file_data_models = FileService.get_spec_data_files(spec_id)
|
||||
return WorkflowProcessor.__get_version_string_for_data_models(file_data_models)
|
||||
|
||||
@staticmethod
|
||||
def get_latest_version_string(workflow_spec_id):
|
||||
def __get_version_string_for_data_models(file_data_models):
|
||||
"""Version is in the format v[VERSION] (FILE_ID_LIST)
|
||||
For example, a single bpmn file with only one version would be
|
||||
v1 (12) Where 12 is the id of the file data model that is used to create the
|
||||
|
@ -205,10 +201,6 @@ class WorkflowProcessor(object):
|
|||
a Spec that includes a BPMN, DMN, an a Word file all on the first
|
||||
version would be v1.1.1 (12.45.21)"""
|
||||
|
||||
# this could potentially become expensive to load all the data in the data models.
|
||||
# in which case we might consider using a deferred loader for the actual data, but
|
||||
# trying not to pre-optimize.
|
||||
file_data_models = WorkflowProcessor.__get_latest_file_models(workflow_spec_id)
|
||||
major_version = 0 # The version of the primary file.
|
||||
minor_version = [] # The versions of the minor files if any.
|
||||
file_ids = []
|
||||
|
@ -224,60 +216,72 @@ class WorkflowProcessor(object):
|
|||
full_version = "v%s (%s)" % (version, files)
|
||||
return full_version
|
||||
|
||||
@staticmethod
|
||||
def get_file_models_for_version(workflow_spec_id, version):
|
||||
file_id_strings = re.findall('\((.*)\)', version)[0].split(".")
|
||||
file_ids = [int(i) for i in file_id_strings]
|
||||
files = session.query(FileDataModel)\
|
||||
.join(FileModel) \
|
||||
.filter(FileModel.workflow_spec_id == workflow_spec_id)\
|
||||
.filter(FileDataModel.id.in_(file_ids)).all()
|
||||
if len(files) != len(file_ids):
|
||||
raise ApiError("invalid_version",
|
||||
"The version '%s' of workflow specification '%s' is invalid. " %
|
||||
(version, workflow_spec_id) +
|
||||
" Unable to locate the correct files to recreate it.")
|
||||
return files
|
||||
|
||||
|
||||
def update_dependencies(self, spec_data_files):
|
||||
existing_dependencies = FileService.get_spec_data_files(
|
||||
workflow_spec_id=self.workflow_model.workflow_spec_id,
|
||||
workflow_id=self.workflow_model.id)
|
||||
|
||||
# Don't save the dependencies if they haven't changed.
|
||||
if existing_dependencies == spec_data_files:
|
||||
return
|
||||
|
||||
# Remove all existing dependencies, and replace them.
|
||||
self.workflow_model.dependencies = []
|
||||
for file_data in spec_data_files:
|
||||
self.workflow_model.dependencies.append(WorkflowSpecDependencyFile(file_data_id=file_data.id))
|
||||
|
||||
@staticmethod
|
||||
def __get_latest_file_models(workflow_spec_id):
|
||||
"""Returns all the latest files related to a workflow specification"""
|
||||
return session.query(FileDataModel) \
|
||||
.join(FileModel) \
|
||||
.filter(FileModel.workflow_spec_id == workflow_spec_id)\
|
||||
.filter(FileDataModel.version == FileModel.latest_version)\
|
||||
.order_by(FileModel.id)\
|
||||
.all()
|
||||
def run_master_spec(spec_model, study):
|
||||
"""Executes a BPMN specification for the given study, without recording any information to the database
|
||||
Useful for running the master specification, which should not persist. """
|
||||
spec_data_files = FileService.get_spec_data_files(spec_model.id)
|
||||
spec = WorkflowProcessor.get_spec(spec_data_files, spec_model.id)
|
||||
try:
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=WorkflowProcessor._script_engine)
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = study.id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = False
|
||||
bpmn_workflow.do_engine_steps()
|
||||
except WorkflowException as we:
|
||||
raise ApiError.from_task_spec("error_running_master_spec", str(we), we.sender)
|
||||
|
||||
if not bpmn_workflow.is_completed():
|
||||
raise ApiError("master_spec_not_automatic",
|
||||
"The master spec should only contain fully automated tasks, it failed to complete.")
|
||||
|
||||
return bpmn_workflow.last_task.data
|
||||
|
||||
@staticmethod
|
||||
def get_spec(workflow_spec_id, version=None):
|
||||
"""Returns the requested version of the specification,
|
||||
or the latest version if none is specified."""
|
||||
def get_parser():
|
||||
parser = MyCustomParser()
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def get_spec(file_data_models: List[FileDataModel], workflow_spec_id):
|
||||
"""Returns a SpiffWorkflow specification for the given workflow spec,
|
||||
using the files provided. The Workflow_spec_id is only used to generate
|
||||
better error messages."""
|
||||
parser = WorkflowProcessor.get_parser()
|
||||
process_id = None
|
||||
|
||||
if version is None:
|
||||
file_data_models = WorkflowProcessor.__get_latest_file_models(workflow_spec_id)
|
||||
else:
|
||||
file_data_models = WorkflowProcessor.get_file_models_for_version(workflow_spec_id, version)
|
||||
|
||||
for file_data in file_data_models:
|
||||
if file_data.file_model.type == FileType.bpmn:
|
||||
bpmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
|
||||
if file_data.file_model.primary:
|
||||
process_id = WorkflowProcessor.get_process_id(bpmn)
|
||||
process_id = FileService.get_process_id(bpmn)
|
||||
parser.add_bpmn_xml(bpmn, filename=file_data.file_model.name)
|
||||
elif file_data.file_model.type == FileType.dmn:
|
||||
dmn: ElementTree.Element = ElementTree.fromstring(file_data.data)
|
||||
parser.add_dmn_xml(dmn, filename=file_data.file_model.name)
|
||||
if process_id is None:
|
||||
raise(ApiError(code="no_primary_bpmn_error",
|
||||
message="There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
|
||||
raise (ApiError(code="no_primary_bpmn_error",
|
||||
message="There is no primary BPMN model defined for workflow %s" % workflow_spec_id))
|
||||
try:
|
||||
spec = parser.get_spec(process_id)
|
||||
except ValidationException as ve:
|
||||
raise ApiError(code="workflow_validation_error",
|
||||
message="Failed to parse Workflow Specification '%s' %s." % (workflow_spec_id, version) +
|
||||
message="Failed to parse Workflow Specification '%s'" % workflow_spec_id +
|
||||
"Error is %s" % str(ve),
|
||||
file_name=ve.filename,
|
||||
task_id=ve.id,
|
||||
|
@ -301,8 +305,8 @@ class WorkflowProcessor(object):
|
|||
|
||||
Returns the new version.
|
||||
"""
|
||||
version = WorkflowProcessor.get_latest_version_string(self.workflow_spec_id)
|
||||
spec = WorkflowProcessor.get_spec(self.workflow_spec_id) # Force latest version by NOT specifying version
|
||||
self.spec_data_files = FileService.get_spec_data_files(workflow_spec_id=self.workflow_spec_id)
|
||||
spec = WorkflowProcessor.get_spec(self.spec_data_files, self.workflow_spec_id)
|
||||
# spec = WorkflowProcessor.get_spec(self.workflow_spec_id, version)
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=self._script_engine)
|
||||
bpmn_workflow.data = self.bpmn_workflow.data
|
||||
|
@ -310,14 +314,10 @@ class WorkflowProcessor(object):
|
|||
task.data = self.bpmn_workflow.last_task.data
|
||||
bpmn_workflow.do_engine_steps()
|
||||
self.bpmn_workflow = bpmn_workflow
|
||||
return version
|
||||
|
||||
def get_status(self):
|
||||
return self.status_of(self.bpmn_workflow)
|
||||
|
||||
def get_spec_version(self):
|
||||
return self.workflow_model.spec_version
|
||||
|
||||
def do_engine_steps(self):
|
||||
try:
|
||||
self.bpmn_workflow.do_engine_steps()
|
||||
|
@ -398,32 +398,18 @@ class WorkflowProcessor(object):
|
|||
return [t for t in all_tasks
|
||||
if not self.bpmn_workflow._is_engine_task(t.task_spec) and t.state in [t.COMPLETED, t.CANCELLED]]
|
||||
|
||||
@staticmethod
|
||||
def get_process_id(et_root: ElementTree.Element):
|
||||
process_elements = []
|
||||
for child in et_root:
|
||||
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')
|
||||
|
||||
# There are multiple root elements
|
||||
if len(process_elements) > 1:
|
||||
|
||||
# Look for the element that has the startEvent in it
|
||||
for e in process_elements:
|
||||
this_element: ElementTree.Element = e
|
||||
for child_element in list(this_element):
|
||||
if child_element.tag.endswith('startEvent'):
|
||||
return this_element.attrib['id']
|
||||
|
||||
raise ValidationException('No start event found in %s' % et_root.attrib['id'])
|
||||
|
||||
return process_elements[0].attrib['id']
|
||||
|
||||
def get_nav_item(self, task):
|
||||
for nav_item in self.bpmn_workflow.get_nav_list():
|
||||
if nav_item['task_id'] == task.id:
|
||||
return nav_item
|
||||
|
||||
def find_task_and_field_by_field_id(self, field_id):
|
||||
"""Tracks down a form field by name in the workflow spec,
|
||||
only looks at ready tasks. Returns a tuple of the task, and form"""
|
||||
for spiff_task in self.bpmn_workflow.get_tasks():
|
||||
if hasattr(spiff_task.task_spec, "form"):
|
||||
for field in spiff_task.task_spec.form.fields:
|
||||
if field.id == field_id:
|
||||
return spiff_task, field
|
||||
raise ApiError("invalid_field",
|
||||
"Unable to find a task in the workflow with a lookup field called: %s" % field_id)
|
||||
|
|
|
@ -17,9 +17,14 @@ from crc import db, app
|
|||
from crc.api.common import ApiError
|
||||
from crc.models.api_models import Task, MultiInstanceType
|
||||
from crc.models.file import LookupDataModel
|
||||
from crc.models.protocol_builder import ProtocolBuilderStatus
|
||||
from crc.models.stats import TaskEventModel
|
||||
from crc.models.study import StudyModel
|
||||
from crc.models.user import UserModel
|
||||
from crc.models.workflow import WorkflowModel, WorkflowStatus
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.lookup_service import LookupService
|
||||
from crc.services.study_service import StudyService
|
||||
from crc.services.workflow_processor import WorkflowProcessor, CustomBpmnScriptEngine
|
||||
|
||||
|
||||
|
@ -36,21 +41,46 @@ class WorkflowService(object):
|
|||
But for now, this contains tools for converting spiff-workflow models into our
|
||||
own API models with additional information and capabilities."""
|
||||
|
||||
@classmethod
|
||||
def test_spec(cls, spec_id):
|
||||
@staticmethod
|
||||
def make_test_workflow(spec_id):
|
||||
user = db.session.query(UserModel).filter_by(uid="test").first()
|
||||
if not user:
|
||||
db.session.add(UserModel(uid="test"))
|
||||
study = db.session.query(StudyModel).filter_by(user_uid="test").first()
|
||||
if not study:
|
||||
db.session.add(StudyModel(user_uid="test", title="test"))
|
||||
db.session.commit()
|
||||
workflow_model = WorkflowModel(status=WorkflowStatus.not_started,
|
||||
workflow_spec_id=spec_id,
|
||||
last_updated=datetime.now(),
|
||||
study=study)
|
||||
return workflow_model
|
||||
|
||||
@staticmethod
|
||||
def delete_test_data():
|
||||
for study in db.session.query(StudyModel).filter(StudyModel.user_uid=="test"):
|
||||
StudyService.delete_study(study.id)
|
||||
db.session.commit()
|
||||
db.session.query(UserModel).filter_by(uid="test").delete()
|
||||
|
||||
@staticmethod
|
||||
def test_spec(spec_id):
|
||||
"""Runs a spec through it's paces to see if it results in any errors. Not fool-proof, but a good
|
||||
sanity check."""
|
||||
version = WorkflowProcessor.get_latest_version_string(spec_id)
|
||||
spec = WorkflowProcessor.get_spec(spec_id, version)
|
||||
bpmn_workflow = BpmnWorkflow(spec, script_engine=CustomBpmnScriptEngine())
|
||||
bpmn_workflow.data[WorkflowProcessor.STUDY_ID_KEY] = 1
|
||||
bpmn_workflow.data[WorkflowProcessor.WORKFLOW_ID_KEY] = spec_id
|
||||
bpmn_workflow.data[WorkflowProcessor.VALIDATION_PROCESS_KEY] = True
|
||||
|
||||
while not bpmn_workflow.is_completed():
|
||||
workflow_model = WorkflowService.make_test_workflow(spec_id)
|
||||
|
||||
try:
|
||||
processor = WorkflowProcessor(workflow_model, validate_only=True)
|
||||
except WorkflowException as we:
|
||||
WorkflowService.delete_test_data()
|
||||
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
|
||||
we.sender)
|
||||
|
||||
while not processor.bpmn_workflow.is_completed():
|
||||
try:
|
||||
bpmn_workflow.do_engine_steps()
|
||||
tasks = bpmn_workflow.get_tasks(SpiffTask.READY)
|
||||
processor.bpmn_workflow.do_engine_steps()
|
||||
tasks = processor.bpmn_workflow.get_tasks(SpiffTask.READY)
|
||||
for task in tasks:
|
||||
task_api = WorkflowService.spiff_task_to_api_task(
|
||||
task,
|
||||
|
@ -58,8 +88,10 @@ class WorkflowService(object):
|
|||
WorkflowService.populate_form_with_random_data(task, task_api)
|
||||
task.complete()
|
||||
except WorkflowException as we:
|
||||
WorkflowService.delete_test_data()
|
||||
raise ApiError.from_task_spec("workflow_execution_exception", str(we),
|
||||
we.sender)
|
||||
WorkflowService.delete_test_data()
|
||||
|
||||
@staticmethod
|
||||
def populate_form_with_random_data(task, task_api):
|
||||
|
@ -82,7 +114,7 @@ class WorkflowService(object):
|
|||
" with no options" % field.id,
|
||||
task)
|
||||
elif field.type == "autocomplete":
|
||||
lookup_model = LookupService.get_lookup_table(task, field)
|
||||
lookup_model = LookupService.get_lookup_model(task, field)
|
||||
if field.has_property(Task.PROP_LDAP_LOOKUP):
|
||||
form_data[field.id] = {
|
||||
"label": "dhf8r",
|
||||
|
@ -248,12 +280,12 @@ class WorkflowService(object):
|
|||
|
||||
@staticmethod
|
||||
def process_options(spiff_task, field):
|
||||
lookup_model = LookupService.get_lookup_table(spiff_task, field)
|
||||
|
||||
# If this is an auto-complete field, do not populate options, a lookup will happen later.
|
||||
if field.type == Task.FIELD_TYPE_AUTO_COMPLETE:
|
||||
pass
|
||||
else:
|
||||
elif field.has_property(Task.PROP_OPTIONS_FILE):
|
||||
lookup_model = LookupService.get_lookup_model(spiff_task, field)
|
||||
data = db.session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_model).all()
|
||||
if not hasattr(field, 'options'):
|
||||
field.options = []
|
||||
|
@ -269,7 +301,7 @@ class WorkflowService(object):
|
|||
user_uid=g.user.uid,
|
||||
workflow_id=workflow_model.id,
|
||||
workflow_spec_id=workflow_model.workflow_spec_id,
|
||||
spec_version=workflow_model.spec_version,
|
||||
spec_version=processor.get_version_string(),
|
||||
action=action,
|
||||
task_id=task.id,
|
||||
task_name=task.name,
|
||||
|
@ -284,3 +316,4 @@ class WorkflowService(object):
|
|||
)
|
||||
db.session.add(task_event)
|
||||
db.session.commit()
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,277 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<definitions xmlns="http://www.omg.org/spec/DMN/20151101/dmn.xsd" id="Definitions_0crc2o7" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
|
||||
<decision id="Decision_ApprovalInfo" name="Approval Info">
|
||||
<decisionTable id="decisionTable_1">
|
||||
<input id="input_1" label="School">
|
||||
<inputExpression id="inputExpression_1" typeRef="string">
|
||||
<text>PISchool</text>
|
||||
</inputExpression>
|
||||
</input>
|
||||
<output id="OutputClause_1138fqx" label="School" name="ApprvlSchool" typeRef="string" />
|
||||
<output id="output_1" label="Approver 1" name="ApprvlApprvr1" typeRef="string" />
|
||||
<output id="OutputClause_0dcb1cr" label="Approver 1 Role" name="ApprvlApprovrRole1" typeRef="string" />
|
||||
<output id="OutputClause_0mftsw9" label="Approver 2" name="ApprvlApprvr2" typeRef="string" />
|
||||
<output id="OutputClause_0iuw224" label="Approver 2 Role" name="ApprvlApprvrRole2" typeRef="string" />
|
||||
<rule id="DecisionRule_1wge2nn">
|
||||
<inputEntry id="UnaryTests_0mfg1fu">
|
||||
<text>"Architecture"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_074qb28">
|
||||
<text>"Architecture"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0vb9mia">
|
||||
<text>PISupervisor.data.uid</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1pqm0u2">
|
||||
<text>"Supervisor"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_17oc2op">
|
||||
<text>"agc9a"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_08dc6cz">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1u9orsf">
|
||||
<inputEntry id="UnaryTests_08ormp3">
|
||||
<text>"Arts & Sciences"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0ua8d03">
|
||||
<text>"Arts & Sciences"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1ejs3h2">
|
||||
<text>PISupervisor.data.uid</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0h7eq4l">
|
||||
<text>"Supervisor"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_06pynb2">
|
||||
<text>"dh2t"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0x965p2">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_0k7lbs6">
|
||||
<inputEntry id="UnaryTests_1tws5wb">
|
||||
<text>"Commerce"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0y7bv69">
|
||||
<text>"Commerce"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0sz476a">
|
||||
<text>"dcs8f"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0wbcswk">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0htwmws">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0ntf026">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_0pcsd6s">
|
||||
<inputEntry id="UnaryTests_07uijrb">
|
||||
<text>"Darden"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1he7hp6">
|
||||
<text>"Darden"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0hdskgi">
|
||||
<text>"mw4m"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1uxxdv0">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0q81xjf">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1rp7s1w">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_0qlavkb">
|
||||
<inputEntry id="UnaryTests_1775ht5">
|
||||
<text>"Data Science"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1fxauop">
|
||||
<text>"Data Science"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0w0ksry">
|
||||
<text>"cws3v"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0847zci">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1xjonk2">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1rb6200">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_0vw6027">
|
||||
<inputEntry id="UnaryTests_1hsal1b">
|
||||
<text>"Education"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1uwd4hj">
|
||||
<text>"Education"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1t3c4oj">
|
||||
<text>PISupervisor.data.uid</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0vkosy7">
|
||||
<text>"Supervisor"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1sj3i3e">
|
||||
<text>"cpb8g"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_11ml53c">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1rgdbw3">
|
||||
<inputEntry id="UnaryTests_022jcbh">
|
||||
<text>"Engineering"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0lzmt29">
|
||||
<text>"Engineering"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0v4df1x">
|
||||
<text>"sb5mc"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_16kmec1">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1bjsn8g">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1rm1jw7">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1hy1sby">
|
||||
<inputEntry id="UnaryTests_1u52cey">
|
||||
<text>"Law"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1w3vu2k">
|
||||
<text>"Law"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1nrqe2j">
|
||||
<text>"kendrick"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_19n45lv">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0nppbew">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0n1f5l1">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1nmyhfi">
|
||||
<inputEntry id="UnaryTests_0uqd08s">
|
||||
<text>"Leadership & Public Policy"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_0dy02ei">
|
||||
<text>"Leadership & Public Policy"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0rxrqkb">
|
||||
<text>"jps3va"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0ejmi7q">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1ph2wlx">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0i6fih4">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_17uxicj">
|
||||
<inputEntry id="UnaryTests_0kttpy1">
|
||||
<text>"Medicine"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1ops4nm">
|
||||
<text>"Medicine"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1cwa3r0">
|
||||
<text>PISupervisor.data.uid</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0ouvo17">
|
||||
<text>"Supervisor"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_18rte97">
|
||||
<text>"mas3x"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1rxh2p0">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1t4241f">
|
||||
<inputEntry id="UnaryTests_0xwnrta">
|
||||
<text>"Nursing"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1va63rh">
|
||||
<text>"Nursing"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_125fuie">
|
||||
<text>"jla7e"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0h1e3bv">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0e8b9ui">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1mq49yg">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_0jnp1wp">
|
||||
<inputEntry id="UnaryTests_164zt99">
|
||||
<text>"Continuing Education"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1h81xwr">
|
||||
<text>"Continuing Education"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_12sudp0">
|
||||
<text>"ado4v"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1jkrv7z">
|
||||
<text>"Associate Research Dean"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0qelytu">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_19tc8kf">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
<rule id="DecisionRule_1ynkili">
|
||||
<inputEntry id="UnaryTests_1dupb6e">
|
||||
<text>"Provost Office"</text>
|
||||
</inputEntry>
|
||||
<outputEntry id="LiteralExpression_1w13wuw">
|
||||
<text>"Provost Office"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_13xizrn">
|
||||
<text>"rammk"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_166hajt">
|
||||
<text>"VP of Research"</text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_1xazzzn">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
<outputEntry id="LiteralExpression_0zr3ar6">
|
||||
<text></text>
|
||||
</outputEntry>
|
||||
</rule>
|
||||
</decisionTable>
|
||||
</decision>
|
||||
</definitions>
|
Binary file not shown.
|
@ -5,27 +5,45 @@
|
|||
<bpmn:outgoing>SequenceFlow_05ja25w</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:manualTask id="ManualTask_Instructions" name="Read RRP Instructions">
|
||||
<bpmn:documentation>### UNIVERSITY OF VIRGINIA RESEARCH
|
||||
#### Request to Ramp Up Research Activities
|
||||
<bpmn:documentation>## Beta Stage: All data entered will be destroyed before public launch
|
||||
|
||||
|
||||
Please note that only “critical research” deemed critical by the PI, department, and school will be ramped up in this phase II. As you fill out the names of personnel/students to request access for in this application, you will be asked for a brief description of the research they will be performing and why it is critical research.
|
||||
### UNIVERSITY OF VIRGINIA RESEARCH
|
||||
[From Research Ramp-up Guidance](https://research.virginia.edu/research-ramp-guidance)
|
||||
|
||||
|
||||
#### Support
|
||||
Report problems and/or submit questions to: askresearch@virginia.edu
|
||||
|
||||
#### Research Guidance
|
||||
|
||||
Our general principle is that only research activities requiring on-Grounds presence would be conducted on-Grounds. All other research-related work would continue to be performed by telework until restrictions are lifted. Separate school, department and building specific plans should supplement these guidelines.
|
||||
|
||||
|
||||
For research that needs to be on Grounds, the plan is to ramp up in phases with emphasis on safety. The goal of this document is to provide a central framework for resuming activities while allowing for coordinated school specific implementation strategies.
|
||||
|
||||
|
||||
The success of the ramp up depends on each researcher placing the safety of themselves and the people around them first, while conducting their research. In order to reduce our risks as much as possible, this must be a partnership between the researchers and the administration.
|
||||
|
||||
|
||||
Schools are developing a process for the approval of ramp up requests and enforcement of safety guidelines described in this document. The VPR office is working with the schools to provide the necessary support for business process infrastructure, and working with the COO’s office to coordinate the acquisition of supplies necessary including face coverings and sanitizing supplies.
|
||||
|
||||
**Instructions for Submitting:**
|
||||
|
||||
1. Add a Request for each lab space you manage in a building. If your lab spans multiple rooms or floors in a single building, one request will be required for that lab. If your lab spans multiple buildings, one request for each building will be required for that lab. The primary reason for this differentiation is that in addition to obtaining approval to restart operations, this information will also be used after start up to assist with any contact tracing that may be needed.
|
||||
2. Select each Request added and step through each form presented, responding to all required and applicable fields. You may be presented with different questions if activities in each lab differ.
|
||||
|
||||
1. The Research Ramp-up Plan allows for one request to be entered for a single Principle Investigator. In the form that follows enter the Primary Investigator this request is for and other identifying information. The PI's School and Supervisor will be used as needed for approval routing.
|
||||
2. Provide all available information in the forms that follow to provide an overview of where the research will resume, who will be involved, what supporting resources will be needed and what steps you have taken to assure compliance with [Research Ramp-up Guidance](https://research.virginia.edu/research-ramp-guidance).
|
||||
3. After all forms have been completed, you will be presented with the option to create your Research Recovery Plan in Word format. Download the document and review it. If you see any corrections that need to be made, return to the corresponding form and make the correction.
|
||||
4. Once the generated Research Recovery Plan is finalize, use the web site to submit it to the Office of the Vice President for Research for review.</bpmn:documentation>
|
||||
4. Once the generated Research Recovery Plan is finalize, proceed to the Plan Submission step to submit your plan for approval.</bpmn:documentation>
|
||||
<bpmn:incoming>SequenceFlow_05ja25w</bpmn:incoming>
|
||||
<bpmn:outgoing>SequenceFlow_0h50bp3</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:userTask id="Activity-PI_Info" name="Enter PI Info" camunda:formKey="PI Information">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="PIComputingID" label="UVA Computing ID of Primary Investigator" type="autocomplete">
|
||||
<camunda:formField id="PIComputingID" label="Primary Investigator" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="placeholder" value="cdr9c or Smith" />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
|
@ -100,6 +118,7 @@ Please note that only “critical research” deemed critical by the PI, departm
|
|||
<camunda:properties>
|
||||
<camunda:property id="description" value="Find the PI's Supervisor by entering Computing I D or Last Name." />
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="Required" config="true" />
|
||||
|
@ -125,6 +144,8 @@ Information on all researchers you are requesting approval for reentry into the
|
|||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="Personnel" />
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="description" value="Find by entering Computing ID or Last Name." />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
|
@ -143,7 +164,7 @@ Information on all researchers you are requesting approval for reentry into the
|
|||
</camunda:formField>
|
||||
<camunda:formField id="PersonnelSpace" label="Space they will work in" type="textarea">
|
||||
<camunda:properties>
|
||||
<camunda:property id="rows" value="5" />
|
||||
<camunda:property id="rows" value="2" />
|
||||
<camunda:property id="autosize" value="true" />
|
||||
<camunda:property id="description" value="Provide building and room number for each lab space this person will work in" />
|
||||
<camunda:property id="repeat" value="Personnel" />
|
||||
|
@ -152,21 +173,10 @@ Information on all researchers you are requesting approval for reentry into the
|
|||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="PersonnelWeeklySchedule" label="Weekly Schedule" type="textarea">
|
||||
<camunda:properties>
|
||||
<camunda:property id="rows" value="10" />
|
||||
<camunda:property id="autosize" value="true" />
|
||||
<camunda:property id="description" value="Provide typical weekly schedule for this person" />
|
||||
<camunda:property id="repeat" value="Personnel" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="PersonnelJustification" label="Research Justification" type="textarea">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Provide a brief description of this person’s research and justification why this is critical research." />
|
||||
<camunda:property id="rows" value="5" />
|
||||
<camunda:property id="rows" value="3" />
|
||||
<camunda:property id="autosize" value="true" />
|
||||
<camunda:property id="repeat" value="Personnel" />
|
||||
</camunda:properties>
|
||||
|
@ -174,6 +184,11 @@ Information on all researchers you are requesting approval for reentry into the
|
|||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="PersonnelWeeklySchedule" label="Personnel Weekly Schedule" type="files">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Upload a file or files of proposed weekly schedules for all personnel which are representative of compliance with ramp-up guidance." />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1eiud85</bpmn:incoming>
|
||||
|
@ -204,97 +219,20 @@ Information on all researchers you are requesting approval for reentry into the
|
|||
<bpmn:endEvent id="EndEvent_09wp7av">
|
||||
<bpmn:documentation>#### End of Workflow
|
||||
Place instruction here,</bpmn:documentation>
|
||||
<bpmn:incoming>Flow_00y3047</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_05w8yd6</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1e2qi9s" sourceRef="Activity_ReviewPlan" targetRef="Activity_SubmitPlan" />
|
||||
<bpmn:manualTask id="Activity_ReviewPlan" name="Review Plan">
|
||||
<bpmn:documentation>Review plan, make changes if needed, continue of ready to submit.</bpmn:documentation>
|
||||
<bpmn:incoming>Flow_06873ag</bpmn:incoming>
|
||||
<bpmn:sequenceFlow id="Flow_1e2qi9s" sourceRef="Activity_AcknowledgePlanReview" targetRef="Activity_ApprovalInfo" />
|
||||
<bpmn:manualTask id="Activity_AcknowledgePlanReview" name="Acknowledge Plan Review">
|
||||
<bpmn:documentation>Your Research Ramp-up Plan has been generated and is available in the Files pop-out found in the upper left hand corner of this application Click on the file name link to download the MS Word file to download, open and review. If changes are needed, choose the appropriate menu choice to make your edits, clicking Save when done. Note that you will need to revisit subsequent steps so the application can check to see if your edits impacted future workflow decisions. All your data will be persevered and you will need to click the Save button on each step to proceed.
|
||||
|
||||
When your Research Ramp-up Plan is complete and ready to submit for review and approval, click the Continue button below.</bpmn:documentation>
|
||||
<bpmn:incoming>Flow_0aqgwvu</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1e2qi9s</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:userTask id="Activity_SpaceInfo" name="Enter Shared Space Info" camunda:formKey="Space Involved in this Request">
|
||||
<bpmn:documentation>#### Space used by {{ PIComputingID.label }} and shared with other PIs.</bpmn:documentation>
|
||||
<bpmn:userTask id="Activity_SharedSpaceInfo" name="Enter Shared Space" camunda:formKey="Space Involved in this Request">
|
||||
<bpmn:documentation>#### Space used by {{ PIComputingID.label }} and shared with other PIs. If all space is exclusive and not shared with one or more other investigators, Click Save to skip this section and proceed to the next section.</bpmn:documentation>
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="SharedSpacePIComputingID" label="Additional PI" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
<camunda:property id="description" value="Enter Computing ID for PI who shares this space" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePISchool" label="Additional PI's School" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="SchoolList.xls" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="School Name" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptArchitecture" label="Select Primary Architecture Department" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="DepartmentList-Architecture.xlsx" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Label" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpacePISchool !== "Architecture" || model.PISchool === null" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptArtsSciences" label="Select Primary Arts & Sciences Department" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="DepartmentList-ArtsSciences.xlsx" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Label" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpacePISchool !== "Arts & Sciences" || model.PISchool === null" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptEducation" label="Select Primary Education Department" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="DepartmentList-Education.xlsx" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Label" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpacePISchool !== "Education" || model.SharedSpacePISchool === null" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptEngineering" label="Select Primary Engineering Department" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="DepartmentList-Engineering.xlsx" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Label" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpacePISchool !== "Engineering" || model.PISchool === null" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptMedicine" label="Select Primary Medicine Department/Center" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="DepartmentList-Medicine.xlsx" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Label" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpacePISchool !== "Medicine" || model.PISchool === null" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptProvostOffice" label="Select Primary Provost Office Department/Center" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="spreadsheet.name" value="DepartmentList-ProvostOffice.xlsx" />
|
||||
<camunda:property id="spreadsheet.value.column" value="Value" />
|
||||
<camunda:property id="spreadsheet.label.column" value="Label" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpacePISchool !== "Provost Office" || model.PISchool === null" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePIDeptOther" label="Primary Department " type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="hide_expression" value="(model.SharedSpacePIDeptArchitecture === null && model.SharedSpacePIDeptArtsSciences === null && model.SharedSpacePIDeptEducation === null && model.SharedSpacePIDeptEngineering === null && model.SharedSpacePIDeptMedicine === null && model.SharedSpacePIDeptProvostOffice === null) || (model.SharedSpacePIDeptArchitecture !== "Other" && model.SharedSpacePIDeptArtsSciences !== "Other" && model.SharedSpacePIDeptEducation !== "Other" && model.SharedSpacePIDeptEngineering !== "Other" && model.SharedSpacePIDeptMedicine !== "Other" && model.SharedSpacePIDeptProvostOffice !== "Other")" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
<camunda:property id="description" value="Enter the PI sharing this space primary department " />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpaceBuilding" label="Building Name" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Select the building where this shared lab space is housed." />
|
||||
|
@ -311,12 +249,19 @@ Place instruction here,</bpmn:documentation>
|
|||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
<camunda:property id="description" value="Enter the shared room number or other unique identifier" />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpaceBuilding === "Other"" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ShareSpaceRoomIDBuilding" label="Room No, and Building Name" type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Enter the Room No and Building of your shared space." />
|
||||
<camunda:property id="hide_expression" value="model.SharedSpaceBuilding !== "Other" | model.SharedSpaceBuilding === null" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpaceAMComputingID" label="Area Monitor" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="placeholder" value="cdr9c or Smith" />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
<camunda:property id="description" value="Enter Area Monitor's Computing ID or last name and select Area Monitor. Leave blank if not known." />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
|
@ -333,7 +278,6 @@ Place instruction here,</bpmn:documentation>
|
|||
<camunda:validation>
|
||||
<camunda:constraint name="min" config="1" />
|
||||
<camunda:constraint name="max" config="100" />
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpaceMaxPersonnel" label="Maximum Number of Personnel Occupying Space" type="long">
|
||||
|
@ -345,35 +289,46 @@ Place instruction here,</bpmn:documentation>
|
|||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="SharedSpacePI" label="Shared Space PI(s)" type="textarea">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="For each PI you share this space with, enter their Name, School, Department and Email Address." />
|
||||
<camunda:property id="rows" value="5" />
|
||||
<camunda:property id="repeat" value="Shared" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_19xeq76</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_16342pm</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:parallelGateway id="Gateway_0frfdnc">
|
||||
<bpmn:incoming>Flow_16y8glw</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_1v7r1tg</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_19xeq76</bpmn:outgoing>
|
||||
<bpmn:outgoing>Flow_0qf2y84</bpmn:outgoing>
|
||||
<bpmn:outgoing>Flow_15zy1q7</bpmn:outgoing>
|
||||
<bpmn:outgoing>Flow_0ya8hw8</bpmn:outgoing>
|
||||
</bpmn:parallelGateway>
|
||||
<bpmn:parallelGateway id="Gateway_1vj4zd3">
|
||||
<bpmn:incoming>Flow_16342pm</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_0tk64b6</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_12ie6w0</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_0zz2hbq</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_16342pm</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1eiud85</bpmn:outgoing>
|
||||
</bpmn:parallelGateway>
|
||||
<bpmn:sequenceFlow id="Flow_19xeq76" sourceRef="Gateway_0frfdnc" targetRef="Activity_SpaceInfo" />
|
||||
<bpmn:sequenceFlow id="Flow_16342pm" sourceRef="Activity_SpaceInfo" targetRef="Gateway_1vj4zd3" />
|
||||
<bpmn:sequenceFlow id="Flow_16y8glw" sourceRef="Activity-PI_Info" targetRef="Gateway_0frfdnc" />
|
||||
<bpmn:sequenceFlow id="Flow_19xeq76" sourceRef="Gateway_0frfdnc" targetRef="Activity_SharedSpaceInfo" />
|
||||
<bpmn:sequenceFlow id="Flow_16342pm" sourceRef="Activity_SharedSpaceInfo" targetRef="Gateway_1vj4zd3" />
|
||||
<bpmn:sequenceFlow id="Flow_16y8glw" sourceRef="Activity-PI_Info" targetRef="Activity_1u58hox" />
|
||||
<bpmn:sequenceFlow id="Flow_0qf2y84" sourceRef="Gateway_0frfdnc" targetRef="Activity_ExclusiveSpace" />
|
||||
<bpmn:sequenceFlow id="Flow_0tk64b6" sourceRef="Activity_ExclusiveSpace" targetRef="Gateway_1vj4zd3" />
|
||||
<bpmn:userTask id="Activity_ExclusiveSpace" name="Enter Exclusive Space Info" camunda:formKey="ExclusiveSpace">
|
||||
<bpmn:documentation>#### Space managed exclusively by {{ PIComputingID.label }}</bpmn:documentation>
|
||||
<bpmn:userTask id="Activity_ExclusiveSpace" name="Enter Exclusive Space" camunda:formKey="ExclusiveSpace">
|
||||
<bpmn:documentation>#### Space managed exclusively by {{ PIComputingID.label }}
|
||||
Submit one entry for each space the PI is the exclusive investigator. If all space is shared with one or more other investigators, Click Save to skip this section and proceed to the Shared Space section.</bpmn:documentation>
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="ExclusiveSpaceBuilding" label="Building Name" type="autocomplete">
|
||||
<camunda:formField id="ExclusiveSpaceBuilding" label="Room No. & Building Name" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Type key word to find the building in which the lab is located." />
|
||||
<camunda:property id="spreadsheet.name" value="BuildingList.xls" />
|
||||
|
@ -385,6 +340,17 @@ Place instruction here,</bpmn:documentation>
|
|||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceRoomID" label="Exclusive Space Room ID" type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="hide_expression" value="model.ExclusiveSpaceBuilding === "Other"" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceRoomIDBuilding" label="Room No, and Building Name" type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Enter the Room No and Building of your exclusive space." />
|
||||
<camunda:property id="hide_expression" value="model.ExclusiveSpaceBuilding !== "Other" | model.ExclusiveSpaceBuilding === null" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceType" label="Space Room Type" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
|
@ -396,21 +362,12 @@ Place instruction here,</bpmn:documentation>
|
|||
<camunda:value id="Lab" name="Lab" />
|
||||
<camunda:value id="Office" name="Office" />
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceRoomID" label="Room ID" type="string">
|
||||
<camunda:properties>
|
||||
<camunda:property id="description" value="Enter room number or other unique identifier" />
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
</camunda:properties>
|
||||
<camunda:validation>
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceAMComputingID" label="Area Monitor" type="autocomplete">
|
||||
<camunda:properties>
|
||||
<camunda:property id="ldap.lookup" value="true" />
|
||||
<camunda:property id="drescription" value="Enter Area Monitor's Computing ID or last name and select Area Monitor. Leave blank if not known." />
|
||||
<camunda:property id="description" value="Enter Area Monitor's Computing ID or last name and select Area Monitor. Leave blank if not known." />
|
||||
<camunda:property id="repeat" value="Exclusive" />
|
||||
<camunda:property id="placeholder" value="cdr9c or Smith" />
|
||||
<camunda:property id="placeholder" value="wxy0z or Smith" />
|
||||
</camunda:properties>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceSqFt" label="Enter the number of square feet usable by personnel in this space." type="long">
|
||||
|
@ -429,7 +386,6 @@ Place instruction here,</bpmn:documentation>
|
|||
<camunda:validation>
|
||||
<camunda:constraint name="min" config="1" />
|
||||
<camunda:constraint name="max" config="100" />
|
||||
<camunda:constraint name="required" config="true" />
|
||||
</camunda:validation>
|
||||
</camunda:formField>
|
||||
<camunda:formField id="ExclusiveSpaceMaxPersonnel" label="Maximum Number of Personnel Occupying Space" type="long">
|
||||
|
@ -532,9 +488,9 @@ Maintain social distancing by designing space between people to be at least 9 fe
|
|||
<bpmn:outgoing>Flow_0zrsh65</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:sequenceFlow id="Flow_1yqkpgu" sourceRef="Gateway_18jn18b" targetRef="Activity_HSR" />
|
||||
<bpmn:userTask id="Activity_HSR" name="Enter Health Safety Requirements" camunda:formKey="Lab Policy Statement">
|
||||
<bpmn:userTask id="Activity_HSR" name="Enter Health Safety Requirements" camunda:formKey="Lab Plan">
|
||||
<bpmn:documentation>#### Health Safety Requirements:
|
||||
Use EHS lab safety manual template for COVID (available May 25) to create and upload a copy of your laboratory policy statement to all members which includes at a minimum the following details:
|
||||
Use EHS [Lab Safety Plan During COVID 19 template](https://www.google.com/url?q=http://ehs.virginia.edu/files/Lab-Safety-Plan-During-COVID-19.docx&source=gmail&ust=1590687968958000&usg=AFQjCNE83uGDFtxGkKaxjuXGhTocu-FDmw) to create and upload a copy of your laboratory policy statement to all members which includes at a minimum the following details:
|
||||
- Laboratory face covering rules, use of other PPE use as required
|
||||
- Adherence to individual schedules, check-in, check out requirements
|
||||
- Completion of online EHS safety training requirement
|
||||
|
@ -544,7 +500,7 @@ Use EHS lab safety manual template for COVID (available May 25) to create and up
|
|||
- Where and how to obtain PPE including face covering</bpmn:documentation>
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="LabPolicyStatement" label="Upload Lab Policy Statement" type="file" />
|
||||
<camunda:formField id="LabPlan" label="Upload Lab Plan" type="file" />
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1yqkpgu</bpmn:incoming>
|
||||
|
@ -597,20 +553,19 @@ Use EHS lab safety manual template for COVID (available May 25) to create and up
|
|||
<bpmn:incoming>Flow_1c6m5wv</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0qbi47d</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:sequenceFlow id="Flow_1tmzfwl" sourceRef="Activity_SubmitPlan" targetRef="Activity_1tub2mc" />
|
||||
<bpmn:manualTask id="Activity_SubmitPlan" name="Acknowledge Plan Submission">
|
||||
<bpmn:documentation>#### By submitting this request, you understand that every member listed in this form for on Grounds laboratory access will:
|
||||
- Complete online COVID awareness & precaution training module (link forthcoming-May 25)
|
||||
- Complete daily health acknowledgement form signed (electronically) –email generated daily to those listed on your plan for access to on Grounds lab/research space
|
||||
- Fill out daily work attendance log for all lab members following your school process to check-in and out of work each day.</bpmn:documentation>
|
||||
<bpmn:incoming>Flow_1e2qi9s</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1tmzfwl</bpmn:outgoing>
|
||||
<bpmn:incoming>Flow_08njvvi</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0j4rs82</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_0zrsh65" sourceRef="Activity_PWA" targetRef="Gateway_0sijkgx" />
|
||||
<bpmn:sequenceFlow id="Flow_0tz5c2v" sourceRef="Activity_DistanceReq" targetRef="Gateway_0sijkgx" />
|
||||
<bpmn:sequenceFlow id="Flow_1ox5nv6" sourceRef="Activity_HSR" targetRef="Gateway_0sijkgx" />
|
||||
<bpmn:sequenceFlow id="Flow_0qbi47d" sourceRef="Activity_OtherReq" targetRef="Gateway_0sijkgx" />
|
||||
<bpmn:sequenceFlow id="Flow_06873ag" sourceRef="Gateway_0sijkgx" targetRef="Activity_ReviewPlan" />
|
||||
<bpmn:sequenceFlow id="Flow_06873ag" sourceRef="Gateway_0sijkgx" targetRef="Activity_1tub2mc" />
|
||||
<bpmn:parallelGateway id="Gateway_0sijkgx">
|
||||
<bpmn:incoming>Flow_0zrsh65</bpmn:incoming>
|
||||
<bpmn:incoming>Flow_0tz5c2v</bpmn:incoming>
|
||||
|
@ -618,190 +573,287 @@ Use EHS lab safety manual template for COVID (available May 25) to create and up
|
|||
<bpmn:incoming>Flow_0qbi47d</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_06873ag</bpmn:outgoing>
|
||||
</bpmn:parallelGateway>
|
||||
<bpmn:sequenceFlow id="Flow_00y3047" sourceRef="Activity_1tub2mc" targetRef="EndEvent_09wp7av" />
|
||||
<bpmn:scriptTask id="Activity_1tub2mc" name="Generate RRP">
|
||||
<bpmn:incoming>Flow_1tmzfwl</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_00y3047</bpmn:outgoing>
|
||||
<bpmn:incoming>Flow_06873ag</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0aqgwvu</bpmn:outgoing>
|
||||
<bpmn:script>CompleteTemplate ResearchRampUpPlan.docx RESEARCH_RAMPUP</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_0aqgwvu" sourceRef="Activity_1tub2mc" targetRef="Activity_AcknowledgePlanReview" />
|
||||
<bpmn:sequenceFlow id="Flow_0j4rs82" sourceRef="Activity_SubmitPlan" targetRef="Activity_0absozl" />
|
||||
<bpmn:sequenceFlow id="Flow_07ge8uf" sourceRef="Activity_0absozl" targetRef="Activity_ReviewStatus" />
|
||||
<bpmn:sequenceFlow id="Flow_1ufh44h" sourceRef="Activity_ReviewStatus" targetRef="Activity_0u9ic8o" />
|
||||
<bpmn:userTask id="Activity_ReviewStatus" name="Update Review Status" camunda:formKey="Review Status">
|
||||
<bpmn:extensionElements>
|
||||
<camunda:formData>
|
||||
<camunda:formField id="ApprovalReceived" label="Please Confirm:" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="enum_type" value="checkbox" />
|
||||
</camunda:properties>
|
||||
<camunda:value id="ApprovalNotificationReceived" name="Approval Notification Received" />
|
||||
</camunda:formField>
|
||||
<camunda:formField id="RequiredTraining" label="Please Confirm:" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="enum_type" value="checkbox" />
|
||||
</camunda:properties>
|
||||
<camunda:value id="AllRequiredTraining" name="All Required Training Completed" />
|
||||
</camunda:formField>
|
||||
<camunda:formField id="NeededSupplies" label="Please Confirm"" type="enum">
|
||||
<camunda:properties>
|
||||
<camunda:property id="enum_type" value="checkbox" />
|
||||
</camunda:properties>
|
||||
<camunda:value id="NeededSupplies" name="All Supplies Needed Have Been Secured" />
|
||||
</camunda:formField>
|
||||
</camunda:formData>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_07ge8uf</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1ufh44h</bpmn:outgoing>
|
||||
</bpmn:userTask>
|
||||
<bpmn:sequenceFlow id="Flow_05w8yd6" sourceRef="Activity_1pklpot" targetRef="EndEvent_09wp7av" />
|
||||
<bpmn:sequenceFlow id="Flow_08njvvi" sourceRef="Activity_ApprovalInfo" targetRef="Activity_SubmitPlan" />
|
||||
<bpmn:businessRuleTask id="Activity_ApprovalInfo" name="Approval Info" camunda:decisionRef="Decision_ApprovalInfo">
|
||||
<bpmn:incoming>Flow_1e2qi9s</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_08njvvi</bpmn:outgoing>
|
||||
</bpmn:businessRuleTask>
|
||||
<bpmn:manualTask id="Activity_1pklpot" name="Review What's Next?">
|
||||
<bpmn:incoming>Flow_0cpmvcw</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_05w8yd6</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_0cpmvcw" sourceRef="Activity_0u9ic8o" targetRef="Activity_1pklpot" />
|
||||
<bpmn:manualTask id="Activity_0u9ic8o" name="Send Area Monitor Notification">
|
||||
<bpmn:incoming>Flow_1ufh44h</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0cpmvcw</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:scriptTask id="Activity_0absozl" name="Execute Plan Submission">
|
||||
<bpmn:incoming>Flow_0j4rs82</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_07ge8uf</bpmn:outgoing>
|
||||
<bpmn:script>RequestApproval ApprvlApprvr1 ApprvlApprvr2</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_1v7r1tg" sourceRef="Activity_1u58hox" targetRef="Gateway_0frfdnc" />
|
||||
<bpmn:scriptTask id="Activity_1u58hox" name="Update Request">
|
||||
<bpmn:incoming>Flow_16y8glw</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1v7r1tg</bpmn:outgoing>
|
||||
<bpmn:script>UpdateStudy title:PIComputingID.label pi:PIComputingID.value</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0ssahs9">
|
||||
<bpmndi:BPMNEdge id="Flow_00y3047_di" bpmnElement="Flow_00y3047">
|
||||
<di:waypoint x="1910" y="307" />
|
||||
<di:waypoint x="1972" y="307" />
|
||||
<bpmndi:BPMNEdge id="Flow_1v7r1tg_di" bpmnElement="Flow_1v7r1tg">
|
||||
<di:waypoint x="630" y="307" />
|
||||
<di:waypoint x="685" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0cpmvcw_di" bpmnElement="Flow_0cpmvcw">
|
||||
<di:waypoint x="2470" y="307" />
|
||||
<di:waypoint x="2520" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_08njvvi_di" bpmnElement="Flow_08njvvi">
|
||||
<di:waypoint x="1900" y="307" />
|
||||
<di:waypoint x="1930" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_05w8yd6_di" bpmnElement="Flow_05w8yd6">
|
||||
<di:waypoint x="2620" y="307" />
|
||||
<di:waypoint x="2692" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ufh44h_di" bpmnElement="Flow_1ufh44h">
|
||||
<di:waypoint x="2330" y="307" />
|
||||
<di:waypoint x="2370" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_07ge8uf_di" bpmnElement="Flow_07ge8uf">
|
||||
<di:waypoint x="2180" y="307" />
|
||||
<di:waypoint x="2230" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0j4rs82_di" bpmnElement="Flow_0j4rs82">
|
||||
<di:waypoint x="2030" y="307" />
|
||||
<di:waypoint x="2080" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0aqgwvu_di" bpmnElement="Flow_0aqgwvu">
|
||||
<di:waypoint x="1640" y="307" />
|
||||
<di:waypoint x="1670" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_06873ag_di" bpmnElement="Flow_06873ag">
|
||||
<di:waypoint x="1415" y="307" />
|
||||
<di:waypoint x="1490" y="307" />
|
||||
<di:waypoint x="1495" y="307" />
|
||||
<di:waypoint x="1540" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0qbi47d_di" bpmnElement="Flow_0qbi47d">
|
||||
<di:waypoint x="1320" y="510" />
|
||||
<di:waypoint x="1390" y="510" />
|
||||
<di:waypoint x="1390" y="332" />
|
||||
<di:waypoint x="1400" y="510" />
|
||||
<di:waypoint x="1470" y="510" />
|
||||
<di:waypoint x="1470" y="332" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ox5nv6_di" bpmnElement="Flow_1ox5nv6">
|
||||
<di:waypoint x="1320" y="380" />
|
||||
<di:waypoint x="1390" y="380" />
|
||||
<di:waypoint x="1390" y="332" />
|
||||
<di:waypoint x="1400" y="380" />
|
||||
<di:waypoint x="1470" y="380" />
|
||||
<di:waypoint x="1470" y="332" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0tz5c2v_di" bpmnElement="Flow_0tz5c2v">
|
||||
<di:waypoint x="1320" y="120" />
|
||||
<di:waypoint x="1390" y="120" />
|
||||
<di:waypoint x="1390" y="282" />
|
||||
<di:waypoint x="1400" y="120" />
|
||||
<di:waypoint x="1470" y="120" />
|
||||
<di:waypoint x="1470" y="282" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0zrsh65_di" bpmnElement="Flow_0zrsh65">
|
||||
<di:waypoint x="1320" y="240" />
|
||||
<di:waypoint x="1390" y="240" />
|
||||
<di:waypoint x="1390" y="282" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1tmzfwl_di" bpmnElement="Flow_1tmzfwl">
|
||||
<di:waypoint x="1740" y="307" />
|
||||
<di:waypoint x="1810" y="307" />
|
||||
<di:waypoint x="1400" y="240" />
|
||||
<di:waypoint x="1470" y="240" />
|
||||
<di:waypoint x="1470" y="282" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1c6m5wv_di" bpmnElement="Flow_1c6m5wv">
|
||||
<di:waypoint x="1150" y="332" />
|
||||
<di:waypoint x="1150" y="510" />
|
||||
<di:waypoint x="1220" y="510" />
|
||||
<di:waypoint x="1230" y="332" />
|
||||
<di:waypoint x="1230" y="510" />
|
||||
<di:waypoint x="1300" y="510" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1yqkpgu_di" bpmnElement="Flow_1yqkpgu">
|
||||
<di:waypoint x="1150" y="332" />
|
||||
<di:waypoint x="1150" y="380" />
|
||||
<di:waypoint x="1220" y="380" />
|
||||
<di:waypoint x="1230" y="332" />
|
||||
<di:waypoint x="1230" y="380" />
|
||||
<di:waypoint x="1300" y="380" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0mkh1wn_di" bpmnElement="Flow_0mkh1wn">
|
||||
<di:waypoint x="1150" y="282" />
|
||||
<di:waypoint x="1150" y="240" />
|
||||
<di:waypoint x="1220" y="240" />
|
||||
<di:waypoint x="1230" y="282" />
|
||||
<di:waypoint x="1230" y="240" />
|
||||
<di:waypoint x="1300" y="240" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0p2r1bo_di" bpmnElement="Flow_0p2r1bo">
|
||||
<di:waypoint x="1150" y="282" />
|
||||
<di:waypoint x="1150" y="120" />
|
||||
<di:waypoint x="1220" y="120" />
|
||||
<di:waypoint x="1230" y="282" />
|
||||
<di:waypoint x="1230" y="120" />
|
||||
<di:waypoint x="1300" y="120" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1nbjr72_di" bpmnElement="Flow_1nbjr72">
|
||||
<di:waypoint x="1060" y="307" />
|
||||
<di:waypoint x="1125" y="307" />
|
||||
<di:waypoint x="1150" y="307" />
|
||||
<di:waypoint x="1205" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1eiud85_di" bpmnElement="Flow_1eiud85">
|
||||
<di:waypoint x="905" y="307" />
|
||||
<di:waypoint x="960" y="307" />
|
||||
<di:waypoint x="995" y="307" />
|
||||
<di:waypoint x="1050" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0zz2hbq_di" bpmnElement="Flow_0zz2hbq">
|
||||
<di:waypoint x="810" y="510" />
|
||||
<di:waypoint x="880" y="510" />
|
||||
<di:waypoint x="880" y="332" />
|
||||
<di:waypoint x="890" y="510" />
|
||||
<di:waypoint x="970" y="510" />
|
||||
<di:waypoint x="970" y="332" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0ya8hw8_di" bpmnElement="Flow_0ya8hw8">
|
||||
<di:waypoint x="630" y="332" />
|
||||
<di:waypoint x="630" y="510" />
|
||||
<di:waypoint x="710" y="332" />
|
||||
<di:waypoint x="710" y="510" />
|
||||
<di:waypoint x="790" y="510" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_12ie6w0_di" bpmnElement="Flow_12ie6w0">
|
||||
<di:waypoint x="810" y="370" />
|
||||
<di:waypoint x="880" y="370" />
|
||||
<di:waypoint x="880" y="332" />
|
||||
<di:waypoint x="890" y="370" />
|
||||
<di:waypoint x="970" y="370" />
|
||||
<di:waypoint x="970" y="332" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_15zy1q7_di" bpmnElement="Flow_15zy1q7">
|
||||
<di:waypoint x="630" y="332" />
|
||||
<di:waypoint x="630" y="370" />
|
||||
<di:waypoint x="710" y="332" />
|
||||
<di:waypoint x="710" y="370" />
|
||||
<di:waypoint x="790" y="370" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0tk64b6_di" bpmnElement="Flow_0tk64b6">
|
||||
<di:waypoint x="810" y="110" />
|
||||
<di:waypoint x="880" y="110" />
|
||||
<di:waypoint x="880" y="282" />
|
||||
<di:waypoint x="890" y="110" />
|
||||
<di:waypoint x="970" y="110" />
|
||||
<di:waypoint x="970" y="282" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0qf2y84_di" bpmnElement="Flow_0qf2y84">
|
||||
<di:waypoint x="630" y="282" />
|
||||
<di:waypoint x="630" y="110" />
|
||||
<di:waypoint x="710" y="282" />
|
||||
<di:waypoint x="710" y="110" />
|
||||
<di:waypoint x="790" y="110" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16y8glw_di" bpmnElement="Flow_16y8glw">
|
||||
<di:waypoint x="550" y="307" />
|
||||
<di:waypoint x="605" y="307" />
|
||||
<di:waypoint x="480" y="307" />
|
||||
<di:waypoint x="530" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_16342pm_di" bpmnElement="Flow_16342pm">
|
||||
<di:waypoint x="810" y="240" />
|
||||
<di:waypoint x="880" y="240" />
|
||||
<di:waypoint x="880" y="282" />
|
||||
<di:waypoint x="890" y="240" />
|
||||
<di:waypoint x="970" y="240" />
|
||||
<di:waypoint x="970" y="282" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_19xeq76_di" bpmnElement="Flow_19xeq76">
|
||||
<di:waypoint x="630" y="282" />
|
||||
<di:waypoint x="630" y="240" />
|
||||
<di:waypoint x="710" y="282" />
|
||||
<di:waypoint x="710" y="240" />
|
||||
<di:waypoint x="790" y="240" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1e2qi9s_di" bpmnElement="Flow_1e2qi9s">
|
||||
<di:waypoint x="1590" y="307" />
|
||||
<di:waypoint x="1640" y="307" />
|
||||
<di:waypoint x="1770" y="307" />
|
||||
<di:waypoint x="1800" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_05ja25w_di" bpmnElement="SequenceFlow_05ja25w">
|
||||
<di:waypoint x="228" y="307" />
|
||||
<di:waypoint x="280" y="307" />
|
||||
<di:waypoint x="168" y="307" />
|
||||
<di:waypoint x="230" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0h50bp3_di" bpmnElement="SequenceFlow_0h50bp3">
|
||||
<di:waypoint x="330" y="307" />
|
||||
<di:waypoint x="380" y="307" />
|
||||
<di:waypoint x="450" y="307" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="192" y="289" width="36" height="36" />
|
||||
<dc:Bounds x="132" y="289" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ManualTask_1ofy9yz_di" bpmnElement="ManualTask_Instructions">
|
||||
<dc:Bounds x="280" y="267" width="100" height="80" />
|
||||
<dc:Bounds x="230" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0xdpoxl_di" bpmnElement="Activity-PI_Info">
|
||||
<dc:Bounds x="450" y="267" width="100" height="80" />
|
||||
<dc:Bounds x="380" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0ecab9j_di" bpmnElement="Personnel">
|
||||
<dc:Bounds x="960" y="267" width="100" height="80" />
|
||||
<dc:Bounds x="1050" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_0l8vxty_di" bpmnElement="UserTask_CoreResource">
|
||||
<dc:Bounds x="710" y="330" width="100" height="80" />
|
||||
<dc:Bounds x="790" y="330" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="EndEvent_09wp7av_di" bpmnElement="EndEvent_09wp7av">
|
||||
<dc:Bounds x="1972" y="289" width="36" height="36" />
|
||||
<dc:Bounds x="2692" y="289" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1mg5lp9_di" bpmnElement="Activity_ReviewPlan">
|
||||
<dc:Bounds x="1490" y="267" width="100" height="80" />
|
||||
<bpmndi:BPMNShape id="Activity_1mg5lp9_di" bpmnElement="Activity_AcknowledgePlanReview">
|
||||
<dc:Bounds x="1670" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1xgrlzr_di" bpmnElement="Activity_SpaceInfo">
|
||||
<dc:Bounds x="710" y="200" width="100" height="80" />
|
||||
<bpmndi:BPMNShape id="Activity_1xgrlzr_di" bpmnElement="Activity_SharedSpaceInfo">
|
||||
<dc:Bounds x="790" y="200" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0tn2il3_di" bpmnElement="Gateway_0frfdnc">
|
||||
<dc:Bounds x="605" y="282" width="50" height="50" />
|
||||
<dc:Bounds x="685" y="282" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_1o1fcbg_di" bpmnElement="Gateway_1vj4zd3">
|
||||
<dc:Bounds x="855" y="282" width="50" height="50" />
|
||||
<dc:Bounds x="945" y="282" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1jefdme_di" bpmnElement="Activity_ExclusiveSpace">
|
||||
<dc:Bounds x="710" y="70" width="100" height="80" />
|
||||
<dc:Bounds x="790" y="70" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0ysw6zo_di" bpmnElement="Activity_nonUVASpaces">
|
||||
<dc:Bounds x="710" y="470" width="100" height="80" />
|
||||
<dc:Bounds x="790" y="470" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1xag6qb_di" bpmnElement="Activity_DistanceReq">
|
||||
<dc:Bounds x="1220" y="80" width="100" height="80" />
|
||||
<dc:Bounds x="1300" y="80" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_0yof76x_di" bpmnElement="Gateway_18jn18b">
|
||||
<dc:Bounds x="1125" y="282" width="50" height="50" />
|
||||
<dc:Bounds x="1205" y="282" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_166b0qq_di" bpmnElement="Activity_PWA">
|
||||
<dc:Bounds x="1220" y="200" width="100" height="80" />
|
||||
<dc:Bounds x="1300" y="200" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0byj9mp_di" bpmnElement="Activity_HSR">
|
||||
<dc:Bounds x="1220" y="340" width="100" height="80" />
|
||||
<dc:Bounds x="1300" y="340" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0j0p13i_di" bpmnElement="Activity_OtherReq">
|
||||
<dc:Bounds x="1220" y="470" width="100" height="80" />
|
||||
<dc:Bounds x="1300" y="470" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_1h30fo8_di" bpmnElement="Activity_SubmitPlan">
|
||||
<dc:Bounds x="1640" y="267" width="100" height="80" />
|
||||
<dc:Bounds x="1930" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Gateway_03lxnzh_di" bpmnElement="Gateway_0sijkgx">
|
||||
<dc:Bounds x="1365" y="282" width="50" height="50" />
|
||||
<dc:Bounds x="1445" y="282" width="50" height="50" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_117owwi_di" bpmnElement="Activity_1tub2mc">
|
||||
<dc:Bounds x="1810" y="267" width="100" height="80" />
|
||||
<dc:Bounds x="1540" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0fxf44t_di" bpmnElement="Activity_ReviewStatus">
|
||||
<dc:Bounds x="2230" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_080o38p_di" bpmnElement="Activity_ApprovalInfo">
|
||||
<dc:Bounds x="1800" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0wuukfn_di" bpmnElement="Activity_1pklpot">
|
||||
<dc:Bounds x="2520" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0js9ww9_di" bpmnElement="Activity_0u9ic8o">
|
||||
<dc:Bounds x="2370" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0wnn9de_di" bpmnElement="Activity_0absozl">
|
||||
<dc:Bounds x="2080" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0f0ak6p_di" bpmnElement="Activity_1u58hox">
|
||||
<dc:Bounds x="530" y="267" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
Binary file not shown.
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_1v1rp1q" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="empty_workflow" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>SequenceFlow_0lvudp8</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="SequenceFlow_0lvudp8" sourceRef="StartEvent_1" targetRef="EndEvent_0q4qzl9" />
|
||||
<bpmn:endEvent id="EndEvent_0q4qzl9">
|
||||
<bpmn:incoming>SequenceFlow_0lvudp8</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="empty_workflow">
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0lvudp8_di" bpmnElement="SequenceFlow_0lvudp8">
|
||||
<di:waypoint x="238" y="117" />
|
||||
<di:waypoint x="432" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="EndEvent_0q4qzl9_di" bpmnElement="EndEvent_0q4qzl9">
|
||||
<dc:Bounds x="432" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="202" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -209,19 +209,17 @@ class ExampleDataLoader:
|
|||
db.session.add(category)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
self.create_spec(id="empty_workflow",
|
||||
name="empty_workflow",
|
||||
self.create_spec(id="rrt_top_level_workflow",
|
||||
name="rrt_top_level_workflow",
|
||||
display_name="Top Level Workflow",
|
||||
description="Does nothing, we don't use the master workflow here.",
|
||||
category_id=None,
|
||||
master_spec=True,
|
||||
from_tests=True)
|
||||
master_spec=True)
|
||||
|
||||
self.create_spec(id="rrt",
|
||||
name="rrt",
|
||||
self.create_spec(id="research_rampup",
|
||||
name="research_rampup",
|
||||
display_name="Research Ramp-up Toolkit",
|
||||
description="The workflow for the Research Ramp-up.",
|
||||
description="Process for creating a new research ramp-up request.",
|
||||
category_id=0,
|
||||
master_spec=False)
|
||||
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 23c62c933848
|
||||
Revises: 9b43e725f39c
|
||||
Create Date: 2020-05-28 10:30:49.409760
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '23c62c933848'
|
||||
down_revision = '9b43e725f39c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint('file_study_id_fkey', 'file', type_='foreignkey')
|
||||
op.drop_column('file', 'task_id')
|
||||
op.drop_column('file', 'study_id')
|
||||
op.drop_column('file', 'form_field_key')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('file', sa.Column('form_field_key', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.add_column('file', sa.Column('study_id', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('file', sa.Column('task_id', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.create_foreign_key('file_study_id_fkey', 'file', 'study', ['study_id'], ['id'])
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,36 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: 5064b72284b7
|
||||
Revises: bec71f7dc652
|
||||
Create Date: 2020-05-28 23:54:45.623361
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5064b72284b7'
|
||||
down_revision = 'bec71f7dc652'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('lookup_file', sa.Column('field_id', sa.String(), nullable=True))
|
||||
op.add_column('lookup_file', sa.Column('is_ldap', sa.Boolean(), nullable=True))
|
||||
op.add_column('lookup_file', sa.Column('workflow_spec_id', sa.String(), nullable=True))
|
||||
op.drop_column('lookup_file', 'value_column')
|
||||
op.drop_column('lookup_file', 'label_column')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('lookup_file', sa.Column('label_column', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.add_column('lookup_file', sa.Column('value_column', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.drop_column('lookup_file', 'workflow_spec_id')
|
||||
op.drop_column('lookup_file', 'is_ldap')
|
||||
op.drop_column('lookup_file', 'field_id')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,63 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: bec71f7dc652
|
||||
Revises: 23c62c933848
|
||||
Create Date: 2020-05-28 20:08:45.891406
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'bec71f7dc652'
|
||||
down_revision = '23c62c933848'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.create_table('workflow_spec_dependency_file',
|
||||
sa.Column('file_data_id', sa.Integer(), nullable=False),
|
||||
sa.Column('workflow_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['file_data_id'], ['file_data.id'], ),
|
||||
sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ),
|
||||
sa.PrimaryKeyConstraint('file_data_id', 'workflow_id')
|
||||
)
|
||||
op.drop_column('approval', 'workflow_hash')
|
||||
op.execute(
|
||||
"""
|
||||
delete from approval_file;
|
||||
delete from approval;
|
||||
"""
|
||||
)
|
||||
op.add_column('approval_file', sa.Column('file_data_id', sa.Integer(), nullable=False))
|
||||
op.drop_constraint('approval_file_file_id_fkey', 'approval_file', type_='foreignkey')
|
||||
op.create_foreign_key(None, 'approval_file', 'file_data', ['file_data_id'], ['id'])
|
||||
op.drop_column('approval_file', 'id')
|
||||
op.drop_column('approval_file', 'file_version')
|
||||
op.drop_column('approval_file', 'file_id')
|
||||
op.drop_column('file', 'latest_version')
|
||||
op.add_column('file_data', sa.Column('date_created', sa.DateTime(timezone=True), nullable=True))
|
||||
op.drop_column('file_data', 'last_updated')
|
||||
op.drop_column('workflow', 'spec_version')
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('workflow', sa.Column('spec_version', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.add_column('file_data', sa.Column('last_updated', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True))
|
||||
op.drop_column('file_data', 'date_created')
|
||||
op.add_column('file', sa.Column('latest_version', sa.INTEGER(), autoincrement=False, nullable=True))
|
||||
op.add_column('approval_file', sa.Column('file_id', sa.INTEGER(), autoincrement=False, nullable=False))
|
||||
op.add_column('approval_file', sa.Column('file_version', sa.INTEGER(), autoincrement=False, nullable=False))
|
||||
op.add_column('approval_file', sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False))
|
||||
op.drop_constraint(None, 'approval_file', type_='foreignkey')
|
||||
op.create_foreign_key('approval_file_file_id_fkey', 'approval_file', 'file', ['file_id'], ['id'])
|
||||
op.drop_column('approval_file', 'file_data_id')
|
||||
op.add_column('approval', sa.Column('workflow_hash', sa.VARCHAR(), autoincrement=False, nullable=True))
|
||||
op.drop_table('workflow_spec_dependency_file')
|
||||
# ### end Alembic commands ###
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96a17d9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.4.1">
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96a17d9" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
|
||||
<bpmn:process id="Process_93a29b3" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>SequenceFlow_0637d8i</bpmn:outgoing>
|
||||
|
@ -27,7 +27,7 @@
|
|||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>SequenceFlow_1i7hk1a</bpmn:incoming>
|
||||
<bpmn:outgoing>SequenceFlow_11c35oq</bpmn:outgoing>
|
||||
<bpmn:script>CompleteTemplate Letter.docx AncillaryDocument.CoCApplication</bpmn:script>
|
||||
<bpmn:script>CompleteTemplate Letter.docx AD_CoCApp</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:endEvent id="EndEvent_0evb22x">
|
||||
<bpmn:incoming>SequenceFlow_11c35oq</bpmn:incoming>
|
||||
|
@ -36,30 +36,30 @@
|
|||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_93a29b3">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0637d8i_di" bpmnElement="SequenceFlow_0637d8i">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="265" y="117" />
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_11c35oq_di" bpmnElement="SequenceFlow_11c35oq">
|
||||
<di:waypoint x="565" y="117" />
|
||||
<di:waypoint x="665" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="UserTask_02o51o8_di" bpmnElement="task_gather_information">
|
||||
<dc:Bounds x="265" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_1i7hk1a_di" bpmnElement="SequenceFlow_1i7hk1a">
|
||||
<di:waypoint x="365" y="117" />
|
||||
<di:waypoint x="465" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_0637d8i_di" bpmnElement="SequenceFlow_0637d8i">
|
||||
<di:waypoint x="215" y="117" />
|
||||
<di:waypoint x="265" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="UserTask_02o51o8_di" bpmnElement="task_gather_information">
|
||||
<dc:Bounds x="265" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="ScriptTask_0xjh8x4_di" bpmnElement="task_generate_document">
|
||||
<dc:Bounds x="465" y="77" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="EndEvent_0evb22x_di" bpmnElement="EndEvent_0evb22x">
|
||||
<dc:Bounds x="665" y="99" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="SequenceFlow_11c35oq_di" bpmnElement="SequenceFlow_11c35oq">
|
||||
<di:waypoint x="565" y="117" />
|
||||
<di:waypoint x="665" y="117" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -9,7 +9,12 @@ from crc.services.workflow_processor import WorkflowProcessor
|
|||
class TestApprovalsService(BaseTest):
|
||||
|
||||
def test_create_approval_record(self):
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow("empty_workflow")
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" )
|
||||
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
self.assertEquals(1, db.session.query(ApprovalModel).count())
|
||||
model = db.session.query(ApprovalModel).first()
|
||||
|
@ -17,10 +22,14 @@ class TestApprovalsService(BaseTest):
|
|||
self.assertEquals(workflow.id, model.workflow_id)
|
||||
self.assertEquals("dhf8r", model.approver_uid)
|
||||
self.assertEquals(1, model.version)
|
||||
self.assertIsNotNone(model.workflow_hash)
|
||||
|
||||
def test_new_requests_dont_add_if_approval_exists_for_current_workflow(self):
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow("empty_workflow")
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" )
|
||||
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
self.assertEquals(1, db.session.query(ApprovalModel).count())
|
||||
|
@ -31,17 +40,15 @@ class TestApprovalsService(BaseTest):
|
|||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow('empty_workflow')
|
||||
processor = WorkflowProcessor(workflow)
|
||||
task = processor.next_task()
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code="AD_CoCAppr")
|
||||
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
|
||||
irb_code_1 = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
|
||||
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
task_id=task.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code=irb_code_1)
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr")
|
||||
|
||||
ApprovalService.add_approval(study_id=workflow.study_id, workflow_id=workflow.id, approver_uid="dhf8r")
|
||||
self.assertEquals(2, db.session.query(ApprovalModel).count())
|
||||
|
@ -50,45 +57,3 @@ class TestApprovalsService(BaseTest):
|
|||
self.assertEquals(2, models[1].version)
|
||||
|
||||
|
||||
def test_generate_workflow_hash_and_version(self):
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
workflow = self.create_workflow('empty_workflow')
|
||||
processor = WorkflowProcessor(workflow)
|
||||
task = processor.next_task()
|
||||
irb_code_1 = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
|
||||
irb_code_2 = "NonUVAIRB_AssuranceForm" # The second file in above.
|
||||
# Add a task file to the workflow.
|
||||
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
task_id=task.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code=irb_code_1)
|
||||
# Add a two form field files with the same irb_code, but
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
task_id=task.id,
|
||||
form_field_key=irb_code_2,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234')
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
form_field_key=irb_code_2,
|
||||
task_id=task.id,
|
||||
name="another_anything.png", content_type="text",
|
||||
binary_data=b'5678')
|
||||
|
||||
|
||||
# Workflow hash should look be id[1]-id[1]-id[1]
|
||||
# Sould be three files, each with a version of 1.
|
||||
# where id is the file id, which we don't know, thus the regex.
|
||||
latest_files = FileService.get_workflow_files(workflow.id)
|
||||
self.assertRegexpMatches(ApprovalService._generate_workflow_hash(latest_files), "\d+\[1\]-\d+\[1\]-\d+\[1\]")
|
||||
|
||||
# Replace last file
|
||||
# should now be id[1]-id[1]-id[2]
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
form_field_key=irb_code_2,
|
||||
task_id=task.id,
|
||||
name="another_anything.png", content_type="text",
|
||||
binary_data=b'9999')
|
||||
self.assertRegexpMatches(ApprovalService._generate_workflow_hash(latest_files), "\d+\[1\]-\d+\[1\]-\d+\[2\]")
|
||||
|
||||
|
|
|
@ -13,21 +13,21 @@ class TestFileService(BaseTest):
|
|||
processor = WorkflowProcessor(workflow)
|
||||
task = processor.next_task()
|
||||
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
|
||||
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
task_id=task.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code=irb_code)
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code=irb_code)
|
||||
# Add the file again with different data
|
||||
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
task_id=task.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code=irb_code)
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678', irb_doc_code=irb_code)
|
||||
|
||||
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
|
||||
self.assertEquals(1, len(file_models))
|
||||
self.assertEquals(2, file_models[0].latest_version)
|
||||
|
||||
file_data = FileService.get_workflow_data_files(workflow_id=workflow.id)
|
||||
self.assertEquals(1, len(file_data))
|
||||
self.assertEquals(2, file_data[0].version)
|
||||
|
||||
|
||||
def test_add_file_from_form_increments_version_and_replaces_on_subsequent_add_with_same_name(self):
|
||||
self.load_example_data()
|
||||
|
@ -36,21 +36,22 @@ class TestFileService(BaseTest):
|
|||
processor = WorkflowProcessor(workflow)
|
||||
task = processor.next_task()
|
||||
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
task_id=task.id,
|
||||
form_field_key=irb_code,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234')
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
irb_doc_code=irb_code,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234')
|
||||
# Add the file again with different data
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
form_field_key=irb_code,
|
||||
task_id=task.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678')
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
irb_doc_code=irb_code,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'5678')
|
||||
|
||||
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
|
||||
self.assertEquals(1, len(file_models))
|
||||
self.assertEquals(2, file_models[0].latest_version)
|
||||
|
||||
file_data = FileService.get_workflow_data_files(workflow_id=workflow.id)
|
||||
self.assertEquals(1, len(file_data))
|
||||
self.assertEquals(2, file_data[0].version)
|
||||
|
||||
def test_add_file_from_form_allows_multiple_files_with_different_names(self):
|
||||
self.load_example_data()
|
||||
|
@ -59,18 +60,14 @@ class TestFileService(BaseTest):
|
|||
processor = WorkflowProcessor(workflow)
|
||||
task = processor.next_task()
|
||||
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
task_id=task.id,
|
||||
form_field_key=irb_code,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234')
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
irb_doc_code=irb_code,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234')
|
||||
# Add the file again with different data
|
||||
FileService.add_form_field_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
form_field_key=irb_code,
|
||||
task_id=task.id,
|
||||
name="a_different_thing.png", content_type="text",
|
||||
binary_data=b'5678')
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
irb_doc_code=irb_code,
|
||||
name="a_different_thing.png", content_type="text",
|
||||
binary_data=b'5678')
|
||||
file_models = FileService.get_workflow_files(workflow_id=workflow.id)
|
||||
self.assertEquals(2, len(file_models))
|
||||
self.assertEquals(1, file_models[0].latest_version)
|
||||
self.assertEquals(1, file_models[1].latest_version)
|
|
@ -1,15 +1,14 @@
|
|||
import io
|
||||
import json
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import session
|
||||
from crc.models.file import FileModel, FileType, FileModelSchema, FileDataModel
|
||||
from crc.models.file import FileModel, FileType, FileSchema, FileModelSchema
|
||||
from crc.models.workflow import WorkflowSpecModel
|
||||
from crc.services.file_service import FileService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from example_data import ExampleDataLoader
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
|
||||
class TestFilesApi(BaseTest):
|
||||
|
@ -166,16 +165,16 @@ class TestFilesApi(BaseTest):
|
|||
content_type='multipart/form-data', headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
||||
self.assertIsNotNone(rv.get_data())
|
||||
json_data = json.loads(rv.get_data(as_text=True))
|
||||
file = FileModelSchema().load(json_data, session=session)
|
||||
self.assertEqual(2, file.latest_version)
|
||||
self.assertEqual(FileType.bpmn, file.type)
|
||||
self.assertEqual("application/octet-stream", file.content_type)
|
||||
file_json = json.loads(rv.get_data(as_text=True))
|
||||
self.assertEqual(2, file_json['latest_version'])
|
||||
self.assertEqual(FileType.bpmn.value, file_json['type'])
|
||||
self.assertEqual("application/octet-stream", file_json['content_type'])
|
||||
self.assertEqual(spec.id, file.workflow_spec_id)
|
||||
|
||||
# Assure it is updated in the database and properly persisted.
|
||||
file_model = session.query(FileModel).filter(FileModel.id == file.id).first()
|
||||
self.assertEqual(2, file_model.latest_version)
|
||||
file_data = FileService.get_file_data(file_model.id)
|
||||
self.assertEqual(2, file_data.version)
|
||||
|
||||
rv = self.app.get('/v1.0/file/%i/data' % file.id, headers=self.logged_in_headers())
|
||||
self.assert_success(rv)
|
||||
|
@ -192,15 +191,13 @@ class TestFilesApi(BaseTest):
|
|||
content_type='multipart/form-data', headers=self.logged_in_headers())
|
||||
self.assertIsNotNone(rv.get_data())
|
||||
json_data = json.loads(rv.get_data(as_text=True))
|
||||
file = FileModelSchema().load(json_data, session=session)
|
||||
self.assertEqual(1, file.latest_version)
|
||||
self.assertEqual(1, json_data['latest_version'])
|
||||
data['file'] = io.BytesIO(self.minimal_bpmn("abcdef")), 'my_new_file.bpmn'
|
||||
rv = self.app.put('/v1.0/file/%i/data' % file.id, data=data, follow_redirects=True,
|
||||
rv = self.app.put('/v1.0/file/%i/data' % json_data['id'], data=data, follow_redirects=True,
|
||||
content_type='multipart/form-data', headers=self.logged_in_headers())
|
||||
self.assertIsNotNone(rv.get_data())
|
||||
json_data = json.loads(rv.get_data(as_text=True))
|
||||
file = FileModelSchema().load(json_data, session=session)
|
||||
self.assertEqual(1, file.latest_version)
|
||||
self.assertEqual(1, json_data['latest_version'])
|
||||
|
||||
def test_get_file(self):
|
||||
self.load_example_data()
|
||||
|
|
|
@ -1,90 +1,119 @@
|
|||
import os
|
||||
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import session
|
||||
from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel
|
||||
from crc.services.file_service import FileService
|
||||
from crc.api.common import ApiError
|
||||
from crc import session, app
|
||||
from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel, CONTENT_TYPES
|
||||
from crc.services.lookup_service import LookupService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
|
||||
|
||||
class TestLookupService(BaseTest):
|
||||
|
||||
|
||||
def test_create_lookup_file_multiple_times_does_not_update_database(self):
|
||||
spec = BaseTest.load_test_spec('enum_options_from_file')
|
||||
def test_lookup_returns_good_error_on_bad_field(self):
|
||||
spec = BaseTest.load_test_spec('enum_options_with_search')
|
||||
workflow = self.create_workflow('enum_options_with_search')
|
||||
file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first()
|
||||
file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model == file_model).first()
|
||||
with self.assertRaises(ApiError):
|
||||
LookupService.lookup(workflow, "not_the_right_field", "sam", limit=10)
|
||||
|
||||
LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER", "CUSTOMER_NAME")
|
||||
LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER", "CUSTOMER_NAME")
|
||||
LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER", "CUSTOMER_NAME")
|
||||
def test_lookup_table_is_not_created_more_than_once(self):
|
||||
spec = BaseTest.load_test_spec('enum_options_with_search')
|
||||
workflow = self.create_workflow('enum_options_with_search')
|
||||
LookupService.lookup(workflow, "sponsor", "sam", limit=10)
|
||||
LookupService.lookup(workflow, "sponsor", "something", limit=10)
|
||||
LookupService.lookup(workflow, "sponsor", "blah", limit=10)
|
||||
lookup_records = session.query(LookupFileModel).all()
|
||||
self.assertIsNotNone(lookup_records)
|
||||
self.assertEqual(1, len(lookup_records))
|
||||
lookup_record = lookup_records[0]
|
||||
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
|
||||
self.assertEquals(28, len(lookup_data))
|
||||
# Using the same table with different lookup lable or value, does create additional records.
|
||||
LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NAME", "CUSTOMER_NUMBER")
|
||||
|
||||
def test_updates_to_file_cause_lookup_rebuild(self):
|
||||
spec = BaseTest.load_test_spec('enum_options_with_search')
|
||||
workflow = self.create_workflow('enum_options_with_search')
|
||||
file_model = session.query(FileModel).filter(FileModel.name == "sponsors.xls").first()
|
||||
LookupService.lookup(workflow, "sponsor", "sam", limit=10)
|
||||
lookup_records = session.query(LookupFileModel).all()
|
||||
self.assertIsNotNone(lookup_records)
|
||||
self.assertEqual(2, len(lookup_records))
|
||||
self.assertEqual(1, len(lookup_records))
|
||||
lookup_record = lookup_records[0]
|
||||
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
|
||||
self.assertEquals(28, len(lookup_data))
|
||||
|
||||
# Update the workflow specification file.
|
||||
file_path = os.path.join(app.root_path, '..', 'tests', 'data',
|
||||
'enum_options_with_search', 'sponsors_modified.xls')
|
||||
file = open(file_path, 'rb')
|
||||
FileService.update_file(file_model, file.read(), CONTENT_TYPES['xls'])
|
||||
file.close()
|
||||
|
||||
# restart the workflow, so it can pick up the changes.
|
||||
WorkflowProcessor(workflow, soft_reset=True)
|
||||
|
||||
LookupService.lookup(workflow, "sponsor", "sam", limit=10)
|
||||
lookup_records = session.query(LookupFileModel).all()
|
||||
lookup_record = lookup_records[0]
|
||||
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
|
||||
self.assertEquals(4, len(lookup_data))
|
||||
|
||||
|
||||
|
||||
def test_some_full_text_queries(self):
|
||||
spec = BaseTest.load_test_spec('enum_options_from_file')
|
||||
file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first()
|
||||
file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model == file_model).first()
|
||||
lookup_table = LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER", "CUSTOMER_NAME")
|
||||
workflow = self.create_workflow('enum_options_from_file')
|
||||
processor = WorkflowProcessor(workflow)
|
||||
processor.do_engine_steps()
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "medicines", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "", limit=10)
|
||||
self.assertEquals(10, len(results), "Blank queries return everything, to the limit")
|
||||
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "medicines", limit=10)
|
||||
self.assertEquals(1, len(results), "words in the middle of label are detected.")
|
||||
self.assertEquals("The Medicines Company", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "", limit=10)
|
||||
self.assertEquals(10, len(results), "Blank queries return everything, to the limit")
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "UVA", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "UVA", limit=10)
|
||||
self.assertEquals(1, len(results), "Beginning of label is found.")
|
||||
self.assertEquals("UVA - INTERNAL - GM USE ONLY", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "uva", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "uva", limit=10)
|
||||
self.assertEquals(1, len(results), "case does not matter.")
|
||||
self.assertEquals("UVA - INTERNAL - GM USE ONLY", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "medici", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "medici", limit=10)
|
||||
self.assertEquals(1, len(results), "partial words are picked up.")
|
||||
self.assertEquals("The Medicines Company", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "Genetics Savings", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "Genetics Savings", limit=10)
|
||||
self.assertEquals(1, len(results), "multiple terms are picked up..")
|
||||
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "Genetics Sav", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "Genetics Sav", limit=10)
|
||||
self.assertEquals(1, len(results), "prefix queries still work with partial terms")
|
||||
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "Gen Sav", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "Gen Sav", limit=10)
|
||||
self.assertEquals(1, len(results), "prefix queries still work with ALL the partial terms")
|
||||
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "Inc", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "Inc", limit=10)
|
||||
self.assertEquals(7, len(results), "short terms get multiple correct results.")
|
||||
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
|
||||
|
||||
results = LookupService._run_lookup_query(lookup_table, "reaction design", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "reaction design", limit=10)
|
||||
self.assertEquals(5, len(results), "all results come back for two terms.")
|
||||
self.assertEquals("Reaction Design", results[0].label, "Exact matches come first.")
|
||||
|
||||
def test_prefer_exact_match(self):
|
||||
spec = BaseTest.load_test_spec('enum_options_from_file')
|
||||
file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first()
|
||||
file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model == file_model).first()
|
||||
|
||||
lookup_table = LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER",
|
||||
"CUSTOMER_NAME")
|
||||
results = LookupService._run_lookup_query(lookup_table, "1 Something", limit=10)
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "1 Something", limit=10)
|
||||
self.assertEquals("1 Something", results[0].label, "Exact matches are prefered")
|
||||
|
||||
results = LookupService.lookup(workflow, "AllTheNames", "1 (!-Something", limit=10)
|
||||
self.assertEquals("1 Something", results[0].label, "special characters don't flake out")
|
||||
|
||||
|
||||
# 1018 10000 Something Industry
|
||||
# 1019 1000 Something Industry
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from crc.services.file_service import FileService
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc.scripts.request_approval import RequestApproval
|
||||
|
@ -17,7 +18,10 @@ class TestRequestApprovalScript(BaseTest):
|
|||
processor = WorkflowProcessor(workflow)
|
||||
task = processor.next_task()
|
||||
task.data = {"study": {"approval1": "dhf8r", 'approval2':'lb3dp'}}
|
||||
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
irb_doc_code="UVACompl_PRCAppr",
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234')
|
||||
script = RequestApproval()
|
||||
script.do_task(task, workflow.study_id, workflow.id, "study.approval1", "study.approval2")
|
||||
self.assertEquals(2, db.session.query(ApprovalModel).count())
|
||||
|
|
|
@ -2,6 +2,8 @@ import json
|
|||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from crc import db, app
|
||||
from crc.models.protocol_builder import ProtocolBuilderStatus
|
||||
from crc.models.study import StudyModel
|
||||
|
@ -12,7 +14,6 @@ from crc.services.file_service import FileService
|
|||
from crc.services.study_service import StudyService
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from example_data import ExampleDataLoader
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
|
||||
class TestStudyService(BaseTest):
|
||||
|
@ -73,7 +74,6 @@ class TestStudyService(BaseTest):
|
|||
|
||||
# workflow should not be started, and it should have 0 completed tasks, and 0 total tasks.
|
||||
self.assertEqual(WorkflowStatus.not_started, workflow.status)
|
||||
self.assertEqual(None, workflow.spec_version)
|
||||
self.assertEqual(0, workflow.total_tasks)
|
||||
self.assertEqual(0, workflow.completed_tasks)
|
||||
|
||||
|
@ -143,11 +143,9 @@ class TestStudyService(BaseTest):
|
|||
# Add a document to the study with the correct code.
|
||||
workflow = self.create_workflow('docx')
|
||||
irb_code = "UVACompl_PRCAppr" # The first file referenced in pb required docs.
|
||||
FileService.add_task_file(study_id=workflow.study_id, workflow_id=workflow.id,
|
||||
workflow_spec_id=workflow.workflow_spec_id,
|
||||
task_id="fakingthisout",
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code=irb_code)
|
||||
FileService.add_workflow_file(workflow_id=workflow.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code=irb_code)
|
||||
|
||||
docs = StudyService().get_documents_status(workflow.study_id)
|
||||
self.assertIsNotNone(docs)
|
||||
|
@ -156,13 +154,31 @@ class TestStudyService(BaseTest):
|
|||
self.assertIsNotNone(docs["UVACompl_PRCAppr"]['files'][0])
|
||||
self.assertIsNotNone(docs["UVACompl_PRCAppr"]['files'][0]['file_id'])
|
||||
self.assertEquals(workflow.id, docs["UVACompl_PRCAppr"]['files'][0]['workflow_id'])
|
||||
self.assertEquals(workflow.workflow_spec_id, docs["UVACompl_PRCAppr"]['files'][0]['workflow_spec_id'])
|
||||
|
||||
# 'file_id': 123,
|
||||
# 'task_id': 'abcdef14236890',
|
||||
# 'workflow_id': 456,
|
||||
# 'workflow_spec_id': 'irb_api_details',
|
||||
# 'status': 'complete',
|
||||
def test_get_all_studies(self):
|
||||
user = self.create_user_with_study_and_workflow()
|
||||
|
||||
# Add a document to the study with the correct code.
|
||||
workflow1 = self.create_workflow('docx')
|
||||
workflow2 = self.create_workflow('empty_workflow')
|
||||
|
||||
# Add files to both workflows.
|
||||
FileService.add_workflow_file(workflow_id=workflow1.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code="UVACompl_PRCAppr" )
|
||||
FileService.add_workflow_file(workflow_id=workflow1.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code="AD_Consent_Model")
|
||||
FileService.add_workflow_file(workflow_id=workflow2.id,
|
||||
name="anything.png", content_type="text",
|
||||
binary_data=b'1234', irb_doc_code="UVACompl_PRCAppr" )
|
||||
|
||||
studies = StudyService().get_all_studies_with_files()
|
||||
self.assertEquals(1, len(studies))
|
||||
self.assertEquals(3, len(studies[0].files))
|
||||
|
||||
|
||||
|
||||
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_docs
|
||||
def test_get_personnel(self, mock_docs):
|
||||
|
|
|
@ -182,7 +182,6 @@ class TestTasksApi(BaseTest):
|
|||
self.assertEquals("Task 2b", nav[5]['title'])
|
||||
self.assertEquals("Task 3", nav[6]['title'])
|
||||
|
||||
|
||||
def test_document_added_to_workflow_shows_up_in_file_list(self):
|
||||
self.load_example_data()
|
||||
self.create_reference_document()
|
||||
|
@ -335,8 +334,8 @@ class TestTasksApi(BaseTest):
|
|||
workflow = self.get_workflow_api(workflow)
|
||||
task = workflow.next_task
|
||||
field_id = task.form['fields'][0]['id']
|
||||
rv = self.app.get('/v1.0/workflow/%i/task/%s/lookup/%s?query=%s&limit=5' %
|
||||
(workflow.id, task.id, field_id, 'c'), # All records with a word that starts with 'c'
|
||||
rv = self.app.get('/v1.0/workflow/%i/lookup/%s?query=%s&limit=5' %
|
||||
(workflow.id, field_id, 'c'), # All records with a word that starts with 'c'
|
||||
headers=self.logged_in_headers(),
|
||||
content_type="application/json")
|
||||
self.assert_success(rv)
|
||||
|
@ -351,8 +350,8 @@ class TestTasksApi(BaseTest):
|
|||
task = workflow.next_task
|
||||
field_id = task.form['fields'][0]['id']
|
||||
# lb3dp is a user record in the mock ldap responses for tests.
|
||||
rv = self.app.get('/v1.0/workflow/%i/task/%s/lookup/%s?query=%s&limit=5' %
|
||||
(workflow.id, task.id, field_id, 'lb3dp'),
|
||||
rv = self.app.get('/v1.0/workflow/%s/lookup/%s?query=%s&limit=5' %
|
||||
(workflow.id, field_id, 'lb3dp'),
|
||||
headers=self.logged_in_headers(),
|
||||
content_type="application/json")
|
||||
self.assert_success(rv)
|
||||
|
|
|
@ -223,10 +223,10 @@ class TestWorkflowProcessor(BaseTest):
|
|||
self._populate_form_with_random_data(task)
|
||||
processor.complete_task(task)
|
||||
|
||||
files = session.query(FileModel).filter_by(study_id=study.id, workflow_id=processor.get_workflow_id()).all()
|
||||
files = session.query(FileModel).filter_by(workflow_id=processor.get_workflow_id()).all()
|
||||
self.assertEqual(0, len(files))
|
||||
processor.do_engine_steps()
|
||||
files = session.query(FileModel).filter_by(study_id=study.id, workflow_id=processor.get_workflow_id()).all()
|
||||
files = session.query(FileModel).filter_by(workflow_id=processor.get_workflow_id()).all()
|
||||
self.assertEqual(1, len(files), "The task should create a new file.")
|
||||
file_data = session.query(FileDataModel).filter(FileDataModel.file_model_id == files[0].id).first()
|
||||
self.assertIsNotNone(file_data.data)
|
||||
|
@ -254,12 +254,12 @@ class TestWorkflowProcessor(BaseTest):
|
|||
study = session.query(StudyModel).first()
|
||||
workflow_spec_model = self.load_test_spec("decision_table")
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
self.assertTrue(processor.get_spec_version().startswith('v1.1'))
|
||||
self.assertTrue(processor.get_version_string().startswith('v1.1'))
|
||||
file_service = FileService()
|
||||
|
||||
file_service.add_workflow_spec_file(workflow_spec_model, "new_file.txt", "txt", b'blahblah')
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
self.assertTrue(processor.get_spec_version().startswith('v1.1.1'))
|
||||
self.assertTrue(processor.get_version_string().startswith('v1.1.1'))
|
||||
|
||||
file_path = os.path.join(app.root_path, '..', 'tests', 'data', 'docx', 'docx.bpmn')
|
||||
file = open(file_path, "rb")
|
||||
|
@ -268,7 +268,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
file_model = db.session.query(FileModel).filter(FileModel.name == "decision_table.bpmn").first()
|
||||
file_service.update_file(file_model, data, "txt")
|
||||
processor = self.get_processor(study, workflow_spec_model)
|
||||
self.assertTrue(processor.get_spec_version().startswith('v2.1.1'))
|
||||
self.assertTrue(processor.get_version_string().startswith('v2.1.1'))
|
||||
|
||||
def test_restart_workflow(self):
|
||||
self.load_example_data()
|
||||
|
@ -339,7 +339,7 @@ class TestWorkflowProcessor(BaseTest):
|
|||
# Assure that creating a new processor doesn't cause any issues, and maintains the spec version.
|
||||
processor.workflow_model.bpmn_workflow_json = processor.serialize()
|
||||
processor2 = WorkflowProcessor(processor.workflow_model)
|
||||
self.assertTrue(processor2.get_spec_version().startswith("v1 ")) # Still at version 1.
|
||||
self.assertFalse(processor2.is_latest_spec) # Still at version 1.
|
||||
|
||||
# Do a hard reset, which should bring us back to the beginning, but retain the data.
|
||||
processor3 = WorkflowProcessor(processor.workflow_model, hard_reset=True)
|
||||
|
@ -349,10 +349,6 @@ class TestWorkflowProcessor(BaseTest):
|
|||
self.assertEqual("New Step", processor3.next_task().task_spec.description)
|
||||
self.assertEqual("blue", processor3.next_task().data["color"])
|
||||
|
||||
def test_get_latest_spec_version(self):
|
||||
workflow_spec_model = self.load_test_spec("two_forms")
|
||||
version = WorkflowProcessor.get_latest_version_string("two_forms")
|
||||
self.assertTrue(version.startswith("v1 "))
|
||||
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies')
|
||||
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators')
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
from tests.base_test import BaseTest
|
||||
from crc import session
|
||||
from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel
|
||||
from crc.services.lookup_service import LookupService
|
||||
|
||||
from crc.services.workflow_processor import WorkflowProcessor
|
||||
from crc.services.workflow_service import WorkflowService
|
||||
|
||||
|
@ -72,36 +70,6 @@ class TestWorkflowService(BaseTest):
|
|||
self.assertEquals('1000', options[0]['id'])
|
||||
self.assertEquals("UVA - INTERNAL - GM USE ONLY", options[0]['name'])
|
||||
|
||||
def test_create_lookup_file(self):
|
||||
spec = self.load_test_spec('enum_options_from_file')
|
||||
file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first()
|
||||
file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model == file_model).first()
|
||||
LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER", "CUSTOMER_NAME")
|
||||
lookup_records = session.query(LookupFileModel).all()
|
||||
self.assertIsNotNone(lookup_records)
|
||||
self.assertEqual(1, len(lookup_records))
|
||||
lookup_record = lookup_records[0]
|
||||
self.assertIsNotNone(lookup_record)
|
||||
self.assertEquals("CUSTOMER_NUMBER", lookup_record.value_column)
|
||||
self.assertEquals("CUSTOMER_NAME", lookup_record.label_column)
|
||||
self.assertEquals("CUSTOMER_NAME", lookup_record.label_column)
|
||||
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
|
||||
self.assertEquals(28, len(lookup_data))
|
||||
|
||||
self.assertEquals("1000", lookup_data[0].value)
|
||||
self.assertEquals("UVA - INTERNAL - GM USE ONLY", lookup_data[0].label)
|
||||
# search_results = session.query(LookupDataModel).\
|
||||
# filter(LookupDataModel.lookup_file_model_id == lookup_record.id).\
|
||||
# filter(LookupDataModel.__ts_vector__.op('@@')(func.plainto_tsquery('INTERNAL'))).all()
|
||||
search_results = LookupDataModel.query.filter(LookupDataModel.label.match("INTERNAL")).all()
|
||||
self.assertEquals(1, len(search_results))
|
||||
search_results = LookupDataModel.query.filter(LookupDataModel.label.match("internal")).all()
|
||||
self.assertEquals(1, len(search_results))
|
||||
# This query finds results where a word starts with "bio"
|
||||
search_results = LookupDataModel.query.filter(LookupDataModel.label.match("bio:*")).all()
|
||||
self.assertEquals(2, len(search_results))
|
||||
|
||||
|
||||
def test_random_data_populate_form_on_auto_complete(self):
|
||||
self.load_example_data()
|
||||
workflow = self.create_workflow('enum_options_with_search')
|
||||
|
|
Loading…
Reference in New Issue