Merge branch 'dev' into feature/approval_request_script

This commit is contained in:
Aaron Louie 2020-05-28 17:02:21 -04:00
commit 04e5a476c4
62 changed files with 1929 additions and 410 deletions

View File

@ -1,7 +1,7 @@
language: python language: python
python: python:
- "3.6.9" - "3.7"
services: services:
- postgresql - postgresql

View File

@ -1,24 +1,22 @@
FROM python:3.6.9-slim FROM python:3.7-slim
WORKDIR /app WORKDIR /app
COPY Pipfile Pipfile.lock /app/ COPY Pipfile Pipfile.lock /app/
RUN pip install pipenv && \ RUN set -xe \
apt-get update && \ && pip install pipenv \
apt-get install -y --no-install-recommends \ && apt-get update -q \
&& apt-get install -y -q \
gcc python3-dev libssl-dev \ gcc python3-dev libssl-dev \
curl postgresql-client git-core && \ curl postgresql-client git-core \
pipenv install --dev && \ gunicorn3 postgresql-client \
apt-get remove -y gcc python3-dev libssl-dev && \ && pipenv install --dev \
apt-get purge -y --auto-remove && \ && apt-get remove -y gcc python3-dev libssl-dev \
rm -rf /var/lib/apt/lists/ * && apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /app \
&& useradd _gunicorn --no-create-home --user-group
COPY . /app/ COPY . /app/
WORKDIR /app
ENV FLASK_APP=/app/crc/__init__.py
CMD ["pipenv", "run", "flask", "db", "upgrade"]
CMD ["pipenv", "run", "python", "/app/run.py"]
# expose ports
EXPOSE 5000

View File

@ -5,6 +5,7 @@ verify_ssl = true
[dev-packages] [dev-packages]
pytest = "*" pytest = "*"
pbr = "*"
[packages] [packages]
connexion = {extras = ["swagger-ui"],version = "*"} connexion = {extras = ["swagger-ui"],version = "*"}
@ -35,6 +36,8 @@ python-dateutil = "*"
pandas = "*" pandas = "*"
xlrd = "*" xlrd = "*"
ldap3 = "*" ldap3 = "*"
gunicorn = "*"
werkzeug = "*"
[requires] [requires]
python_version = "3.6.9" python_version = "3.7"

41
Pipfile.lock generated
View File

@ -1,11 +1,11 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "1ca737db75750ea4351c15b4b0b26155d90bc5522705ed293a0c2773600b6a0a" "sha256": "fad2f86b02a85b074e8ff9d8f3822784444d0925b0bf4227d4b26bbff1c45c2f"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
"python_version": "3.6.9" "python_version": "3.7"
}, },
"sources": [ "sources": [
{ {
@ -235,11 +235,11 @@
}, },
"docxtpl": { "docxtpl": {
"hashes": [ "hashes": [
"sha256:16a76d360c12f7da3a28821fc740b9a84b891895233493ff0b002ffaa6026905", "sha256:0e031ea5da63339f2bac0fd7eb7b3b137303571a9a92c950501148240ea22047",
"sha256:f19adf2a713a753c1e056ef0ce395bc8da62d495b091ebf9fe67dfc6d1115f9f" "sha256:45f04661b9ab1fd66b975a0a547b30c8811f457bef2f85249c2f3c5784a00052"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.9.2" "version": "==0.10.0"
}, },
"et-xmlfile": { "et-xmlfile": {
"hashes": [ "hashes": [
@ -296,10 +296,10 @@
}, },
"flask-sqlalchemy": { "flask-sqlalchemy": {
"hashes": [ "hashes": [
"sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327", "sha256:0b656fbf87c5f24109d859bafa791d29751fabbda2302b606881ae5485b557a5",
"sha256:6974785d913666587949f7c2946f7001e4fa2cb2d19f4e69ead02e4b8f50b33d" "sha256:fcfe6df52cd2ed8a63008ca36b86a51fa7a4b70cef1c39e5625f722fca32308e"
], ],
"version": "==2.4.1" "version": "==2.4.3"
}, },
"future": { "future": {
"hashes": [ "hashes": [
@ -307,6 +307,14 @@
], ],
"version": "==0.18.2" "version": "==0.18.2"
}, },
"gunicorn": {
"hashes": [
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
],
"index": "pypi",
"version": "==20.0.4"
},
"httpretty": { "httpretty": {
"hashes": [ "hashes": [
"sha256:24a6fd2fe1c76e94801b74db8f52c0fb42718dc4a199a861b305b1a492b9d868" "sha256:24a6fd2fe1c76e94801b74db8f52c0fb42718dc4a199a861b305b1a492b9d868"
@ -719,11 +727,11 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:62edfd92d955b868d6c124c0942eba966d54b5f3dcb4ded39e65f74abac3f572", "sha256:779a519adbd3a70fc7c468af08c5e74829868b0a5b34587b33340e010291856c",
"sha256:f5505d74cf9592f3b997380f9bdb2d2d0320ed74dd69691e3ee0644b956b8d83" "sha256:ea64df287958ee5aac46be7ac2b7277305b0381d213728c3a49d8bb9b8415807"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.0.3" "version": "==3.0.4"
}, },
"sphinxcontrib-applehelp": { "sphinxcontrib-applehelp": {
"hashes": [ "hashes": [
@ -770,7 +778,7 @@
"spiffworkflow": { "spiffworkflow": {
"editable": true, "editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git", "git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "cb098ee6d55b85bf7795997f4ad5f78c27d15381" "ref": "c8d87826d496af825a184bdc3f0a751e603cfe44"
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
@ -855,6 +863,7 @@
"sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43", "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43",
"sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c" "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"
], ],
"index": "pypi",
"version": "==1.0.1" "version": "==1.0.1"
}, },
"xlrd": { "xlrd": {
@ -911,6 +920,14 @@
], ],
"version": "==20.4" "version": "==20.4"
}, },
"pbr": {
"hashes": [
"sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c",
"sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8"
],
"index": "pypi",
"version": "==5.4.5"
},
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",

View File

@ -13,6 +13,9 @@ DEVELOPMENT = environ.get('DEVELOPMENT', default="true") == "true"
TESTING = environ.get('TESTING', default="false") == "true" TESTING = environ.get('TESTING', default="false") == "true"
PRODUCTION = (environ.get('PRODUCTION', default="false") == "true") or (not DEVELOPMENT and not TESTING) PRODUCTION = (environ.get('PRODUCTION', default="false") == "true") or (not DEVELOPMENT and not TESTING)
# Add trailing slash to base path
APPLICATION_ROOT = re.sub(r'//', '/', '/%s/' % environ.get('APPLICATION_ROOT', default="/").strip('/'))
DB_HOST = environ.get('DB_HOST', default="localhost") DB_HOST = environ.get('DB_HOST', default="localhost")
DB_PORT = environ.get('DB_PORT', default="5432") DB_PORT = environ.get('DB_PORT', default="5432")
DB_NAME = environ.get('DB_NAME', default="crc_dev") DB_NAME = environ.get('DB_NAME', default="crc_dev")
@ -22,27 +25,20 @@ SQLALCHEMY_DATABASE_URI = environ.get(
'SQLALCHEMY_DATABASE_URI', 'SQLALCHEMY_DATABASE_URI',
default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME) default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME)
) )
TOKEN_AUTH_TTL_HOURS = environ.get('TOKEN_AUTH_TTL_HOURS', default=4) TOKEN_AUTH_TTL_HOURS = int(environ.get('TOKEN_AUTH_TTL_HOURS', default=4))
TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.") TOKEN_AUTH_SECRET_KEY = environ.get('TOKEN_AUTH_SECRET_KEY', default="Shhhh!!! This is secret! And better darn well not show up in prod.")
FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session") FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://localhost:4200/session")
SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER") SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER")
# %s/%i placeholders expected for uva_id and study_id in various calls. # %s/%i placeholders expected for uva_id and study_id in various calls.
PB_ENABLED = environ.get('PB_ENABLED', default=True) PB_ENABLED = environ.get('PB_ENABLED', default="false") == "true"
PB_BASE_URL = environ.get('PB_BASE_URL', default="http://localhost:5001/pb/") PB_BASE_URL = environ.get('PB_BASE_URL', default="http://localhost:5001/pb/").strip('/') + '/' # Trailing slash required
PB_USER_STUDIES_URL = environ.get('PB_USER_STUDIES_URL', default=PB_BASE_URL + "user_studies?uva_id=%s") PB_USER_STUDIES_URL = environ.get('PB_USER_STUDIES_URL', default=PB_BASE_URL + "user_studies?uva_id=%s")
PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i") PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i")
PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL + "required_docs?studyid=%i") PB_REQUIRED_DOCS_URL = environ.get('PB_REQUIRED_DOCS_URL', default=PB_BASE_URL + "required_docs?studyid=%i")
PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL + "study?studyid=%i") PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL + "study?studyid=%i")
LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu") LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No trailing slash or http://
LDAP_TIMEOUT_SEC = environ.get('LDAP_TIMEOUT_SEC', default=3) LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=3))
print('=== USING DEFAULT CONFIG: ===')
print('DB_HOST = ', DB_HOST)
print('CORS_ALLOW_ORIGINS = ', CORS_ALLOW_ORIGINS)
print('DEVELOPMENT = ', DEVELOPMENT)
print('TESTING = ', TESTING)
print('PRODUCTION = ', PRODUCTION)
print('PB_BASE_URL = ', PB_BASE_URL)
print('LDAP_URL = ', LDAP_URL)

View File

@ -1,13 +1,29 @@
import os import os
from os import environ
basedir = os.path.abspath(os.path.dirname(__file__)) basedir = os.path.abspath(os.path.dirname(__file__))
NAME = "CR Connect Workflow" NAME = "CR Connect Workflow"
DEVELOPMENT = True DEVELOPMENT = True
TESTING = True TESTING = True
SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@localhost:5432/crc_test"
TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod." TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
PB_ENABLED = False PB_ENABLED = False
# This is here, for when we are running the E2E Tests in the frontend code bases.
# which will set the TESTING envronment to true, causing this to execute, but we need
# to respect the environment variables in that case.
# when running locally the defaults apply, meaning we use crc_test for doing the tests
# locally, and we don't over-write the database. Did you read this far? Have a cookie!
DB_HOST = environ.get('DB_HOST', default="localhost")
DB_PORT = environ.get('DB_PORT', default="5432")
DB_NAME = environ.get('DB_NAME', default="crc_test")
DB_USER = environ.get('DB_USER', default="crc_user")
DB_PASSWORD = environ.get('DB_PASSWORD', default="crc_pass")
SQLALCHEMY_DATABASE_URI = environ.get(
'SQLALCHEMY_DATABASE_URI',
default="postgresql://%s:%s@%s:%s/%s" % (DB_USER, DB_PASSWORD, DB_HOST, DB_PORT, DB_NAME)
)
print('### USING TESTING CONFIG: ###') print('### USING TESTING CONFIG: ###')
print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI) print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI)
print('DEVELOPMENT = ', DEVELOPMENT) print('DEVELOPMENT = ', DEVELOPMENT)

View File

@ -8,18 +8,7 @@ SQLALCHEMY_DATABASE_URI = "postgresql://postgres:@localhost:5432/crc_test"
TOKEN_AUTH_TTL_HOURS = 2 TOKEN_AUTH_TTL_HOURS = 2
TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod." TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod."
FRONTEND_AUTH_CALLBACK = "http://localhost:4200/session" # Not Required FRONTEND_AUTH_CALLBACK = "http://localhost:4200/session" # Not Required
PB_ENABLED = False
#: Default attribute map for single signon.
SSO_ATTRIBUTE_MAP = {
'eppn': (False, 'eppn'), # dhf8r@virginia.edu
'uid': (True, 'uid'), # dhf8r
'givenName': (False, 'first_name'), # Daniel
'mail': (False, 'email_address'), # dhf8r@Virginia.EDU
'sn': (False, 'last_name'), # Funk
'affiliation': (False, 'affiliation'), # 'staff@virginia.edu;member@virginia.edu'
'displayName': (False, 'display_name'), # Daniel Harold Funk
'title': (False, 'title') # SOFTWARE ENGINEER V
}
print('+++ USING TRAVIS TESTING CONFIG: +++') print('+++ USING TRAVIS TESTING CONFIG: +++')
print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI) print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI)

View File

@ -34,12 +34,22 @@ ma = Marshmallow(app)
from crc import models from crc import models
from crc import api from crc import api
connexion_app.add_api('api.yml') connexion_app.add_api('api.yml', base_path='/v1.0')
# Convert list of allowed origins to list of regexes # Convert list of allowed origins to list of regexes
origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['CORS_ALLOW_ORIGINS']] origins_re = [r"^https?:\/\/%s(.*)" % o.replace('.', '\.') for o in app.config['CORS_ALLOW_ORIGINS']]
cors = CORS(connexion_app.app, origins=origins_re) cors = CORS(connexion_app.app, origins=origins_re)
print('=== USING THESE CONFIG SETTINGS: ===')
print('DB_HOST = ', )
print('CORS_ALLOW_ORIGINS = ', app.config['CORS_ALLOW_ORIGINS'])
print('DEVELOPMENT = ', app.config['DEVELOPMENT'])
print('TESTING = ', app.config['TESTING'])
print('PRODUCTION = ', app.config['PRODUCTION'])
print('PB_BASE_URL = ', app.config['PB_BASE_URL'])
print('LDAP_URL = ', app.config['LDAP_URL'])
print('APPLICATION_ROOT = ', app.config['APPLICATION_ROOT'])
print('PB_ENABLED = ', app.config['PB_ENABLED'])
@app.cli.command() @app.cli.command()
def load_example_data(): def load_example_data():
@ -47,3 +57,11 @@ def load_example_data():
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
ExampleDataLoader.clean_db() ExampleDataLoader.clean_db()
ExampleDataLoader().load_all() ExampleDataLoader().load_all()
@app.cli.command()
def load_example_rrt_data():
"""Load example data into the database."""
from example_data import ExampleDataLoader
ExampleDataLoader.clean_db()
ExampleDataLoader().load_rrt()

View File

@ -56,7 +56,7 @@ paths:
required: false required: false
schema: schema:
type: string type: string
- name: redirect_url - name: redirect
in: query in: query
required: false required: false
schema: schema:

View File

@ -1,21 +1,32 @@
from datetime import datetime
from flask import g from flask import g
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from crc import session from crc import session
from crc.api.common import ApiError, ApiErrorSchema from crc.api.common import ApiError, ApiErrorSchema
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.study import StudySchema, StudyFilesSchema, StudyModel, Study from crc.models.study import StudySchema, StudyFilesSchema, StudyModel, Study
from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.study_service import StudyService from crc.services.study_service import StudyService
def add_study(body): def add_study(body):
"""Or any study like object. """ """Or any study like object. Body should include a title, and primary_investigator_id """
study: Study = StudySchema().load(body) if 'primary_investigator_id' not in body:
study_model = StudyModel(**study.model_args()) raise ApiError("missing_pi", "Can't create a new study without a Primary Investigator.")
if 'title' not in body:
raise ApiError("missing_title", "Can't create a new study without a title.")
study_model = StudyModel(user_uid=g.user.uid,
title=body['title'],
primary_investigator_id=body['primary_investigator_id'],
last_updated=datetime.now(),
protocol_builder_status=ProtocolBuilderStatus.ACTIVE)
session.add(study_model) session.add(study_model)
errors = StudyService._add_all_workflow_specs_to_study(study) errors = StudyService._add_all_workflow_specs_to_study(study_model)
session.commit() session.commit()
study = StudyService().get_study(study_model.id)
study_data = StudySchema().dump(study) study_data = StudySchema().dump(study)
study_data["errors"] = ApiErrorSchema(many=True).dump(errors) study_data["errors"] = ApiErrorSchema(many=True).dump(errors)
return study_data return study_data
@ -67,5 +78,3 @@ def all_studies_and_files():
studies = StudyService.get_studies_with_files() studies = StudyService.get_studies_with_files()
results = StudyFilesSchema(many=True).dump(studies) results = StudyFilesSchema(many=True).dump(studies)
return results return results

View File

@ -32,7 +32,7 @@ def verify_token(token):
def get_current_user(): def get_current_user():
return UserModelSchema().dump(g.user) return UserModelSchema().dump(g.user)
@app.route('/login') @app.route('/v1.0/login')
def sso_login(): def sso_login():
# This what I see coming back: # This what I see coming back:
# X-Remote-Cn: Daniel Harold Funk (dhf8r) # X-Remote-Cn: Daniel Harold Funk (dhf8r)
@ -126,7 +126,7 @@ def backdoor(
first_name=None, first_name=None,
last_name=None, last_name=None,
title=None, title=None,
redirect_url=None, redirect=None,
): ):
"""A backdoor for end-to-end system testing that allows the system to simulate logging in as a specific user. """A backdoor for end-to-end system testing that allows the system to simulate logging in as a specific user.
Only works if the application is running in a non-production environment. Only works if the application is running in a non-production environment.
@ -149,9 +149,8 @@ def backdoor(
ApiError. If on production, returns a 404 error. ApiError. If on production, returns a 404 error.
""" """
if not 'PRODUCTION' in app.config or not app.config['PRODUCTION']: if not 'PRODUCTION' in app.config or not app.config['PRODUCTION']:
ldap_info = LdapUserInfo()
ldap_info.uid = connexion.request.args["uid"] ldap_info = LdapService().user_info(uid)
ldap_info.email_address = connexion.request.args["email_address"] return _handle_login(ldap_info, redirect)
return _handle_login(ldap_info, redirect_url)
else: else:
raise ApiError('404', 'unknown') raise ApiError('404', 'unknown')

View File

@ -4,6 +4,7 @@ from crc import session
from crc.api.common import ApiError, ApiErrorSchema from crc.api.common import ApiError, ApiErrorSchema
from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema from crc.models.api_models import WorkflowApi, WorkflowApiSchema, NavigationItem, NavigationItemSchema
from crc.models.file import FileModel, LookupDataSchema from crc.models.file import FileModel, LookupDataSchema
from crc.models.stats import TaskEventModel
from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \ from crc.models.workflow import WorkflowModel, WorkflowSpecModelSchema, WorkflowSpecModel, WorkflowSpecCategoryModel, \
WorkflowSpecCategoryModelSchema WorkflowSpecCategoryModelSchema
from crc.services.file_service import FileService from crc.services.file_service import FileService
@ -77,6 +78,8 @@ def delete_workflow_specification(spec_id):
for file in files: for file in files:
FileService.delete_file(file.id) FileService.delete_file(file.id)
session.query(TaskEventModel).filter(TaskEventModel.workflow_spec_id == spec_id).delete()
# Delete all stats and workflow models related to this specification # Delete all stats and workflow models related to this specification
for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id): for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id):
StudyService.delete_workflow(workflow) StudyService.delete_workflow(workflow)

View File

@ -35,7 +35,7 @@ class ApprovalModel(db.Model):
__tablename__ = 'approval' __tablename__ = 'approval'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False) study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False)
study = db.relationship(StudyModel) study = db.relationship(StudyModel, backref='approval', cascade='all,delete')
workflow_id = db.Column(db.Integer, db.ForeignKey(WorkflowModel.id), nullable=False) workflow_id = db.Column(db.Integer, db.ForeignKey(WorkflowModel.id), nullable=False)
workflow = db.relationship(WorkflowModel) workflow = db.relationship(WorkflowModel)
approver_uid = db.Column(db.String) # Not linked to user model, as they may not have logged in yet. approver_uid = db.Column(db.String) # Not linked to user model, as they may not have logged in yet.

View File

@ -107,7 +107,8 @@ class CategorySchema(ma.Schema):
class Study(object): class Study(object):
def __init__(self, id, title, last_updated, primary_investigator_id, user_uid, def __init__(self, title, last_updated, primary_investigator_id, user_uid,
id=None,
protocol_builder_status=None, protocol_builder_status=None,
sponsor="", hsr_number="", ind_number="", categories=[], **argsv): sponsor="", hsr_number="", ind_number="", categories=[], **argsv):
self.id = id self.id = id
@ -125,7 +126,8 @@ class Study(object):
@classmethod @classmethod
def from_model(cls, study_model: StudyModel): def from_model(cls, study_model: StudyModel):
args = {k: v for k, v in study_model.__dict__.items() if not k.startswith('_')} id = study_model.id # Just read some value, in case the dict expired, otherwise dict may be empty.
args = dict((k, v) for k, v in study_model.__dict__.items() if not k.startswith('_'))
instance = cls(**args) instance = cls(**args)
return instance return instance
@ -144,10 +146,13 @@ class Study(object):
class StudySchema(ma.Schema): class StudySchema(ma.Schema):
id = fields.Integer(required=False, allow_none=True)
categories = fields.List(fields.Nested(CategorySchema), dump_only=True) categories = fields.List(fields.Nested(CategorySchema), dump_only=True)
warnings = fields.List(fields.Nested(ApiErrorSchema), dump_only=True) warnings = fields.List(fields.Nested(ApiErrorSchema), dump_only=True)
protocol_builder_status = EnumField(ProtocolBuilderStatus) protocol_builder_status = EnumField(ProtocolBuilderStatus)
hsr_number = fields.String(allow_none=True) hsr_number = fields.String(allow_none=True)
sponsor = fields.String(allow_none=True)
ind_number = fields.String(allow_none=True)
class Meta: class Meta:
model = Study model = Study

View File

@ -10,11 +10,11 @@ class RequestApproval(Script):
def get_description(self): def get_description(self):
return """ return """
Creates an approval request on this workflow, by the given approver_uid(s)," Creates an approval request on this workflow, by the given approver_uid(s),"
Takes one argument, which should point to data located in current task. Takes multiple arguments, which should point to data located in current task
or be quoted strings.
Example: Example:
RequestApproval approver1 "dhf8r"
RequestApproval required_approvals.uids
""" """
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs): def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
@ -29,15 +29,16 @@ RequestApproval required_approvals.uids
ApprovalService.add_approval(study_id, workflow_id, id) ApprovalService.add_approval(study_id, workflow_id, id)
def get_uids(self, task, args): def get_uids(self, task, args):
if len(args) != 1: if len(args) < 1:
raise ApiError(code="missing_argument", raise ApiError(code="missing_argument",
message="The RequestApproval script requires 1 argument. The " message="The RequestApproval script requires at least one argument. The "
"the name of the variable in the task data that contains user" "the name of the variable in the task data that contains user"
"ids to process.") "id to process. Multiple arguments are accepted.")
uids = []
uids = task.workflow.script_engine.evaluate(task, args[0]) for arg in args:
id = task.workflow.script_engine.evaluate_expression(task, arg)
if not isinstance(uids, str) and not isinstance(uids, list): uids.append(id)
if not isinstance(id, str):
raise ApiError(code="invalid_argument", raise ApiError(code="invalid_argument",
message="The RequestApproval script requires 1 argument. The " message="The RequestApproval script requires 1 argument. The "
"the name of the variable in the task data that contains user" "the name of the variable in the task data that contains user"

View File

@ -0,0 +1,58 @@
import requests
from crc import db
from crc.api.common import ApiError
from crc.models.study import StudyModel
from crc.scripts.script import Script
class mock_study:
def __init__(self):
self.title = ""
self.principle_investigator_id = ""
class UpdateStudy(Script):
argument_error_message = "You must supply at least one argument to the " \
"update_study task, in the form [study_field]:[value]",
def get_description(self):
return """
Allows you to set specific attributes on the Study model by mapping them to
values in the task data. Should be called with the value to set (either title, or pi)
followed by a ":" and then the value to use in dot notation.
Example:
UpdateStudy title:PIComputingID.label pi:PIComputingID.value
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
study = mock_study
self.__update_study(task, study, *args)
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
study = db.session.query(StudyModel).filter(StudyModel.id == study_id).first()
self.__update_study(task, study, *args)
db.session.add(study)
def __update_study(self, task, study, *args):
if len(args) < 1:
raise ApiError.from_task("missing_argument", self.argument_error_message,
task=task)
for arg in args:
try:
field, value_lookup = arg.split(':')
except:
raise ApiError.from_task("invalid_argument", self.argument_error_message,
task=task)
value = task.workflow.script_engine.evaluate_expression(task, value_lookup)
if field.lower() == "title":
study.title = value
elif field.lower() == "pi":
study.primary_investigator_id = value
else:
raise ApiError.from_task("invalid_argument", self.argument_error_message,
task=task)

View File

@ -133,7 +133,6 @@ class FileService(object):
return file_extension.lower().strip()[1:] return file_extension.lower().strip()[1:]
@staticmethod @staticmethod
def update_file(file_model, binary_data, content_type): def update_file(file_model, binary_data, content_type):
session.flush() # Assure the database is up-to-date before running this. session.flush() # Assure the database is up-to-date before running this.

View File

@ -35,9 +35,10 @@ class LdapUserInfo(object):
class LdapService(object): class LdapService(object):
search_base = "ou=People,o=University of Virginia,c=US" search_base = "ou=People,o=University of Virginia,c=US"
attributes = ['uid', 'cn', 'displayName', 'givenName', 'mail', 'objectClass', 'UvaDisplayDepartment', attributes = ['uid', 'cn', 'sn', 'displayName', 'givenName', 'mail', 'objectClass', 'UvaDisplayDepartment',
'telephoneNumber', 'title', 'uvaPersonIAMAffiliation', 'uvaPersonSponsoredType'] 'telephoneNumber', 'title', 'uvaPersonIAMAffiliation', 'uvaPersonSponsoredType']
uid_search_string = "(&(objectclass=person)(uid=%s))" uid_search_string = "(&(objectclass=person)(uid=%s))"
user_or_last_name_search_string = "(&(objectclass=person)(|(uid=%s*)(sn=%s*)))"
def __init__(self): def __init__(self):
if app.config['TESTING']: if app.config['TESTING']:
@ -66,7 +67,8 @@ class LdapService(object):
return LdapUserInfo.from_entry(entry) return LdapUserInfo.from_entry(entry)
def search_users(self, query, limit): def search_users(self, query, limit):
search_string = LdapService.uid_search_string % query if len(query) < 3: return []
search_string = LdapService.user_or_last_name_search_string % (query, query)
self.conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes) self.conn.search(LdapService.search_base, search_string, attributes=LdapService.attributes)
# Entries are returned as a generator, accessing entries # Entries are returned as a generator, accessing entries

View File

@ -1,4 +1,8 @@
import logging
from pandas import ExcelFile from pandas import ExcelFile
from sqlalchemy import func, desc
from sqlalchemy.sql.functions import GenericFunction
from crc import db from crc import db
from crc.api.common import ApiError from crc.api.common import ApiError
@ -7,6 +11,9 @@ from crc.models.file import FileDataModel, LookupFileModel, LookupDataModel
from crc.services.file_service import FileService from crc.services.file_service import FileService
from crc.services.ldap_service import LdapService from crc.services.ldap_service import LdapService
class TSRank(GenericFunction):
package = 'full_text'
name = 'ts_rank'
class LookupService(object): class LookupService(object):
@ -112,20 +119,31 @@ class LookupService(object):
db_query = LookupDataModel.query.filter(LookupDataModel.lookup_file_model == lookup_file_model) db_query = LookupDataModel.query.filter(LookupDataModel.lookup_file_model == lookup_file_model)
query = query.strip() query = query.strip()
if len(query) > 1: if len(query) > 0:
if ' ' in query: if ' ' in query:
terms = query.split(' ') terms = query.split(' ')
new_terms = [] new_terms = ["'%s'" % query]
for t in terms: for t in terms:
new_terms.append(t + ":*") new_terms.append("%s:*" % t)
query = '|'.join(new_terms) new_query = ' | '.join(new_terms)
else: else:
query = "%s:*" % query new_query = "%s:*" % query
db_query = db_query.filter(LookupDataModel.label.match(query))
# db_query = db_query.filter(text("lookup_data.label @@ to_tsquery('simple', '%s')" % query)) # Run the full text query
db_query = db_query.filter(LookupDataModel.label.match(new_query))
# But hackishly order by like, which does a good job of
# pulling more relevant matches to the top.
db_query = db_query.order_by(desc(LookupDataModel.label.like("%" + query + "%")))
#ORDER BY name LIKE concat('%', ticker, '%') desc, rank DESC
return db_query.limit(limit).all() # db_query = db_query.order_by(desc(func.full_text.ts_rank(
# func.to_tsvector(LookupDataModel.label),
# func.to_tsquery(query))))
from sqlalchemy.dialects import postgresql
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
result = db_query.limit(limit).all()
logging.getLogger('sqlalchemy.engine').setLevel(logging.ERROR)
return result
@staticmethod @staticmethod
def _run_ldap_query(query, limit): def _run_ldap_query(query, limit):

View File

@ -5,17 +5,22 @@ import requests
from crc import app from crc import app
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStudySchema, ProtocolBuilderInvestigator, \ from crc.models.protocol_builder import ProtocolBuilderStudySchema, ProtocolBuilderRequiredDocument
ProtocolBuilderRequiredDocument, ProtocolBuilderRequiredDocumentSchema
class ProtocolBuilderService(object): class ProtocolBuilderService(object):
ENABLED = app.config['PB_ENABLED']
STUDY_URL = app.config['PB_USER_STUDIES_URL'] STUDY_URL = app.config['PB_USER_STUDIES_URL']
INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL'] INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL']
REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL'] REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL']
STUDY_DETAILS_URL = app.config['PB_STUDY_DETAILS_URL'] STUDY_DETAILS_URL = app.config['PB_STUDY_DETAILS_URL']
@staticmethod
def is_enabled():
if isinstance(app.config['PB_ENABLED'], str):
return app.config['PB_ENABLED'].lower() == "true"
else:
return app.config['PB_ENABLED'] is True
@staticmethod @staticmethod
def get_studies(user_id) -> {}: def get_studies(user_id) -> {}:
ProtocolBuilderService.__enabled_or_raise() ProtocolBuilderService.__enabled_or_raise()
@ -44,7 +49,7 @@ class ProtocolBuilderService(object):
@staticmethod @staticmethod
def __enabled_or_raise(): def __enabled_or_raise():
if not ProtocolBuilderService.ENABLED: if not ProtocolBuilderService.is_enabled():
raise ApiError("protocol_builder_disabled", "The Protocol Builder Service is currently disabled.") raise ApiError("protocol_builder_disabled", "The Protocol Builder Service is currently disabled.")
@staticmethod @staticmethod
@ -60,4 +65,3 @@ class ProtocolBuilderService(object):
"Received an invalid response from the protocol builder (status %s): %s when calling " "Received an invalid response from the protocol builder (status %s): %s when calling "
"url '%s'." % "url '%s'." %
(response.status_code, response.text, url)) (response.status_code, response.text, url))

View File

@ -64,16 +64,16 @@ class StudyService(object):
def delete_study(study_id): def delete_study(study_id):
session.query(TaskEventModel).filter_by(study_id=study_id).delete() session.query(TaskEventModel).filter_by(study_id=study_id).delete()
for workflow in session.query(WorkflowModel).filter_by(study_id=study_id): for workflow in session.query(WorkflowModel).filter_by(study_id=study_id):
StudyService.delete_workflow(workflow.id) StudyService.delete_workflow(workflow)
session.query(StudyModel).filter_by(id=study_id).delete() session.query(StudyModel).filter_by(id=study_id).delete()
session.commit() session.commit()
@staticmethod @staticmethod
def delete_workflow(workflow_id): def delete_workflow(workflow):
for file in session.query(FileModel).filter_by(workflow_id=workflow_id).all(): for file in session.query(FileModel).filter_by(workflow_id=workflow.id).all():
FileService.delete_file(file.id) FileService.delete_file(file.id)
session.query(TaskEventModel).filter_by(workflow_id=workflow_id).delete() session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete()
session.query(WorkflowModel).filter_by(id=workflow_id).delete() session.query(WorkflowModel).filter_by(id=workflow.id).delete()
@staticmethod @staticmethod
def get_categories(): def get_categories():
@ -117,7 +117,7 @@ class StudyService(object):
that is available..""" that is available.."""
# Get PB required docs, if Protocol Builder Service is enabled. # Get PB required docs, if Protocol Builder Service is enabled.
if ProtocolBuilderService.ENABLED: if ProtocolBuilderService.is_enabled():
try: try:
pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id) pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id)
except requests.exceptions.ConnectionError as ce: except requests.exceptions.ConnectionError as ce:
@ -133,7 +133,7 @@ class StudyService(object):
documents = {} documents = {}
for code, doc in doc_dictionary.items(): for code, doc in doc_dictionary.items():
if ProtocolBuilderService.ENABLED: if ProtocolBuilderService.is_enabled():
pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None) pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None)
doc['required'] = False doc['required'] = False
if pb_data: if pb_data:
@ -216,8 +216,10 @@ class StudyService(object):
"""Assures that the studies we have locally for the given user are """Assures that the studies we have locally for the given user are
in sync with the studies available in protocol builder. """ in sync with the studies available in protocol builder. """
if not ProtocolBuilderService.ENABLED: if ProtocolBuilderService.is_enabled():
return
app.logger.info("The Protocol Builder is enabled. app.config['PB_ENABLED'] = " +
str(app.config['PB_ENABLED']))
# Get studies matching this user from Protocol Builder # Get studies matching this user from Protocol Builder
pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid) pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid)
@ -290,8 +292,8 @@ class StudyService(object):
return WorkflowProcessor.run_master_spec(master_specs[0], study_model) return WorkflowProcessor.run_master_spec(master_specs[0], study_model)
@staticmethod @staticmethod
def _add_all_workflow_specs_to_study(study): def _add_all_workflow_specs_to_study(study_model:StudyModel):
existing_models = session.query(WorkflowModel).filter(WorkflowModel.study_id == study.id).all() existing_models = session.query(WorkflowModel).filter(WorkflowModel.study == study_model).all()
existing_specs = list(m.workflow_spec_id for m in existing_models) existing_specs = list(m.workflow_spec_id for m in existing_models)
new_specs = session.query(WorkflowSpecModel). \ new_specs = session.query(WorkflowSpecModel). \
filter(WorkflowSpecModel.is_master_spec == False). \ filter(WorkflowSpecModel.is_master_spec == False). \
@ -300,15 +302,15 @@ class StudyService(object):
errors = [] errors = []
for workflow_spec in new_specs: for workflow_spec in new_specs:
try: try:
StudyService._create_workflow_model(study, workflow_spec) StudyService._create_workflow_model(study_model, workflow_spec)
except WorkflowException as we: except WorkflowException as we:
errors.append(ApiError.from_task_spec("workflow_execution_exception", str(we), we.sender)) errors.append(ApiError.from_task_spec("workflow_execution_exception", str(we), we.sender))
return errors return errors
@staticmethod @staticmethod
def _create_workflow_model(study, spec): def _create_workflow_model(study: StudyModel, spec):
workflow_model = WorkflowModel(status=WorkflowStatus.not_started, workflow_model = WorkflowModel(status=WorkflowStatus.not_started,
study_id=study.id, study=study,
workflow_spec_id=spec.id, workflow_spec_id=spec.id,
last_updated=datetime.now()) last_updated=datetime.now())
session.add(workflow_model) session.add(workflow_model)

View File

@ -70,6 +70,14 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
"Unable to locate Script: '%s:%s'" % (module_name, class_name), "Unable to locate Script: '%s:%s'" % (module_name, class_name),
task=task) task=task)
def evaluate_expression(self, task, expression):
"""
Evaluate the given expression, within the context of the given task and
return the result.
"""
exp,valid = self.validateExpression(expression)
return self._eval(exp, **task.data)
@staticmethod @staticmethod
def camel_to_snake(camel): def camel_to_snake(camel):
camel = camel.strip() camel = camel.strip()
@ -276,43 +284,6 @@ class WorkflowProcessor(object):
tag=ve.tag) tag=ve.tag)
return spec return spec
@staticmethod
def populate_form_with_random_data(task, task_api):
"""populates a task with random data - useful for testing a spec."""
if not hasattr(task.task_spec, 'form'): return
form_data = {}
for field in task_api.form.fields:
if field.type == "enum":
if len(field.options) > 0:
form_data[field.id] = random.choice(field.options)
else:
raise ApiError.from_task("invalid_enum", "You specified an enumeration field (%s),"
" with no options" % field.id,
task)
elif field.type == "long":
form_data[field.id] = random.randint(1, 1000)
elif field.type == 'boolean':
form_data[field.id] = random.choice([True, False])
elif field.type == 'file':
form_data[field.id] = random.randint(1, 100)
elif field.type == 'files':
form_data[field.id] = random.randrange(1, 100)
else:
form_data[field.id] = WorkflowProcessor._random_string()
if task.data is None:
task.data = {}
task.data.update(form_data)
@staticmethod
def _random_string(string_length=10):
"""Generate a random string of fixed length """
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(string_length))
@staticmethod @staticmethod
def status_of(bpmn_workflow): def status_of(bpmn_workflow):
if bpmn_workflow.is_completed(): if bpmn_workflow.is_completed():

View File

@ -1,4 +1,6 @@
import string
from datetime import datetime from datetime import datetime
import random
import jinja2 import jinja2
from SpiffWorkflow import Task as SpiffTask, WorkflowException from SpiffWorkflow import Task as SpiffTask, WorkflowException
@ -53,12 +55,82 @@ class WorkflowService(object):
task_api = WorkflowService.spiff_task_to_api_task( task_api = WorkflowService.spiff_task_to_api_task(
task, task,
add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors. add_docs_and_forms=True) # Assure we try to process the documenation, and raise those errors.
WorkflowProcessor.populate_form_with_random_data(task, task_api) WorkflowService.populate_form_with_random_data(task, task_api)
task.complete() task.complete()
except WorkflowException as we: except WorkflowException as we:
raise ApiError.from_task_spec("workflow_execution_exception", str(we), raise ApiError.from_task_spec("workflow_execution_exception", str(we),
we.sender) we.sender)
@staticmethod
def populate_form_with_random_data(task, task_api):
"""populates a task with random data - useful for testing a spec."""
if not hasattr(task.task_spec, 'form'): return
form_data = {}
for field in task_api.form.fields:
if field.type == "enum":
if len(field.options) > 0:
random_choice = random.choice(field.options)
if isinstance(random_choice, dict):
form_data[field.id] = random.choice(field.options)['id']
else:
# fixme: why it is sometimes an EnumFormFieldOption, and other times not?
form_data[field.id] = random_choice.id ## Assume it is an EnumFormFieldOption
else:
raise ApiError.from_task("invalid_enum", "You specified an enumeration field (%s),"
" with no options" % field.id,
task)
elif field.type == "autocomplete":
lookup_model = LookupService.get_lookup_table(task, field)
if field.has_property(Task.PROP_LDAP_LOOKUP):
form_data[field.id] = {
"label": "dhf8r",
"value": "Dan Funk",
"data": {
"uid": "dhf8r",
"display_name": "Dan Funk",
"given_name": "Dan",
"email_address": "dhf8r@virginia.edu",
"department": "Depertment of Psychocosmographictology",
"affiliation": "Rousabout",
"sponsor_type": "Staff"
}
}
elif lookup_model:
data = db.session.query(LookupDataModel).filter(
LookupDataModel.lookup_file_model == lookup_model).limit(10).all()
options = []
for d in data:
options.append({"id": d.value, "name": d.label})
form_data[field.id] = random.choice(options)
else:
raise ApiError.from_task("invalid_autocomplete", "The settings for this auto complete field "
"are incorrect: %s " % field.id, task)
elif field.type == "long":
form_data[field.id] = random.randint(1, 1000)
elif field.type == 'boolean':
form_data[field.id] = random.choice([True, False])
elif field.type == 'file':
form_data[field.id] = random.randint(1, 100)
elif field.type == 'files':
form_data[field.id] = random.randrange(1, 100)
else:
form_data[field.id] = WorkflowService._random_string()
if task.data is None:
task.data = {}
task.data.update(form_data)
def __get_options(self):
pass
@staticmethod
def _random_string(string_length=10):
"""Generate a random string of fixed length """
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(string_length))
@staticmethod @staticmethod
def spiff_task_to_api_task(spiff_task, add_docs_and_forms=False): def spiff_task_to_api_task(spiff_task, add_docs_and_forms=False):
task_type = spiff_task.task_spec.__class__.__name__ task_type = spiff_task.task_spec.__class__.__name__

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?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" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_0be39yr" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0"> <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" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" id="Definitions_0be39yr" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_1cme33c" isExecutable="false"> <bpmn:process id="Process_1cme33c" isExecutable="false">
<bpmn:parallelGateway id="ParallelGateway_0ecwf3g"> <bpmn:parallelGateway id="ParallelGateway_0ecwf3g">
<bpmn:incoming>Flow_1wqp7vf</bpmn:incoming> <bpmn:incoming>Flow_1wqp7vf</bpmn:incoming>
@ -36,10 +36,9 @@
</camunda:formField> </camunda:formField>
<camunda:formField id="ProtocolOwnerName" label="Protocol Owner Name" type="autocomplete"> <camunda:formField id="ProtocolOwnerName" label="Protocol Owner Name" type="autocomplete">
<camunda:properties> <camunda:properties>
<camunda:property id="enum.options.file" value="SponsorList.xls" /> <camunda:property id="spreadsheet.name" value="SponsorList.xls" />
<camunda:property id="enum.options.value.column" value="CUSTOMER_NUMBER" /> <camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="enum.options.label.column" value="CUSTOMER_NAME" /> <camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
<camunda:property id="enum.options.lookup" value="True" />
<camunda:property id="help" value="#### How To:\nYou can find the name by typing any part (at least 3 characters) of the name.\n\nNote: This source of this list is in the Integration System (Oracle) and the information is owned by and managed by the OSP team.\n\nIf you are not finding the name or need to make any changes.\n1. Email &#39;Information Team listserve&#39; osp-infoteam@virginia.edu with the Subject Line &#34;Requesting New Sponsor Setup&#34; and provide the following information:\n - Sponsor Legal Name, Address, Sponsor Classification (Federal Government, Foreign Entity, Foundation, Industry, Local Government, Other Colleges &#38; Universities or State Government) as stated in the agreement/notification.\n - Copies of the agreement from the sponsor (contract, award letter, email, etc.).\n2. Once all the required information is received, OSP will add the name to the list.\n3. The updated list should be available for your selection in the workflow within 2 business days." /> <camunda:property id="help" value="#### How To:\nYou can find the name by typing any part (at least 3 characters) of the name.\n\nNote: This source of this list is in the Integration System (Oracle) and the information is owned by and managed by the OSP team.\n\nIf you are not finding the name or need to make any changes.\n1. Email &#39;Information Team listserve&#39; osp-infoteam@virginia.edu with the Subject Line &#34;Requesting New Sponsor Setup&#34; and provide the following information:\n - Sponsor Legal Name, Address, Sponsor Classification (Federal Government, Foreign Entity, Foundation, Industry, Local Government, Other Colleges &#38; Universities or State Government) as stated in the agreement/notification.\n - Copies of the agreement from the sponsor (contract, award letter, email, etc.).\n2. Once all the required information is received, OSP will add the name to the list.\n3. The updated list should be available for your selection in the workflow within 2 business days." />
<camunda:property id="description" value="The protocol owner name is always an entity. For example, if this is a UVA Primary Investigator - Investigator initiated study, the Protocol Owner Name will be &#34;University of Virginia&#34;" /> <camunda:property id="description" value="The protocol owner name is always an entity. For example, if this is a UVA Primary Investigator - Investigator initiated study, the Protocol Owner Name will be &#34;University of Virginia&#34;" />
</camunda:properties> </camunda:properties>
@ -235,104 +234,104 @@
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1cme33c"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1cme33c">
<bpmndi:BPMNEdge id="Flow_1wqp7vf_di" bpmnElement="Flow_1wqp7vf"> <bpmndi:BPMNEdge id="Flow_1wqp7vf_di" bpmnElement="Flow_1wqp7vf">
<di:waypoint x="450" y="325" /> <di:waypoint x="820" y="325" />
<di:waypoint x="495" y="325" /> <di:waypoint x="865" y="325" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tfyk5m_di" bpmnElement="Flow_1tfyk5m"> <bpmndi:BPMNEdge id="Flow_1tfyk5m_di" bpmnElement="Flow_1tfyk5m">
<di:waypoint x="300" y="325" /> <di:waypoint x="670" y="325" />
<di:waypoint x="350" y="325" /> <di:waypoint x="720" y="325" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d4dncx_di" bpmnElement="Flow_1d4dncx"> <bpmndi:BPMNEdge id="Flow_1d4dncx_di" bpmnElement="Flow_1d4dncx">
<di:waypoint x="520" y="300" /> <di:waypoint x="890" y="300" />
<di:waypoint x="520" y="250" /> <di:waypoint x="890" y="250" />
<di:waypoint x="620" y="250" /> <di:waypoint x="990" y="250" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16v64sg_di" bpmnElement="Flow_16v64sg"> <bpmndi:BPMNEdge id="Flow_16v64sg_di" bpmnElement="Flow_16v64sg">
<di:waypoint x="140" y="325" /> <di:waypoint x="510" y="325" />
<di:waypoint x="200" y="325" /> <di:waypoint x="570" y="325" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_09h1imz_di" bpmnElement="Flow_09h1imz"> <bpmndi:BPMNEdge id="Flow_09h1imz_di" bpmnElement="Flow_09h1imz">
<di:waypoint x="-20" y="325" /> <di:waypoint x="350" y="325" />
<di:waypoint x="40" y="325" /> <di:waypoint x="410" y="325" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1v7oplk_di" bpmnElement="SequenceFlow_1v7oplk"> <bpmndi:BPMNEdge id="SequenceFlow_1v7oplk_di" bpmnElement="SequenceFlow_1v7oplk">
<di:waypoint x="845" y="325" /> <di:waypoint x="1215" y="325" />
<di:waypoint x="898" y="325" /> <di:waypoint x="1268" y="325" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0rw17h2_di" bpmnElement="SequenceFlow_0rw17h2"> <bpmndi:BPMNEdge id="SequenceFlow_0rw17h2_di" bpmnElement="SequenceFlow_0rw17h2">
<di:waypoint x="720" y="500" /> <di:waypoint x="1090" y="500" />
<di:waypoint x="820" y="500" /> <di:waypoint x="1190" y="500" />
<di:waypoint x="820" y="350" /> <di:waypoint x="1190" y="350" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0gsy7mo_di" bpmnElement="SequenceFlow_0gsy7mo"> <bpmndi:BPMNEdge id="SequenceFlow_0gsy7mo_di" bpmnElement="SequenceFlow_0gsy7mo">
<di:waypoint x="720" y="380" /> <di:waypoint x="1090" y="380" />
<di:waypoint x="820" y="380" /> <di:waypoint x="1190" y="380" />
<di:waypoint x="820" y="350" /> <di:waypoint x="1190" y="350" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1o39rt4_di" bpmnElement="SequenceFlow_1o39rt4"> <bpmndi:BPMNEdge id="SequenceFlow_1o39rt4_di" bpmnElement="SequenceFlow_1o39rt4">
<di:waypoint x="720" y="250" /> <di:waypoint x="1090" y="250" />
<di:waypoint x="820" y="250" /> <di:waypoint x="1190" y="250" />
<di:waypoint x="820" y="300" /> <di:waypoint x="1190" y="300" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_02nbqkn_di" bpmnElement="SequenceFlow_02nbqkn"> <bpmndi:BPMNEdge id="SequenceFlow_02nbqkn_di" bpmnElement="SequenceFlow_02nbqkn">
<di:waypoint x="720" y="130" /> <di:waypoint x="1090" y="130" />
<di:waypoint x="820" y="130" /> <di:waypoint x="1190" y="130" />
<di:waypoint x="820" y="300" /> <di:waypoint x="1190" y="300" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0xj8i4c_di" bpmnElement="SequenceFlow_0xj8i4c"> <bpmndi:BPMNEdge id="SequenceFlow_0xj8i4c_di" bpmnElement="SequenceFlow_0xj8i4c">
<di:waypoint x="520" y="350" /> <di:waypoint x="890" y="350" />
<di:waypoint x="520" y="500" /> <di:waypoint x="890" y="500" />
<di:waypoint x="620" y="500" /> <di:waypoint x="990" y="500" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1idbomg_di" bpmnElement="SequenceFlow_1idbomg"> <bpmndi:BPMNEdge id="SequenceFlow_1idbomg_di" bpmnElement="SequenceFlow_1idbomg">
<di:waypoint x="520" y="350" /> <di:waypoint x="890" y="350" />
<di:waypoint x="520" y="380" /> <di:waypoint x="890" y="380" />
<di:waypoint x="620" y="380" /> <di:waypoint x="990" y="380" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_0f61fxp_di" bpmnElement="SequenceFlow_0f61fxp"> <bpmndi:BPMNEdge id="SequenceFlow_0f61fxp_di" bpmnElement="SequenceFlow_0f61fxp">
<di:waypoint x="520" y="300" /> <di:waypoint x="890" y="300" />
<di:waypoint x="520" y="130" /> <di:waypoint x="890" y="130" />
<di:waypoint x="620" y="130" /> <di:waypoint x="990" y="130" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1r3yrhy_di" bpmnElement="SequenceFlow_1r3yrhy"> <bpmndi:BPMNEdge id="SequenceFlow_1r3yrhy_di" bpmnElement="SequenceFlow_1r3yrhy">
<di:waypoint x="-182" y="325" /> <di:waypoint x="188" y="325" />
<di:waypoint x="-120" y="325" /> <di:waypoint x="250" y="325" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="ParallelGateway_0ecwf3g_di" bpmnElement="ParallelGateway_0ecwf3g"> <bpmndi:BPMNShape id="ParallelGateway_0ecwf3g_di" bpmnElement="ParallelGateway_0ecwf3g">
<dc:Bounds x="495" y="300" width="50" height="50" /> <dc:Bounds x="865" y="300" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ParallelGateway_01234ff_di" bpmnElement="ParallelGateway_01234ff"> <bpmndi:BPMNShape id="ParallelGateway_01234ff_di" bpmnElement="ParallelGateway_01234ff">
<dc:Bounds x="795" y="300" width="50" height="50" /> <dc:Bounds x="1165" y="300" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_16uwhzg_di" bpmnElement="EndEvent_16uwhzg"> <bpmndi:BPMNShape id="EndEvent_16uwhzg_di" bpmnElement="EndEvent_16uwhzg">
<dc:Bounds x="898" y="307" width="36" height="36" /> <dc:Bounds x="1268" y="307" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="StartEvent_1mhzkcr_di" bpmnElement="StartEvent_1mhzkcr"> <bpmndi:BPMNShape id="StartEvent_1mhzkcr_di" bpmnElement="StartEvent_1mhzkcr">
<dc:Bounds x="-218" y="307" width="36" height="36" /> <dc:Bounds x="152" y="307" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_1y1qon7_di" bpmnElement="UserTask_1y1qon7"> <bpmndi:BPMNShape id="UserTask_1y1qon7_di" bpmnElement="UserTask_1y1qon7">
<dc:Bounds x="620" y="340" width="100" height="80" /> <dc:Bounds x="990" y="340" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_01zzzg9_di" bpmnElement="UserTask_01zzzg9"> <bpmndi:BPMNShape id="UserTask_01zzzg9_di" bpmnElement="UserTask_01zzzg9">
<dc:Bounds x="620" y="460" width="100" height="80" /> <dc:Bounds x="990" y="460" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0gtuk1e_di" bpmnElement="UserTask_EnterMultiSiteInfo"> <bpmndi:BPMNShape id="UserTask_0gtuk1e_di" bpmnElement="UserTask_EnterMultiSiteInfo">
<dc:Bounds x="620" y="210" width="100" height="80" /> <dc:Bounds x="990" y="210" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0ebxkp7_di" bpmnElement="UserTask_0ebxkp7"> <bpmndi:BPMNShape id="UserTask_0ebxkp7_di" bpmnElement="UserTask_0ebxkp7">
<dc:Bounds x="620" y="90" width="100" height="80" /> <dc:Bounds x="990" y="90" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0vthni9_di" bpmnElement="Activity_10nxpt2"> <bpmndi:BPMNShape id="Activity_0vthni9_di" bpmnElement="Activity_10nxpt2">
<dc:Bounds x="-120" y="285" width="100" height="80" /> <dc:Bounds x="250" y="285" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0spxv8q_di" bpmnElement="Activity_PBMultiSiteCheckQ12"> <bpmndi:BPMNShape id="Activity_0spxv8q_di" bpmnElement="Activity_PBMultiSiteCheckQ12">
<dc:Bounds x="40" y="285" width="100" height="80" /> <dc:Bounds x="410" y="285" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ah6heg_di" bpmnElement="Activity_PBMultiSiteCheckQ14"> <bpmndi:BPMNShape id="Activity_0ah6heg_di" bpmnElement="Activity_PBMultiSiteCheckQ14">
<dc:Bounds x="200" y="285" width="100" height="80" /> <dc:Bounds x="570" y="285" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0x7b58m_di" bpmnElement="Activity_PBMultiSiteCheckQ28"> <bpmndi:BPMNShape id="Activity_0x7b58m_di" bpmnElement="Activity_PBMultiSiteCheckQ28">
<dc:Bounds x="350" y="285" width="100" height="80" /> <dc:Bounds x="720" y="285" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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 &amp; Sciences"</text>
</inputEntry>
<outputEntry id="LiteralExpression_0ua8d03">
<text>"Arts &amp; 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 &amp; Public Policy"</text>
</inputEntry>
<outputEntry id="LiteralExpression_0dy02ei">
<text>"Leadership &amp; 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.

View File

@ -0,0 +1,860 @@
<?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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_1oogn9j" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0">
<bpmn:process id="Process_0ssahs9" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_05ja25w</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:manualTask id="ManualTask_Instructions" name="Read RRP Instructions">
<bpmn:documentation>## Beta Stage: All data entered will be destroyed before public launch
### 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 COOs office to coordinate the acquisition of supplies necessary including face coverings and sanitizing supplies.
**Instructions for Submitting:**
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, 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="Primary Investigator" type="autocomplete">
<camunda:properties>
<camunda:property id="placeholder" value="wxy0z or Smith" />
<camunda:property id="ldap.lookup" value="true" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
<camunda:constraint name="description" config="Find the PI by entering Computing ID or Last Name." />
<camunda:constraint name="ldap.lookup" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="PISchool" label="PI&#39;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:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptArchitecture" label="PI&#39;s 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.PISchool &#38;&#38; model.PISchool !== &#34;Architecture&#34;) || model.PISchool === null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptArtsSciences" label="PI&#39;s Primary Arts &#38; 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.PISchool &#38;&#38; model.PISchool !== &#34;Arts &#38; Sciences&#34;) || model.PISchool === null" />
<camunda:property id="description" value="Type key words to find department" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptEducation" label="PI&#39;s 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.PISchool &#38;&#38; model.PISchool !== &#34;Education&#34;) || model.PISchool === null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptEngineering" label="PI&#39;s 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.PISchool &#38;&#38; model.PISchool !== &#34;Engineering&#34;) || model.PISchool === null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptMedicine" label="PI&#39;s 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.PISchool &#38;&#38; model.PISchool !== &#34;Medicine&#34;) || model.PISchool === null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptProvostOffice" label="PI&#39;s 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.PISchool &#38;&#38; model.PISchool !== &#34;Provost Office&#34;) || model.PISchool === null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PIPrimaryDeptOther" label="Primary Department " type="string">
<camunda:properties>
<camunda:property id="hide_expression" value="(model.PIPrimaryDeptArchitecture === null &#38;&#38; model.PIPrimaryDeptArtsSciences === null &#38;&#38; model.PIPrimaryDeptEducation === null &#38;&#38; model.PIPrimaryDeptEngineering === null &#38;&#38; model.PIPrimaryDeptMedicine === null &#38;&#38; model.PIPrimaryDeptProvostOffice === null) || (model.PIPrimaryDeptArchitecture !== &#34;Other&#34; &#38;&#38; model.PIPrimaryDeptArtsSciences !== &#34;Other&#34; &#38;&#38; model.PIPrimaryDeptEducation !== &#34;Other&#34; &#38;&#38; model.PIPrimaryDeptEngineering !== &#34;Other&#34; &#38;&#38; model.PIPrimaryDeptMedicine !== &#34;Other&#34; &#38;&#38; model.PIPrimaryDeptProvostOffice !== &#34;Other&#34;)" />
<camunda:property id="description" value="Enter the PI&#39;s Primary Department " />
</camunda:properties>
</camunda:formField>
<camunda:formField id="PISupervisor" label="Pi&#39;s Supervisor" type="autocomplete">
<camunda:properties>
<camunda:property id="description" value="Find the PI&#39;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" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>SequenceFlow_0h50bp3</bpmn:incoming>
<bpmn:outgoing>Flow_16y8glw</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="SequenceFlow_0h50bp3" sourceRef="ManualTask_Instructions" targetRef="Activity-PI_Info" />
<bpmn:sequenceFlow id="SequenceFlow_05ja25w" sourceRef="StartEvent_1" targetRef="ManualTask_Instructions" />
<bpmn:userTask id="Personnel" name="Enter Personnel" camunda:formKey="Personnel">
<bpmn:documentation>#### People for whom you are requesting access
Information on all researchers you are requesting approval for reentry into the lab/research space for conducting research on-Grounds. (If there are personnel already working in the space, include them).
**Note: no undergraduates will be allowed to work on-Grounds during Phase II.**</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="PersonnelComputingID" label="Computer ID" type="autocomplete">
<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" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="PersonnelType" label="Personnel Type" type="enum">
<camunda:properties>
<camunda:property id="repeat" value="Personnel" />
</camunda:properties>
<camunda:value id="Faculty" name="Faculty" />
<camunda:value id="AcademicResearcher" name="Academic Researcher" />
<camunda:value id="Staff" name="Staff" />
<camunda:value id="Postdoc" name="Postdoc" />
<camunda:value id="DoctoralStudent" name="Doctoral Student" />
<camunda:value id="UndeGraduate" name="Undergraduate" />
</camunda:formField>
<camunda:formField id="PersonnelSpace" label="Space they will work in" type="textarea">
<camunda:properties>
<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" />
</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 persons research and justification why this is critical research." />
<camunda:property id="rows" value="3" />
<camunda:property id="autosize" value="true" />
<camunda:property id="repeat" value="Personnel" />
</camunda:properties>
<camunda:validation>
<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>
<bpmn:outgoing>Flow_1nbjr72</bpmn:outgoing>
</bpmn:userTask>
<bpmn:userTask id="UserTask_CoreResource" name="Enter Core Resources" camunda:formKey="Core Resources">
<bpmn:documentation>#### If applicable, provide a list of any [Core Resources](https://research.virginia.edu/research-core-resources) you will utilize space or instruments in and name/email of contact person in the core you have coordinated your plan with. (Core facility managers are responsible for developing a plan for their space)</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="isCoreResourcesUse" label="Core Resources Use" type="boolean">
<camunda:properties>
<camunda:property id="description" value="Will you need access to core resources to resume research?" />
<camunda:property id="help" value="[From Research Ramp-up Guidance](https://research.virginia.edu/research-ramp-guidance)\n#### ADDITIONAL CONSIDERATIONS FOR A SUCCESSFUL RAMP-UP:\n##### Lab-based Research\n- (1) Core facilities operational\n\n[EHS Lab Ramp up Checklist for Laboratories](https://research.virginia.edu/sites/vpr/files/2020-05/EHS.LabRampUpChecklistForLaboratories_0_0.pdf)\n##### General\n- Note that shared facilities, such as stockrooms or core labs, may be on different ramp up schedules or in more demand than during normal operation." />
</camunda:properties>
</camunda:formField>
<camunda:formField id="CoreResources" label="Core Resources" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="10" />
<camunda:property id="autosize" value="true" />
<camunda:property id="hide_expression" value="!model.isCoreResourcesUse | model.isCoreResourcesUse == null" />
</camunda:properties>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_15zy1q7</bpmn:incoming>
<bpmn:outgoing>Flow_12ie6w0</bpmn:outgoing>
</bpmn:userTask>
<bpmn:endEvent id="EndEvent_09wp7av">
<bpmn:documentation>#### End of Workflow
Place instruction here,</bpmn:documentation>
<bpmn:incoming>Flow_05w8yd6</bpmn:incoming>
</bpmn:endEvent>
<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_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="SharedSpaceBuilding" label="Building Name" type="autocomplete">
<camunda:properties>
<camunda:property id="description" value="Select the building where this shared lab space is housed." />
<camunda:property id="spreadsheet.name" value="BuildingList.xls" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Building Name" />
<camunda:property id="repeat" value="Shared" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="SharedSpaceRoomID" label="Shared Space Room ID" type="string">
<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 === &#34;Other&#34;" />
</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 !== &#34;Other&#34; | 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="wxy0z or Smith" />
<camunda:property id="description" value="Enter Area Monitor&#39;s Computing ID or last name and select Area Monitor. Leave blank if not known." />
<camunda:property id="repeat" value="Shared" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="SharedSpaceSqFt" label="Enter the number of square feet usable by personnel in this space." type="long">
<camunda:properties>
<camunda:property id="repeat" value="Shared" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="SharedSpacePercentUsable" label="Percent of Space Usable By Personnel" type="long">
<camunda:properties>
<camunda:property id="repeat" value="Shared" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="min" config="1" />
<camunda:constraint name="max" config="100" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="SharedSpaceMaxPersonnel" label="Maximum Number of Personnel Occupying Space" type="long">
<camunda:properties>
<camunda:property id="description" value="Enter the maximum number of personnel, including yourself, who will occupy this space." />
<camunda:property id="repeat" value="Shared" />
</camunda:properties>
<camunda:validation>
<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_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_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_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" 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="Room No. &#38; 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" />
<camunda:property id="spreadsheet.value.column" value="Value" />
<camunda:property id="spreadsheet.label.column" value="Building Name" />
<camunda:property id="repeat" value="Exclusive" />
</camunda:properties>
<camunda:validation>
<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 === &#34;Other&#34;" />
</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 !== &#34;Other&#34; | 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" />
<camunda:property id="enum_type" value="radio" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
<camunda:value id="Lab" name="Lab" />
<camunda:value id="Office" name="Office" />
</camunda:formField>
<camunda:formField id="ExclusiveSpaceAMComputingID" label="Area Monitor" type="autocomplete">
<camunda:properties>
<camunda:property id="ldap.lookup" value="true" />
<camunda:property id="description" value="Enter Area Monitor&#39;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="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">
<camunda:properties>
<camunda:property id="repeat" value="Exclusive" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="ExclusiveSpacePercentUsable" label="Percent of Space Usable By Personnel" type="long">
<camunda:properties>
<camunda:property id="repeat" value="Exclusive" />
<camunda:property id="description" value="Enter number between 1 &#38; 100 which indicates the percent of space this space personnel will work and move around in during the workday." />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="min" config="1" />
<camunda:constraint name="max" config="100" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="ExclusiveSpaceMaxPersonnel" label="Maximum Number of Personnel Occupying Space" type="long">
<camunda:properties>
<camunda:property id="description" value="Enter the maximum number of personnel, including yourself, who will occupy this space." />
<camunda:property id="repeat" value="Exclusive" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0qf2y84</bpmn:incoming>
<bpmn:outgoing>Flow_0tk64b6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_15zy1q7" sourceRef="Gateway_0frfdnc" targetRef="UserTask_CoreResource" />
<bpmn:sequenceFlow id="Flow_12ie6w0" sourceRef="UserTask_CoreResource" targetRef="Gateway_1vj4zd3" />
<bpmn:sequenceFlow id="Flow_0ya8hw8" sourceRef="Gateway_0frfdnc" targetRef="Activity_nonUVASpaces" />
<bpmn:userTask id="Activity_nonUVASpaces" name="Enter non-UVA Spaces" camunda:formKey="nonUVA Spaces">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="isNonUVASpaces" label="non-UVA Spaces?" type="boolean">
<camunda:properties>
<camunda:property id="description" value="Does any of your research occur in non-UVA spaces? (For example, field stations or UVA leased buildings off Grounds) " />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="NonUVASpaces" label="Where?" type="textarea">
<camunda:properties>
<camunda:property id="description" value="Does any of your research occur in non-UVA spaces? (For example, field stations or UVA leased buildings off Grounds) " />
<camunda:property id="hide_expression" value="!model.isNonUVASpaces | model.isNonUVASpaces == null" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0ya8hw8</bpmn:incoming>
<bpmn:outgoing>Flow_0zz2hbq</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0zz2hbq" sourceRef="Activity_nonUVASpaces" targetRef="Gateway_1vj4zd3" />
<bpmn:sequenceFlow id="Flow_1eiud85" sourceRef="Gateway_1vj4zd3" targetRef="Personnel" />
<bpmn:sequenceFlow id="Flow_1nbjr72" sourceRef="Personnel" targetRef="Gateway_18jn18b" />
<bpmn:userTask id="Activity_DistanceReq" name="Enter Distancing Requirements" camunda:formKey="Distancing Requirements">
<bpmn:documentation>#### Distancing requirements:
Maintain social distancing by designing space between people to be at least 9 feet during prolonged work which will be accomplished by restricting the number of people in the lab to a density of ~250 sq. ft. /person in lab areas. When moving around, a minimum of 6 feet social distancing is required. Ideally only one person per lab bench and not more than one person can work at the same time in the same bay.</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="CIDR_TotalSqFt" label="What is the total square footage of your lab?" type="long">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="CIDR_MaxPersonnel" label="How many personnel will be using the lab at any one time, at a maximum?" type="long">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0p2r1bo</bpmn:incoming>
<bpmn:outgoing>Flow_0tz5c2v</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_0p2r1bo" sourceRef="Gateway_18jn18b" targetRef="Activity_DistanceReq" />
<bpmn:parallelGateway id="Gateway_18jn18b">
<bpmn:incoming>Flow_1nbjr72</bpmn:incoming>
<bpmn:outgoing>Flow_0p2r1bo</bpmn:outgoing>
<bpmn:outgoing>Flow_0mkh1wn</bpmn:outgoing>
<bpmn:outgoing>Flow_1yqkpgu</bpmn:outgoing>
<bpmn:outgoing>Flow_1c6m5wv</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:sequenceFlow id="Flow_0mkh1wn" sourceRef="Gateway_18jn18b" targetRef="Activity_PWA" />
<bpmn:userTask id="Activity_PWA" name="Enter Physical Work Arrangements" camunda:formKey="Physical Work Arrangements">
<bpmn:documentation>Describe physical work arrangements for each lab. Show schematic of the lab and space organization to meet the distancing guidelines (see key safety expectations for ramp-up).
- Show gross dimensions, location of desks, and equipment in blocks (not details) that show available space for work and foot traffic.
- Indicate total square footage for every lab/space that you are requesting adding personnel to in this application. If you would like help obtaining a floor plan for your lab, your department or deans office can help. You can also create a hand drawing/block diagram of your space and the location of objects on a graph paper.
- Upload your physical layout and workspace organization in the form of a jpg image or a pdf file. This can be hand-drawn or actual floor plans.
- Show and/or describe designated work location for each member (during their shift) in the lab when multiple members are present at a time to meet the distancing guidelines.
- Provide a foot traffic plan (on the schematic) to indicate how people can move around while maintaining distancing requirements. This can be a freeform sketch on your floor plan showing where foot traffic can occur in your lab, and conditions, if any, to ensure distancing at all times. (e.g., direction to walk around a lab bench, rules for using shared equipment located in the lab, certain areas of lab prohibited from access, etc.).
- Provide your initial weekly laboratory schedule (see excel template) for all members that you are requesting access for, indicating all shifts as necessary. If schedule changes, please submit your revised schedule through the web portal.</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="PWADescribe" label="Describe" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="10" />
<camunda:property id="autosize" value="true" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="PWAFiles" label="Upload supporting files" type="files" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_0mkh1wn</bpmn:incoming>
<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 Plan">
<bpmn:documentation>#### Health Safety Requirements:
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&amp;source=gmail&amp;ust=1590687968958000&amp;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
- Health self-attestation requirement
- Sanitizing procedures including frequency and type of disinfectants to use
-
- Where and how to obtain PPE including face covering</bpmn:documentation>
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="LabPlan" label="Upload Lab Plan" type="file" />
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1yqkpgu</bpmn:incoming>
<bpmn:outgoing>Flow_1ox5nv6</bpmn:outgoing>
</bpmn:userTask>
<bpmn:sequenceFlow id="Flow_1c6m5wv" sourceRef="Gateway_18jn18b" targetRef="Activity_OtherReq" />
<bpmn:userTask id="Activity_OtherReq" name="Enter Other Requirements" camunda:formKey="Other Requirements">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="isAnimalResearch" label="Will you be using animals in your research? " type="boolean">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="isAnimalResearchCoordinated" label="Have you coordinated with the vivarium manager to meet your needs?" type="boolean">
<camunda:properties>
<camunda:property id="hide_expression" value="!model.isAnimalResearch | model.isAnimalResearch == null" />
</camunda:properties>
</camunda:formField>
<camunda:formField id="isHumanSubjects" label="Does your research involve human subjects? " type="boolean">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="IRBApprovalRelevantNumbers" label="List IRB Approval Relevant Numbers" type="textarea">
<camunda:properties>
<camunda:property id="rows" value="5" />
<camunda:property id="hide_expression" value="!model.isHumanSubjects | model.isHumanSubjects == null" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="isNecessarySupplies" label="Do you have the necessary materials, supplies, cleaning supplies and PPE for your laboratory?" type="boolean">
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
<camunda:formField id="SupplyList" label="List your needs" type="textarea">
<camunda:properties>
<camunda:property id="hide_expression" value="model.isNecessarySupplies | model.isNecessarySupplies == null" />
<camunda:property id="rows" value="5" />
</camunda:properties>
<camunda:validation>
<camunda:constraint name="required" config="true" />
</camunda:validation>
</camunda:formField>
</camunda:formData>
</bpmn:extensionElements>
<bpmn:incoming>Flow_1c6m5wv</bpmn:incoming>
<bpmn:outgoing>Flow_0qbi47d</bpmn:outgoing>
</bpmn:userTask>
<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 &amp; 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_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_1tub2mc" />
<bpmn:parallelGateway id="Gateway_0sijkgx">
<bpmn:incoming>Flow_0zrsh65</bpmn:incoming>
<bpmn:incoming>Flow_0tz5c2v</bpmn:incoming>
<bpmn:incoming>Flow_1ox5nv6</bpmn:incoming>
<bpmn:incoming>Flow_0qbi47d</bpmn:incoming>
<bpmn:outgoing>Flow_06873ag</bpmn:outgoing>
</bpmn:parallelGateway>
<bpmn:scriptTask id="Activity_1tub2mc" name="Generate RRP">
<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&#34;" 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&#39;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_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="1495" y="307" />
<di:waypoint x="1540" y="307" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0qbi47d_di" bpmnElement="Flow_0qbi47d">
<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="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="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="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="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="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="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="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="1150" y="307" />
<di:waypoint x="1205" y="307" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1eiud85_di" bpmnElement="Flow_1eiud85">
<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="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="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="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="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="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="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="480" y="307" />
<di:waypoint x="530" y="307" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16342pm_di" bpmnElement="Flow_16342pm">
<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="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="1770" y="307" />
<di:waypoint x="1800" y="307" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_05ja25w_di" bpmnElement="SequenceFlow_05ja25w">
<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" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="132" y="289" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ManualTask_1ofy9yz_di" bpmnElement="ManualTask_Instructions">
<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="380" y="267" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0ecab9j_di" bpmnElement="Personnel">
<dc:Bounds x="1050" y="267" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_0l8vxty_di" bpmnElement="UserTask_CoreResource">
<dc:Bounds x="790" y="330" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_09wp7av_di" bpmnElement="EndEvent_09wp7av">
<dc:Bounds x="2692" y="289" width="36" height="36" />
</bpmndi:BPMNShape>
<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_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="685" y="282" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1o1fcbg_di" bpmnElement="Gateway_1vj4zd3">
<dc:Bounds x="945" y="282" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1jefdme_di" bpmnElement="Activity_ExclusiveSpace">
<dc:Bounds x="790" y="70" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0ysw6zo_di" bpmnElement="Activity_nonUVASpaces">
<dc:Bounds x="790" y="470" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1xag6qb_di" bpmnElement="Activity_DistanceReq">
<dc:Bounds x="1300" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0yof76x_di" bpmnElement="Gateway_18jn18b">
<dc:Bounds x="1205" y="282" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_166b0qq_di" bpmnElement="Activity_PWA">
<dc:Bounds x="1300" y="200" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0byj9mp_di" bpmnElement="Activity_HSR">
<dc:Bounds x="1300" y="340" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0j0p13i_di" bpmnElement="Activity_OtherReq">
<dc:Bounds x="1300" y="470" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1h30fo8_di" bpmnElement="Activity_SubmitPlan">
<dc:Bounds x="1930" y="267" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_03lxnzh_di" bpmnElement="Gateway_0sijkgx">
<dc:Bounds x="1445" y="282" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_117owwi_di" bpmnElement="Activity_1tub2mc">
<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>
</bpmn:definitions>

View File

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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?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_1x1akiz" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.5.0"> <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_1x1akiz" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.3">
<bpmn:process id="Process_0quormc" isExecutable="true"> <bpmn:process id="Process_0quormc" isExecutable="true">
<bpmn:startEvent id="StartEvent_1"> <bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_17znkku</bpmn:outgoing> <bpmn:outgoing>SequenceFlow_17znkku</bpmn:outgoing>
@ -16,10 +16,9 @@
</camunda:formField> </camunda:formField>
<camunda:formField id="FormField_FromOSP" label="From OSP" type="autocomplete"> <camunda:formField id="FormField_FromOSP" label="From OSP" type="autocomplete">
<camunda:properties> <camunda:properties>
<camunda:property id="enum.options.file" value="sponsors.xls" /> <camunda:property id="spreadsheet.name" value="sponsors.xls" />
<camunda:property id="enum.options.value.column" value="CUSTOMER_NUMBER" /> <camunda:property id="spreadsheet.value.column" value="CUSTOMER_NUMBER" />
<camunda:property id="enum.options.label.column" value="CUSTOMER_NAME" /> <camunda:property id="spreadsheet.label.column" value="CUSTOMER_NAME" />
<camunda:property id="enum.options.lookup" value="True" />
</camunda:properties> </camunda:properties>
</camunda:formField> </camunda:formField>
<camunda:formField id="FormField_Type" label="Select all that apply:" type="enum"> <camunda:formField id="FormField_Type" label="Select all that apply:" type="enum">
@ -70,51 +69,51 @@
</bpmn:process> </bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0quormc"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0quormc">
<bpmndi:BPMNEdge id="SequenceFlow_1n3utyf_di" bpmnElement="SequenceFlow_1n3utyf"> <bpmndi:BPMNEdge id="Flow_1l3gw28_di" bpmnElement="Flow_1l3gw28">
<di:waypoint x="430" y="40" /> <di:waypoint x="430" y="280" />
<di:waypoint x="490" y="40" /> <di:waypoint x="490" y="280" />
<di:waypoint x="490" y="92" /> <di:waypoint x="490" y="222" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_17znkku_di" bpmnElement="SequenceFlow_17znkku">
<di:waypoint x="188" y="117" />
<di:waypoint x="235" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13604n2_di" bpmnElement="Flow_13604n2">
<di:waypoint x="260" y="92" />
<di:waypoint x="260" y="40" />
<di:waypoint x="330" y="40" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_030v94s_di" bpmnElement="Flow_030v94s">
<di:waypoint x="515" y="117" />
<di:waypoint x="552" y="117" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0hdjgx6_di" bpmnElement="Flow_0hdjgx6"> <bpmndi:BPMNEdge id="Flow_0hdjgx6_di" bpmnElement="Flow_0hdjgx6">
<di:waypoint x="260" y="142" /> <di:waypoint x="260" y="222" />
<di:waypoint x="260" y="200" /> <di:waypoint x="260" y="280" />
<di:waypoint x="330" y="200" /> <di:waypoint x="330" y="280" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1l3gw28_di" bpmnElement="Flow_1l3gw28"> <bpmndi:BPMNEdge id="Flow_030v94s_di" bpmnElement="Flow_030v94s">
<di:waypoint x="430" y="200" /> <di:waypoint x="515" y="197" />
<di:waypoint x="490" y="200" /> <di:waypoint x="552" y="197" />
<di:waypoint x="490" y="142" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="EndEvent_19upzzo_di" bpmnElement="EndEvent_19upzzo"> <bpmndi:BPMNEdge id="Flow_13604n2_di" bpmnElement="Flow_13604n2">
<dc:Bounds x="552" y="99" width="36" height="36" /> <di:waypoint x="260" y="172" />
</bpmndi:BPMNShape> <di:waypoint x="260" y="120" />
<bpmndi:BPMNShape id="Gateway_1s4ro2h_di" bpmnElement="Gateway_0bgimhg"> <di:waypoint x="330" y="120" />
<dc:Bounds x="235" y="92" width="50" height="50" /> </bpmndi:BPMNEdge>
</bpmndi:BPMNShape> <bpmndi:BPMNEdge id="SequenceFlow_1n3utyf_di" bpmnElement="SequenceFlow_1n3utyf">
<bpmndi:BPMNShape id="Gateway_1kowkjp_di" bpmnElement="Gateway_1924s77"> <di:waypoint x="430" y="120" />
<dc:Bounds x="465" y="92" width="50" height="50" /> <di:waypoint x="490" y="120" />
<di:waypoint x="490" y="172" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_17znkku_di" bpmnElement="SequenceFlow_17znkku">
<di:waypoint x="188" y="197" />
<di:waypoint x="235" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="179" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="UserTask_15oiwqt_di" bpmnElement="Task_14cuhvm"> <bpmndi:BPMNShape id="UserTask_15oiwqt_di" bpmnElement="Task_14cuhvm">
<dc:Bounds x="330" y="0" width="100" height="80" /> <dc:Bounds x="330" y="80" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_19upzzo_di" bpmnElement="EndEvent_19upzzo">
<dc:Bounds x="552" y="179" width="36" height="36" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1oeywwl_di" bpmnElement="Activity_0xxhfyh"> <bpmndi:BPMNShape id="Activity_1oeywwl_di" bpmnElement="Activity_0xxhfyh">
<dc:Bounds x="330" y="160" width="100" height="80" /> <dc:Bounds x="330" y="240" width="100" height="80" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1"> <bpmndi:BPMNShape id="Gateway_1s4ro2h_di" bpmnElement="Gateway_0bgimhg">
<dc:Bounds x="152" y="99" width="36" height="36" /> <dc:Bounds x="235" y="172" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1kowkjp_di" bpmnElement="Gateway_1924s77">
<dc:Bounds x="465" y="172" width="50" height="50" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>

Binary file not shown.

View File

@ -1,23 +1,30 @@
#!/bin/bash #!/bin/bash
# run migrations # run migrations
export FLASK_APP=./crc/__init__.py export FLASK_APP=/app/crc/__init__.py
for entry in ./instance/* ; do
echo "$entry"
cat $entry
done
if [ "$DOWNGRADE_DB" = "true" ]; then if [ "$DOWNGRADE_DB" = "true" ]; then
echo 'Downgrading...' echo 'Downgrading database...'
pipenv run flask db downgrade pipenv run flask db downgrade
fi fi
if [ "$UPGRADE_DB" = "true" ]; then
echo 'Upgrading database...'
pipenv run flask db upgrade pipenv run flask db upgrade
fi
if [ "$RESET_DB" = "true" ]; then if [ "$RESET_DB" = "true" ]; then
echo 'Resetting database...' echo 'Resetting database and seeding it with example CR Connect data...'
pipenv run flask load-example-data pipenv run flask load-example-data
fi fi
pipenv run python ./run.py if [ "$RESET_DB_RRT" = "true" ]; then
echo 'Resetting database and seeding it with example RRT data...'
pipenv run flask load-example-rrt-data
fi
if [ "$APPLICATION_ROOT" = "/" ]; then
pipenv run gunicorn --bind 0.0.0.0:$PORT0 wsgi:app
else
pipenv run gunicorn -e SCRIPT_NAME="$APPLICATION_ROOT" --bind 0.0.0.0:$PORT0 wsgi:app
fi

View File

@ -14,6 +14,7 @@ class ExampleDataLoader:
session.flush() # Clear out any transactions before deleting it all to avoid spurious errors. session.flush() # Clear out any transactions before deleting it all to avoid spurious errors.
for table in reversed(db.metadata.sorted_tables): for table in reversed(db.metadata.sorted_tables):
session.execute(table.delete()) session.execute(table.delete())
session.commit()
session.flush() session.flush()
def load_all(self): def load_all(self):
@ -191,8 +192,68 @@ class ExampleDataLoader:
category_id=None, category_id=None,
master_spec=True) master_spec=True)
def load_rrt(self):
file_path = os.path.join(app.root_path, 'static', 'reference', 'rrt_documents.xlsx')
file = open(file_path, "rb")
FileService.add_reference_file(FileService.DOCUMENT_LIST,
binary_data=file.read(),
content_type=CONTENT_TYPES['xls'])
file.close()
def create_spec(self, id, name, display_name="", description="", filepath=None, master_spec=False, category_id=None, display_order=None): category = WorkflowSpecCategoryModel(
id=0,
name='research_rampup_category',
display_name='Research Ramp-up Category',
display_order=0
)
db.session.add(category)
db.session.commit()
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)
self.create_spec(id="research_rampup",
name="research_rampup",
display_name="Research Ramp-up Toolkit",
description="Process for creating a new research ramp-up request.",
category_id=0,
master_spec=False)
def load_test_data(self):
self.load_reference_documents()
category = WorkflowSpecCategoryModel(
id=0,
name='test_category',
display_name='Test Category',
display_order=0
)
db.session.add(category)
db.session.commit()
self.create_spec(id="empty_workflow",
name="empty_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)
self.create_spec(id="random_fact",
name="random_fact",
display_name="Random Fact",
description="The workflow for a Random Fact.",
category_id=0,
master_spec=False,
from_tests=True)
def create_spec(self, id, name, display_name="", description="", filepath=None, master_spec=False,
category_id=None, display_order=None, from_tests=False):
"""Assumes that a directory exists in static/bpmn with the same name as the given id. """Assumes that a directory exists in static/bpmn with the same name as the given id.
further assumes that the [id].bpmn is the primary file for the workflow. further assumes that the [id].bpmn is the primary file for the workflow.
returns an array of data models to be added to the database.""" returns an array of data models to be added to the database."""
@ -207,8 +268,11 @@ class ExampleDataLoader:
display_order=display_order) display_order=display_order)
db.session.add(spec) db.session.add(spec)
db.session.commit() db.session.commit()
if not filepath: if not filepath and not from_tests:
filepath = os.path.join(app.root_path, 'static', 'bpmn', id, "*") filepath = os.path.join(app.root_path, 'static', 'bpmn', id, "*")
if not filepath and from_tests:
filepath = os.path.join(app.root_path, '..', 'tests', 'data', id, "*")
files = glob.glob(filepath) files = glob.glob(filepath)
for file_path in files: for file_path in files:
noise, file_extension = os.path.splitext(file_path) noise, file_extension = os.path.splitext(file_path)

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: dadf16305482 Revision ID: 55c6cd407d89
Revises: cc4bccc5e5a8 Revises: cc4bccc5e5a8
Create Date: 2020-05-23 20:43:43.882267 Create Date: 2020-05-22 22:02:46.650170
""" """
from alembic import op from alembic import op
@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision = 'dadf16305482' revision = '55c6cd407d89'
down_revision = 'cc4bccc5e5a8' down_revision = 'cc4bccc5e5a8'
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -22,30 +22,18 @@ def upgrade():
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('study_id', sa.Integer(), nullable=False), sa.Column('study_id', sa.Integer(), nullable=False),
sa.Column('workflow_id', sa.Integer(), nullable=False), sa.Column('workflow_id', sa.Integer(), nullable=False),
sa.Column('workflow_version', sa.String(), nullable=True),
sa.Column('approver_uid', sa.String(), nullable=True), sa.Column('approver_uid', sa.String(), nullable=True),
sa.Column('status', sa.String(), nullable=True), sa.Column('status', sa.String(), nullable=True),
sa.Column('message', sa.String(), nullable=True), sa.Column('message', sa.String(), nullable=True),
sa.Column('date_created', sa.DateTime(timezone=True), nullable=True),
sa.Column('version', sa.Integer(), nullable=True),
sa.Column('workflow_hash', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ), sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ), sa.ForeignKeyConstraint(['workflow_id'], ['workflow.id'], ),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('approval_file',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('file_id', sa.Integer(), nullable=False),
sa.Column('approval_id', sa.Integer(), nullable=False),
sa.Column('file_version', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['approval_id'], ['approval.id'], ),
sa.ForeignKeyConstraint(['file_id'], ['file.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ### # ### end Alembic commands ###
def downgrade(): def downgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.drop_table('approval_file')
op.drop_table('approval') op.drop_table('approval')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -0,0 +1,44 @@
"""empty message
Revision ID: 9b43e725f39c
Revises: 55c6cd407d89
Create Date: 2020-05-25 23:09:14.761831
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '9b43e725f39c'
down_revision = '55c6cd407d89'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('approval_file',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('file_id', sa.Integer(), nullable=False),
sa.Column('approval_id', sa.Integer(), nullable=False),
sa.Column('file_version', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['approval_id'], ['approval.id'], ),
sa.ForeignKeyConstraint(['file_id'], ['file.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.add_column('approval', sa.Column('date_created', sa.DateTime(timezone=True), nullable=True))
op.add_column('approval', sa.Column('version', sa.Integer(), nullable=True))
op.add_column('approval', sa.Column('workflow_hash', sa.String(), nullable=True))
op.drop_column('approval', 'workflow_version')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('approval', sa.Column('workflow_version', sa.VARCHAR(), autoincrement=False, nullable=True))
op.drop_column('approval', 'workflow_hash')
op.drop_column('approval', 'version')
op.drop_column('approval', 'date_created')
op.drop_table('approval_file')
# ### end Alembic commands ###

5
setup.cfg Normal file
View File

@ -0,0 +1,5 @@
[metadata]
name = crc
[files]
packages = crc

3
setup.py Normal file
View File

@ -0,0 +1,3 @@
from setuptools import setup
setup(setup_requires=["pbr"], pbr=True)

View File

@ -1,6 +1,9 @@
# Set environment variable to testing before loading. # Set environment variable to testing before loading.
# IMPORTANT - Environment must be loaded before app, models, etc.... # IMPORTANT - Environment must be loaded before app, models, etc....
import os import os
from sqlalchemy import Sequence
os.environ["TESTING"] = "true" os.environ["TESTING"] = "true"
import json import json
@ -20,9 +23,8 @@ from crc import app, db, session
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
#UNCOMMENT THIS FOR DEBUGGING SQL ALCHEMY QUERIES #UNCOMMENT THIS FOR DEBUGGING SQL ALCHEMY QUERIES
# import logging import logging
# logging.basicConfig() logging.basicConfig()
# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
class BaseTest(unittest.TestCase): class BaseTest(unittest.TestCase):
@ -99,12 +101,10 @@ class BaseTest(unittest.TestCase):
def logged_in_headers(self, user=None, redirect_url='http://some/frontend/url'): def logged_in_headers(self, user=None, redirect_url='http://some/frontend/url'):
if user is None: if user is None:
uid = self.test_uid uid = self.test_uid
user_info = {'uid': self.test_uid, 'first_name': 'Daniel', 'last_name': 'Funk', user_info = {'uid': self.test_uid}
'email_address': 'dhf8r@virginia.edu'}
else: else:
uid = user.uid uid = user.uid
user_info = {'uid': user.uid, 'first_name': user.first_name, 'last_name': user.last_name, user_info = {'uid': user.uid}
'email_address': user.email_address}
query_string = self.user_info_to_query_string(user_info, redirect_url) query_string = self.user_info_to_query_string(user_info, redirect_url)
rv = self.app.get("/v1.0/sso_backdoor%s" % query_string, follow_redirects=False) rv = self.app.get("/v1.0/sso_backdoor%s" % query_string, follow_redirects=False)
@ -115,10 +115,17 @@ class BaseTest(unittest.TestCase):
self.assertIsNotNone(user_model.display_name) self.assertIsNotNone(user_model.display_name)
return dict(Authorization='Bearer ' + user_model.encode_auth_token().decode()) return dict(Authorization='Bearer ' + user_model.encode_auth_token().decode())
def load_example_data(self): def load_example_data(self, use_crc_data=False):
"""use_crc_data will cause this to load the mammoth collection of documents
we built up developing crc, otherwise it depends on a small setup for
running tests."""
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
ExampleDataLoader.clean_db() ExampleDataLoader.clean_db()
if(use_crc_data):
ExampleDataLoader().load_all() ExampleDataLoader().load_all()
else:
ExampleDataLoader().load_test_data()
for user_json in self.users: for user_json in self.users:
db.session.add(UserModel(**user_json)) db.session.add(UserModel(**user_json))
@ -127,6 +134,7 @@ class BaseTest(unittest.TestCase):
study_model = StudyModel(**study_json) study_model = StudyModel(**study_json)
db.session.add(study_model) db.session.add(study_model)
StudyService._add_all_workflow_specs_to_study(study_model) StudyService._add_all_workflow_specs_to_study(study_model)
db.session.execute(Sequence(StudyModel.__tablename__ + '_id_seq'))
db.session.commit() db.session.commit()
db.session.flush() db.session.flush()
@ -189,7 +197,7 @@ class BaseTest(unittest.TestCase):
for key, value in items: for key, value in items:
query_string_list.append('%s=%s' % (key, urllib.parse.quote(value))) query_string_list.append('%s=%s' % (key, urllib.parse.quote(value)))
query_string_list.append('redirect_url=%s' % redirect_url) query_string_list.append('redirect=%s' % redirect_url)
return '?%s' % '&'.join(query_string_list) return '?%s' % '&'.join(query_string_list)
@ -225,12 +233,11 @@ class BaseTest(unittest.TestCase):
def create_workflow(self, workflow_name, study=None, category_id=None): def create_workflow(self, workflow_name, study=None, category_id=None):
db.session.flush() db.session.flush()
workflow = db.session.query(WorkflowSpecModel).filter(WorkflowSpecModel.name == workflow_name).first() spec = db.session.query(WorkflowSpecModel).filter(WorkflowSpecModel.name == workflow_name).first()
if workflow: if spec is None:
return workflow spec = self.load_test_spec(workflow_name, category_id=category_id)
if study is None: if study is None:
study = self.create_study() study = self.create_study()
spec = self.load_test_spec(workflow_name, category_id=category_id)
workflow_model = StudyService._create_workflow_model(study, spec) workflow_model = StudyService._create_workflow_model(study, spec)
return workflow_model return workflow_model

View File

@ -1,6 +1,7 @@
from tests.base_test import BaseTest
from crc import db from crc import db
from crc.models.user import UserModel from crc.models.user import UserModel
from tests.base_test import BaseTest
class TestAuthentication(BaseTest): class TestAuthentication(BaseTest):
@ -13,7 +14,7 @@ class TestAuthentication(BaseTest):
self.assertEqual("dhf8r", user.decode_auth_token(auth_token).get("sub")) self.assertEqual("dhf8r", user.decode_auth_token(auth_token).get("sub"))
def test_backdoor_auth_creates_user(self): def test_backdoor_auth_creates_user(self):
new_uid = 'czn1z'; new_uid = 'lb3dp' ## Assure this user id is in the fake responses from ldap.
self.load_example_data() self.load_example_data()
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first() user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
self.assertIsNone(user) self.assertIsNone(user)
@ -44,7 +45,7 @@ class TestAuthentication(BaseTest):
self.assertIsNone(user) self.assertIsNone(user)
redirect_url = 'http://worlds.best.website/admin' redirect_url = 'http://worlds.best.website/admin'
headers = dict(Uid=new_uid) headers = dict(Uid=new_uid)
rv = self.app.get('login', follow_redirects=False, headers=headers) rv = self.app.get('v1.0/login', follow_redirects=False, headers=headers)
self.assert_success(rv) self.assert_success(rv)
user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first() user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
self.assertIsNotNone(user) self.assertIsNotNone(user)
@ -62,6 +63,7 @@ class TestAuthentication(BaseTest):
rv = self.app.get('/v1.0/user', headers=self.logged_in_headers()) rv = self.app.get('/v1.0/user', headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
user = UserModel(uid="ajl2j", first_name='Aaron', last_name='Louie', email_address='ajl2j@virginia.edu') # User must exist in the mock ldap responses.
user = UserModel(uid="dhf8r", first_name='Dan', last_name='Funk', email_address='dhf8r@virginia.edu')
rv = self.app.get('/v1.0/user', headers=self.logged_in_headers(user, redirect_url='http://omg.edu/lolwut')) rv = self.app.get('/v1.0/user', headers=self.logged_in_headers(user, redirect_url='http://omg.edu/lolwut'))
self.assert_success(rv) self.assert_success(rv)

View File

@ -20,7 +20,7 @@ class TestFilesApi(BaseTest):
return (minimal_dbpm % content).encode() return (minimal_dbpm % content).encode()
def test_list_files_for_workflow_spec(self): def test_list_files_for_workflow_spec(self):
self.load_example_data() self.load_example_data(use_crc_data=True)
spec_id = 'core_info' spec_id = 'core_info'
spec = session.query(WorkflowSpecModel).filter_by(id=spec_id).first() spec = session.query(WorkflowSpecModel).filter_by(id=spec_id).first()
rv = self.app.get('/v1.0/file?workflow_spec_id=%s' % spec_id, rv = self.app.get('/v1.0/file?workflow_spec_id=%s' % spec_id,

View File

@ -1,18 +1,20 @@
from tests.base_test import BaseTest
from crc import session from crc import session
from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel
from crc.services.file_service import FileService from crc.services.file_service import FileService
from crc.services.lookup_service import LookupService from crc.services.lookup_service import LookupService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestLookupService(BaseTest): class TestLookupService(BaseTest):
def test_create_lookup_file_multiple_times_does_not_update_database(self): def test_create_lookup_file_multiple_times_does_not_update_database(self):
spec = self.load_test_spec('enum_options_from_file') spec = BaseTest.load_test_spec('enum_options_from_file')
file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first() 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() 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") 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")
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")
@ -21,18 +23,16 @@ class TestLookupService(BaseTest):
self.assertEqual(1, len(lookup_records)) self.assertEqual(1, len(lookup_records))
lookup_record = lookup_records[0] lookup_record = lookup_records[0]
lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all() lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
self.assertEquals(19, len(lookup_data)) self.assertEquals(28, len(lookup_data))
# Using the same table with different lookup lable or value, does create additional records. # 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") LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NAME", "CUSTOMER_NUMBER")
lookup_records = session.query(LookupFileModel).all() lookup_records = session.query(LookupFileModel).all()
self.assertIsNotNone(lookup_records) self.assertIsNotNone(lookup_records)
self.assertEqual(2, len(lookup_records)) self.assertEqual(2, len(lookup_records))
FileService.delete_file(file_model.id) ## Assure we can delete the file.
def test_some_full_text_queries(self): def test_some_full_text_queries(self):
self.load_test_spec('enum_options_from_file') spec = BaseTest.load_test_spec('enum_options_from_file')
file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first() file_model = session.query(FileModel).filter(FileModel.name == "customer_list.xls").first()
self.assertIsNotNone(file_model)
file_data_model = session.query(FileDataModel).filter(FileDataModel.file_model == file_model).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") lookup_table = LookupService.get_lookup_table_from_data_model(file_data_model, "CUSTOMER_NUMBER", "CUSTOMER_NAME")
@ -51,8 +51,6 @@ class TestLookupService(BaseTest):
self.assertEquals(1, len(results), "case does not matter.") self.assertEquals(1, len(results), "case does not matter.")
self.assertEquals("UVA - INTERNAL - GM USE ONLY", results[0].label) self.assertEquals("UVA - INTERNAL - GM USE ONLY", results[0].label)
results = LookupService._run_lookup_query(lookup_table, "medici", limit=10) results = LookupService._run_lookup_query(lookup_table, "medici", limit=10)
self.assertEquals(1, len(results), "partial words are picked up.") self.assertEquals(1, len(results), "partial words are picked up.")
self.assertEquals("The Medicines Company", results[0].label) self.assertEquals("The Medicines Company", results[0].label)
@ -73,7 +71,29 @@ class TestLookupService(BaseTest):
self.assertEquals(7, len(results), "short terms get multiple correct results.") self.assertEquals(7, len(results), "short terms get multiple correct results.")
self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label) self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)
results = LookupService._run_lookup_query(lookup_table, "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)
self.assertEquals("1 Something", results[0].label, "Exact matches are prefered")
# 1018 10000 Something Industry
# 1019 1000 Something Industry
# 1020 1 Something Industry
# 1021 10 Something Industry
# 1022 10000 Something Industry
# Fixme: Stop words are taken into account on the query side, and haven't found a fix yet. # Fixme: Stop words are taken into account on the query side, and haven't found a fix yet.
#results = WorkflowService.run_lookup_query(lookup_table.id, "in", limit=10) #results = WorkflowService.run_lookup_query(lookup_table.id, "in", limit=10)
#self.assertEquals(7, len(results), "stop words are not removed.") #self.assertEquals(7, len(results), "stop words are not removed.")
#self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label) #self.assertEquals("Genetics Savings & Clone, Inc.", results[0].label)

View File

@ -1,5 +1,6 @@
from unittest.mock import patch from unittest.mock import patch
from crc import app
from tests.base_test import BaseTest from tests.base_test import BaseTest
from crc.services.protocol_builder import ProtocolBuilderService from crc.services.protocol_builder import ProtocolBuilderService
@ -10,7 +11,7 @@ class TestProtocolBuilder(BaseTest):
@patch('crc.services.protocol_builder.requests.get') @patch('crc.services.protocol_builder.requests.get')
def test_get_studies(self, mock_get): def test_get_studies(self, mock_get):
ProtocolBuilderService.ENABLED = True app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('user_studies.json') mock_get.return_value.text = self.protocol_builder_response('user_studies.json')
response = ProtocolBuilderService.get_studies(self.test_uid) response = ProtocolBuilderService.get_studies(self.test_uid)
@ -18,7 +19,7 @@ class TestProtocolBuilder(BaseTest):
@patch('crc.services.protocol_builder.requests.get') @patch('crc.services.protocol_builder.requests.get')
def test_get_investigators(self, mock_get): def test_get_investigators(self, mock_get):
ProtocolBuilderService.ENABLED = True app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json') mock_get.return_value.text = self.protocol_builder_response('investigators.json')
response = ProtocolBuilderService.get_investigators(self.test_study_id) response = ProtocolBuilderService.get_investigators(self.test_study_id)
@ -30,7 +31,7 @@ class TestProtocolBuilder(BaseTest):
@patch('crc.services.protocol_builder.requests.get') @patch('crc.services.protocol_builder.requests.get')
def test_get_required_docs(self, mock_get): def test_get_required_docs(self, mock_get):
ProtocolBuilderService.ENABLED = True app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('required_docs.json') mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
response = ProtocolBuilderService.get_required_docs(self.test_study_id) response = ProtocolBuilderService.get_required_docs(self.test_study_id)
@ -40,7 +41,7 @@ class TestProtocolBuilder(BaseTest):
@patch('crc.services.protocol_builder.requests.get') @patch('crc.services.protocol_builder.requests.get')
def test_get_details(self, mock_get): def test_get_details(self, mock_get):
ProtocolBuilderService.ENABLED = True app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('study_details.json') mock_get.return_value.text = self.protocol_builder_response('study_details.json')
response = ProtocolBuilderService.get_study_details(self.test_study_id) response = ProtocolBuilderService.get_study_details(self.test_study_id)

View File

@ -16,10 +16,10 @@ class TestRequestApprovalScript(BaseTest):
workflow = self.create_workflow('empty_workflow') workflow = self.create_workflow('empty_workflow')
processor = WorkflowProcessor(workflow) processor = WorkflowProcessor(workflow)
task = processor.next_task() task = processor.next_task()
task.data = {"approvals": ['dhf8r', 'lb3dp']} task.data = {"study": {"approval1": "dhf8r", 'approval2':'lb3dp'}}
script = RequestApproval() script = RequestApproval()
script.do_task(task, workflow.study_id, workflow.id, "approvals") script.do_task(task, workflow.study_id, workflow.id, "study.approval1", "study.approval2")
self.assertEquals(2, db.session.query(ApprovalModel).count()) self.assertEquals(2, db.session.query(ApprovalModel).count())
def test_do_task_with_incorrect_argument(self): def test_do_task_with_incorrect_argument(self):
@ -29,7 +29,7 @@ class TestRequestApprovalScript(BaseTest):
workflow = self.create_workflow('empty_workflow') workflow = self.create_workflow('empty_workflow')
processor = WorkflowProcessor(workflow) processor = WorkflowProcessor(workflow)
task = processor.next_task() task = processor.next_task()
task.data = {"approvals": {'dhf8r':"invalid", 'lb3dp':"invalid"}} task.data = {"approvals": {'dhf8r':["invalid"], 'lb3dp':"invalid"}}
script = RequestApproval() script = RequestApproval()
with self.assertRaises(ApiError): with self.assertRaises(ApiError):
script.do_task(task, workflow.study_id, workflow.id, "approvals") script.do_task(task, workflow.study_id, workflow.id, "approvals")
@ -40,9 +40,9 @@ class TestRequestApprovalScript(BaseTest):
workflow = self.create_workflow('empty_workflow') workflow = self.create_workflow('empty_workflow')
processor = WorkflowProcessor(workflow) processor = WorkflowProcessor(workflow)
task = processor.next_task() task = processor.next_task()
task.data = {"approvals": ['dhf8r', 'lb3dp']} task.data = {"study": {"approval1": "dhf8r", 'approval2':'lb3dp'}}
script = RequestApproval() script = RequestApproval()
script.do_task_validate_only(task, workflow.study_id, workflow.id, "approvals") script.do_task_validate_only(task, workflow.study_id, workflow.id, "study.approval1")
self.assertEquals(0, db.session.query(ApprovalModel).count()) self.assertEquals(0, db.session.query(ApprovalModel).count())

View File

@ -3,7 +3,7 @@ from tests.base_test import BaseTest
from datetime import datetime, timezone from datetime import datetime, timezone
from unittest.mock import patch from unittest.mock import patch
from crc import session from crc import session, app
from crc.models.protocol_builder import ProtocolBuilderStatus, \ from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema ProtocolBuilderStudySchema
from crc.models.stats import TaskEventModel from crc.models.stats import TaskEventModel
@ -15,14 +15,11 @@ from crc.services.protocol_builder import ProtocolBuilderService
class TestStudyApi(BaseTest): class TestStudyApi(BaseTest):
TEST_STUDY = { TEST_STUDY = {
"id": 12345,
"title": "Phase III Trial of Genuine People Personalities (GPP) Autonomous Intelligent Emotional Agents " "title": "Phase III Trial of Genuine People Personalities (GPP) Autonomous Intelligent Emotional Agents "
"for Interstellar Spacecraft", "for Interstellar Spacecraft",
"last_updated": datetime.now(tz=timezone.utc), "last_updated": datetime.now(tz=timezone.utc),
"protocol_builder_status": ProtocolBuilderStatus.ACTIVE, "protocol_builder_status": ProtocolBuilderStatus.ACTIVE,
"primary_investigator_id": "tricia.marie.mcmillan@heartofgold.edu", "primary_investigator_id": "tmm2x",
"sponsor": "Sirius Cybernetics Corporation",
"ind_number": "567890",
"user_uid": "dhf8r", "user_uid": "dhf8r",
} }
@ -45,17 +42,9 @@ class TestStudyApi(BaseTest):
"""NOTE: The protocol builder is not enabled or mocked out. As the master workflow (which is empty), """NOTE: The protocol builder is not enabled or mocked out. As the master workflow (which is empty),
and the test workflow do not need it, and it is disabled in the configuration.""" and the test workflow do not need it, and it is disabled in the configuration."""
self.load_example_data()
new_study = self.add_test_study() new_study = self.add_test_study()
new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first() new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first()
# Add a category
new_category = WorkflowSpecCategoryModel(id=21, name="test_cat", display_name="Test Category", display_order=0)
session.add(new_category)
session.commit()
# Create a workflow specification
self.create_workflow("random_fact", study=new_study, category_id=new_category.id)
# Assure there is a master specification, and it has the lookup files it needs.
spec = self.load_test_spec("empty_workflow", master_spec=True)
self.create_reference_document()
api_response = self.app.get('/v1.0/study/%i' % new_study.id, api_response = self.app.get('/v1.0/study/%i' % new_study.id,
headers=self.logged_in_headers(), content_type="application/json") headers=self.logged_in_headers(), content_type="application/json")
@ -64,13 +53,12 @@ class TestStudyApi(BaseTest):
self.assertEqual(study.title, self.TEST_STUDY['title']) self.assertEqual(study.title, self.TEST_STUDY['title'])
self.assertEqual(study.primary_investigator_id, self.TEST_STUDY['primary_investigator_id']) self.assertEqual(study.primary_investigator_id, self.TEST_STUDY['primary_investigator_id'])
self.assertEqual(study.sponsor, self.TEST_STUDY['sponsor'])
self.assertEqual(study.ind_number, self.TEST_STUDY['ind_number'])
self.assertEqual(study.user_uid, self.TEST_STUDY['user_uid']) self.assertEqual(study.user_uid, self.TEST_STUDY['user_uid'])
# Categories are read only, so switching to sub-scripting here. # Categories are read only, so switching to sub-scripting here.
category = [c for c in study.categories if c['name'] == "test_cat"][0] # This assumes there is one test category set up in the example data.
self.assertEqual("test_cat", category['name']) category = study.categories[0]
self.assertEqual("test_category", category['name'])
self.assertEqual("Test Category", category['display_name']) self.assertEqual("Test Category", category['display_name'])
self.assertEqual(1, len(category["workflows"])) self.assertEqual(1, len(category["workflows"]))
workflow = category["workflows"][0] workflow = category["workflows"][0]
@ -83,7 +71,7 @@ class TestStudyApi(BaseTest):
def test_add_study(self): def test_add_study(self):
self.load_example_data() self.load_example_data()
study = self.add_test_study() study = self.add_test_study()
db_study = session.query(StudyModel).filter_by(id=12345).first() db_study = session.query(StudyModel).filter_by(id=study['id']).first()
self.assertIsNotNone(db_study) self.assertIsNotNone(db_study)
self.assertEqual(study["title"], db_study.title) self.assertEqual(study["title"], db_study.title)
self.assertEqual(study["primary_investigator_id"], db_study.primary_investigator_id) self.assertEqual(study["primary_investigator_id"], db_study.primary_investigator_id)
@ -92,7 +80,7 @@ class TestStudyApi(BaseTest):
self.assertEqual(study["user_uid"], db_study.user_uid) self.assertEqual(study["user_uid"], db_study.user_uid)
workflow_spec_count =session.query(WorkflowSpecModel).filter(WorkflowSpecModel.is_master_spec == False).count() workflow_spec_count =session.query(WorkflowSpecModel).filter(WorkflowSpecModel.is_master_spec == False).count()
workflow_count = session.query(WorkflowModel).filter(WorkflowModel.study_id == 12345).count() workflow_count = session.query(WorkflowModel).filter(WorkflowModel.study_id == study['id']).count()
error_count = len(study["errors"]) error_count = len(study["errors"])
self.assertEqual(workflow_spec_count, workflow_count + error_count) self.assertEqual(workflow_spec_count, workflow_count + error_count)
@ -117,7 +105,7 @@ class TestStudyApi(BaseTest):
def test_get_all_studies(self, mock_studies, mock_details, mock_docs, mock_investigators): def test_get_all_studies(self, mock_studies, mock_details, mock_docs, mock_investigators):
# Enable the protocol builder for these tests, as the master_workflow and other workflows # Enable the protocol builder for these tests, as the master_workflow and other workflows
# depend on using the PB for data. # depend on using the PB for data.
ProtocolBuilderService.ENABLED = True app.config['PB_ENABLED'] = True
self.load_example_data() self.load_example_data()
s = StudyModel( s = StudyModel(
id=54321, # This matches one of the ids from the study_details_json data. id=54321, # This matches one of the ids from the study_details_json data.
@ -209,20 +197,13 @@ class TestStudyApi(BaseTest):
def test_delete_study_with_workflow_and_status(self): def test_delete_study_with_workflow_and_status(self):
self.load_example_data() self.load_example_data()
study = session.query(StudyModel).first() workflow = session.query(WorkflowModel).first()
new_category = WorkflowSpecCategoryModel(id=21, name="test_cat", display_name="Test Category", display_order=0) stats2 = TaskEventModel(study_id=workflow.study_id, workflow_id=workflow.id, user_uid=self.users[0]['uid'])
session.add(new_category)
session.commit()
# Create a workflow specification, and complete some stuff that would log stats
workflow = self.create_workflow("random_fact", study=study, category_id=new_category.id)
session.add(workflow)
session.commit()
stats2 = TaskEventModel(study_id=study.id, workflow_id=workflow.id, user_uid=self.users[0]['uid'])
session.add(stats2) session.add(stats2)
session.commit() session.commit()
rv = self.app.delete('/v1.0/study/%i' % study.id, headers=self.logged_in_headers()) rv = self.app.delete('/v1.0/study/%i' % workflow.study_id, headers=self.logged_in_headers())
self.assert_success(rv) self.assert_success(rv)
del_study = session.query(StudyModel).filter(StudyModel.id == study.id).first() del_study = session.query(StudyModel).filter(StudyModel.id == workflow.study_id).first()
self.assertIsNone(del_study) self.assertIsNone(del_study)

View File

@ -2,7 +2,7 @@ import json
from datetime import datetime from datetime import datetime
from unittest.mock import patch from unittest.mock import patch
from crc import db from crc import db, app
from crc.models.protocol_builder import ProtocolBuilderStatus from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.study import StudyModel from crc.models.study import StudyModel
from crc.models.user import UserModel from crc.models.user import UserModel
@ -57,7 +57,7 @@ class TestStudyService(BaseTest):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_total_tasks_updated(self, mock_docs): def test_total_tasks_updated(self, mock_docs):
"""Assure that as a users progress is available when getting a list of studies for that user.""" """Assure that as a users progress is available when getting a list of studies for that user."""
app.config['PB_ENABLED'] = True
docs_response = self.protocol_builder_response('required_docs.json') docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response) mock_docs.return_value = json.loads(docs_response)
@ -106,7 +106,7 @@ class TestStudyService(BaseTest):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
def test_get_required_docs(self, mock_docs): def test_get_required_docs(self, mock_docs):
app.config['PB_ENABLED'] = True
# mock out the protocol builder # mock out the protocol builder
docs_response = self.protocol_builder_response('required_docs.json') docs_response = self.protocol_builder_response('required_docs.json')
mock_docs.return_value = json.loads(docs_response) mock_docs.return_value = json.loads(docs_response)

View File

@ -3,6 +3,8 @@ import os
import random import random
from unittest.mock import patch from unittest.mock import patch
from tests.base_test import BaseTest
from crc import session, app from crc import session, app
from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSchema from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSchema
from crc.models.file import FileModelSchema from crc.models.file import FileModelSchema
@ -10,7 +12,6 @@ from crc.models.stats import TaskEventModel
from crc.models.workflow import WorkflowStatus from crc.models.workflow import WorkflowStatus
from crc.services.protocol_builder import ProtocolBuilderService from crc.services.protocol_builder import ProtocolBuilderService
from crc.services.workflow_service import WorkflowService from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestTasksApi(BaseTest): class TestTasksApi(BaseTest):
@ -303,14 +304,16 @@ class TestTasksApi(BaseTest):
@patch('crc.services.protocol_builder.requests.get') @patch('crc.services.protocol_builder.requests.get')
def test_multi_instance_task(self, mock_get): def test_multi_instance_task(self, mock_get):
self.load_example_data()
# Enable the protocol builder. # Enable the protocol builder.
ProtocolBuilderService.ENABLED = True app.config['PB_ENABLED'] = True
# This depends on getting a list of investigators back from the protocol builder. # This depends on getting a list of investigators back from the protocol builder.
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json') mock_get.return_value.text = self.protocol_builder_response('investigators.json')
self.load_example_data()
workflow = self.create_workflow('multi_instance') workflow = self.create_workflow('multi_instance')
# get the first form in the two form workflow. # get the first form in the two form workflow.
@ -423,11 +426,12 @@ class TestTasksApi(BaseTest):
def test_parallel_multi_instance(self, mock_get): def test_parallel_multi_instance(self, mock_get):
# Assure we get nine investigators back from the API Call, as set in the investigators.json file. # Assure we get nine investigators back from the API Call, as set in the investigators.json file.
app.config['PB_ENABLED'] = True
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('investigators.json') mock_get.return_value.text = self.protocol_builder_response('investigators.json')
self.load_example_data() self.load_example_data()
workflow = self.create_workflow('multi_instance_parallel') workflow = self.create_workflow('multi_instance_parallel')
workflow_api = self.get_workflow_api(workflow) workflow_api = self.get_workflow_api(workflow)

View File

@ -0,0 +1,23 @@
from tests.base_test import BaseTest
from crc.scripts.update_study import UpdateStudy
from crc.services.workflow_processor import WorkflowProcessor
class TestUpdateStudyScript(BaseTest):
def test_do_task(self):
self.load_example_data()
self.create_reference_document()
workflow = self.create_workflow('empty_workflow')
processor = WorkflowProcessor(workflow)
task = processor.next_task()
task.data = {"details": {
"label": "My New Title",
"value": "dhf8r"}
}
script = UpdateStudy()
script.do_task(task, workflow.study_id, workflow.id, "title:details.label", "pi:details.value")
self.assertEquals("My New Title", workflow.study.title)
self.assertEquals("dhf8r", workflow.study.primary_investigator_id)

View File

@ -1,34 +1,31 @@
import json
import logging import logging
import os import os
import json
import string
import random
from unittest.mock import patch from unittest.mock import patch
from SpiffWorkflow import Task as SpiffTask from tests.base_test import BaseTest
from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent from SpiffWorkflow.bpmn.specs.EndEvent import EndEvent
from SpiffWorkflow.camunda.specs.UserTask import Form, FormField from SpiffWorkflow.camunda.specs.UserTask import FormField
from SpiffWorkflow.specs import TaskSpec
from crc import session, db, app from crc import session, db, app
from crc.api.common import ApiError from crc.api.common import ApiError
from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES from crc.models.file import FileModel, FileDataModel
from crc.models.protocol_builder import ProtocolBuilderStudySchema
from crc.services.protocol_builder import ProtocolBuilderService
from crc.models.study import StudyModel from crc.models.study import StudyModel
from crc.models.workflow import WorkflowSpecModel, WorkflowStatus, WorkflowModel from crc.models.workflow import WorkflowSpecModel, WorkflowStatus
from crc.services.file_service import FileService from crc.services.file_service import FileService
from crc.services.study_service import StudyService from crc.services.study_service import StudyService
from crc.models.protocol_builder import ProtocolBuilderStudySchema, ProtocolBuilderInvestigatorSchema, \
ProtocolBuilderRequiredDocumentSchema
from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
class TestWorkflowProcessor(BaseTest): class TestWorkflowProcessor(BaseTest):
def _populate_form_with_random_data(self, task): def _populate_form_with_random_data(self, task):
api_task = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True) api_task = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
WorkflowProcessor.populate_form_with_random_data(task, api_task) WorkflowService.populate_form_with_random_data(task, api_task)
def get_processor(self, study_model, spec_model): def get_processor(self, study_model, spec_model):
workflow_model = StudyService._create_workflow_model(study_model, spec_model) workflow_model = StudyService._create_workflow_model(study_model, spec_model)
@ -361,7 +358,7 @@ class TestWorkflowProcessor(BaseTest):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators')
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs')
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details')
def test_master_bpmn(self, mock_details, mock_required_docs, mock_investigators, mock_studies): def test_master_bpmn_for_crc(self, mock_details, mock_required_docs, mock_investigators, mock_studies):
# Mock Protocol Builder response # Mock Protocol Builder response
studies_response = self.protocol_builder_response('user_studies.json') studies_response = self.protocol_builder_response('user_studies.json')
@ -376,7 +373,8 @@ class TestWorkflowProcessor(BaseTest):
details_response = self.protocol_builder_response('study_details.json') details_response = self.protocol_builder_response('study_details.json')
mock_details.return_value = json.loads(details_response) mock_details.return_value = json.loads(details_response)
self.load_example_data() self.load_example_data(use_crc_data=True)
app.config['PB_ENABLED'] = True
study = session.query(StudyModel).first() study = session.query(StudyModel).first()
workflow_spec_model = db.session.query(WorkflowSpecModel).\ workflow_spec_model = db.session.query(WorkflowSpecModel).\

View File

@ -1,10 +1,9 @@
from tests.base_test import BaseTest
from crc import session from crc import session
from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel from crc.models.file import FileDataModel, FileModel, LookupFileModel, LookupDataModel
from crc.services.file_service import FileService
from crc.services.lookup_service import LookupService from crc.services.lookup_service import LookupService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService from crc.services.workflow_service import WorkflowService
from tests.base_test import BaseTest
class TestWorkflowService(BaseTest): class TestWorkflowService(BaseTest):
@ -69,7 +68,7 @@ class TestWorkflowService(BaseTest):
task = processor.next_task() task = processor.next_task()
WorkflowService.process_options(task, task.task_spec.form.fields[0]) WorkflowService.process_options(task, task.task_spec.form.fields[0])
options = task.task_spec.form.fields[0].options options = task.task_spec.form.fields[0].options
self.assertEquals(19, len(options)) self.assertEquals(28, len(options))
self.assertEquals('1000', options[0]['id']) self.assertEquals('1000', options[0]['id'])
self.assertEquals("UVA - INTERNAL - GM USE ONLY", options[0]['name']) self.assertEquals("UVA - INTERNAL - GM USE ONLY", options[0]['name'])
@ -87,7 +86,7 @@ class TestWorkflowService(BaseTest):
self.assertEquals("CUSTOMER_NAME", lookup_record.label_column) self.assertEquals("CUSTOMER_NAME", lookup_record.label_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() lookup_data = session.query(LookupDataModel).filter(LookupDataModel.lookup_file_model == lookup_record).all()
self.assertEquals(19, len(lookup_data)) self.assertEquals(28, len(lookup_data))
self.assertEquals("1000", lookup_data[0].value) self.assertEquals("1000", lookup_data[0].value)
self.assertEquals("UVA - INTERNAL - GM USE ONLY", lookup_data[0].label) self.assertEquals("UVA - INTERNAL - GM USE ONLY", lookup_data[0].label)
@ -103,4 +102,12 @@ class TestWorkflowService(BaseTest):
self.assertEquals(2, len(search_results)) 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')
processor = WorkflowProcessor(workflow)
processor.do_engine_steps()
task = processor.next_task()
task_api = WorkflowService.spiff_task_to_api_task(task, add_docs_and_forms=True)
WorkflowService.populate_form_with_random_data(task, task_api)
self.assertTrue(isinstance(task.data["sponsor"], dict))

View File

@ -1,9 +1,9 @@
import json import json
from tests.base_test import BaseTest
from crc import session from crc import session
from crc.models.file import FileModel from crc.models.file import FileModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowSpecCategoryModel from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowSpecCategoryModel
from tests.base_test import BaseTest
class TestWorkflowSpec(BaseTest): class TestWorkflowSpec(BaseTest):

View File

@ -1,14 +1,14 @@
import json import json
import unittest
from unittest.mock import patch from unittest.mock import patch
from crc import session
from crc.api.common import ApiErrorSchema
from crc.models.file import FileModel
from crc.models.protocol_builder import ProtocolBuilderStudySchema
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowSpecCategoryModel
from tests.base_test import BaseTest from tests.base_test import BaseTest
from crc.services.protocol_builder import ProtocolBuilderService
from crc import session, app
from crc.api.common import ApiErrorSchema
from crc.models.protocol_builder import ProtocolBuilderStudySchema
from crc.models.workflow import WorkflowSpecModel
class TestWorkflowSpecValidation(BaseTest): class TestWorkflowSpecValidation(BaseTest):
@ -21,7 +21,7 @@ class TestWorkflowSpecValidation(BaseTest):
return ApiErrorSchema(many=True).load(json_data) return ApiErrorSchema(many=True).load(json_data)
def test_successful_validation_of_test_workflows(self): def test_successful_validation_of_test_workflows(self):
app.config['PB_ENABLED'] = False # Assure this is disabled.
self.assertEqual(0, len(self.validate_workflow("parallel_tasks"))) self.assertEqual(0, len(self.validate_workflow("parallel_tasks")))
self.assertEqual(0, len(self.validate_workflow("decision_table"))) self.assertEqual(0, len(self.validate_workflow("decision_table")))
self.assertEqual(0, len(self.validate_workflow("docx"))) self.assertEqual(0, len(self.validate_workflow("docx")))
@ -35,7 +35,7 @@ class TestWorkflowSpecValidation(BaseTest):
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies
def test_successful_validation_of_auto_loaded_workflows(self, mock_studies, mock_details, mock_docs, mock_investigators): def test_successful_validation_of_crc_workflows(self, mock_studies, mock_details, mock_docs, mock_investigators):
# Mock Protocol Builder responses # Mock Protocol Builder responses
studies_response = self.protocol_builder_response('user_studies.json') studies_response = self.protocol_builder_response('user_studies.json')
@ -47,7 +47,8 @@ class TestWorkflowSpecValidation(BaseTest):
investigators_response = self.protocol_builder_response('investigators.json') investigators_response = self.protocol_builder_response('investigators.json')
mock_investigators.return_value = json.loads(investigators_response) mock_investigators.return_value = json.loads(investigators_response)
self.load_example_data() self.load_example_data(use_crc_data=True)
app.config['PB_ENABLED'] = True
workflows = session.query(WorkflowSpecModel).all() workflows = session.query(WorkflowSpecModel).all()
errors = [] errors = []
for w in workflows: for w in workflows:

23
wsgi.py Normal file
View File

@ -0,0 +1,23 @@
from werkzeug.exceptions import NotFound
from werkzeug.middleware.dispatcher import DispatcherMiddleware
from werkzeug.middleware.proxy_fix import ProxyFix
from crc import app
if __name__ == "__main__":
def no_app(environ, start_response):
return NotFound()(environ, start_response)
# Remove trailing slash, but add leading slash
base_url = '/' + app.config['APPLICATION_ROOT'].strip('/')
routes = {'/': app.wsgi_app}
if base_url != '/':
routes[base_url] = app.wsgi_app
app.wsgi_app = DispatcherMiddleware(no_app, routes)
app.wsgi_app = ProxyFix(app.wsgi_app)
flask_port = app.config['FLASK_PORT']
app.run(host='0.0.0.0', port=flask_port)