Merge pull request #250 from sartography/206-extend-study-access

206 extend study access
This commit is contained in:
Dan Funk 2021-03-01 14:55:38 -05:00 committed by GitHub
commit 3e0541fa15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 735 additions and 11 deletions

View File

@ -6,7 +6,8 @@ from sqlalchemy.exc import IntegrityError
from crc import session
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.study import Study, StudyEvent, StudyEventType, StudyModel, StudySchema, StudyForUpdateSchema, StudyStatus
from crc.models.study import Study, StudyEvent, StudyEventType, StudyModel, StudySchema, StudyForUpdateSchema, \
StudyStatus
from crc.services.study_service import StudyService
from crc.services.user_service import UserService
from crc.services.workflow_service import WorkflowService

View File

@ -35,6 +35,7 @@ class StudyEventType(enum.Enum):
automatic = 'automatic'
class StudyModel(db.Model):
__tablename__ = 'study'
id = db.Column(db.Integer, primary_key=True)
@ -51,7 +52,7 @@ class StudyModel(db.Model):
requirements = db.Column(db.ARRAY(db.Integer), nullable=True)
on_hold = db.Column(db.Boolean, default=False)
enrollment_date = db.Column(db.DateTime(timezone=True), nullable=True)
# events = db.relationship("TaskEventModel")
#events = db.relationship("TaskEventModel")
events_history = db.relationship("StudyEvent", cascade="all, delete, delete-orphan")
def update_from_protocol_builder(self, pbs: ProtocolBuilderStudy):
@ -63,6 +64,21 @@ class StudyModel(db.Model):
self.irb_status = IrbStatus.incomplete_in_protocol_builder
class StudyAssociated(db.Model):
"""
This model allows us to associate people with a study, and optionally
give them edit access. This allows us to create a table with PI, D_CH, etc.
and give access to people other than the study owner.
Task_Events will still work as they have previously
"""
__tablename__ = 'study_associated_user'
id = db.Column(db.Integer, primary_key=True)
study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False)
uid = db.Column(db.String, db.ForeignKey('ldap_model.uid'), nullable=False)
role = db.Column(db.String, nullable=True)
send_email = db.Column(db.Boolean, nullable=True)
access = db.Column(db.Boolean, nullable=True)
class StudyEvent(db.Model):
__tablename__ = 'study_event'
id = db.Column(db.Integer, primary_key=True)

View File

@ -0,0 +1,31 @@
from crc.api.common import ApiError
from crc.scripts.script import Script
from crc.services.study_service import StudyService
class GetStudyAssociates(Script):
def get_description(self):
return """
Returns person assocated with study or an error if one is not associated.
example : get_study_associate('sbp3ey') => {'uid':'sbp3ey','role':'Unicorn Herder', 'send_email': False,
'access':True}
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
if len(args)<1:
return False
return True
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
if len(args)<1:
raise ApiError('no_user_id_specified', 'A uva uid is the sole argument to this function')
if not isinstance(args[0],type('')):
raise ApiError('argument_should_be_string', 'A uva uid is always a string, please check type')
return StudyService.get_study_associate(study_id=study_id,uid=args[0])

View File

@ -0,0 +1,29 @@
from crc.api.common import ApiError
from crc.scripts.script import Script
from crc.services.study_service import StudyService
class GetStudyAssociates(Script):
argument_error_message = "You must supply at least one argument to the " \
"update_study_associates task, an array of objects in the form " \
"{'uid':'someid', 'role': 'text', 'send_email: 'boolean', " \
"'access':'boolean'} "
def get_description(self):
return """
Returns all people associated with the study - Will always return the study owner as assocated
example : get_study_associates() => [{'uid':'sbp3ey','role':'Unicorn Herder', 'send_email': False, 'access':True}]
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
return True
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
return StudyService.get_study_associates(study_id)

View File

@ -35,8 +35,17 @@ class Script(object):
updating the task data.
"""
def make_closure(subclass,task,study_id,workflow_id):
"""
yes - this is black magic
Essentially, we want to build a list of all of the submodules (i.e. email, user_data_get, etc)
and a function that is assocated with them.
This basically creates an Instance of the class and returns a function that calls do_task
on the instance of that class.
the next for x in range, then grabs the name of the module and associates it with the function
that we created.
"""
instance = subclass()
return lambda *a : subclass.do_task(instance,task,study_id,workflow_id,*a)
return lambda *ar,**kw: subclass.do_task(instance,task,study_id,workflow_id,*ar,**kw)
execlist = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):
@ -59,7 +68,7 @@ class Script(object):
def make_closure_validate(subclass,task,study_id,workflow_id):
instance = subclass()
return lambda *a : subclass.do_task_validate_only(instance,task,study_id,workflow_id,*a)
return lambda *a, **b: subclass.do_task_validate_only(instance,task,study_id,workflow_id,*a,**b)
execlist = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):

View File

@ -4,7 +4,7 @@ from crc.scripts.script import Script
class StudyDataSet(Script,DataStoreBase):
def get_description(self):
return """Sets study data from the data store."""
return """Sets study data from the data store. Takes two positional arguments key and value"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.set_validate_common(study_id,

View File

@ -0,0 +1,38 @@
from crc.api.common import ApiError
from crc.scripts.script import Script
from crc.services.study_service import StudyService
class UpdateStudyAssociates(Script):
argument_error_message = "You must supply at least one argument to the " \
"update_study_associates task, an array of objects in the form " \
"{'uid':'someid', 'role': 'text', 'send_email: 'boolean', " \
"'access':'boolean'} "
def get_description(self):
return """
Allows you to associate other users with a study - only 'uid' is a required keyword argument
An empty list will delete the existing Associated list (except owner)
The UID will be validated vs ldap and will raise an error if the uva_uid is not found. This will replace any
association already in place for this user.
example : update_study_associate(uid='sbp3ey',role='Unicorn Herder',send_email=False, access=True)
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
if kwargs.get('uid') is None:
raise ApiError('uid_is_required_argument','a valid keyword argument of uid is required, it should be the '
'uva uid for this user')
return True
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
return StudyService.update_study_associate(study_id=study_id,**kwargs)

View File

@ -0,0 +1,45 @@
from crc.api.common import ApiError
from crc.scripts.script import Script
from crc.services.study_service import StudyService
class UpdateStudyAssociates(Script):
argument_error_message = "You must supply at least one argument to the " \
"update_study_associates task, an array of objects in the form " \
"{'uid':'someid', 'role': 'text', 'send_email: 'boolean', " \
"'access':'boolean'} "
def get_description(self):
return """
Allows you to associate other users with a study - only 'uid' is required in the
incoming dictionary, but will be useless without other information - all values will default to
false or blank
An empty list will delete the existing Associated list (except owner)
Each UID will be validated vs ldap and will raise an error if the uva_uid is not found. This supplied list will replace
any
associations already in place.
example : update_study_associates([{'uid':'sbp3ey','role':'Unicorn Herder', 'send_email': False, 'access':True}])
"""
def validate_arg(self,arg):
if not isinstance(arg,list):
raise ApiError("invalid parameter", "This function is expecting a list of dictionaries")
if not len(arg) > 0 and not isinstance(arg[0],dict):
raise ApiError("invalid paramemter","This function is expecting a list of dictionaries")
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
items = args[0]
self.validate_arg(items)
return all([x.get('uid',False) for x in items])
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
access_list = args[0]
self.validate_arg(access_list)
return StudyService.update_study_associates(study_id,access_list)

View File

@ -6,7 +6,7 @@ from crc.scripts.script import Script
class UserDataGet(Script, DataStoreBase):
def get_description(self):
return """Gets user data from the data store."""
return """Gets user data from the data store - takes only one argument 'key' """
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.do_task(task, study_id, workflow_id, *args, **kwargs)

View File

@ -6,7 +6,9 @@ from crc.scripts.script import Script
class UserDataSet(Script,DataStoreBase):
def get_description(self):
return """Sets user data to the data store."""
return """Sets user data to the data store these are positional arguments key and value.
example: user_data_set('mykey','myvalue')
"""
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
self.set_validate_common(None,

View File

@ -48,6 +48,13 @@ class LdapService(object):
LdapService.conn = conn
return LdapService.conn
@staticmethod
def user_exists(uva_uid):
try:
x = LdapService.user_info(uva_uid)
except:
return False
return True
@staticmethod
def user_info(uva_uid):

View File

@ -13,7 +13,7 @@ from crc.models.file import FileDataModel, FileModel, FileModelSchema, File, Loo
from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEventType, StudyEvent, \
IrbStatus
IrbStatus, StudyAssociated
from crc.models.task_event import TaskEventModel, TaskEvent
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus, WorkflowSpecDependencyFile
@ -29,7 +29,11 @@ class StudyService(object):
@staticmethod
def get_studies_for_user(user):
"""Returns a list of all studies for the given user."""
db_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
associated = session.query(StudyAssociated).filter_by(uid=user.uid,access=True).all()
associated_studies = [x.study_id for x in associated]
db_studies = session.query(StudyModel).filter((StudyModel.user_uid==user.uid)|
(StudyModel.id.in_(associated_studies))).all()
studies = []
for study_model in db_studies:
studies.append(StudyService.get_study(study_model.id, study_model))
@ -86,6 +90,122 @@ class StudyService(object):
return study
@staticmethod
def get_study_associate(study_id = None, uid=None):
"""
gets all associated people for a study from the database
"""
study = db.session.query(StudyModel).filter(StudyModel.id == study_id).first()
if study is None:
raise ApiError('study_not_found', 'No study found with id = %d' % study_id)
if uid is None:
raise ApiError('uid not specified','A valid uva uid is required for this function')
if uid == study.user_uid:
return {'uid': ownerid, 'role': 'owner', 'send_email': True, 'access': True}
person = db.session.query(StudyAssociated).filter((StudyAssociated.study_id == study_id)&(
StudyAssociated.uid == uid)).first()
if person:
newAssociate = {'uid':person.uid}
newAssociate['role'] = person.role
newAssociate['send_email'] = person.send_email
newAssociate['access'] = person.access
return newAssociate
raise ApiError('uid_not_associated_with_study',"user id %s was not associated with study number %d"%(uid,
study_id))
@staticmethod
def get_study_associates(study_id):
"""
gets all associated people for a study from the database
"""
study = db.session.query(StudyModel).filter(StudyModel.id == study_id).first()
if study is None:
raise ApiError('study_not_found','No study found with id = %d'%study_id)
ownerid = study.user_uid
people_list = [{'uid':ownerid,'role':'owner','send_email':True,'access':True}]
people = db.session.query(StudyAssociated).filter(StudyAssociated.study_id == study_id)
for person in people:
newAssociate = {'uid':person.uid}
newAssociate['role'] = person.role
newAssociate['send_email'] = person.send_email
newAssociate['access'] = person.access
people_list.append(newAssociate)
return people_list
@staticmethod
def update_study_associates(study_id,associates):
"""
updates the list of associates in the database for a study_id and a list
of dicts that contains associates
"""
if study_id is None:
raise ApiError('study_id not specified', "This function requires the study_id parameter")
for person in associates:
if not LdapService.user_exists(person.get('uid','impossible_uid')):
if person.get('uid','impossible_uid') == 'impossible_uid':
raise ApiError('associate with no uid','One of the associates passed as a parameter doesnt have '
'a uid specified')
raise ApiError('trying_to_grant_access_to_user_not_found_in_ldap',"You are trying to grant access to "
"%s, but that user was not found in "
"ldap "
"- please check to ensure it is a "
"valid uva uid"%person.get('uid'))
study = db.session.query(StudyModel).filter(StudyModel.id == study_id).first()
if study is None:
raise ApiError('study_id not found', "A study with id# %d was not found"%study_id)
db.session.query(StudyAssociated).filter(StudyAssociated.study_id == study_id).delete()
for person in associates:
newAssociate = StudyAssociated()
newAssociate.study_id = study_id
newAssociate.uid = person['uid']
newAssociate.role = person.get('role', None)
newAssociate.send_email = person.get('send_email', False)
newAssociate.access = person.get('access',False)
session.add(newAssociate)
session.commit()
@staticmethod
def update_study_associate(study_id=None,uid=None,role="",send_email=False,access=False):
if study_id is None:
raise ApiError('study_id not specified', "This function requires the study_id parameter")
if uid is None:
raise ApiError('uid not specified', "This function requires a uva uid parameter")
if not LdapService.user_exists(uid):
raise ApiError('trying_to_grant_access_to_user_not_found_in_ldap',"You are trying to grant access to "
"%s but they were not found in ldap "
"- please check to ensure it is a "
"valid uva uid"%uid)
study = db.session.query(StudyModel).filter(StudyModel.id == study_id).first()
if study is None:
raise ApiError('study_id not found', "A study with id# %d was not found"%study_id)
db.session.query(StudyAssociated).filter((StudyAssociated.study_id == study_id)&(StudyAssociated.uid ==
uid) ).delete()
newAssociate = StudyAssociated()
newAssociate.study_id = study_id
newAssociate.uid = uid
newAssociate.role = role
newAssociate.send_email = send_email
newAssociate.access = access
session.add(newAssociate)
session.commit()
return True
@staticmethod
def delete_study(study_id):
session.query(TaskEventModel).filter_by(study_id=study_id).delete()

View File

@ -649,8 +649,8 @@ class WorkflowService(object):
@staticmethod
def get_users_assigned_to_task(processor, spiff_task) -> List[str]:
if not hasattr(spiff_task.task_spec, 'lane') or spiff_task.task_spec.lane is None:
return [processor.workflow_model.study.user_uid]
# todo: return a list of all users that can edit the study by default
associated = StudyService.get_study_associates(processor.workflow_model.study.id)
return [user['uid'] for user in associated if user['access']]
if spiff_task.task_spec.lane not in spiff_task.data:
return [] # No users are assignable to the task at this moment
lane_users = spiff_task.data[spiff_task.task_spec.lane]

View File

@ -0,0 +1,38 @@
"""empty message
Revision ID: cb892916166a
Revises: ff29528a9909
Create Date: 2021-02-24 12:16:45.779821
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb892916166a'
down_revision = 'ff29528a9909'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('study_associated_user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('study_id', sa.Integer(), nullable=False),
sa.Column('uid', sa.String(), nullable=False),
sa.Column('role', sa.String(), nullable=True),
sa.Column('send_email', sa.Boolean(), nullable=True),
sa.Column('access', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['study_id'], ['study.id'], ),
sa.ForeignKeyConstraint(['uid'], ['ldap_model.uid'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('study_associated_user')
# ### end Alembic commands ###

View File

@ -0,0 +1,93 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0exnnpv" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1nfe5m9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1nfe5m9" sourceRef="StartEvent_1" targetRef="Task_Script_Load_Study_Sponsors" />
<bpmn:scriptTask id="Task_Script_Load_Study_Sponsors" name="Load Study Sponsors">
<bpmn:incoming>SequenceFlow_1nfe5m9</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1bqiin0</bpmn:outgoing>
<bpmn:script>sponsors = study_info('sponsors')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1bqiin0" sourceRef="Task_Script_Load_Study_Sponsors" targetRef="Activity_0cm6tn2" />
<bpmn:sequenceFlow id="Flow_09cika8" sourceRef="Activity_0cm6tn2" targetRef="Activity_14td33q" />
<bpmn:scriptTask id="Activity_0cm6tn2" name="setval">
<bpmn:incoming>SequenceFlow_1bqiin0</bpmn:incoming>
<bpmn:outgoing>Flow_09cika8</bpmn:outgoing>
<bpmn:script>update_study_associate(uid='lb3dp',role='SuperDude',send_email=False,access=True)</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_0d8iftx" name="getval2">
<bpmn:incoming>Flow_1vlh6s0</bpmn:incoming>
<bpmn:outgoing>Flow_0cttkwp</bpmn:outgoing>
<bpmn:script>out3 = get_study_associates()
out4 = get_study_associate('lb3dp')</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_0c8gcuh">
<bpmn:incoming>Flow_0cttkwp</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0cttkwp" sourceRef="Activity_0d8iftx" targetRef="Event_0c8gcuh" />
<bpmn:scriptTask id="Activity_0run091" name="setval - 2">
<bpmn:incoming>Flow_14n3ixy</bpmn:incoming>
<bpmn:outgoing>Flow_1vlh6s0</bpmn:outgoing>
<bpmn:script>update_study_associates([{'uid':'lb3dp','role':'SuperGal','send_email':False,'access':True}])</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1vlh6s0" sourceRef="Activity_0run091" targetRef="Activity_0d8iftx" />
<bpmn:scriptTask id="Activity_14td33q" name="getval">
<bpmn:incoming>Flow_09cika8</bpmn:incoming>
<bpmn:outgoing>Flow_14n3ixy</bpmn:outgoing>
<bpmn:script>out = get_study_associates()
out2 = get_study_associate('lb3dp')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_14n3ixy" sourceRef="Activity_14td33q" targetRef="Activity_0run091" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0exnnpv">
<bpmndi:BPMNEdge id="SequenceFlow_1bqiin0_di" bpmnElement="SequenceFlow_1bqiin0">
<di:waypoint x="370" y="117" />
<di:waypoint x="440" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1nfe5m9_di" bpmnElement="SequenceFlow_1nfe5m9">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1mp6xid_di" bpmnElement="Task_Script_Load_Study_Sponsors">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_09cika8_di" bpmnElement="Flow_09cika8">
<di:waypoint x="540" y="117" />
<di:waypoint x="580" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0wnwluq_di" bpmnElement="Activity_0cm6tn2">
<dc:Bounds x="440" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0cq37mm_di" bpmnElement="Activity_0d8iftx">
<dc:Bounds x="900" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0c8gcuh_di" bpmnElement="Event_0c8gcuh">
<dc:Bounds x="1042" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0cttkwp_di" bpmnElement="Flow_0cttkwp">
<di:waypoint x="1000" y="117" />
<di:waypoint x="1042" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0run091_di" bpmnElement="Activity_0run091">
<dc:Bounds x="750" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1vlh6s0_di" bpmnElement="Flow_1vlh6s0">
<di:waypoint x="850" y="117" />
<di:waypoint x="900" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_14td33q_di" bpmnElement="Activity_14td33q">
<dc:Bounds x="580" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_14n3ixy_di" bpmnElement="Flow_14n3ixy">
<di:waypoint x="680" y="117" />
<di:waypoint x="750" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,66 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0exnnpv" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1nfe5m9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1nfe5m9" sourceRef="StartEvent_1" targetRef="Task_Script_Load_Study_Sponsors" />
<bpmn:scriptTask id="Task_Script_Load_Study_Sponsors" name="Load Study Sponsors">
<bpmn:incoming>SequenceFlow_1nfe5m9</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1bqiin0</bpmn:outgoing>
<bpmn:script>sponsors = study_info('sponsors')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1bqiin0" sourceRef="Task_Script_Load_Study_Sponsors" targetRef="Activity_0cm6tn2" />
<bpmn:sequenceFlow id="Flow_09cika8" sourceRef="Activity_0cm6tn2" targetRef="Activity_0d8iftx" />
<bpmn:scriptTask id="Activity_0cm6tn2" name="setval">
<bpmn:incoming>SequenceFlow_1bqiin0</bpmn:incoming>
<bpmn:outgoing>Flow_09cika8</bpmn:outgoing>
<bpmn:script>update_study_associate(uid='cah5us',role='SuperDude',send_email=False,access=True)</bpmn:script>
</bpmn:scriptTask>
<bpmn:scriptTask id="Activity_0d8iftx" name="getval">
<bpmn:incoming>Flow_09cika8</bpmn:incoming>
<bpmn:outgoing>Flow_0cttkwp</bpmn:outgoing>
<bpmn:script>out = get_study_associates()
out2 = get_study_associate('lb3dp')</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_0c8gcuh">
<bpmn:incoming>Flow_0cttkwp</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0cttkwp" sourceRef="Activity_0d8iftx" targetRef="Event_0c8gcuh" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0exnnpv">
<bpmndi:BPMNEdge id="SequenceFlow_1bqiin0_di" bpmnElement="SequenceFlow_1bqiin0">
<di:waypoint x="370" y="117" />
<di:waypoint x="440" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1nfe5m9_di" bpmnElement="SequenceFlow_1nfe5m9">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1mp6xid_di" bpmnElement="Task_Script_Load_Study_Sponsors">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_09cika8_di" bpmnElement="Flow_09cika8">
<di:waypoint x="540" y="117" />
<di:waypoint x="600" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0wnwluq_di" bpmnElement="Activity_0cm6tn2">
<dc:Bounds x="440" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0cq37mm_di" bpmnElement="Activity_0d8iftx">
<dc:Bounds x="600" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0c8gcuh_di" bpmnElement="Event_0c8gcuh">
<dc:Bounds x="762" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0cttkwp_di" bpmnElement="Flow_0cttkwp">
<di:waypoint x="700" y="117" />
<di:waypoint x="762" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,66 @@
<?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:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_0kmksnn" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.7.0">
<bpmn:process id="Process_0exnnpv" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>SequenceFlow_1nfe5m9</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="SequenceFlow_1nfe5m9" sourceRef="StartEvent_1" targetRef="Task_Script_Load_Study_Sponsors" />
<bpmn:scriptTask id="Task_Script_Load_Study_Sponsors" name="Load Study Sponsors">
<bpmn:incoming>SequenceFlow_1nfe5m9</bpmn:incoming>
<bpmn:outgoing>SequenceFlow_1bqiin0</bpmn:outgoing>
<bpmn:script>sponsors = study_info('sponsors')</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="SequenceFlow_1bqiin0" sourceRef="Task_Script_Load_Study_Sponsors" targetRef="Activity_0cm6tn2" />
<bpmn:sequenceFlow id="Flow_09cika8" sourceRef="Activity_0cm6tn2" targetRef="Activity_0s3e9zu" />
<bpmn:scriptTask id="Activity_0cm6tn2" name="setval">
<bpmn:incoming>SequenceFlow_1bqiin0</bpmn:incoming>
<bpmn:outgoing>Flow_09cika8</bpmn:outgoing>
<bpmn:script>update_study_associate(uid='lb3dp',role='SuperGal',send_email=False,access=True)
update_study_associate(uid='lje5u',role='SuperGal2',send_email=False,access=False)</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_0c8gcuh">
<bpmn:incoming>Flow_0axwrzg</bpmn:incoming>
</bpmn:endEvent>
<bpmn:task id="Activity_0s3e9zu" name="Meaningless User Task">
<bpmn:documentation>This should just leave us a task to complete after the update_study_assocate script</bpmn:documentation>
<bpmn:incoming>Flow_09cika8</bpmn:incoming>
<bpmn:outgoing>Flow_0axwrzg</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_0axwrzg" sourceRef="Activity_0s3e9zu" targetRef="Event_0c8gcuh" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_0exnnpv">
<bpmndi:BPMNEdge id="SequenceFlow_1bqiin0_di" bpmnElement="SequenceFlow_1bqiin0">
<di:waypoint x="370" y="117" />
<di:waypoint x="440" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="SequenceFlow_1nfe5m9_di" bpmnElement="SequenceFlow_1nfe5m9">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="ScriptTask_1mp6xid_di" bpmnElement="Task_Script_Load_Study_Sponsors">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_09cika8_di" bpmnElement="Flow_09cika8">
<di:waypoint x="540" y="117" />
<di:waypoint x="610" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0wnwluq_di" bpmnElement="Activity_0cm6tn2">
<dc:Bounds x="440" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0c8gcuh_di" bpmnElement="Event_0c8gcuh">
<dc:Bounds x="1042" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0s3e9zu_di" bpmnElement="Activity_0s3e9zu">
<dc:Bounds x="610" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0axwrzg_di" bpmnElement="Flow_0axwrzg">
<di:waypoint x="710" y="117" />
<di:waypoint x="1042" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,163 @@
import json
from unittest.mock import patch
import flask
from crc.api.common import ApiError
from crc.services.user_service import UserService
from tests.base_test import BaseTest
from crc import session, app
from crc.models.study import StudyModel
from crc.models.user import UserModel
from crc.api.study import user_studies
from crc.services.study_service import StudyService
from crc.services.workflow_processor import WorkflowProcessor
from crc.services.workflow_service import WorkflowService
class TestSudySponsorsScript(BaseTest):
test_uid = "dhf8r"
test_study_id = 1
def test_study_sponsors_script_validation(self):
flask.g.user = UserModel(uid='dhf8r')
self.load_example_data() # study_info script complains if irb_documents.xls is not loaded
# during the validate phase I'm going to assume that we will never
# have a case where irb_documents.xls is not loaded ??
self.load_test_spec("study_sponsors_associate")
WorkflowService.test_spec("study_sponsors_associate") # This would raise errors if it didn't validate
@patch('crc.services.protocol_builder.requests.get')
def test_study_sponsors_script(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('sponsors.json')
flask.g.user = UserModel(uid='dhf8r')
app.config['PB_ENABLED'] = True
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("study_sponsors_associate")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
WorkflowService.test_spec("study_sponsors_associate")
processor = WorkflowProcessor(workflow_model)
processor.do_engine_steps()
self.assertTrue(processor.bpmn_workflow.is_completed())
data = processor.next_task().data
self.assertIn('sponsors', data)
self.assertIn('out', data)
print(data['out'])
self.assertEquals([{'uid': 'dhf8r', 'role': 'owner', 'send_email': True, 'access': True},
{'uid': 'lb3dp', 'role': 'SuperDude', 'send_email': False, 'access': True}]
, data['out'])
self.assertEquals({'uid': 'lb3dp', 'role': 'SuperDude', 'send_email': False, 'access': True}
, data['out2'])
self.assertEquals([{'uid': 'dhf8r', 'role': 'owner', 'send_email': True, 'access': True},
{'uid': 'lb3dp', 'role': 'SuperGal', 'send_email': False, 'access': True}]
, data['out3'])
self.assertEquals({'uid': 'lb3dp', 'role': 'SuperGal', 'send_email': False, 'access': True}
, data['out4'])
self.assertEquals(3, len(data['sponsors']))
@patch('crc.services.protocol_builder.requests.get')
def test_study_sponsors_script_fail(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('sponsors.json')
flask.g.user = UserModel(uid='dhf8r')
app.config['PB_ENABLED'] = True
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("study_sponsors_associate_fail")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
WorkflowService.test_spec("study_sponsors_associate_fail")
processor = WorkflowProcessor(workflow_model)
with self.assertRaises(ApiError):
processor.do_engine_steps()
@patch('crc.services.protocol_builder.requests.get')
def test_study_sponsors_script_primary_user(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('sponsors.json')
flask.g.user = UserModel(uid='dhf8r')
app.config['PB_ENABLED'] = True
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("study_sponsors_associate_switch_user")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
WorkflowService.test_spec("study_sponsors_associate_switch_user")
processor = WorkflowProcessor(workflow_model)
processor.do_engine_steps()
tasks = processor.next_user_tasks()
self.assertEqual(len(tasks),1)
processor.complete_task(tasks[0])
processor.do_engine_steps()
self.assertTrue(processor.bpmn_workflow.is_completed())
@patch('crc.services.protocol_builder.requests.get')
def test_study_sponsors_script_valid_users(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('sponsors.json')
flask.g.user = UserModel(uid='dhf8r')
app.config['PB_ENABLED'] = True
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("study_sponsors_associate_switch_user")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
WorkflowService.test_spec("study_sponsors_associate_switch_user")
processor = WorkflowProcessor(workflow_model)
processor.do_engine_steps()
tasks = processor.next_user_tasks()
self.assertEqual(len(tasks),1)
users = WorkflowService.get_users_assigned_to_task(processor,tasks[0])
self.assertFalse('cah3us' in users)
self.assertFalse('lje5u' in users)
self.assertTrue('lb3dp' in users)
self.assertTrue('dhf8r' in users)
# the above should emulate what is going on when we determine if a user can
# make changes to a study or not.
# in theory all endpoints that need to be limited are calling the
# WorkflowService.get_users_assigned_to_task function to determine
# who is allowed access
@patch('crc.services.protocol_builder.requests.get')
def test_study_sponsors_script_ensure_access(self, mock_get):
mock_get.return_value.ok = True
mock_get.return_value.text = self.protocol_builder_response('sponsors.json')
flask.g.user = UserModel(uid='dhf8r')
app.config['PB_ENABLED'] = True
self.load_example_data()
self.create_reference_document()
study = session.query(StudyModel).first()
workflow_spec_model = self.load_test_spec("study_sponsors_associate_switch_user")
workflow_model = StudyService._create_workflow_model(study, workflow_spec_model)
WorkflowService.test_spec("study_sponsors_associate_switch_user")
processor = WorkflowProcessor(workflow_model)
processor.do_engine_steps()
# change user and make sure we can access the study
flask.g.user = UserModel(uid='lb3dp')
flask.g.token = 'my spiffy token'
app.config['PB_ENABLED'] = False
output = user_studies()
self.assertEqual(output[0]['id'], 0)
self.assertEqual(output[0]['user_uid'], 'dhf8r')
flask.g.user = UserModel(uid='lje5u')
flask.g.token = 'my spiffy token'
app.config['PB_ENABLED'] = False
output = user_studies()
self.assertEqual(len(output),0)