diff --git a/config/default.py b/config/default.py index d2486f86..f31f2889 100644 --- a/config/default.py +++ b/config/default.py @@ -28,6 +28,7 @@ FRONTEND_AUTH_CALLBACK = environ.get('FRONTEND_AUTH_CALLBACK', default="http://l SWAGGER_AUTH_KEY = environ.get('SWAGGER_AUTH_KEY', default="SWAGGER") # %s/%i placeholders expected for uva_id and study_id in various calls. +PB_ENABLED = environ.get('PB_ENABLED', default=True) PB_BASE_URL = environ.get('PB_BASE_URL', default="http://localhost:5001/pb/") PB_USER_STUDIES_URL = environ.get('PB_USER_STUDIES_URL', default=PB_BASE_URL + "user_studies?uva_id=%s") PB_INVESTIGATORS_URL = environ.get('PB_INVESTIGATORS_URL', default=PB_BASE_URL + "investigators?studyid=%i") diff --git a/config/testing.py b/config/testing.py index 823a0178..f481224a 100644 --- a/config/testing.py +++ b/config/testing.py @@ -6,6 +6,7 @@ DEVELOPMENT = True TESTING = True SQLALCHEMY_DATABASE_URI = "postgresql://crc_user:crc_pass@localhost:5432/crc_test" TOKEN_AUTH_SECRET_KEY = "Shhhh!!! This is secret! And better darn well not show up in prod." +PB_ENABLED = False print('### USING TESTING CONFIG: ###') print('SQLALCHEMY_DATABASE_URI = ', SQLALCHEMY_DATABASE_URI) diff --git a/crc/api.yml b/crc/api.yml index 52abbde4..4cd35c7d 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -110,6 +110,19 @@ paths: application/json: schema: $ref: "#/components/schemas/Study" + /study-files: + get: + operationId: crc.api.study.all_studies_and_files + summary: Provides a list of studies with submitted files + tags: + - Studies + responses: + '200': + description: An array of studies, with submitted files, ordered by the last modified date. + content: + application/json: + schema: + $ref: "#/components/schemas/Study" /study/{study_id}: parameters: - name: study_id @@ -156,26 +169,6 @@ paths: application/json: schema: $ref: "#/components/schemas/Study" - /study-update/{study_id}: - post: - operationId: crc.api.study.post_update_study_from_protocol_builder - summary: If the study is up-to-date with Protocol Builder, returns a 304 Not Modified. If out of date, return a 202 Accepted and study state changes to updating. - tags: - - Study Status - parameters: - - name: study_id - in: path - required: true - description: The id of the study that should be checked for updates. - schema: - type: integer - format: int32 - responses: - '304': - description: Study is currently up to date and does not need to be reloaded from Protocol Builder - '202': - description: Request accepted, will preform an update. Study state set to "updating" - /workflow-specification: get: operationId: crc.api.workflow.all_specifications @@ -755,13 +748,6 @@ paths: schema: type: string /render_docx: - parameters: - - name: data - in: query - required: true - description: The json data to use in populating the template - schema: - type: string put: operationId: crc.api.tools.render_docx security: [] # Disable security for this endpoint only. @@ -777,6 +763,9 @@ paths: file: type: string format: binary + data: + type: string + format: json responses: '200': description: Returns the generated document. @@ -802,6 +791,54 @@ paths: type: array items: $ref: "#/components/schemas/Script" + /approval: + parameters: + - name: approver_uid + in: query + required: false + description: Restrict results to a given approver uid, maybe we restrict the use of this at somepoint. + schema: + type: string + get: + operationId: crc.api.approval.get_approvals + summary: Provides a list of workflows approvals + tags: + - Approvals + responses: + '200': + description: An array of approvals + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Approval" + /approval/{approval_id}: + parameters: + - name: approval_id + in: path + required: true + description: The id of the approval in question. + schema: + type: integer + format: int32 + put: + operationId: crc.api.approval.update_approval + summary: Updates an approval with the given parameters + tags: + - Approvals + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Approval' + responses: + '200': + description: Study updated successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/Approval" components: securitySchemes: jwt: @@ -1209,7 +1246,10 @@ components: readOnly: true task: $ref: "#/components/schemas/Task" - - - + Approval: + properties: + id: + type: number + format: integer + example: 5 diff --git a/crc/api/approval.py b/crc/api/approval.py new file mode 100644 index 00000000..134f9a8c --- /dev/null +++ b/crc/api/approval.py @@ -0,0 +1,10 @@ +from crc.models.approval import ApprovalModel, Approval + + +def get_approvals(approver_uid = None): + approval_model = ApprovalModel() + approval = Approval.from_model(approval_model) + return {} + +def update_approval(approval_id): + return {} \ No newline at end of file diff --git a/crc/api/study.py b/crc/api/study.py index c162e15a..d4df4e1a 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -1,20 +1,16 @@ -from typing import List - -from connexion import NoContent from flask import g from sqlalchemy.exc import IntegrityError from crc import session from crc.api.common import ApiError, ApiErrorSchema from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy -from crc.models.study import StudySchema, StudyModel, Study +from crc.models.study import StudySchema, StudyFilesSchema, StudyModel, Study from crc.services.protocol_builder import ProtocolBuilderService from crc.services.study_service import StudyService def add_study(body): - """This should never get called, and is subject to deprication. Studies - should be added through the protocol builder only.""" + """Or any study like object. """ study: Study = StudySchema().load(body) study_model = StudyModel(**study.model_args()) session.add(study_model) @@ -59,30 +55,17 @@ def delete_study(study_id): def all_studies(): - """Returns all the studies associated with the current user. Assures we are - in sync with values read in from the protocol builder. """ - StudyService.synch_all_studies_with_protocol_builder(g.user) + """Returns all the studies associated with the current user. """ + StudyService.synch_with_protocol_builder_if_enabled(g.user) studies = StudyService.get_studies_for_user(g.user) results = StudySchema(many=True).dump(studies) return results -def post_update_study_from_protocol_builder(study_id): - """Update a single study based on data received from - the protocol builder.""" - - db_study = session.query(StudyModel).filter_by(study_id=study_id).all() - pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(g.user.uid) - pb_study = next((pbs for pbs in pb_studies if pbs.STUDYID == study_id), None) - if pb_study: - db_study.update_from_protocol_builder(pb_study) - else: - db_study.inactive = True - db_study.protocol_builder_status = ProtocolBuilderStatus.ABANDONED - - return NoContent, 304 - - - +def all_studies_and_files(): + """Returns all studies with submitted files""" + studies = StudyService.get_studies_with_files() + results = StudyFilesSchema(many=True).dump(studies) + return results diff --git a/crc/api/tools.py b/crc/api/tools.py index ee732b5c..6fb31b71 100644 --- a/crc/api/tools.py +++ b/crc/api/tools.py @@ -25,13 +25,14 @@ def render_markdown(data, template): raise ApiError(code="invalid", message=str(e)) -def render_docx(data): +def render_docx(): """ Provides a quick way to verify that a Jinja docx template will work properly on a given json data structure. Useful for folks that are building these templates. """ try: file = connexion.request.files['file'] + data = connexion.request.form['data'] target_stream = CompleteTemplate().make_template(file, json.loads(data)) return send_file( io.BytesIO(target_stream.read()), diff --git a/crc/models/approval.py b/crc/models/approval.py new file mode 100644 index 00000000..4860bf59 --- /dev/null +++ b/crc/models/approval.py @@ -0,0 +1,77 @@ +import enum + +from marshmallow import INCLUDE + +from crc import db, ma +from crc.models.study import StudyModel +from crc.models.workflow import WorkflowModel + + +class ApprovalStatus(enum.Enum): + WAITING = "WAITING" # no one has done jack. + APPROVED = "APPROVED" # approved by the reviewer + DECLINED = "DECLINED" # rejected by the reviewer + CANCELED = "CANCELED" # The document was replaced with a new version and this review is no longer needed. + + +class ApprovalModel(db.Model): + __tablename__ = 'approval' + id = db.Column(db.Integer, primary_key=True) + study_id = db.Column(db.Integer, db.ForeignKey(StudyModel.id), nullable=False) + study = db.relationship(StudyModel, backref='approval') + workflow_id = db.Column(db.Integer, db.ForeignKey(WorkflowModel.id), nullable=False) + workflow_version = db.Column(db.String) + approver_uid = db.Column(db.String) # Not linked to user model, as they may not have logged in yet. + status = db.Column(db.String) + message = db.Column(db.String) + + +class Approval(object): + + @classmethod + def from_model(cls, model: ApprovalModel): + instance = cls() + + instance.id = model.id + instance.workflow_version = model.workflow_version + instance.approver_uid = model.approver_uid + instance.status = model.status + instance.study_id = model.study_id + if model.study: + instance.title = model.study.title + + +class ApprovalSchema(ma.Schema): + class Meta: + model = Approval + fields = ["id", "workflow_version", "approver_uid", "status", + "study_id", "title"] + unknown = INCLUDE + +# Carlos: Here is the data structure I was trying to imagine. +# If I were to continue down my current traing of thought, I'd create +# another class called just "Approval" that can take an ApprovalModel from the +# database and construct a data structure like this one, that can +# be provided to the API at an /approvals endpoint with GET and PUT +# dat = { "approvals": [ +# {"id": 1, +# "study_id": 20, +# "workflow_id": 454, +# "study_title": "Dan Funk (dhf8r)", # Really it's just the name of the Principal Investigator +# "workflow_version": "21", +# "approver": { # Pulled from ldap +# "uid": "bgb22", +# "display_name": "Billy Bob (bgb22)", +# "title": "E42:He's a hoopy frood", +# "department": "E0:EN-Eng Study of Parallel Universes", +# }, +# "files": [ +# { +# "id": 124, +# "name": "ResearchRestart.docx", +# "content_type": "docx-something-whatever" +# } +# ] +# } +# ... +# ] diff --git a/crc/models/file.py b/crc/models/file.py index 6d83e4b2..942b2a80 100644 --- a/crc/models/file.py +++ b/crc/models/file.py @@ -6,7 +6,7 @@ from marshmallow_sqlalchemy import SQLAlchemyAutoSchema from sqlalchemy import func, Index from sqlalchemy.dialects.postgresql import UUID -from crc import db +from crc import db, ma class FileType(enum.Enum): @@ -139,3 +139,9 @@ class LookupDataSchema(SQLAlchemyAutoSchema): include_relationships = False include_fk = False # Includes foreign keys + +class SimpleFileSchema(ma.Schema): + + class Meta: + model = FileModel + fields = ["name"] diff --git a/crc/models/study.py b/crc/models/study.py index 6716df8e..79186f3f 100644 --- a/crc/models/study.py +++ b/crc/models/study.py @@ -5,6 +5,7 @@ from sqlalchemy import func from crc import db, ma from crc.api.common import ApiErrorSchema +from crc.models.file import FileModel, SimpleFileSchema from crc.models.protocol_builder import ProtocolBuilderStatus, ProtocolBuilderStudy from crc.models.workflow import WorkflowSpecCategoryModel, WorkflowState, WorkflowStatus, WorkflowSpecModel, \ WorkflowModel @@ -39,6 +40,10 @@ class StudyModel(db.Model): if self.on_hold: self.protocol_builder_status = ProtocolBuilderStatus.HOLD + def files(self): + _files = FileModel.query.filter_by(workflow_id=self.workflow[0].id) + return _files + class WorkflowMetadata(object): def __init__(self, id, name, display_name, description, spec_version, category_id, state: WorkflowState, status: WorkflowStatus, @@ -154,3 +159,16 @@ class StudySchema(ma.Schema): def make_study(self, data, **kwargs): """Can load the basic study data for updates to the database, but categories are write only""" return Study(**data) + + +class StudyFilesSchema(ma.Schema): + + # files = fields.List(fields.Nested(SimpleFileSchema), dump_only=True) + files = fields.Method('_files') + + class Meta: + model = Study + additional = ["id", "title", "last_updated", "primary_investigator_id"] + + def _files(self, obj): + return [file.name for file in obj.files()] diff --git a/crc/models/user.py b/crc/models/user.py index 9080f42d..d9ee8f72 100644 --- a/crc/models/user.py +++ b/crc/models/user.py @@ -19,6 +19,8 @@ class UserModel(db.Model): last_name = db.Column(db.String, nullable=True) title = db.Column(db.String, nullable=True) + # Add Department and School + def encode_auth_token(self): """ diff --git a/crc/models/workflow.py b/crc/models/workflow.py index ea072e93..7d690a4b 100644 --- a/crc/models/workflow.py +++ b/crc/models/workflow.py @@ -73,10 +73,12 @@ class WorkflowModel(db.Model): bpmn_workflow_json = db.Column(db.JSON) status = db.Column(db.Enum(WorkflowStatus)) study_id = db.Column(db.Integer, db.ForeignKey('study.id')) + study = db.relationship("StudyModel", backref='workflow') workflow_spec_id = db.Column(db.String, db.ForeignKey('workflow_spec.id')) workflow_spec = db.relationship("WorkflowSpecModel") spec_version = db.Column(db.String) total_tasks = db.Column(db.Integer, default=0) completed_tasks = db.Column(db.Integer, default=0) -# task_history = db.Column(db.ARRAY(db.String), default=[]) # The history stack of user completed tasks. - last_updated = db.Column(db.DateTime) \ No newline at end of file + last_updated = db.Column(db.DateTime) + # todo: Add a version that represents the files associated with this workflow + # version = "32" \ No newline at end of file diff --git a/crc/services/protocol_builder.py b/crc/services/protocol_builder.py index 118d871a..efe4bd72 100644 --- a/crc/services/protocol_builder.py +++ b/crc/services/protocol_builder.py @@ -10,6 +10,7 @@ from crc.models.protocol_builder import ProtocolBuilderStudy, ProtocolBuilderStu class ProtocolBuilderService(object): + ENABLED = app.config['PB_ENABLED'] STUDY_URL = app.config['PB_USER_STUDIES_URL'] INVESTIGATOR_URL = app.config['PB_INVESTIGATORS_URL'] REQUIRED_DOCS_URL = app.config['PB_REQUIRED_DOCS_URL'] @@ -17,6 +18,7 @@ class ProtocolBuilderService(object): @staticmethod def get_studies(user_id) -> {}: + ProtocolBuilderService.__enabled_or_raise() if not isinstance(user_id, str): raise ApiError("invalid_user_id", "This user id is invalid: " + str(user_id)) response = requests.get(ProtocolBuilderService.STUDY_URL % user_id) @@ -30,40 +32,32 @@ class ProtocolBuilderService(object): @staticmethod def get_investigators(study_id) -> {}: - ProtocolBuilderService.check_args(study_id) - response = requests.get(ProtocolBuilderService.INVESTIGATOR_URL % study_id) - if response.ok and response.text: - pb_studies = json.loads(response.text) - return pb_studies - else: - raise ApiError("protocol_builder_error", - "Received an invalid response from the protocol builder (status %s): %s" % - (response.status_code, response.text)) + return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.INVESTIGATOR_URL) @staticmethod def get_required_docs(study_id) -> Optional[List[ProtocolBuilderRequiredDocument]]: - ProtocolBuilderService.check_args(study_id) - response = requests.get(ProtocolBuilderService.REQUIRED_DOCS_URL % study_id) + return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.REQUIRED_DOCS_URL) + + @staticmethod + def get_study_details(study_id) -> {}: + return ProtocolBuilderService.__make_request(study_id, ProtocolBuilderService.STUDY_DETAILS_URL) + + @staticmethod + def __enabled_or_raise(): + if not ProtocolBuilderService.ENABLED: + raise ApiError("protocol_builder_disabled", "The Protocol Builder Service is currently disabled.") + + @staticmethod + def __make_request(study_id, url): + ProtocolBuilderService.__enabled_or_raise() + if not isinstance(study_id, int): + raise ApiError("invalid_study_id", "This study id is invalid: " + str(study_id)) + response = requests.get(url % study_id) if response.ok and response.text: return json.loads(response.text) else: raise ApiError("protocol_builder_error", - "Received an invalid response from the protocol builder (status %s): %s" % - (response.status_code, response.text)) + "Received an invalid response from the protocol builder (status %s): %s when calling " + "url '%s'." % + (response.status_code, response.text, url)) - @staticmethod - def get_study_details(study_id) -> {}: - ProtocolBuilderService.check_args(study_id) - response = requests.get(ProtocolBuilderService.STUDY_DETAILS_URL % study_id) - if response.ok and response.text: - pb_study_details = json.loads(response.text) - return pb_study_details - else: - raise ApiError("protocol_builder_error", - "Received an invalid response from the protocol builder (status %s): %s" % - (response.status_code, response.text)) - - @staticmethod - def check_args(study_id): - if not isinstance(study_id, int): - raise ApiError("invalid_study_id", "This study id is invalid: " + str(study_id)) diff --git a/crc/services/study_service.py b/crc/services/study_service.py index 6d1f4bc5..76586e8a 100644 --- a/crc/services/study_service.py +++ b/crc/services/study_service.py @@ -32,6 +32,12 @@ class StudyService(object): studies.append(StudyService.get_study(study_model.id, study_model)) return studies + @staticmethod + def get_studies_with_files(): + """Returns a list of all studies""" + db_studies = session.query(StudyModel).all() + return db_studies + @staticmethod def get_study(study_id, study_model: StudyModel = None): """Returns a study model that contains all the workflows organized by category. @@ -110,23 +116,29 @@ class StudyService(object): """Returns a list of documents related to the study, and any file information that is available..""" - # Get PB required docs - try: - pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id) - except requests.exceptions.ConnectionError as ce: - app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce)) + # Get PB required docs, if Protocol Builder Service is enabled. + if ProtocolBuilderService.ENABLED: + try: + pb_docs = ProtocolBuilderService.get_required_docs(study_id=study_id) + except requests.exceptions.ConnectionError as ce: + app.logger.error("Failed to connect to the Protocol Builder - %s" % str(ce)) + pb_docs = [] + else: pb_docs = [] - # Loop through all known document types, get the counts for those files, and use pb_docs to mark those required. + # Loop through all known document types, get the counts for those files, + # and use pb_docs to mark those as required. doc_dictionary = FileService.get_reference_data(FileService.DOCUMENT_LIST, 'code', ['id']) documents = {} for code, doc in doc_dictionary.items(): - pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None) - doc['required'] = False - if pb_data: - doc['required'] = True + if ProtocolBuilderService.ENABLED: + pb_data = next((item for item in pb_docs if int(item['AUXDOCID']) == int(doc['id'])), None) + doc['required'] = False + if pb_data: + doc['required'] = True + doc['study_id'] = study_id doc['code'] = code @@ -153,7 +165,6 @@ class StudyService(object): doc['status'] = workflow.status.value documents[code] = doc - return documents @@ -201,9 +212,13 @@ class StudyService(object): @staticmethod - def synch_all_studies_with_protocol_builder(user): + def synch_with_protocol_builder_if_enabled(user): """Assures that the studies we have locally for the given user are in sync with the studies available in protocol builder. """ + + if not ProtocolBuilderService.ENABLED: + return + # Get studies matching this user from Protocol Builder pb_studies: List[ProtocolBuilderStudy] = ProtocolBuilderService.get_studies(user.uid) diff --git a/tests/base_test.py b/tests/base_test.py index 290b1506..990e72cf 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -1,7 +1,9 @@ # Set environment variable to testing before loading. # IMPORTANT - Environment must be loaded before app, models, etc.... -import json import os +os.environ["TESTING"] = "true" + +import json import unittest import urllib.parse import datetime @@ -10,10 +12,6 @@ from crc.models.protocol_builder import ProtocolBuilderStatus from crc.models.study import StudyModel from crc.services.file_service import FileService from crc.services.study_service import StudyService -from crc.services.workflow_processor import WorkflowProcessor - -os.environ["TESTING"] = "true" - from crc.models.file import FileModel, FileDataModel, CONTENT_TYPES from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel from crc.models.user import UserModel @@ -32,6 +30,10 @@ class BaseTest(unittest.TestCase): efficiently when we have a database in place. """ + if not app.config['TESTING']: + raise (Exception("INVALID TEST CONFIGURATION. This is almost always in import order issue." + "The first class to import in each test should be the base_test.py file.")) + auths = {} test_uid = "dhf8r" diff --git a/tests/data/empty_workflow/empty_workflow.bpmn b/tests/data/empty_workflow/empty_workflow.bpmn new file mode 100644 index 00000000..17b8351c --- /dev/null +++ b/tests/data/empty_workflow/empty_workflow.bpmn @@ -0,0 +1,26 @@ + + + + + SequenceFlow_0lvudp8 + + + + SequenceFlow_0lvudp8 + + + + + + + + + + + + + + + + + diff --git a/tests/test_approvals.py b/tests/test_approvals.py new file mode 100644 index 00000000..38000b88 --- /dev/null +++ b/tests/test_approvals.py @@ -0,0 +1,14 @@ +from tests.base_test import BaseTest + + +class TestApprovals(BaseTest): + + def test_list_approvals(self): + rv = self.app.get('/v1.0/approval', headers=self.logged_in_headers()) + self.assert_success(rv) + + def test_update_approval(self): + rv = self.app.put('/v1.0/approval/1', + headers=self.logged_in_headers(), + data={}) + self.assert_success(rv) diff --git a/tests/test_protocol_builder.py b/tests/test_protocol_builder.py index 20ce567e..a386a218 100644 --- a/tests/test_protocol_builder.py +++ b/tests/test_protocol_builder.py @@ -1,7 +1,7 @@ from unittest.mock import patch -from crc.services.protocol_builder import ProtocolBuilderService from tests.base_test import BaseTest +from crc.services.protocol_builder import ProtocolBuilderService class TestProtocolBuilder(BaseTest): @@ -10,6 +10,7 @@ class TestProtocolBuilder(BaseTest): @patch('crc.services.protocol_builder.requests.get') def test_get_studies(self, mock_get): + ProtocolBuilderService.ENABLED = True mock_get.return_value.ok = True mock_get.return_value.text = self.protocol_builder_response('user_studies.json') response = ProtocolBuilderService.get_studies(self.test_uid) @@ -17,6 +18,7 @@ class TestProtocolBuilder(BaseTest): @patch('crc.services.protocol_builder.requests.get') def test_get_investigators(self, mock_get): + ProtocolBuilderService.ENABLED = True mock_get.return_value.ok = True mock_get.return_value.text = self.protocol_builder_response('investigators.json') response = ProtocolBuilderService.get_investigators(self.test_study_id) @@ -28,6 +30,7 @@ class TestProtocolBuilder(BaseTest): @patch('crc.services.protocol_builder.requests.get') def test_get_required_docs(self, mock_get): + ProtocolBuilderService.ENABLED = True mock_get.return_value.ok = True mock_get.return_value.text = self.protocol_builder_response('required_docs.json') response = ProtocolBuilderService.get_required_docs(self.test_study_id) @@ -37,6 +40,7 @@ class TestProtocolBuilder(BaseTest): @patch('crc.services.protocol_builder.requests.get') def test_get_details(self, mock_get): + ProtocolBuilderService.ENABLED = True 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) diff --git a/tests/test_study_api.py b/tests/test_study_api.py index 3d081da9..ce1cbf64 100644 --- a/tests/test_study_api.py +++ b/tests/test_study_api.py @@ -1,4 +1,5 @@ import json +from tests.base_test import BaseTest from datetime import datetime, timezone from unittest.mock import patch @@ -8,7 +9,7 @@ from crc.models.protocol_builder import ProtocolBuilderStatus, \ from crc.models.stats import TaskEventModel from crc.models.study import StudyModel, StudySchema from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecCategoryModel -from tests.base_test import BaseTest +from crc.services.protocol_builder import ProtocolBuilderService class TestStudyApi(BaseTest): @@ -38,24 +39,12 @@ class TestStudyApi(BaseTest): study = session.query(StudyModel).first() self.assertIsNotNone(study) - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies - def test_get_study(self, mock_studies, mock_details, mock_docs, mock_investigators): + def test_get_study(self): """Generic test, but pretty detailed, in that the study should return a categorized list of workflows This starts with out loading the example data, to show that all the bases are covered from ground 0.""" - # Mock Protocol Builder responses - studies_response = self.protocol_builder_response('user_studies.json') - mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response) - details_response = self.protocol_builder_response('study_details.json') - mock_details.return_value = json.loads(details_response) - docs_response = self.protocol_builder_response('required_docs.json') - mock_docs.return_value = json.loads(docs_response) - investigators_response = self.protocol_builder_response('investigators.json') - mock_investigators.return_value = json.loads(investigators_response) - + """NOTE: The protocol builder is not enabled or mocked out. As the master workflow (which is empty), + and the test workflow do not need it, and it is disabled in the configuration.""" new_study = self.add_test_study() new_study = session.query(StudyModel).filter_by(id=new_study["id"]).first() # Add a category @@ -65,7 +54,7 @@ class TestStudyApi(BaseTest): # Create a workflow specification self.create_workflow("random_fact", study=new_study, category_id=new_category.id) # Assure there is a master specification, and it has the lookup files it needs. - spec = self.load_test_spec("top_level_workflow", master_spec=True) + spec = self.load_test_spec("empty_workflow", master_spec=True) self.create_reference_document() api_response = self.app.get('/v1.0/study/%i' % new_study.id, @@ -126,6 +115,9 @@ class TestStudyApi(BaseTest): @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies def test_get_all_studies(self, mock_studies, mock_details, mock_docs, mock_investigators): + # Enable the protocol builder for these tests, as the master_workflow and other workflows + # depend on using the PB for data. + ProtocolBuilderService.ENABLED = True self.load_example_data() s = StudyModel( id=54321, # This matches one of the ids from the study_details_json data. @@ -208,6 +200,7 @@ 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() diff --git a/tests/test_tasks_api.py b/tests/test_tasks_api.py index 7cf3c8a2..9c8f8c52 100644 --- a/tests/test_tasks_api.py +++ b/tests/test_tasks_api.py @@ -8,6 +8,7 @@ from crc.models.api_models import WorkflowApiSchema, MultiInstanceType, TaskSche from crc.models.file import FileModelSchema from crc.models.stats import TaskEventModel from crc.models.workflow import WorkflowStatus +from crc.services.protocol_builder import ProtocolBuilderService from crc.services.workflow_service import WorkflowService from tests.base_test import BaseTest @@ -302,6 +303,9 @@ class TestTasksApi(BaseTest): @patch('crc.services.protocol_builder.requests.get') def test_multi_instance_task(self, mock_get): + # Enable the protocol builder. + ProtocolBuilderService.ENABLED = True + # This depends on getting a list of investigators back from the protocol builder. mock_get.return_value.ok = True mock_get.return_value.text = self.protocol_builder_response('investigators.json') diff --git a/tests/test_tools_api.py b/tests/test_tools_api.py index 19f885ad..48ac65a7 100644 --- a/tests/test_tools_api.py +++ b/tests/test_tools_api.py @@ -1,8 +1,8 @@ import json import os -from crc import app from tests.base_test import BaseTest +from crc import app class TestStudyApi(BaseTest): @@ -22,11 +22,13 @@ class TestStudyApi(BaseTest): {"option": "Address", "selected": False}, {"option": "Phone", "selected": True, "stored": ["Send or Transmit outside of UVA"]}]} with open(filepath, 'rb') as f: - file_data = {'file': (f, 'my_new_file.bpmn')} - rv = self.app.put('/v1.0/render_docx?data=%s' % json.dumps(template_data), + file_data = {'file': (f, 'my_new_file.bpmn'), 'data': json.dumps(template_data)} + rv = self.app.put('/v1.0/render_docx', data=file_data, follow_redirects=True, content_type='multipart/form-data') self.assert_success(rv) + self.assertIsNotNone(rv.data) + self.assertEquals('application/octet-stream', rv.content_type) def test_list_scripts(self): rv = self.app.get('/v1.0/list_scripts') diff --git a/tests/test_workflow_spec_validation_api.py b/tests/test_workflow_spec_validation_api.py index 7cb7a3c7..123c42c8 100644 --- a/tests/test_workflow_spec_validation_api.py +++ b/tests/test_workflow_spec_validation_api.py @@ -20,21 +20,7 @@ class TestWorkflowSpecValidation(BaseTest): json_data = json.loads(rv.get_data(as_text=True)) return ApiErrorSchema(many=True).load(json_data) - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_investigators') # mock_studies - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_required_docs') # mock_docs - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_study_details') # mock_details - @patch('crc.services.protocol_builder.ProtocolBuilderService.get_studies') # mock_studies - def test_successful_validation_of_test_workflows(self, mock_studies, mock_details, mock_docs, mock_investigators): - - # Mock Protocol Builder responses - studies_response = self.protocol_builder_response('user_studies.json') - mock_studies.return_value = ProtocolBuilderStudySchema(many=True).loads(studies_response) - details_response = self.protocol_builder_response('study_details.json') - mock_details.return_value = json.loads(details_response) - docs_response = self.protocol_builder_response('required_docs.json') - mock_docs.return_value = json.loads(docs_response) - investigators_response = self.protocol_builder_response('investigators.json') - mock_investigators.return_value = json.loads(investigators_response) + def test_successful_validation_of_test_workflows(self): self.assertEqual(0, len(self.validate_workflow("parallel_tasks"))) self.assertEqual(0, len(self.validate_workflow("decision_table")))