Merge pull request #14 from sartography/feature/protocol-builder

Feature/protocol builder
This commit is contained in:
Aaron Louie 2020-02-28 11:58:53 -05:00 committed by GitHub
commit cd07d9d95e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 576 additions and 260 deletions

View File

@ -33,6 +33,7 @@ recommonmark = "*"
psycopg2-binary = "*" psycopg2-binary = "*"
docxtpl = "*" docxtpl = "*"
flask-sso = "*" flask-sso = "*"
python-dateutil = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"

35
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "4bcfdceb9683a7885711d4d96296d8ba78fd60bebd5ab56cd3bb1b5eb1dee7b2" "sha256": "60e48d05048f627878a5c81377318bc2ff2a94b2574441c166fda3a523c789df"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -91,10 +91,10 @@
}, },
"billiard": { "billiard": {
"hashes": [ "hashes": [
"sha256:26fd494dc3251f8ce1f5559744f18aeed427fdaf29a75d7baae26752a5d3816f", "sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
"sha256:f4e09366653aa3cb3ae8ed16423f9ba1665ff426f087bcdbbed86bf3664fe02c" "sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"
], ],
"version": "==3.6.2.0" "version": "==3.6.3.0"
}, },
"blinker": { "blinker": {
"hashes": [ "hashes": [
@ -597,6 +597,7 @@
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
], ],
"index": "pypi",
"version": "==2.8.1" "version": "==2.8.1"
}, },
"python-docx": { "python-docx": {
@ -668,18 +669,18 @@
}, },
"soupsieve": { "soupsieve": {
"hashes": [ "hashes": [
"sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5", "sha256:e914534802d7ffd233242b785229d5ba0766a7f487385e3f714446a07bf540ae",
"sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda" "sha256:fcd71e08c0aee99aca1b73f45478549ee7e7fc006d51b37bec9e9def7dc22b69"
], ],
"version": "==1.9.5" "version": "==2.0"
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:525527074f2e0c2585f68f73c99b4dc257c34bbe308b27f5f8c7a6e20642742f", "sha256:776ff8333181138fae52df65be733127539623bb46cc692e7fa0fcfc80d7aa88",
"sha256:543d39db5f82d83a5c1aa0c10c88f2b6cff2da3e711aa849b2c627b4b403bbd9" "sha256:ca762da97c3b5107cbf0ab9e11d3ec7ab8d3c31377266fd613b962ed971df709"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.4.2" "version": "==2.4.3"
}, },
"sphinxcontrib-applehelp": { "sphinxcontrib-applehelp": {
"hashes": [ "hashes": [
@ -697,10 +698,10 @@
}, },
"sphinxcontrib-htmlhelp": { "sphinxcontrib-htmlhelp": {
"hashes": [ "hashes": [
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
], ],
"version": "==1.0.2" "version": "==1.0.3"
}, },
"sphinxcontrib-jsmath": { "sphinxcontrib-jsmath": {
"hashes": [ "hashes": [
@ -726,7 +727,7 @@
"spiffworkflow": { "spiffworkflow": {
"editable": true, "editable": true,
"git": "https://github.com/sartography/SpiffWorkflow.git", "git": "https://github.com/sartography/SpiffWorkflow.git",
"ref": "bab9a280f94b8a94ade5107c0bce104aade9ad04" "ref": "0da57a83bfa0edaf1cfd5500f87757553621c412"
}, },
"sqlalchemy": { "sqlalchemy": {
"hashes": [ "hashes": [
@ -788,11 +789,11 @@
}, },
"xlsxwriter": { "xlsxwriter": {
"hashes": [ "hashes": [
"sha256:18fe8f891a4adf7556c05d56059e136f9fbce5b19f9335f6d7b42c389c4592bc", "sha256:488e1988ab16ff3a9cd58c7656d0a58f8abe46ee58b98eecea78c022db28656b",
"sha256:5d3630ff9b2a277c939bd5053d0e7466499593abebbab9ce1dc9b1481a8ebbb6" "sha256:97ab487b81534415c5313154203f3e8a637d792b1e6a8201e8f7f71da0203c2a"
], ],
"index": "pypi", "index": "pypi",
"version": "==1.2.7" "version": "==1.2.8"
}, },
"zipp": { "zipp": {
"hashes": [ "hashes": [

View File

@ -19,7 +19,7 @@ auth = HTTPTokenAuth('Bearer')
if "TESTING" in os.environ and os.environ["TESTING"] == "true": if "TESTING" in os.environ and os.environ["TESTING"] == "true":
app.config.from_object('config.testing') app.config.from_object('config.testing')
app.config.from_pyfile('testing.py') app.config.from_pyfile('../config/testing.py')
else: else:
app.config.root_path = app.instance_path app.config.root_path = app.instance_path
app.config.from_pyfile('config.py', silent=True) app.config.from_pyfile('config.py', silent=True)

View File

@ -670,6 +670,9 @@ components:
type: string type: string
enum: [out_of_date, in_process, complete, updating] enum: [out_of_date, in_process, complete, updating]
example: done example: done
user_uid:
type: string
example: dhf8r
primary_investigator_id: primary_investigator_id:
type: string type: string
example: dhf8r example: dhf8r

View File

@ -8,7 +8,7 @@ from flask import send_file
from crc import session from crc import session
from crc.api.common import ApiErrorSchema, ApiError from crc.api.common import ApiErrorSchema, ApiError
from crc.models.file import FileModelSchema, FileModel, FileDataModel, FileType from crc.models.file import FileModelSchema, FileModel, FileDataModel, FileType
from crc.services.FileService import FileService from crc.services.file_service import FileService
def get_files(workflow_spec_id=None, study_id=None, workflow_id=None, task_id=None, form_field_key=None): def get_files(workflow_spec_id=None, study_id=None, workflow_id=None, task_id=None, form_field_key=None):

View File

@ -1,29 +1,36 @@
from connexion import NoContent from typing import List, Optional, Union, Tuple, Dict
from crc import session from connexion import NoContent
from flask import g
from crc import session, auth
from crc.api.common import ApiError, ApiErrorSchema from crc.api.common import ApiError, ApiErrorSchema
from crc.api.workflow import __get_workflow_api_model from crc.api.workflow import __get_workflow_api_model
from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy
from crc.models.study import StudyModelSchema, StudyModel from crc.models.study import StudyModelSchema, StudyModel
from crc.models.workflow import WorkflowModel, WorkflowApiSchema, WorkflowSpecModel from crc.models.workflow import WorkflowModel, WorkflowApiSchema, WorkflowSpecModel, WorkflowApi
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
from crc.services.protocol_builder import ProtocolBuilderService
@auth.login_required
def all_studies(): def all_studies():
# todo: Limit returned studies to a user return update_from_protocol_builder()
schema = StudyModelSchema(many=True)
return schema.dump(session.query(StudyModel).all())
@auth.login_required
def add_study(body): def add_study(body):
study = StudyModelSchema().load(body, session=session) study = StudyModelSchema().load(body, session=session)
session.add(study) session.add(study)
session.commit() session.commit()
# FIXME: We need to ask the protocol builder what workflows to add to the study, not just add them all. # FIXME: We need to ask the protocol builder what workflows to add to the study, not just add them all.
for spec in session.query(WorkflowSpecModel).all(): for spec in session.query(WorkflowSpecModel).all():
WorkflowProcessor.create(study.id, spec.id) WorkflowProcessor.create(study.id, spec.id)
return StudyModelSchema().dump(study) return StudyModelSchema().dump(study)
@auth.login_required
def update_study(study_id, body): def update_study(study_id, body):
if study_id is None: if study_id is None:
error = ApiError('unknown_study', 'Please provide a valid Study ID.') error = ApiError('unknown_study', 'Please provide a valid Study ID.')
@ -35,12 +42,14 @@ def update_study(study_id, body):
error = ApiError('unknown_study', 'The study "' + study_id + '" is not recognized.') error = ApiError('unknown_study', 'The study "' + study_id + '" is not recognized.')
return ApiErrorSchema.dump(error), 404 return ApiErrorSchema.dump(error), 404
study = StudyModelSchema().load(body, session=session, instance=study) schema = StudyModelSchema()
study = schema.load(body, session=session, instance=study, partial=True)
session.add(study) session.add(study)
session.commit() session.commit()
return StudyModelSchema().dump(study) return schema.dump(study)
@auth.login_required
def get_study(study_id): def get_study(study_id):
study = session.query(StudyModel).filter_by(id=study_id).first() study = session.query(StudyModel).filter_by(id=study_id).first()
schema = StudyModelSchema() schema = StudyModelSchema()
@ -48,28 +57,71 @@ def get_study(study_id):
return NoContent, 404 return NoContent, 404
return schema.dump(study) return schema.dump(study)
def update_from_protocol_builder():
"""Call the """
def post_update_study_from_protocol_builder(study_id): @auth.login_required
"""This will update the list of known studies based on data received from def update_from_protocol_builder():
"""Updates the list of known studies for a given user based on data received from
the protocol builder.""" the protocol builder."""
# todo: Actually get data from an external service here user = g.user
""":type: crc.models.user.UserModel"""
# Get studies matching this user from Protocol Builder
pb_studies: List[ProtocolBuilderStudy] = get_user_pb_studies()
# Get studies from the database
db_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
db_study_ids = list(map(lambda s: s.id, db_studies))
pb_study_ids = list(map(lambda s: s['STUDYID'], pb_studies))
for pb_study in pb_studies:
# Update studies with latest data from Protocol Builder
if pb_study['STUDYID'] in db_study_ids:
update_study(pb_study['STUDYID'], map_pb_study_to_study(pb_study))
# Add studies from Protocol Builder that aren't in the database yet
else:
new_study = map_pb_study_to_study(pb_study)
add_study(new_study)
# Mark studies as inactive that are no longer in Protocol Builder
for study_id in db_study_ids:
if study_id not in pb_study_ids:
update_study(study_id=study_id, body={'inactive': True})
# Return updated studies
updated_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
results = StudyModelSchema(many=True).dump(updated_studies)
return results
@auth.login_required
def post_update_study_from_protocol_builder(study_id):
"""Update a single study based on data received from
the protocol builder."""
pb_studies: List[ProtocolBuilderStudy] = get_user_pb_studies()
for pb_study in pb_studies:
if pb_study['STUDYID'] == study_id:
return update_study(study_id, map_pb_study_to_study(pb_study))
return NoContent, 304 return NoContent, 304
@auth.login_required
def get_study_workflows(study_id): def get_study_workflows(study_id):
workflow_models = session.query(WorkflowModel).filter_by(study_id=study_id).all() workflow_models = session.query(WorkflowModel).filter_by(study_id=study_id).all()
api_models = [] api_models = []
for workflow_model in workflow_models: for workflow_model in workflow_models:
processor = WorkflowProcessor(workflow_model.workflow_spec_id, processor = WorkflowProcessor(workflow_model.workflow_spec_id,
workflow_model.bpmn_workflow_json) workflow_model.bpmn_workflow_json)
api_models.append( __get_workflow_api_model(processor)) api_models.append(__get_workflow_api_model(processor))
schema = WorkflowApiSchema(many=True) schema = WorkflowApiSchema(many=True)
return schema.dump(api_models) return schema.dump(api_models)
@auth.login_required
def add_workflow_to_study(study_id, body): def add_workflow_to_study(study_id, body):
workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id=body["id"]).first() workflow_spec_model = session.query(WorkflowSpecModel).filter_by(id=body["id"]).first()
if workflow_spec_model is None: if workflow_spec_model is None:
@ -78,3 +130,37 @@ def add_workflow_to_study(study_id, body):
processor = WorkflowProcessor.create(study_id, workflow_spec_model.id) processor = WorkflowProcessor.create(study_id, workflow_spec_model.id)
return WorkflowApiSchema().dump(__get_workflow_api_model(processor)) return WorkflowApiSchema().dump(__get_workflow_api_model(processor))
@auth.login_required
def get_user_pb_studies() -> List[ProtocolBuilderStudy]:
"""Get studies from Protocol Builder matching the given user"""
user = g.user
""":type: crc.models.user.UserModel"""
return ProtocolBuilderService.get_studies(user.uid)
def map_pb_study_to_study(pb_study):
"""Translates the given dict of ProtocolBuilderStudy properties to dict of StudyModel attributes"""
prop_map = {
'STUDYID': 'id',
'HSRNUMBER': 'hsr_number',
'TITLE': 'title',
'NETBADGEID': 'user_uid',
'DATE_MODIFIED': 'last_updated',
}
study_info = {}
# Translate Protocol Builder property names to Study attributes
for k, v in pb_study.items():
if k in prop_map:
study_info[prop_map[k]] = v
if pb_study['Q_COMPLETE']:
study_info['protocol_builder_status'] = ProtocolBuilderStatus.complete._value_
else:
study_info['protocol_builder_status'] = ProtocolBuilderStatus.in_process._value_
return study_info

View File

@ -0,0 +1,247 @@
import enum
from marshmallow import INCLUDE
from crc import ma
class ProtocolBuilderInvestigatorType(enum.Enum):
PI = "Primary Investigator"
SI = "Sub Investigator"
DC = "Department Contact"
SC_I = "Study Coordinator 1"
SC_II = "Study Coordinator 2"
AS_C = "Additional Study Coordinators"
DEPT_CH = "Department Chair"
IRBC = "IRB Coordinator"
SCI = "Scientific Contact"
class ProtocolBuilderStatus(enum.Enum):
out_of_date = "out_of_date"
in_process = "in_process"
complete = "complete"
updating = "updating"
class ProtocolBuilderStudy(object):
def __init__(
self, STUDYID: int, HSRNUMBER: str, TITLE: str, NETBADGEID: str,
Q_COMPLETE: bool, DATE_MODIFIED: str
):
self.STUDYID = STUDYID
self.HSRNUMBER = HSRNUMBER
self.TITLE = TITLE
self.NETBADGEID = NETBADGEID
self.Q_COMPLETE = Q_COMPLETE
self.DATE_MODIFIED = DATE_MODIFIED
class ProtocolBuilderStudySchema(ma.Schema):
class Meta:
model = ProtocolBuilderStudy
unknown = INCLUDE
class ProtocolBuilderInvestigator(object):
def __init__(self, STUDYID: int, NETBADGEID: str, INVESTIGATORTYPE: str, INVESTIGATORTYPEFULL: str):
self.STUDYID = STUDYID
self.NETBADGEID = NETBADGEID
self.INVESTIGATORTYPE = INVESTIGATORTYPE
self.INVESTIGATORTYPEFULL = INVESTIGATORTYPEFULL
class ProtocolBuilderInvestigatorSchema(ma.Schema):
class Meta:
model = ProtocolBuilderInvestigator
unknown = INCLUDE
class ProtocolBuilderRequiredDocument(object):
DOC_TYPES = {
1: "Investigators Brochure",
6: "Cancer Center's PRC Approval Form",
8: "SOM CTO IND/IDE Review Letter",
9: "HIRE Approval",
10: "Cancer Center's PRC Approval Waiver",
12: "Certificate of Confidentiality Application",
14: "Institutional Biosafety Committee Approval",
18: "SOM CTO Approval Letter - UVA PI Multisite Trial",
20: "IRB Approval or Letter of Approval from Administration: Study Conducted at non- UVA Facilities ",
21: "New Medical Device Form",
22: "SOM CTO Review regarding need for IDE",
23: "SOM CTO Review regarding need for IND",
24: "InfoSec Approval",
25: "Scientific Pre-review Documentation",
26: "IBC Number",
32: "IDS - Investigational Drug Service Approval",
36: "RDRC Approval ",
40: "SBS/IRB Approval-FERPA",
41: "HIRE Standard Radiation Language",
42: "COI Management Plan ",
43: "SOM CTO Approval Letter-Non UVA, Non Industry PI MultiSite Study",
44: "GRIME Approval",
45: "GMEC Approval",
46: "IRB Reliance Agreement Request Form- IRB-HSR is IRB of Record",
47: "Non UVA IRB Approval - Initial and Last Continuation",
48: "MR Physicist Approval- Use of Gadolinium",
49: "SOM CTO Approval- Non- UVA Academia PI of IDE",
51: "IDS Waiver",
52: "Package Inserts",
53: "IRB Reliance Agreement Request Form- IRB-HSR Not IRB of Record",
54: "ESCRO Approval",
57: "Laser Safety Officer Approval",
}
def __init__(self, AUXDOCID: str, AUXDOC: str):
self.AUXDOCID = AUXDOCID
self.AUXDOC = AUXDOC
class ProtocolBuilderRequiredDocumentSchema(ma.Schema):
class Meta:
model = ProtocolBuilderRequiredDocument
unknown = INCLUDE
class ProtocolBuilderStudyDetails(object):
def __init__(
self,
STUDYID: int,
IS_IND: int,
IND_1: str,
IND_2: str,
IND_3: str,
IS_UVA_IND: int,
IS_IDE: int,
IS_UVA_IDE: int,
IDE: str,
IS_CHART_REVIEW: int,
IS_RADIATION: int,
GCRC_NUMBER: str,
IS_GCRC: int,
IS_PRC_DSMP: int,
IS_PRC: int,
PRC_NUMBER: str,
IS_IBC: int,
IBC_NUMBER: str,
SPONSORS_PROTOCOL_REVISION_DATE: int,
IS_SPONSOR_MONITORING: int,
IS_AUX: int,
IS_SPONSOR: int,
IS_GRANT: int,
IS_COMMITTEE_CONFLICT: int,
DSMB: int,
DSMB_FREQUENCY: int,
IS_DB: int,
IS_UVA_DB: int,
IS_CENTRAL_REG_DB: int,
IS_CONSENT_WAIVER: int,
IS_HGT: int,
IS_GENE_TRANSFER: int,
IS_TISSUE_BANKING: int,
IS_SURROGATE_CONSENT: int,
IS_ADULT_PARTICIPANT: int,
IS_MINOR_PARTICIPANT: int,
IS_MINOR: int,
IS_BIOMEDICAL: int,
IS_QUALITATIVE: int,
IS_PI_SCHOOL: int,
IS_PRISONERS_POP: int,
IS_PREGNANT_POP: int,
IS_FETUS_POP: int,
IS_MENTAL_IMPAIRMENT_POP: int,
IS_ELDERLY_POP: int,
IS_OTHER_VULNERABLE_POP: int,
OTHER_VULNERABLE_DESC: str,
IS_MULTI_SITE: int,
IS_UVA_LOCATION: int,
NON_UVA_LOCATION: str,
MULTI_SITE_LOCATIONS: str,
IS_OUTSIDE_CONTRACT: int,
IS_UVA_PI_MULTI: int,
IS_NOT_PRC_WAIVER: int,
IS_CANCER_PATIENT: int,
UPLOAD_COMPLETE: int,
IS_FUNDING_SOURCE: int,
IS_PI_INITIATED: int,
IS_ENGAGED_RESEARCH: int,
IS_APPROVED_DEVICE: int,
IS_FINANCIAL_CONFLICT: int,
IS_NOT_CONSENT_WAIVER: int,
IS_FOR_CANCER_CENTER: int,
IS_REVIEW_BY_CENTRAL_IRB: int,
IRBREVIEWERADMIN: str
):
self.STUDYID = STUDYID
self.IS_IND = IS_IND
self.IND_1 = IND_1
self.IND_2 = IND_2
self.IND_3 = IND_3
self.IS_UVA_IND = IS_UVA_IND
self.IS_IDE = IS_IDE
self.IS_UVA_IDE = IS_UVA_IDE
self.IDE = IDE
self.IS_CHART_REVIEW = IS_CHART_REVIEW
self.IS_RADIATION = IS_RADIATION
self.GCRC_NUMBER = GCRC_NUMBER
self.IS_GCRC = IS_GCRC
self.IS_PRC_DSMP = IS_PRC_DSMP
self.IS_PRC = IS_PRC
self.PRC_NUMBER = PRC_NUMBER
self.IS_IBC = IS_IBC
self.IBC_NUMBER = IBC_NUMBER
self.SPONSORS_PROTOCOL_REVISION_DATE = SPONSORS_PROTOCOL_REVISION_DATE
self.IS_SPONSOR_MONITORING = IS_SPONSOR_MONITORING
self.IS_AUX = IS_AUX
self.IS_SPONSOR = IS_SPONSOR
self.IS_GRANT = IS_GRANT
self.IS_COMMITTEE_CONFLICT = IS_COMMITTEE_CONFLICT
self.DSMB = DSMB
self.DSMB_FREQUENCY = DSMB_FREQUENCY
self.IS_DB = IS_DB
self.IS_UVA_DB = IS_UVA_DB
self.IS_CENTRAL_REG_DB = IS_CENTRAL_REG_DB
self.IS_CONSENT_WAIVER = IS_CONSENT_WAIVER
self.IS_HGT = IS_HGT
self.IS_GENE_TRANSFER = IS_GENE_TRANSFER
self.IS_TISSUE_BANKING = IS_TISSUE_BANKING
self.IS_SURROGATE_CONSENT = IS_SURROGATE_CONSENT
self.IS_ADULT_PARTICIPANT = IS_ADULT_PARTICIPANT
self.IS_MINOR_PARTICIPANT = IS_MINOR_PARTICIPANT
self.IS_MINOR = IS_MINOR
self.IS_BIOMEDICAL = IS_BIOMEDICAL
self.IS_QUALITATIVE = IS_QUALITATIVE
self.IS_PI_SCHOOL = IS_PI_SCHOOL
self.IS_PRISONERS_POP = IS_PRISONERS_POP
self.IS_PREGNANT_POP = IS_PREGNANT_POP
self.IS_FETUS_POP = IS_FETUS_POP
self.IS_MENTAL_IMPAIRMENT_POP = IS_MENTAL_IMPAIRMENT_POP
self.IS_ELDERLY_POP = IS_ELDERLY_POP
self.IS_OTHER_VULNERABLE_POP = IS_OTHER_VULNERABLE_POP
self.OTHER_VULNERABLE_DESC = OTHER_VULNERABLE_DESC
self.IS_MULTI_SITE = IS_MULTI_SITE
self.IS_UVA_LOCATION = IS_UVA_LOCATION
self.NON_UVA_LOCATION = NON_UVA_LOCATION
self.MULTI_SITE_LOCATIONS = MULTI_SITE_LOCATIONS
self.IS_OUTSIDE_CONTRACT = IS_OUTSIDE_CONTRACT
self.IS_UVA_PI_MULTI = IS_UVA_PI_MULTI
self.IS_NOT_PRC_WAIVER = IS_NOT_PRC_WAIVER
self.IS_CANCER_PATIENT = IS_CANCER_PATIENT
self.UPLOAD_COMPLETE = UPLOAD_COMPLETE
self.IS_FUNDING_SOURCE = IS_FUNDING_SOURCE
self.IS_PI_INITIATED = IS_PI_INITIATED
self.IS_ENGAGED_RESEARCH = IS_ENGAGED_RESEARCH
self.IS_APPROVED_DEVICE = IS_APPROVED_DEVICE
self.IS_FINANCIAL_CONFLICT = IS_FINANCIAL_CONFLICT
self.IS_NOT_CONSENT_WAIVER = IS_NOT_CONSENT_WAIVER
self.IS_FOR_CANCER_CENTER = IS_FOR_CANCER_CENTER
self.IS_REVIEW_BY_CENTRAL_IRB = IS_REVIEW_BY_CENTRAL_IRB
self.IRBREVIEWERADMIN = IRBREVIEWERADMIN
class ProtocolBuilderStudyDetailsSchema(ma.Schema):
class Meta:
model = ProtocolBuilderStudyDetails
unknown = INCLUDE

View File

@ -1,17 +1,9 @@
import enum
from marshmallow_enum import EnumField from marshmallow_enum import EnumField
from marshmallow_sqlalchemy import ModelSchema from marshmallow_sqlalchemy import ModelSchema
from sqlalchemy import func from sqlalchemy import func
from crc import db from crc import db
from crc.models.protocol_builder import ProtocolBuilderStatus
class ProtocolBuilderStatus(enum.Enum):
out_of_date = "out_of_date"
in_process = "in_process"
complete = "complete"
updating = "updating"
class StudyModel(db.Model): class StudyModel(db.Model):
@ -20,13 +12,21 @@ class StudyModel(db.Model):
title = db.Column(db.String) title = db.Column(db.String)
last_updated = db.Column(db.DateTime(timezone=True), default=func.now()) last_updated = db.Column(db.DateTime(timezone=True), default=func.now())
protocol_builder_status = db.Column(db.Enum(ProtocolBuilderStatus)) protocol_builder_status = db.Column(db.Enum(ProtocolBuilderStatus))
primary_investigator_id = db.Column(db.String) primary_investigator_id = db.Column(db.String, nullable=True)
sponsor = db.Column(db.String) sponsor = db.Column(db.String, nullable=True)
ind_number = db.Column(db.String) hsr_number = db.Column(db.String, nullable=True)
ind_number = db.Column(db.String, nullable=True)
user_uid = db.Column(db.String, db.ForeignKey('user.uid'), nullable=False)
investigator_uids = db.Column(db.ARRAY(db.String), nullable=True)
inactive = db.Column(db.Boolean, default=False)
requirements = db.Column(db.ARRAY(db.Integer), nullable=True)
class StudyModelSchema(ModelSchema): class StudyModelSchema(ModelSchema):
class Meta: class Meta:
model = StudyModel model = StudyModel
include_fk = True # Includes foreign keys
protocol_builder_status = EnumField(ProtocolBuilderStatus) protocol_builder_status = EnumField(ProtocolBuilderStatus)

View File

@ -9,7 +9,7 @@ from crc.models.workflow import WorkflowSpecModel
from docxtpl import DocxTemplate from docxtpl import DocxTemplate
import jinja2 import jinja2
from crc.services.FileService import FileService from crc.services.file_service import FileService
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
@ -19,7 +19,6 @@ class CompleteTemplate(object):
as much feedback as possible. Some of this might move up to a higher level object or be as much feedback as possible. Some of this might move up to a higher level object or be
passed into all tasks as we complete more work.""" passed into all tasks as we complete more work."""
def do_task(self, task, *args, **kwargs): def do_task(self, task, *args, **kwargs):
"""Entry point, mostly worried about wiring it all up.""" """Entry point, mostly worried about wiring it all up."""
if len(args) != 1: if len(args) != 1:
@ -38,7 +37,6 @@ class CompleteTemplate(object):
.filter(FileModel.name == file_name) \ .filter(FileModel.name == file_name) \
.filter(FileModel.workflow_spec_id == workflow_spec_model.id).first() .filter(FileModel.workflow_spec_id == workflow_spec_model.id).first()
if file_data_model is None: if file_data_model is None:
raise ApiError(code="file_missing", raise ApiError(code="file_missing",
message="Can not find a file called '%s' " message="Can not find a file called '%s' "

View File

@ -11,7 +11,6 @@ class FileService(object):
DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
@staticmethod @staticmethod
def add_workflow_spec_file(workflow_spec_id, name, content_type, binary_data): def add_workflow_spec_file(workflow_spec_id, name, content_type, binary_data):
"""Create a new file and associate it with a workflow spec.""" """Create a new file and associate it with a workflow spec."""

View File

@ -1,41 +1,52 @@
import json
from typing import List, Optional
import requests import requests
from crc import app from crc import app
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStudySchema, ProtocolBuilderInvestigator, \
STUDY_URL = app.config['PB_USER_STUDIES_URL'] ProtocolBuilderRequiredDocument, ProtocolBuilderStudyDetails, ProtocolBuilderInvestigatorSchema, \
INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL'] ProtocolBuilderRequiredDocumentSchema, ProtocolBuilderStudyDetailsSchema
REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL']
STUDY_DETAILS_URL = app.config['PB_STUDY_DETAILS_URL']
class ProtocolBuilderService(object):
STUDY_URL = app.config['PB_USER_STUDIES_URL']
INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL']
REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL']
STUDY_DETAILS_URL = app.config['PB_STUDY_DETAILS_URL']
def get_studies(user_id): @staticmethod
response = requests.get(STUDY_URL % user_id) def get_studies(user_id) -> Optional[List[ProtocolBuilderStudy]]:
if response.ok: response = requests.get(ProtocolBuilderService.STUDY_URL % user_id)
return response if response.ok and response.text:
else: pb_studies = ProtocolBuilderStudySchema(many=True).loads(response.text)
return None return pb_studies
else:
return None
@staticmethod
def get_investigators(study_id) -> Optional[List[ProtocolBuilderInvestigator]]:
response = requests.get(ProtocolBuilderService.INVESTIGATOR_URL % study_id)
if response.ok and response.text:
pb_studies = ProtocolBuilderInvestigatorSchema(many=True).loads(response.text)
return pb_studies
else:
return None
def get_investigators(study_id): @staticmethod
response = requests.get(INVESTIGATOR_URL % study_id) def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]:
if response.ok: response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id)
return response if response.ok and response.text:
else: pb_studies = ProtocolBuilderRequiredDocumentSchema(many=True).loads(response.text)
return None return pb_studies
else:
return None
@staticmethod
def get_required_docs(study_id): def get_study_details(study_id) -> Optional[ProtocolBuilderStudyDetails]:
response = requests.get(REQUIRED_DOCS_URL % study_id) response = requests.get(ProtocolBuilderService.STUDY_DETAILS_URL % study_id)
if response.ok: if response.ok and response.text:
return response pb_study_details = ProtocolBuilderStudyDetailsSchema().loads(response.text)
else: return pb_study_details
return None else:
return None
def get_study_details(study_id):
response = requests.get(STUDY_DETAILS_URL % study_id)
if response.ok:
return response
else:
return None

View File

@ -54,7 +54,6 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
'The expression you provided does not exist:' + expression) 'The expression you provided does not exist:' + expression)
class MyCustomParser(BpmnDmnParser): class MyCustomParser(BpmnDmnParser):
""" """
A BPMN and DMN parser that can also parse Camunda forms. A BPMN and DMN parser that can also parse Camunda forms.
@ -62,6 +61,7 @@ class MyCustomParser(BpmnDmnParser):
OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES OVERRIDE_PARSER_CLASSES = BpmnDmnParser.OVERRIDE_PARSER_CLASSES
OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES) OVERRIDE_PARSER_CLASSES.update(CamundaParser.OVERRIDE_PARSER_CLASSES)
class WorkflowProcessor(object): class WorkflowProcessor(object):
_script_engine = CustomBpmnScriptEngine() _script_engine = CustomBpmnScriptEngine()
_serializer = BpmnSerializer() _serializer = BpmnSerializer()
@ -171,9 +171,6 @@ class WorkflowProcessor(object):
next_task = task next_task = task
return next_task return next_task
def complete_task(self, task): def complete_task(self, task):
self.bpmn_workflow.complete_task_from_id(task.id) self.bpmn_workflow.complete_task_from_id(task.id)

View File

@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
docker push sartography/cr-connect-workflow docker push sartography/cr-connect-workflow || true

View File

@ -1,16 +1,31 @@
import datetime import datetime
import glob import glob
import os import os
import xml.etree.ElementTree as ElementTree
from crc import app, db, session from crc import app, db, session
from crc.models.file import FileType, FileModel, FileDataModel from crc.models.file import FileType, FileModel, FileDataModel
from crc.models.study import StudyModel from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.models.workflow import WorkflowSpecModel from crc.models.workflow import WorkflowSpecModel
import xml.etree.ElementTree as ElementTree
from crc.services.workflow_processor import WorkflowProcessor from crc.services.workflow_processor import WorkflowProcessor
class ExampleDataLoader: class ExampleDataLoader:
def make_data(self): def make_data(self):
users = [
UserModel(
uid='dhf8r',
email_address='dhf8r@virginia.EDU',
display_name='Daniel Harold Funk',
affiliation='staff@virginia.edu;member@virginia.edu',
eppn='dhf8r@virginia.edu',
first_name='Daniel',
last_name='Funk',
title='SOFTWARE ENGINEER V'
)
]
studies = [ studies = [
StudyModel( StudyModel(
id=1, id=1,
@ -19,7 +34,8 @@ class ExampleDataLoader:
protocol_builder_status='in_process', protocol_builder_status='in_process',
primary_investigator_id='dhf8r', primary_investigator_id='dhf8r',
sponsor='Sartography Pharmaceuticals', sponsor='Sartography Pharmaceuticals',
ind_number='1234' ind_number='1234',
user_uid='dhf8r'
), ),
StudyModel( StudyModel(
id=2, id=2,
@ -28,7 +44,8 @@ class ExampleDataLoader:
protocol_builder_status='in_process', protocol_builder_status='in_process',
primary_investigator_id='dhf8r', primary_investigator_id='dhf8r',
sponsor='Makerspace & Co.', sponsor='Makerspace & Co.',
ind_number='5678' ind_number='5678',
user_uid='dhf8r'
), ),
] ]
@ -69,11 +86,11 @@ class ExampleDataLoader:
description='How to take different paths based on input.') description='How to take different paths based on input.')
workflow_specifications += \ workflow_specifications += \
self.create_spec(id="docx", self.create_spec(id="docx",
name="docx", name="docx",
display_name="Form with document generation", display_name="Form with document generation",
description='the name says it all') description='the name says it all')
all_data = studies + workflow_specifications all_data = users + studies + workflow_specifications
return all_data return all_data
def create_spec(self, id, name, display_name="", description="", filepath=None): def create_spec(self, id, name, display_name="", description="", filepath=None):
@ -112,7 +129,7 @@ class ExampleDataLoader:
try: try:
file = open(file_path, "rb") file = open(file_path, "rb")
data = file.read() data = file.read()
if(is_primary): if (is_primary):
bpmn: ElementTree.Element = ElementTree.fromstring(data) bpmn: ElementTree.Element = ElementTree.fromstring(data)
spec.primary_process_id = WorkflowProcessor.get_process_id(bpmn) spec.primary_process_id = WorkflowProcessor.get_process_id(bpmn)
print("Locating Process Id for " + filename + " " + spec.primary_process_id) print("Locating Process Id for " + filename + " " + spec.primary_process_id)
@ -121,7 +138,6 @@ class ExampleDataLoader:
file.close() file.close()
return models return models
@staticmethod @staticmethod
def clean_db(): def clean_db():
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.

View File

@ -1,36 +0,0 @@
"""empty message
Revision ID: 0a6e0b829398
Revises: ad5483cb7f3b
Create Date: 2020-02-20 15:42:16.473470
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '0a6e0b829398'
down_revision = 'ad5483cb7f3b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('affiliation', sa.String(), nullable=True))
op.add_column('user', sa.Column('eppn', sa.String(), nullable=True))
op.add_column('user', sa.Column('first_name', sa.String(), nullable=True))
op.add_column('user', sa.Column('last_name', sa.String(), nullable=True))
op.add_column('user', sa.Column('title', sa.String(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'title')
op.drop_column('user', 'last_name')
op.drop_column('user', 'first_name')
op.drop_column('user', 'eppn')
op.drop_column('user', 'affiliation')
# ### end Alembic commands ###

View File

@ -1,35 +0,0 @@
"""empty message
Revision ID: ad5483cb7f3b
Revises: 02fcf09d9085
Create Date: 2020-02-19 11:59:09.948767
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ad5483cb7f3b'
down_revision = '02fcf09d9085'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uid', sa.String(), nullable=True),
sa.Column('email_address', sa.String(), nullable=True),
sa.Column('display_name', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('uid')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
# ### end Alembic commands ###

View File

@ -1,8 +1,8 @@
"""empty message """empty message
Revision ID: 02fcf09d9085 Revision ID: cb3a03c10a0e
Revises: Revises:
Create Date: 2020-02-05 17:18:35.324675 Create Date: 2020-02-28 11:12:56.150837
""" """
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 = '02fcf09d9085' revision = 'cb3a03c10a0e'
down_revision = None down_revision = None
branch_labels = None branch_labels = None
depends_on = None depends_on = None
@ -18,15 +18,18 @@ depends_on = None
def upgrade(): def upgrade():
# ### commands auto generated by Alembic - please adjust! ### # ### commands auto generated by Alembic - please adjust! ###
op.create_table('study', op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('uid', sa.String(), nullable=True),
sa.Column('email_address', sa.String(), nullable=True),
sa.Column('display_name', sa.String(), nullable=True),
sa.Column('affiliation', sa.String(), nullable=True),
sa.Column('eppn', sa.String(), nullable=True),
sa.Column('first_name', sa.String(), nullable=True),
sa.Column('last_name', sa.String(), nullable=True),
sa.Column('title', sa.String(), nullable=True), sa.Column('title', sa.String(), nullable=True),
sa.Column('last_updated', sa.DateTime(timezone=True), nullable=True), sa.PrimaryKeyConstraint('id'),
sa.Column('protocol_builder_status', sa.Enum('out_of_date', 'in_process', 'complete', 'updating', name='protocolbuilderstatus'), nullable=True), sa.UniqueConstraint('uid')
sa.Column('primary_investigator_id', sa.String(), nullable=True),
sa.Column('sponsor', sa.String(), nullable=True),
sa.Column('ind_number', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id')
) )
op.create_table('workflow_spec', op.create_table('workflow_spec',
sa.Column('id', sa.String(), nullable=False), sa.Column('id', sa.String(), nullable=False),
@ -36,6 +39,22 @@ def upgrade():
sa.Column('primary_process_id', sa.String(), nullable=True), sa.Column('primary_process_id', sa.String(), nullable=True),
sa.PrimaryKeyConstraint('id') sa.PrimaryKeyConstraint('id')
) )
op.create_table('study',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('title', sa.String(), nullable=True),
sa.Column('last_updated', sa.DateTime(timezone=True), nullable=True),
sa.Column('protocol_builder_status', sa.Enum('out_of_date', 'in_process', 'complete', 'updating', name='protocolbuilderstatus'), nullable=True),
sa.Column('primary_investigator_id', sa.String(), nullable=True),
sa.Column('sponsor', sa.String(), nullable=True),
sa.Column('hsr_number', sa.String(), nullable=True),
sa.Column('ind_number', sa.String(), nullable=True),
sa.Column('user_uid', sa.String(), nullable=False),
sa.Column('investigator_uids', sa.ARRAY(sa.String()), nullable=True),
sa.Column('inactive', sa.Boolean(), nullable=True),
sa.Column('requirements', sa.ARRAY(sa.Integer()), nullable=True),
sa.ForeignKeyConstraint(['user_uid'], ['user.uid'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('workflow', op.create_table('workflow',
sa.Column('id', sa.Integer(), nullable=False), sa.Column('id', sa.Integer(), nullable=False),
sa.Column('bpmn_workflow_json', sa.JSON(), nullable=True), sa.Column('bpmn_workflow_json', sa.JSON(), nullable=True),
@ -79,6 +98,7 @@ def downgrade():
op.drop_table('file_data') op.drop_table('file_data')
op.drop_table('file') op.drop_table('file')
op.drop_table('workflow') op.drop_table('workflow')
op.drop_table('workflow_spec')
op.drop_table('study') op.drop_table('study')
op.drop_table('workflow_spec')
op.drop_table('user')
# ### end Alembic commands ### # ### end Alembic commands ###

View File

@ -3,11 +3,13 @@
import json import json
import os import os
import unittest import unittest
import urllib.parse
os.environ["TESTING"] = "true"
from crc.models.file import FileModel, FileDataModel from crc.models.file import FileModel, FileDataModel
from crc.models.workflow import WorkflowSpecModel from crc.models.workflow import WorkflowSpecModel
from crc.models.user import UserModel
os.environ["TESTING"] = "true"
from crc import app, db, session from crc import app, db, session
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
@ -18,14 +20,13 @@ from example_data import ExampleDataLoader
# logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) # logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
# Great class to inherit from, as it sets up and tears down
# classes efficiently when we have a database in place.
class BaseTest(unittest.TestCase): class BaseTest(unittest.TestCase):
""" Great class to inherit from, as it sets up and tears down classes
efficiently when we have a database in place.
"""
auths = {} auths = {}
test_uid = "dhf8r"
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -48,6 +49,25 @@ class BaseTest(unittest.TestCase):
self.ctx.pop() self.ctx.pop()
self.auths = {} self.auths = {}
def logged_in_headers(self, user=None, redirect_url='http://some/frontend/url'):
if user is None:
uid = self.test_uid
user_info = {'uid': self.test_uid, 'first_name': 'Daniel', 'last_name': 'Funk',
'email_address': 'dhf8r@virginia.edu'}
else:
uid = user.uid
user_info = {'uid': user.uid, 'first_name': user.first_name, 'last_name': user.last_name,
'email_address': user.email_address}
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)
self.assertTrue(rv.status_code == 302)
self.assertTrue(str.startswith(rv.location, redirect_url))
user_model = session.query(UserModel).filter_by(uid=uid).first()
self.assertIsNotNone(user_model.display_name)
return dict(Authorization='Bearer ' + user_model.encode_auth_token().decode())
def load_example_data(self): def load_example_data(self):
from example_data import ExampleDataLoader from example_data import ExampleDataLoader
ExampleDataLoader.clean_db() ExampleDataLoader.clean_db()
@ -66,7 +86,8 @@ class BaseTest(unittest.TestCase):
self.assertIsNotNone(file_data) self.assertIsNotNone(file_data)
self.assertGreater(len(file_data), 0) self.assertGreater(len(file_data), 0)
def load_test_spec(self, dir_name): @staticmethod
def load_test_spec(dir_name):
"""Loads a spec into the database based on a directory in /tests/data""" """Loads a spec into the database based on a directory in /tests/data"""
if session.query(WorkflowSpecModel).filter_by(id=dir_name).count() > 0: if session.query(WorkflowSpecModel).filter_by(id=dir_name).count() > 0:
return return
@ -81,7 +102,8 @@ class BaseTest(unittest.TestCase):
session.flush() session.flush()
return spec return spec
def protocol_builder_response(self, file_name): @staticmethod
def protocol_builder_response(file_name):
filepath = os.path.join(app.root_path, '..', 'tests', 'data', 'pb_responses', file_name) filepath = os.path.join(app.root_path, '..', 'tests', 'data', 'pb_responses', file_name)
with open(filepath, 'r') as myfile: with open(filepath, 'r') as myfile:
data = myfile.read() data = myfile.read()
@ -102,3 +124,16 @@ class BaseTest(unittest.TestCase):
"Incorrect Valid Response:" + rv.status + ".") "Incorrect Valid Response:" + rv.status + ".")
if code != 0: if code != 0:
self.assertEqual(code, rv.status_code) self.assertEqual(code, rv.status_code)
@staticmethod
def user_info_to_query_string(user_info, redirect_url):
query_string_list = []
items = user_info.items()
for key, value in items:
query_string_list.append('%s=%s' % (key, urllib.parse.quote(value)))
query_string_list.append('redirect_url=%s' % redirect_url)
return '?%s' % '&'.join(query_string_list)

View File

@ -1,31 +1,9 @@
import urllib.parse
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 from tests.base_test import BaseTest
class TestAuthentication(BaseTest): class TestAuthentication(BaseTest):
test_uid = "dhf8r"
def logged_in_headers(self, user=None, redirect_url='http://some/frontend/url'):
if user is None:
uid = self.test_uid
user_info = {'uid': self.test_uid, 'first_name': 'Daniel', 'last_name': 'Funk',
'email_address': 'dhf8r@virginia.edu'}
else:
uid = user.uid
user_info = {'uid': user.uid, 'first_name': user.first_name, 'last_name': user.last_name,
'email_address': user.email_address}
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)
self.assertTrue(rv.status_code == 302)
self.assertTrue(str.startswith(rv.location, redirect_url))
user_model = UserModel.query.filter_by(uid=uid).first()
self.assertIsNotNone(user_model.display_name)
return dict(Authorization='Bearer ' + user_model.encode_auth_token().decode())
def test_auth_token(self): def test_auth_token(self):
self.load_example_data() self.load_example_data()
@ -35,12 +13,13 @@ class TestAuthentication(BaseTest):
self.assertEqual("dhf8r", user.decode_auth_token(auth_token)) self.assertEqual("dhf8r", user.decode_auth_token(auth_token))
def test_auth_creates_user(self): def test_auth_creates_user(self):
new_uid = 'czn1z';
self.load_example_data() self.load_example_data()
user = db.session.query(UserModel).filter(UserModel.uid == self.test_uid).first() user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
self.assertIsNone(user) self.assertIsNone(user)
user_info = {'uid': self.test_uid, 'first_name': 'Daniel', 'last_name': 'Funk', user_info = {'uid': new_uid, 'first_name': 'Cordi', 'last_name': 'Nator',
'email_address': 'dhf8r@virginia.edu'} 'email_address': 'czn1z@virginia.edu'}
redirect_url = 'http://worlds.best.website/admin' redirect_url = 'http://worlds.best.website/admin'
query_string = self.user_info_to_query_string(user_info, redirect_url) query_string = self.user_info_to_query_string(user_info, redirect_url)
url = '/v1.0/sso_backdoor%s' % query_string url = '/v1.0/sso_backdoor%s' % query_string
@ -48,7 +27,7 @@ class TestAuthentication(BaseTest):
self.assertTrue(rv_1.status_code == 302) self.assertTrue(rv_1.status_code == 302)
self.assertTrue(str.startswith(rv_1.location, redirect_url)) self.assertTrue(str.startswith(rv_1.location, redirect_url))
user = db.session.query(UserModel).filter(UserModel.uid == self.test_uid).first() user = db.session.query(UserModel).filter(UserModel.uid == new_uid).first()
self.assertIsNotNone(user) self.assertIsNotNone(user)
self.assertIsNotNone(user.display_name) self.assertIsNotNone(user.display_name)
self.assertIsNotNone(user.email_address) self.assertIsNotNone(user.email_address)
@ -69,15 +48,3 @@ class TestAuthentication(BaseTest):
user = UserModel(uid="ajl2j", first_name='Aaron', last_name='Louie', email_address='ajl2j@virginia.edu') user = UserModel(uid="ajl2j", first_name='Aaron', last_name='Louie', email_address='ajl2j@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)
def user_info_to_query_string(self, user_info, redirect_url):
query_string_list = []
items = user_info.items()
for key, value in items:
query_string_list.append('%s=%s' % (key, urllib.parse.quote(value)))
query_string_list.append('redirect_url=%s' % redirect_url)
return '?%s' % '&'.join(query_string_list)

View File

@ -1,40 +1,37 @@
from unittest.mock import patch from unittest.mock import patch
from crc import db from crc.services.protocol_builder import ProtocolBuilderService
from crc.models.user import UserModel
from crc.services import protocol_builder
from tests.base_test import BaseTest from tests.base_test import BaseTest
class TestProtocolBuilder(BaseTest): class TestProtocolBuilder(BaseTest):
test_uid = "dhf8r" test_uid = "dhf8r"
test_study_id = 1 test_study_id = 1
@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):
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.json.return_value = self.protocol_builder_response('study_details.json') mock_get.return_value.text = self.protocol_builder_response('user_studies.json')
response = protocol_builder.get_studies(self.test_uid) response = ProtocolBuilderService.get_studies(self.test_uid)
self.assertIsNotNone(response) self.assertIsNotNone(response)
@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):
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.json.return_value = self.protocol_builder_response('investigators.json') mock_get.return_value.text = self.protocol_builder_response('investigators.json')
response = protocol_builder.get_studies(self.test_study_id) response = ProtocolBuilderService.get_investigators(self.test_study_id)
self.assertIsNotNone(response)
@patch('crc.services.protocol_builder.requests.get')
def test_get_details(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.json.return_value = self.protocol_builder_response('study_details.json')
response = protocol_builder.get_studies(self.test_study_id)
self.assertIsNotNone(response) self.assertIsNotNone(response)
@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):
mock_get.return_value.ok = True mock_get.return_value.ok = True
mock_get.return_value.json.return_value = self.protocol_builder_response('required_docs.json') mock_get.return_value.text = self.protocol_builder_response('required_docs.json')
response = protocol_builder.get_studies(self.test_study_id) response = ProtocolBuilderService.get_required_docs(self.test_study_id)
self.assertIsNotNone(response)
@patch('crc.services.protocol_builder.requests.get')
def test_get_details(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('study_details.json')
response = ProtocolBuilderService.get_study_details(self.test_study_id)
self.assertIsNotNone(response) self.assertIsNotNone(response)

View File

@ -1,9 +1,9 @@
import json import json
from datetime import datetime, tzinfo, timezone from datetime import datetime, timezone
from crc import session from crc import session
from crc.models.file import FileModel from crc.models.study import StudyModel, StudyModelSchema
from crc.models.study import StudyModel, StudyModelSchema, ProtocolBuilderStatus from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \ from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \
WorkflowApiSchema WorkflowApiSchema
from tests.base_test import BaseTest from tests.base_test import BaseTest
@ -20,15 +20,18 @@ class TestStudyApi(BaseTest):
self.load_example_data() self.load_example_data()
study = { study = {
"id": 12345, "id": 12345,
"title": "Phase III Trial of Genuine People Personalities (GPP) Autonomous Intelligent Emotional Agents for Interstellar Spacecraft", "title": "Phase III Trial of Genuine People Personalities (GPP) Autonomous Intelligent Emotional Agents "
"for Interstellar Spacecraft",
"last_updated": datetime.now(tz=timezone.utc), "last_updated": datetime.now(tz=timezone.utc),
"protocol_builder_status": ProtocolBuilderStatus.in_process, "protocol_builder_status": ProtocolBuilderStatus.in_process,
"primary_investigator_id": "tricia.marie.mcmillan@heartofgold.edu", "primary_investigator_id": "tricia.marie.mcmillan@heartofgold.edu",
"sponsor": "Sirius Cybernetics Corporation", "sponsor": "Sirius Cybernetics Corporation",
"ind_number": "567890", "ind_number": "567890",
"user_uid": "dhf8r",
} }
rv = self.app.post('/v1.0/study', rv = self.app.post('/v1.0/study',
content_type="application/json", content_type="application/json",
headers=self.logged_in_headers(),
data=json.dumps(StudyModelSchema().dump(study))) data=json.dumps(StudyModelSchema().dump(study)))
self.assert_success(rv) self.assert_success(rv)
db_study = session.query(StudyModel).filter_by(id=12345).first() db_study = session.query(StudyModel).filter_by(id=12345).first()
@ -39,8 +42,7 @@ class TestStudyApi(BaseTest):
self.assertEqual(study["primary_investigator_id"], db_study.primary_investigator_id) self.assertEqual(study["primary_investigator_id"], db_study.primary_investigator_id)
self.assertEqual(study["sponsor"], db_study.sponsor) self.assertEqual(study["sponsor"], db_study.sponsor)
self.assertEqual(study["ind_number"], db_study.ind_number) self.assertEqual(study["ind_number"], db_study.ind_number)
self.assertEqual(study["user_uid"], db_study.user_uid)
def test_update_study(self): def test_update_study(self):
self.load_example_data() self.load_example_data()
@ -48,8 +50,9 @@ class TestStudyApi(BaseTest):
study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility" study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility"
study.protocol_builder_status = ProtocolBuilderStatus.complete study.protocol_builder_status = ProtocolBuilderStatus.complete
rv = self.app.put('/v1.0/study/%i' % study.id, rv = self.app.put('/v1.0/study/%i' % study.id,
content_type="application/json", content_type="application/json",
data=json.dumps(StudyModelSchema().dump(study))) headers=self.logged_in_headers(),
data=json.dumps(StudyModelSchema().dump(study)))
self.assert_success(rv) self.assert_success(rv)
db_study = session.query(StudyModel).filter_by(id=study.id).first() db_study = session.query(StudyModel).filter_by(id=study.id).first()
self.assertIsNotNone(db_study) self.assertIsNotNone(db_study)
@ -61,6 +64,7 @@ class TestStudyApi(BaseTest):
study = session.query(StudyModel).first() study = session.query(StudyModel).first()
rv = self.app.get('/v1.0/study/%i' % study.id, rv = self.app.get('/v1.0/study/%i' % study.id,
follow_redirects=True, follow_redirects=True,
headers=self.logged_in_headers(),
content_type="application/json") content_type="application/json")
self.assert_success(rv) self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))
@ -79,7 +83,9 @@ class TestStudyApi(BaseTest):
study = session.query(StudyModel).first() study = session.query(StudyModel).first()
self.assertEqual(0, session.query(WorkflowModel).count()) self.assertEqual(0, session.query(WorkflowModel).count())
spec = session.query(WorkflowSpecModel).first() spec = session.query(WorkflowSpecModel).first()
rv = self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", rv = self.app.post('/v1.0/study/%i/workflows' % study.id,
content_type="application/json",
headers=self.logged_in_headers(),
data=json.dumps(WorkflowSpecModelSchema().dump(spec))) data=json.dumps(WorkflowSpecModelSchema().dump(spec)))
self.assert_success(rv) self.assert_success(rv)
self.assertEqual(1, session.query(WorkflowModel).count()) self.assertEqual(1, session.query(WorkflowModel).count())
@ -97,7 +103,9 @@ class TestStudyApi(BaseTest):
self.load_example_data() self.load_example_data()
study = session.query(StudyModel).first() study = session.query(StudyModel).first()
spec = session.query(WorkflowSpecModel).first() spec = session.query(WorkflowSpecModel).first()
rv = self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", rv = self.app.post('/v1.0/study/%i/workflows' % study.id,
content_type="application/json",
headers=self.logged_in_headers(),
data=json.dumps(WorkflowSpecModelSchema().dump(spec))) data=json.dumps(WorkflowSpecModelSchema().dump(spec)))
self.assertEqual(1, session.query(WorkflowModel).count()) self.assertEqual(1, session.query(WorkflowModel).count())
json_data = json.loads(rv.get_data(as_text=True)) json_data = json.loads(rv.get_data(as_text=True))

View File

@ -13,8 +13,12 @@ class TestTasksApi(BaseTest):
def create_workflow(self, workflow_name): def create_workflow(self, workflow_name):
study = session.query(StudyModel).first() study = session.query(StudyModel).first()
spec = session.query(WorkflowSpecModel).filter_by(id=workflow_name).first() spec = session.query(WorkflowSpecModel).filter_by(id=workflow_name).first()
self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", rv = self.app.post(
data=json.dumps(WorkflowSpecModelSchema().dump(spec))) '/v1.0/study/%i/workflows' % study.id,
headers=self.logged_in_headers(),
content_type="application/json",
data=json.dumps(WorkflowSpecModelSchema().dump(spec)))
self.assert_success(rv)
workflow = session.query(WorkflowModel).filter_by(study_id=study.id, workflow_spec_id=workflow_name).first() workflow = session.query(WorkflowModel).filter_by(study_id=study.id, workflow_spec_id=workflow_name).first()
return workflow return workflow

View File

@ -1,11 +1,8 @@
import json import json
from datetime import datetime, tzinfo, timezone
from crc import session from crc import session
from crc.models.file import FileModel from crc.models.file import FileModel
from crc.models.study import StudyModel, StudyModelSchema, ProtocolBuilderStatus from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel
from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \
WorkflowApiSchema
from tests.base_test import BaseTest from tests.base_test import BaseTest