Merge pull request #190 from sartography/dev

dev to testing
This commit is contained in:
Dan Funk 2020-08-19 08:11:25 -04:00 committed by GitHub
commit 68c97e3d82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 266 additions and 149 deletions

View File

@ -46,8 +46,10 @@ PB_STUDY_DETAILS_URL = environ.get('PB_STUDY_DETAILS_URL', default=PB_BASE_URL +
LDAP_URL = environ.get('LDAP_URL', default="ldap.virginia.edu").strip('/') # No trailing slash or http://
LDAP_TIMEOUT_SEC = int(environ.get('LDAP_TIMEOUT_SEC', default=1))
# Github token
# Github settings
GITHUB_TOKEN = environ.get('GITHUB_TOKEN', None)
GITHUB_REPO = environ.get('GITHUB_REPO', None)
TARGET_BRANCH = environ.get('TARGET_BRANCH', None)
# Email configuration
DEFAULT_SENDER = 'askresearch@virginia.edu'

View File

@ -6,7 +6,7 @@ from sqlalchemy.exc import IntegrityError
from crc import session
from crc.api.common import ApiError, ApiErrorSchema
from crc.models.protocol_builder import ProtocolBuilderStatus
from crc.models.study import Study, 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
@ -23,8 +23,12 @@ def add_study(body):
primary_investigator_id=body['primary_investigator_id'],
last_updated=datetime.now(),
status=StudyStatus.in_progress)
session.add(study_model)
StudyService.add_study_update_event(study_model,
status=StudyStatus.in_progress,
event_type=StudyEventType.user,
user_uid=g.user.uid)
errors = StudyService._add_all_workflow_specs_to_study(study_model)
session.commit()
study = StudyService().get_study(study_model.id)
@ -34,6 +38,7 @@ def add_study(body):
def update_study(study_id, body):
"""Pretty limited, but allows manual modifications to the study status """
if study_id is None:
raise ApiError('unknown_study', 'Please provide a valid Study ID.')
@ -42,7 +47,20 @@ def update_study(study_id, body):
raise ApiError('unknown_study', 'The study "' + study_id + '" is not recognized.')
study: Study = StudyForUpdateSchema().load(body)
study.update_model(study_model)
status = StudyStatus(study.status)
study_model.last_updated = datetime.now()
if study_model.status != status:
study_model.status = status
StudyService.add_study_update_event(study_model, status, StudyEventType.user,
user_uid=UserService.current_user().uid if UserService.has_user() else None,
comment='' if not hasattr(study, 'comment') else study.comment,
)
if status == StudyStatus.open_for_enrollment:
study_model.enrollment_date = study.enrollment_date
session.add(study_model)
session.commit()
# Need to reload the full study to return it to the frontend

View File

@ -91,7 +91,7 @@ def delete_workflow_specification(spec_id):
# Delete all events and workflow models related to this specification
for workflow in session.query(WorkflowModel).filter_by(workflow_spec_id=spec_id):
StudyService.delete_workflow(workflow)
StudyService.delete_workflow(workflow.id)
session.query(WorkflowSpecModel).filter_by(id=spec_id).delete()
session.commit()

View File

@ -61,11 +61,6 @@ class StudyModel(db.Model):
self.irb_status = IrbStatus.incomplete_in_protocol_builder
self.status = StudyStatus.in_progress
if pbs.HSRNUMBER:
self.irb_status = IrbStatus.hsr_assigned
self.status = StudyStatus.open_for_enrollment
if self.on_hold:
self.status = StudyStatus.hold
class StudyEvent(db.Model):
@ -177,28 +172,6 @@ class Study(object):
instance = cls(**args)
return instance
def update_model(self, study_model: StudyModel):
"""As the case for update was very reduced, it's mostly and specifically
updating only the study status and generating a history record
"""
status = StudyStatus(self.status)
study_model.last_updated = datetime.datetime.now()
study_model.status = status
if status == StudyStatus.open_for_enrollment:
study_model.enrollment_date = self.enrollment_date
study_event = StudyEvent(
study=study_model,
status=status,
comment='' if not hasattr(self, 'comment') else self.comment,
event_type=StudyEventType.user,
user_uid=UserService.current_user().uid if UserService.has_user() else None,
)
db.session.add(study_event)
db.session.commit()
def model_args(self):
"""Arguments that can be passed into the Study Model to update it."""
self_dict = self.__dict__.copy()

View File

@ -24,8 +24,19 @@ Examples:
supervisor_info = ldap(supervisor_uid) // Sets the supervisor information to ldap details for the given uid.
"""
def do_task_validate_only(self, task, *args, **kwargs):
return self.set_users_info_in_task(task, args)
def do_task_validate_only(self, task, study_id, workflow_id, *args, **kwargs):
return {
"display_name": "John Smith",
"given_name": "Johnismidges Ego Smithogglesnots",
"email_address": "jes@ogglesnots.org",
"telephone_number": "540-457-0023",
"title": "Prodigious Experilisious Emeritus Eqs.",
"department": "Department of Cheese and Fungus Combustibles",
"affiliation": "Not really",
"sponsor_type": "Department of Fungus",
"uid": "jes42",
"proper_name": "Smith"
}
def do_task(self, task, study_id, workflow_id, *args, **kwargs):
return self.set_users_info_in_task(task, args)

View File

@ -2,7 +2,7 @@ import hashlib
import json
import os
from datetime import datetime
from github import Github, UnknownObjectException
from github import Github, GithubObject, UnknownObjectException
from uuid import UUID
from lxml import etree
@ -336,10 +336,20 @@ class FileService(object):
app.logger.info("Failed to delete file, so archiving it instead. %i, due to %s" % (file_id, str(ie)))
@staticmethod
def update_from_github(file_ids):
def get_repo_branches():
gh_token = app.config['GITHUB_TOKEN']
github_repo = app.config['GITHUB_REPO']
_github = Github(gh_token)
repo = _github.get_user().get_repo('crispy-fiesta')
repo = _github.get_user().get_repo(github_repo)
branches = [branch.name for branch in repo.get_branches()]
return branches
@staticmethod
def update_from_github(file_ids, source_target=GithubObject.NotSet):
gh_token = app.config['GITHUB_TOKEN']
github_repo = app.config['GITHUB_REPO']
_github = Github(gh_token)
repo = _github.get_user().get_repo(github_repo)
for file_id in file_ids:
file_data_model = FileDataModel.query.filter_by(
@ -348,10 +358,9 @@ class FileService(object):
desc(FileDataModel.version)
).first()
try:
repo_file = repo.get_contents(file_data_model.file_model.name)
repo_file = repo.get_contents(file_data_model.file_model.name, ref=source_target)
except UnknownObjectException:
# TODO: Add message indicating file is not in the repo
pass
return {'error': 'Attempted to update from repository but file was not present'}
else:
file_data_model.data = repo_file.decoded_content
session.add(file_data_model)
@ -359,26 +368,29 @@ class FileService(object):
@staticmethod
def publish_to_github(file_ids):
target_branch = app.config['TARGET_BRANCH'] if app.config['TARGET_BRANCH'] else GithubObject.NotSet
gh_token = app.config['GITHUB_TOKEN']
github_repo = app.config['GITHUB_REPO']
_github = Github(gh_token)
repo = _github.get_user().get_repo('crispy-fiesta')
repo = _github.get_user().get_repo(github_repo)
for file_id in file_ids:
file_data_model = FileDataModel.query.filter_by(file_model_id=file_id).first()
try:
repo_file = repo.get_contents(file_data_model.file_model.name)
repo_file = repo.get_contents(file_data_model.file_model.name, ref=target_branch)
except UnknownObjectException:
repo.create_file(
path=file_data_model.file_model.name,
message=f'Creating {file_data_model.file_model.name}',
content=file_data_model.data
content=file_data_model.data,
branch=target_branch
)
return {'created': True}
else:
updated = repo.update_file(
path=repo_file.path,
message=f'Updating {file_data_model.file_model.name}',
content=file_data_model.data,
sha=repo_file.sha
content=file_data_model.data + b'brah-model',
sha=repo_file.sha,
branch=target_branch
)
return {'updated': True}

View File

@ -9,13 +9,15 @@ from ldap3.core.exceptions import LDAPSocketOpenError
from crc import db, session, app
from crc.api.common import ApiError
from crc.models.file import FileModel, FileModelSchema, File
from crc.models.approval import ApprovalFile, ApprovalModel
from crc.models.file import FileDataModel, FileModel, FileModelSchema, File, LookupFileModel, LookupDataModel
from crc.models.ldap import LdapSchema
from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStatus
from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEvent
from crc.models.study import StudyModel, Study, StudyStatus, Category, WorkflowMetadata, StudyEventType, StudyEvent, \
IrbStatus
from crc.models.task_event import TaskEventModel, TaskEvent
from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowModel, WorkflowSpecModel, WorkflowState, \
WorkflowStatus
WorkflowStatus, WorkflowSpecDependencyFile
from crc.services.approval_service import ApprovalService
from crc.services.file_service import FileService
from crc.services.ldap_service import LdapService
@ -60,7 +62,7 @@ class StudyService(object):
study.approvals = ApprovalService.get_approvals_for_study(study.id)
files = FileService.get_files_for_study(study.id)
files = (File.from_models(model, FileService.get_file_data(model.id),
FileService.get_doc_dictionary()) for model in files)
FileService.get_doc_dictionary()) for model in files)
study.files = list(files)
# Calling this line repeatedly is very very slow. It creates the
# master spec and runs it. Don't execute this for Abandoned studies, as
@ -78,21 +80,27 @@ class StudyService(object):
@staticmethod
def delete_study(study_id):
session.query(TaskEventModel).filter_by(study_id=study_id).delete()
# session.query(StudyEvent).filter_by(study_id=study_id).delete()
for workflow in session.query(WorkflowModel).filter_by(study_id=study_id):
StudyService.delete_workflow(workflow)
StudyService.delete_workflow(workflow.id)
study = session.query(StudyModel).filter_by(id=study_id).first()
session.delete(study)
session.commit()
@staticmethod
def delete_workflow(workflow):
for file in session.query(FileModel).filter_by(workflow_id=workflow.id).all():
FileService.delete_file(file.id)
for dep in workflow.dependencies:
session.delete(dep)
def delete_workflow(workflow_id):
workflow = session.query(WorkflowModel).get(workflow_id)
if not workflow:
return
session.query(TaskEventModel).filter_by(workflow_id=workflow.id).delete()
session.query(WorkflowModel).filter_by(id=workflow.id).delete()
session.query(WorkflowSpecDependencyFile).filter_by(workflow_id=workflow_id).delete(synchronize_session='fetch')
session.query(FileModel).filter_by(workflow_id=workflow_id).update({'archived': True, 'workflow_id': None})
# Todo: Remove approvals completely.
session.query(ApprovalFile).filter(ApprovalModel.workflow_id == workflow_id).delete(synchronize_session='fetch')
session.query(ApprovalModel).filter_by(workflow_id=workflow.id).delete()
session.delete(workflow)
session.commit()
@staticmethod
@ -230,9 +238,9 @@ class StudyService(object):
@staticmethod
def get_protocol(study_id):
"""Returns the study protocol, if it has been uploaded."""
file = db.session.query(FileModel)\
.filter_by(study_id=study_id)\
.filter_by(form_field_key='Study_Protocol_Document')\
file = db.session.query(FileModel) \
.filter_by(study_id=study_id) \
.filter_by(form_field_key='Study_Protocol_Document') \
.first()
return FileModelSchema().dump(file)
@ -254,25 +262,54 @@ class StudyService(object):
db_studies = session.query(StudyModel).filter_by(user_uid=user.uid).all()
# Update all studies from the protocol builder, create new studies as needed.
# Futher assures that every active study (that does exist in the protocol builder)
# Further assures that every active study (that does exist in the protocol builder)
# has a reference to every available workflow (though some may not have started yet)
for pb_study in pb_studies:
new_status = None
db_study = next((s for s in db_studies if s.id == pb_study.STUDYID), None)
if not db_study:
db_study = StudyModel(id=pb_study.STUDYID)
db_study.status = None # Force a new sa
new_status = StudyStatus.in_progress
session.add(db_study)
db_studies.append(db_study)
if pb_study.HSRNUMBER:
db_study.irb_status = IrbStatus.hsr_assigned
if db_study.status != StudyStatus.open_for_enrollment:
new_status = StudyStatus.open_for_enrollment
db_study.update_from_protocol_builder(pb_study)
StudyService._add_all_workflow_specs_to_study(db_study)
# If there is a new automatic status change and there isn't a manual change in place, record it.
if new_status and db_study.status != StudyStatus.hold:
db_study.status = new_status
StudyService.add_study_update_event(db_study,
status=new_status,
event_type=StudyEventType.automatic)
# Mark studies as inactive that are no longer in Protocol Builder
for study in db_studies:
pb_study = next((pbs for pbs in pb_studies if pbs.STUDYID == study.id), None)
if not pb_study:
if not pb_study and study.status != StudyStatus.abandoned:
study.status = StudyStatus.abandoned
StudyService.add_study_update_event(study,
status=StudyStatus.abandoned,
event_type=StudyEventType.automatic)
db.session.commit()
@staticmethod
def add_study_update_event(study, status, event_type, user_uid=None, comment=''):
study_event = StudyEvent(study=study,
status=status,
event_type=event_type,
user_uid=user_uid,
comment=comment)
db.session.add(study_event)
db.session.commit()
@staticmethod
def __update_status_of_workflow_meta(workflow_metas, status):
# Update the status on each workflow
@ -318,7 +355,7 @@ class StudyService(object):
return WorkflowProcessor.run_master_spec(master_specs[0], study_model)
@staticmethod
def _add_all_workflow_specs_to_study(study_model:StudyModel):
def _add_all_workflow_specs_to_study(study_model: StudyModel):
existing_models = session.query(WorkflowModel).filter(WorkflowModel.study == study_model).all()
existing_specs = list(m.workflow_spec_id for m in existing_models)
new_specs = session.query(WorkflowSpecModel). \

View File

@ -55,7 +55,7 @@ class CustomBpmnScriptEngine(BpmnScriptEngine):
except NameError as e:
raise ApiError('name_error',
f'something you are referencing does not exist:'
f' {script}, {e.name}')
f' {script}, {e}')
# else:
# self.run_predefined_script(task, script[2:], data) # strip off the first two characters.

View File

View File

@ -7,16 +7,17 @@
<bpmn:endEvent id="Event_0izrcj4">
<bpmn:incoming>Flow_11e7jgz</bpmn:incoming>
</bpmn:endEvent>
<bpmn:scriptTask id="Activity_0s5v97n" name="Ldap Replace">
<bpmn:scriptTask id="Activity_0s5v97n" name="Ldap Script">
<bpmn:incoming>Flow_08n2npe</bpmn:incoming>
<bpmn:outgoing>Flow_1xlrgne</bpmn:outgoing>
<bpmn:script>Supervisor = ldap(Supervisor)
Investigator = ldap(Investigator)</bpmn:script>
Investigator = ldap(Investigator)
me = ldap()</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1synsig" sourceRef="StartEvent_1" targetRef="Activity_1l9vih3" />
<bpmn:sequenceFlow id="Flow_1xlrgne" sourceRef="Activity_0s5v97n" targetRef="Activity_0f78ek5" />
<bpmn:sequenceFlow id="Flow_08n2npe" sourceRef="Activity_1l9vih3" targetRef="Activity_0s5v97n" />
<bpmn:userTask id="Activity_1l9vih3" name="Set UIDs">
<bpmn:userTask id="Activity_1l9vih3" name="Set UIDs" camunda:formKey="SetUids">
<bpmn:extensionElements>
<camunda:formData>
<camunda:formField id="Supervisor" label="Approver" type="string" />

0
tests/emails/__init__.py Normal file
View File

0
tests/files/__init__.py Normal file
View File

View File

@ -14,9 +14,9 @@ class FakeGithubCreates(Mock):
class FakeUser(Mock):
def get_repo(var, name):
class FakeRepo(Mock):
def get_contents(var, filename):
def get_contents(var, filename, ref):
raise UnknownObjectException(status='Failure', data='Failed data')
def update_file(var, path, message, content, sha):
def update_file(var, path, message, content, sha, branch):
pass
return FakeRepo()
return FakeUser()
@ -27,14 +27,22 @@ class FakeGithub(Mock):
class FakeUser(Mock):
def get_repo(var, name):
class FakeRepo(Mock):
def get_contents(var, filename):
def get_contents(var, filename, ref):
fake_file = Mock()
fake_file.decoded_content = b'Some bytes'
fake_file.path = '/el/path/'
fake_file.data = 'Serious data'
fake_file.sha = 'Sha'
return fake_file
def update_file(var, path, message, content, sha):
def get_branches(var):
branch1 = Mock()
branch1.name = 'branch1'
branch2 = Mock()
branch2.name = 'branch2'
master = Mock()
master.name = 'master'
return [branch1, branch2, master]
def update_file(var, path, message, content, sha, branch):
pass
return FakeRepo()
return FakeUser()
@ -198,3 +206,11 @@ class TestFileService(BaseTest):
result = FileService.publish_to_github([file_model.id])
self.assertEqual(result['updated'], True)
@patch('crc.services.file_service.Github')
def test_get_repo_branches(self, mock_github):
mock_github.return_value = FakeGithub()
branches = FileService.get_repo_branches()
self.assertIsInstance(branches, list)

0
tests/ldap/__init__.py Normal file
View File

View File

@ -3,6 +3,7 @@ from flask import g
from tests.base_test import BaseTest
from crc import db
from crc.services.workflow_service import WorkflowService
from crc.models.user import UserModel
from crc.services.workflow_processor import WorkflowProcessor
from crc.scripts.ldap import Ldap
@ -62,7 +63,7 @@ class TestLdapLookupScript(BaseTest):
def test_bpmn_task_receives_user_details(self):
workflow = self.create_workflow('ldap_replace')
workflow = self.create_workflow('ldap_script')
task_data = {
'Supervisor': 'dhf8r',
@ -84,3 +85,8 @@ class TestLdapLookupScript(BaseTest):
self.assertEqual(task.data['Supervisor']['sponsor_type'], 'Staff')
self.assertEqual(task.data['Supervisor']['uid'], 'dhf8r')
self.assertEqual(task.data['Supervisor']['proper_name'], 'Dan Funk - (dhf8r)')
def test_ldap_validation(self):
workflow = self.create_workflow('ldap_script')
# This should not raise an error.
WorkflowService.test_spec('ldap_script', required_only=False)

0
tests/study/__init__.py Normal file
View File

View File

@ -10,6 +10,7 @@ from crc import session, app
from crc.models.protocol_builder import ProtocolBuilderStatus, \
ProtocolBuilderStudySchema
from crc.models.approval import ApprovalStatus
from crc.models.file import FileModel
from crc.models.task_event import TaskEventModel
from crc.models.study import StudyEvent, StudyModel, StudySchema, StudyStatus, StudyEventType
from crc.models.workflow import WorkflowSpecModel, WorkflowModel
@ -132,13 +133,20 @@ class TestStudyApi(BaseTest):
error_count = len(study["errors"])
self.assertEqual(workflow_spec_count, workflow_count + error_count)
study_event = session.query(StudyEvent).first()
self.assertIsNotNone(study_event)
self.assertEqual(study_event.status, StudyStatus.in_progress)
self.assertEqual(study_event.event_type, StudyEventType.user)
self.assertFalse(study_event.comment)
self.assertEqual(study_event.user_uid, self.test_uid)
def test_update_study(self):
self.load_example_data()
update_comment = 'Updating the study'
study: StudyModel = session.query(StudyModel).first()
study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility"
study_schema = StudySchema().dump(study)
study_schema['status'] = StudyStatus.in_progress.value
study_schema['status'] = StudyStatus.hold.value
study_schema['comment'] = update_comment
rv = self.app.put('/v1.0/study/%i' % study.id,
content_type="application/json",
@ -152,7 +160,8 @@ class TestStudyApi(BaseTest):
# Making sure events history is being properly recorded
study_event = session.query(StudyEvent).first()
self.assertIsNotNone(study_event)
self.assertEqual(study_event.status, StudyStatus.in_progress)
self.assertEqual(study_event.status, StudyStatus.hold)
self.assertEqual(study_event.event_type, StudyEventType.user)
self.assertEqual(study_event.comment, update_comment)
self.assertEqual(study_event.user_uid, self.test_uid)
@ -190,7 +199,6 @@ class TestStudyApi(BaseTest):
self.assert_success(api_response)
json_data = json.loads(api_response.get_data(as_text=True))
num_incomplete = 0
num_abandoned = 0
num_in_progress = 0
num_open = 0
@ -209,9 +217,18 @@ class TestStudyApi(BaseTest):
self.assertEqual(num_abandoned, 1)
self.assertEqual(num_open, 1)
self.assertEqual(num_in_progress, 2)
self.assertEqual(num_incomplete, 0)
self.assertEqual(len(json_data), num_db_studies_after)
self.assertEqual(num_open + num_in_progress + num_incomplete + num_abandoned, num_db_studies_after)
self.assertEqual(num_open + num_in_progress + num_abandoned, num_db_studies_after)
# Automatic events check
in_progress_events = session.query(StudyEvent).filter_by(status=StudyStatus.in_progress)
self.assertEqual(in_progress_events.count(), 1) # 1 study is in progress
abandoned_events = session.query(StudyEvent).filter_by(status=StudyStatus.abandoned)
self.assertEqual(abandoned_events.count(), 1) # 1 study has been abandoned
open_for_enrollment_events = session.query(StudyEvent).filter_by(status=StudyStatus.open_for_enrollment)
self.assertEqual(open_for_enrollment_events.count(), 1) # 1 study was moved to open for enrollment
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs
@ -244,13 +261,36 @@ class TestStudyApi(BaseTest):
self.assertEqual(study.sponsor, json_data['sponsor'])
self.assertEqual(study.ind_number, json_data['ind_number'])
def test_delete_study(self):
self.load_example_data()
study = session.query(StudyModel).first()
rv = self.app.delete('/v1.0/study/%i' % study.id, headers=self.logged_in_headers())
self.assert_success(rv)
def test_delete_workflow(self):
self.load_example_data()
workflow = session.query(WorkflowModel).first()
FileService.add_workflow_file(workflow_id=workflow.id,
name="anything.png", content_type="text",
binary_data=b'5678', irb_doc_code="UVACompl_PRCAppr" )
workflow_files = session.query(FileModel).filter_by(workflow_id=workflow.id)
self.assertEqual(workflow_files.count(), 1)
workflow_files_ids = [file.id for file in workflow_files]
rv = self.app.delete(f'/v1.0/workflow/{workflow.id}', headers=self.logged_in_headers())
self.assert_success(rv)
# No files should have the deleted workflow id anymore
workflow_files = session.query(FileModel).filter_by(workflow_id=workflow.id)
self.assertEqual(workflow_files.count(), 0)
# Finally, let's confirm the file was archived
workflow_files = session.query(FileModel).filter(FileModel.id.in_(workflow_files_ids))
for file in workflow_files:
self.assertTrue(file.archived)
self.assertIsNone(file.workflow_id)
def test_delete_study_with_workflow_and_status(self):
self.load_example_data()
workflow = session.query(WorkflowModel).first()

View File

@ -33,7 +33,7 @@ class TestTasksApi(BaseTest):
except ApiError as ae:
error = ae
self.assertIsNotNone(error, "An error should be raised.")
self.assertEquals("invalid_role", error.code)
self.assertEqual("invalid_role", error.code)
def test_raise_error_if_user_does_not_have_the_correct_role(self):
submitter = self.create_user(uid='lje5u')
@ -62,8 +62,8 @@ class TestTasksApi(BaseTest):
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals("supervisor", nav[1]['lane'])
self.assertEqual(5, len(nav))
self.assertEqual("supervisor", nav[1]['lane'])
def test_get_outstanding_tasks_awaiting_current_user(self):
submitter = self.create_user(uid='lje5u')
@ -81,7 +81,7 @@ class TestTasksApi(BaseTest):
task_logs = db.session.query(TaskEventModel). \
filter(TaskEventModel.user_uid == supervisor.uid). \
filter(TaskEventModel.action == WorkflowService.TASK_ACTION_ASSIGNMENT).all()
self.assertEquals(1, len(task_logs))
self.assertEqual(1, len(task_logs))
# A call to the /task endpoint as the supervisor user should return a list of
# tasks that need their attention.
@ -91,10 +91,10 @@ class TestTasksApi(BaseTest):
self.assert_success(rv)
json_data = json.loads(rv.get_data(as_text=True))
tasks = TaskEventSchema(many=True).load(json_data)
self.assertEquals(1, len(tasks))
self.assertEquals(workflow.id, tasks[0]['workflow']['id'])
self.assertEquals(workflow.study.id, tasks[0]['study']['id'])
self.assertEquals("Test Workflows", tasks[0]['workflow']['category_display_name'])
self.assertEqual(1, len(tasks))
self.assertEqual(workflow.id, tasks[0]['workflow']['id'])
self.assertEqual(workflow.study.id, tasks[0]['study']['id'])
self.assertEqual("Test Workflows", tasks[0]['workflow']['category_display_name'])
# Assure we can say something sensible like:
# You have a task called "Approval" to be completed in the "Supervisor Approval" workflow
@ -103,9 +103,9 @@ class TestTasksApi(BaseTest):
# Display name isn't set in the tests, so just checking name, but the full workflow details are included.
# I didn't delve into the full user details to keep things decoupled from ldap, so you just get the
# uid back, but could query to get the full entry.
self.assertEquals("roles", tasks[0]['workflow']['name'])
self.assertEquals("Beer consumption in the bipedal software engineer", tasks[0]['study']['title'])
self.assertEquals("lje5u", tasks[0]['study']['user_uid'])
self.assertEqual("roles", tasks[0]['workflow']['name'])
self.assertEqual("Beer consumption in the bipedal software engineer", tasks[0]['study']['title'])
self.assertEqual("lje5u", tasks[0]['study']['user_uid'])
# Completing the next step of the workflow will close the task.
data['approval'] = True
@ -121,39 +121,39 @@ class TestTasksApi(BaseTest):
# Navigation as Submitter with ready task.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('READY', nav[0]['state']) # First item is ready, no progress yet.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('NOOP', nav[3]['state']) # Approved Path, has no operation
self.assertEquals('NOOP', nav[4]['state']) # Rejected Path, has no operation.
self.assertEquals('READY', workflow_api.next_task.state)
self.assertEqual(5, len(nav))
self.assertEqual('READY', nav[0]['state']) # First item is ready, no progress yet.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEqual('NOOP', nav[3]['state']) # Approved Path, has no operation
self.assertEqual('NOOP', nav[4]['state']) # Rejected Path, has no operation.
self.assertEqual('READY', workflow_api.next_task.state)
# Navigation as Submitter after handoff to supervisor
data = workflow_api.next_task.data
data['supervisor'] = supervisor.uid
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals('COMPLETED', nav[0]['state']) # First item is ready, no progress yet.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('LOCKED', workflow_api.next_task.state)
self.assertEqual('COMPLETED', nav[0]['state']) # First item is ready, no progress yet.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEqual('LOCKED', workflow_api.next_task.state)
# In the event the next task is locked, we should say something sensible here.
# It is possible to look at the role of the task, and say The next task "TASK TITLE" will
# be handled by 'dhf8r', who is full-filling the role of supervisor. the Task Data
# is guaranteed to have a supervisor attribute in it that will contain the users uid, which
# could be looked up through an ldap service.
self.assertEquals('supervisor', workflow_api.next_task.lane)
self.assertEqual('supervisor', workflow_api.next_task.lane)
# Navigation as Supervisor
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('READY', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
self.assertEqual(5, len(nav))
self.assertEqual('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEqual('READY', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway, and belongs to no one, and is locked.
self.assertEqual('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
data["approval"] = False
@ -161,47 +161,47 @@ class TestTasksApi(BaseTest):
# Navigation as Supervisor, after completing task.
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('COMPLETED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('COMPLETED', nav[2]['state']) # third item is a gateway, and is now complete.
self.assertEquals('LOCKED', workflow_api.next_task.state)
self.assertEqual(5, len(nav))
self.assertEqual('LOCKED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEqual('COMPLETED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('COMPLETED', nav[2]['state']) # third item is a gateway, and is now complete.
self.assertEqual('LOCKED', workflow_api.next_task.state)
# Navigation as Submitter, coming back in to a rejected workflow to view the rejection message.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('COMPLETED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
self.assertEqual(5, len(nav))
self.assertEqual('COMPLETED', nav[0]['state']) # First item belongs to the submitter, and is locked.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEqual('READY', workflow_api.next_task.state)
# Navigation as Submitter, re-completing the original request a second time, and sending it for review.
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
nav = workflow_api.navigation
self.assertEquals(5, len(nav))
self.assertEquals('READY', nav[0]['state'])
self.assertEquals('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEquals('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEquals('READY', workflow_api.next_task.state)
self.assertEqual(5, len(nav))
self.assertEqual('READY', nav[0]['state']) # When you loop back the task is again in the ready state.
self.assertEqual('LOCKED', nav[1]['state']) # Second item is locked, it is the review and doesn't belong to this user.
self.assertEqual('LOCKED', nav[2]['state']) # third item is a gateway belonging to the supervisor, and is locked.
self.assertEqual('READY', workflow_api.next_task.state)
data["favorite_color"] = "blue"
data["quest"] = "to seek the holy grail"
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
self.assertEquals('LOCKED', workflow_api.next_task.state)
self.assertEqual('LOCKED', workflow_api.next_task.state)
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
self.assertEquals('READY', workflow_api.next_task.state)
self.assertEqual('READY', workflow_api.next_task.state)
data = workflow_api.next_task.data
data["approval"] = True
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
self.assertEquals('LOCKED', workflow_api.next_task.state)
self.assertEqual('LOCKED', workflow_api.next_task.state)
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
self.assertEquals('COMPLETED', workflow_api.next_task.state)
self.assertEquals('EndEvent', workflow_api.next_task.type) # Are are at the end.
self.assertEquals(WorkflowStatus.complete, workflow_api.status)
self.assertEqual('COMPLETED', workflow_api.next_task.state)
self.assertEqual('EndEvent', workflow_api.next_task.type) # Are are at the end.
self.assertEqual(WorkflowStatus.complete, workflow_api.status)
def get_assignment_task_events(self, uid):
return db.session.query(TaskEventModel). \
@ -222,31 +222,31 @@ class TestTasksApi(BaseTest):
# At this point there should be a task_log with an action of ASSIGNMENT on it for
# the supervisor.
self.assertEquals(1, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(1, len(self.get_assignment_task_events(supervisor.uid)))
# Resetting the workflow at this point should clear the event log.
workflow_api = self.get_workflow_api(workflow, hard_reset=True, user_uid=submitter.uid)
self.assertEquals(0, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(supervisor.uid)))
# Re-complete first task, and awaiting tasks should shift to 0 for for submitter, and 1 for supervisor
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
self.assertEquals(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEquals(1, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEqual(1, len(self.get_assignment_task_events(supervisor.uid)))
# Complete the supervisor task with rejected approval, and the assignments should switch.
workflow_api = self.get_workflow_api(workflow, user_uid=supervisor.uid)
data = workflow_api.next_task.data
data["approval"] = False
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
self.assertEquals(1, len(self.get_assignment_task_events(submitter.uid)))
self.assertEquals(0, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(1, len(self.get_assignment_task_events(submitter.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(supervisor.uid)))
# Mark the return form review page as complete, and then recomplete the form, and assignments switch yet again.
workflow_api = self.get_workflow_api(workflow, user_uid=submitter.uid)
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
self.assertEquals(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEquals(1, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEqual(1, len(self.get_assignment_task_events(supervisor.uid)))
# Complete the supervisor task, accepting the approval, and the workflow is completed.
# When it is all done, there should be no outstanding assignments.
@ -254,14 +254,14 @@ class TestTasksApi(BaseTest):
data = workflow_api.next_task.data
data["approval"] = True
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=supervisor.uid)
self.assertEquals(WorkflowStatus.complete, workflow_api.status)
self.assertEquals('EndEvent', workflow_api.next_task.type) # Are are at the end.
self.assertEquals(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEquals(0, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(WorkflowStatus.complete, workflow_api.status)
self.assertEqual('EndEvent', workflow_api.next_task.type) # Are are at the end.
self.assertEqual(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(supervisor.uid)))
# Sending any subsequent complete forms does not result in a new task event
with self.assertRaises(AssertionError) as _api_error:
workflow_api = self.complete_form(workflow, workflow_api.next_task, data, user_uid=submitter.uid)
self.assertEquals(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEquals(0, len(self.get_assignment_task_events(supervisor.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(submitter.uid)))
self.assertEqual(0, len(self.get_assignment_task_events(supervisor.uid)))

View File

View File

@ -388,5 +388,5 @@ class TestWorkflowProcessor(BaseTest):
self._populate_form_with_random_data(task)
processor.complete_task(task)
supervisor_task = processor.next_user_tasks()[0]
self.assertEquals("supervisor", supervisor_task.task_spec.lane)
self.assertEqual("supervisor", supervisor_task.task_spec.lane)

View File

@ -158,7 +158,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
self.assertEqual(3, len(next_user_tasks))
# There should be six tasks in the navigation: start event, the script task, end event, and three tasks
# for the three executions of hte multi-instance.
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
# We can complete the tasks out of order.
task = next_user_tasks[2]
@ -171,12 +171,12 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
# Assure navigation picks up the label of the current element variable.
nav = WorkflowService.processor_to_workflow_api(processor, task).navigation
self.assertEquals("Primary Investigator", nav[2].title)
self.assertEqual("Primary Investigator", nav[2].title)
task.update_data({"investigator": {"email": "dhf8r@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
task = next_user_tasks[0]
api_task = WorkflowService.spiff_task_to_api_task(task)
@ -184,7 +184,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.update_data({"investigator":{"email":"asd3v@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
task = next_user_tasks[1]
api_task = WorkflowService.spiff_task_to_api_task(task)
@ -192,7 +192,7 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.update_data({"investigator":{"email":"asdf32@virginia.edu"}})
processor.complete_task(task)
processor.do_engine_steps()
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))
# Completing the tasks out of order, still provides the correct information.
expected = self.mock_investigator_response
@ -203,4 +203,4 @@ class TestWorkflowProcessorMultiInstance(BaseTest):
task.data['StudyInfo']['investigators'])
self.assertEqual(WorkflowStatus.complete, processor.get_status())
self.assertEquals(6, len(processor.bpmn_workflow.get_nav_list()))
self.assertEqual(6, len(processor.bpmn_workflow.get_nav_list()))

View File

@ -31,6 +31,7 @@ class TestWorkflowSpecValidation(BaseTest):
self.assertEqual(0, len(self.validate_workflow("random_fact")))
self.assertEqual(0, len(self.validate_workflow("study_details")))
self.assertEqual(0, len(self.validate_workflow("two_forms")))
self.assertEqual(0, len(self.validate_workflow("ldap_lookup")))
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies
@patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs