From e75da611cab3913b0221a6c4fe607676e59fc572 Mon Sep 17 00:00:00 2001 From: Aaron Louie Date: Fri, 3 Jan 2020 11:44:24 -0500 Subject: [PATCH] Adds endpoints for creating and updating a Study. --- Pipfile.lock | 17 +++++------ crc/api.yml | 54 +++++++++++++++++++++++++++++++++ crc/api/file.py | 12 ++++---- crc/api/study.py | 36 ++++++++++++++++++---- crc/api/workflow.py | 6 ++-- crc/models/file.py | 5 ++-- crc/models/study.py | 4 +-- crc/models/workflow.py | 6 ++-- tests/test_api.py | 66 +++++++++++++++++++++++++++++++++-------- tests/test_api_files.py | 10 +++---- 10 files changed, 168 insertions(+), 48 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index f8841b26..b149aafa 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -544,10 +544,10 @@ }, "waitress": { "hashes": [ - "sha256:3776cbb9abebefb51e5b654f8728928aa17b656d9f6943c58ce8f48e87cef4e3", - "sha256:f4118cbce75985fd60aeb4f0d781aba8dc7ae28c18e50753e913d7a7dee76b62" + "sha256:67a60a376f0eb335ed88967c42b73983a58d66a2a72eb9009a42725f7453b142", + "sha256:cbf1c62fc41393a6f27cb78483f8f6e252630a3598984668244b7bf4e35856f1" ], - "version": "==1.4.1" + "version": "==1.4.2" }, "webob": { "hashes": [ @@ -626,10 +626,10 @@ }, "py": { "hashes": [ - "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", - "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa", + "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0" ], - "version": "==1.8.0" + "version": "==1.8.1" }, "pyparsing": { "hashes": [ @@ -655,10 +655,9 @@ }, "wcwidth": { "hashes": [ - "sha256:3df37372226d6e63e1b1e1eda15c594bca98a22d33a23832a90998faa96bc65e", - "sha256:f4ebe71925af7b40a864553f761ed559b43544f8f71746c2d756c7fe788ade7c" + "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603" ], - "version": "==0.1.7" + "version": "==0.1.8" }, "zipp": { "hashes": [ diff --git a/crc/api.yml b/crc/api.yml index 8fa367f0..55c5bf81 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -27,6 +27,29 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + post: + operationId: crc.api.study.add_study + summary: Creates a new study with the given parameters. + tags: + - Studies + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Study' + responses: + '200': + description: Study created successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/Study" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /study/{study_id}: get: operationId: crc.api.study.get_study @@ -54,6 +77,37 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + post: + operationId: crc.api.study.update_study + summary: Updates an existing study with the given parameters. + tags: + - Studies + parameters: + - name: study_id + in: path + required: true + description: The id of the study for which workflows should be returned. + schema: + type: integer + format: int32 + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Study' + responses: + '200': + description: Study updated successfully. + content: + application/json: + schema: + $ref: "#/components/schemas/Study" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /study-update/{study_id}: post: operationId: crc.api.study.post_update_study_from_protocol_builder diff --git a/crc/api/file.py b/crc/api/file.py index 93b0ebdc..39c34b8d 100644 --- a/crc/api/file.py +++ b/crc/api/file.py @@ -7,7 +7,7 @@ from flask import send_file from crc import db from crc.api.common import ApiErrorSchema, ApiError -from crc.models.file import FileSchema, FileModel, FileDataModel, FileType +from crc.models.file import FileModelSchema, FileModel, FileDataModel, FileType def update_file_from_request(file_model): @@ -39,12 +39,12 @@ def update_file_from_request(file_model): db.session.add(file_data_model) db.session.add(file_model) db.session.commit() - db.session.flush() # Assure the id is set on the model before returning it. + db.session.flush() # Assure the id is set on the model before returning it. def get_files(spec_id): if spec_id: - schema = FileSchema(many=True) + schema = FileModelSchema(many=True) return schema.dump(db.session.query(FileModel).filter_by(workflow_spec_id=spec_id).all()) else: error = ApiError('no_files_found', 'Please provide some parameters so we can find the files you need.') @@ -57,7 +57,7 @@ def add_file(): 'Please specify a workflow_spec_id for this file in the form')), 404 file_model = FileModel(version=0, workflow_spec_id=connexion.request.form['workflow_spec_id']) update_file_from_request(file_model) - return FileSchema().dump(file_model) + return FileModelSchema().dump(file_model) def update_file(file_id): @@ -65,7 +65,7 @@ def update_file(file_id): if file_model is None: return ApiErrorSchema().dump(ApiError('no_such_file', 'The file id you provided does not exist')), 404 update_file_from_request(file_model) - return FileSchema().dump(file_model) + return FileModelSchema().dump(file_model) def get_file(file_id): @@ -84,7 +84,7 @@ def get_file_info(file_id): file_model = db.session.query(FileModel).filter_by(id=file_id).with_for_update().first() if file_model is None: return ApiErrorSchema().dump(ApiError('no_such_file', 'The file id you provided does not exist')), 404 - return FileSchema().dump(file_model) + return FileModelSchema().dump(file_model) def delete_file(file_id): diff --git a/crc/api/study.py b/crc/api/study.py index d5349456..9900a820 100644 --- a/crc/api/study.py +++ b/crc/api/study.py @@ -2,20 +2,44 @@ from connexion import NoContent from crc import db from crc.api.common import ApiError, ApiErrorSchema -from crc.models.study import StudySchema, StudyModel -from crc.models.workflow import WorkflowModel, WorkflowSchema, WorkflowSpecModel +from crc.models.study import StudyModelSchema, StudyModel +from crc.models.workflow import WorkflowModel, WorkflowModelSchema, WorkflowSpecModel from crc.workflow_processor import WorkflowProcessor def all_studies(): # todo: Limit returned studies to a user - schema = StudySchema(many=True) + schema = StudyModelSchema(many=True) return schema.dump(db.session.query(StudyModel).all()) +def add_study(body): + study = StudyModelSchema().load(body, session=db.session) + db.session.add(study) + db.session.commit() + return StudyModelSchema().dump(study) + + +def update_study(study_id, body): + if study_id is None: + error = ApiError('unknown_study', 'Please provide a valid Study ID.') + return ApiErrorSchema.dump(error), 404 + + study = db.session.query(StudyModel).filter_by(id=study_id).first() + + if study is None: + error = ApiError('unknown_study', 'The study "' + study_id + '" is not recognized.') + return ApiErrorSchema.dump(error), 404 + + study = StudyModelSchema().load(body, session=db.session) + db.session.add(study) + db.session.commit() + return StudyModelSchema().dump(study) + + def get_study(study_id): study = db.session.query(StudyModel).filter_by(id=study_id).first() - schema = StudySchema() + schema = StudyModelSchema() if study is None: return NoContent, 404 return schema.dump(study) @@ -28,7 +52,7 @@ def post_update_study_from_protocol_builder(study_id): def get_study_workflows(study_id): workflows = db.session.query(WorkflowModel).filter_by(study_id=study_id).all() - schema = WorkflowSchema(many=True) + schema = WorkflowModelSchema(many=True) return schema.dump(workflows) @@ -45,4 +69,4 @@ def add_workflow_to_study(study_id, body): workflow_spec_id=workflow_spec_model.id) db.session.add(workflow) db.session.commit() - return WorkflowSchema().dump(workflow) + return WorkflowModelSchema().dump(workflow) diff --git a/crc/api/workflow.py b/crc/api/workflow.py index 12b8dc4b..f24d440e 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -1,16 +1,16 @@ from crc import db -from crc.models.workflow import WorkflowModel, WorkflowSchema, WorkflowSpecSchema, WorkflowSpecModel, \ +from crc.models.workflow import WorkflowModel, WorkflowModelSchema, WorkflowSpecModelSchema, WorkflowSpecModel, \ Task, TaskSchema from crc.workflow_processor import WorkflowProcessor def all_specifications(): - schema = WorkflowSpecSchema(many=True) + schema = WorkflowSpecModelSchema(many=True) return schema.dump(db.session.query(WorkflowSpecModel).all()) def get_workflow(workflow_id): - schema = WorkflowSchema() + schema = WorkflowModelSchema() workflow = db.session.query(WorkflowModel).filter_by(id=workflow_id).first() return schema.dump(workflow) diff --git a/crc/models/file.py b/crc/models/file.py index 773b61a2..a977e9df 100644 --- a/crc/models/file.py +++ b/crc/models/file.py @@ -1,7 +1,7 @@ import enum -from flask_marshmallow.sqla import ModelSchema from marshmallow_enum import EnumField +from marshmallow_sqlalchemy import ModelSchema from sqlalchemy import func from crc import db @@ -20,6 +20,7 @@ class FileDataModel(db.Model): file_model_id = db.Column(db.Integer, db.ForeignKey('file.id')) file_model = db.relationship("FileModel") + class FileModel(db.Model): __tablename__ = 'file' id = db.Column(db.Integer, primary_key=True) @@ -32,7 +33,7 @@ class FileModel(db.Model): workflow_spec_id = db.Column(db.Integer, db.ForeignKey('workflow_spec.id')) -class FileSchema(ModelSchema): +class FileModelSchema(ModelSchema): class Meta: model = FileModel type = EnumField(FileType) diff --git a/crc/models/study.py b/crc/models/study.py index d204d7b9..1045a876 100644 --- a/crc/models/study.py +++ b/crc/models/study.py @@ -25,8 +25,8 @@ class StudyModel(db.Model): ind_number = db.Column(db.String) -class StudySchema(ModelSchema): +class StudyModelSchema(ModelSchema): class Meta: model = StudyModel - protocol_builder_status = EnumField(ProtocolBuilderStatus) \ No newline at end of file + protocol_builder_status = EnumField(ProtocolBuilderStatus) diff --git a/crc/models/workflow.py b/crc/models/workflow.py index 8c85c256..3197177c 100644 --- a/crc/models/workflow.py +++ b/crc/models/workflow.py @@ -14,10 +14,11 @@ class WorkflowSpecModel(db.Model): description = db.Column(db.Text) -class WorkflowSpecSchema(ModelSchema): +class WorkflowSpecModelSchema(ModelSchema): class Meta: model = WorkflowSpecModel + class WorkflowStatus(enum.Enum): new = "new" user_input_required = "user_input_required" @@ -34,7 +35,7 @@ class WorkflowModel(db.Model): workflow_spec_id = db.Column(db.Integer, db.ForeignKey('workflow_spec.id')) -class WorkflowSchema(ModelSchema): +class WorkflowModelSchema(ModelSchema): class Meta: model = WorkflowModel @@ -100,6 +101,7 @@ class FormSchema(ma.Schema): class TaskSchema(ma.Schema): class Meta: fields = ["id", "name", "title", "type", "state", "form", "documentation"] + documentation = marshmallow.fields.String(required=False, allow_none=True) form = marshmallow.fields.Nested(FormSchema) diff --git a/tests/test_api.py b/tests/test_api.py index 1d6afeca..836a103b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,10 +1,11 @@ +from datetime import datetime import json import unittest from crc import db -from crc.models.study import StudyModel, StudySchema -from crc.models.workflow import WorkflowSpecModel, WorkflowSpecSchema, WorkflowModel, WorkflowStatus, \ - WorkflowSchema, TaskSchema +from crc.models.study import StudyModel, StudyModelSchema, ProtocolBuilderStatus +from crc.models.workflow import WorkflowSpecModel, WorkflowSpecModelSchema, WorkflowModel, WorkflowStatus, \ + WorkflowModelSchema, TaskSchema from tests.base_test import BaseTest @@ -15,6 +16,45 @@ class TestStudy(BaseTest, unittest.TestCase): study = db.session.query(StudyModel).first() self.assertIsNotNone(study) + def test_add_study(self): + study = { + "id": 12345, + "title": "Phase III Trial of Genuine People Personalities (GPP) Autonomous Intelligent Emotional Agents for Interstellar Spacecraft", + "last_updated": datetime.now(), + "protocol_builder_status": ProtocolBuilderStatus.in_process, + "primary_investigator_id": "tricia.marie.mcmillan@heartofgold.edu", + "sponsor": "Sirius Cybernetics Corporation", + "ind_number": "567890", + } + rv = self.app.post('/v1.0/study', + content_type="application/json", + data=json.dumps(StudyModelSchema().dump(study))) + self.assert_success(rv) + db_study = db.session.query(StudyModel).first() + self.assertIsNotNone(db_study) + self.assertEqual(study["id"], db_study.id) + self.assertEqual(study["title"], db_study.title) + self.assertEqual(study["last_updated"], db_study.last_updated) + self.assertEqual(study["protocol_builder_status"], db_study.protocol_builder_status) + self.assertEqual(study["primary_investigator_id"], db_study.primary_investigator_id) + self.assertEqual(study["sponsor"], db_study.sponsor) + self.assertEqual(study["ind_number"], db_study.ind_number) + + def test_update_study(self): + self.load_example_data() + study: StudyModel = db.session.query(StudyModel).first() + study.title = "Pilot Study of Fjord Placement for Single Fraction Outcomes to Cortisol Susceptibility" + study.protocol_builder_status = ProtocolBuilderStatus.complete + + rv = self.app.post('/v1.0/study', + content_type="application/json", + data=json.dumps(StudyModelSchema().dump(study))) + self.assert_success(rv) + db_study = db.session.query(StudyModel).first() + self.assertIsNotNone(db_study) + self.assertEqual(study.title, db_study.title) + self.assertEqual(study.protocol_builder_status, db_study.protocol_builder_status) + def test_study_api_get_single_study(self): self.load_example_data() study = db.session.query(StudyModel).first() @@ -23,7 +63,7 @@ class TestStudy(BaseTest, unittest.TestCase): content_type="application/json") self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) - study2 = StudySchema().load(json_data, session=db.session) + study2 = StudyModelSchema().load(json_data, session=db.session) self.assertEqual(study, study2) self.assertEqual(study.id, study2.id) self.assertEqual(study.title, study2.title) @@ -41,7 +81,7 @@ class TestStudy(BaseTest, unittest.TestCase): content_type="application/json") self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) - specs = WorkflowSpecSchema(many=True).load(json_data, session=db.session) + specs = WorkflowSpecModelSchema(many=True).load(json_data, session=db.session) spec2 = specs[0] self.assertEqual(spec.id, spec2.id) self.assertEqual(spec.display_name, spec2.display_name) @@ -52,8 +92,8 @@ class TestStudy(BaseTest, unittest.TestCase): study = db.session.query(StudyModel).first() self.assertEqual(0, db.session.query(WorkflowModel).count()) spec = db.session.query(WorkflowSpecModel).first() - rv = self.app.post('/v1.0/study/%i/workflows' % study.id,content_type="application/json", - data=json.dumps(WorkflowSpecSchema().dump(spec))) + rv = self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", + data=json.dumps(WorkflowSpecModelSchema().dump(spec))) self.assert_success(rv) self.assertEqual(1, db.session.query(WorkflowModel).count()) workflow = db.session.query(WorkflowModel).first() @@ -63,7 +103,7 @@ class TestStudy(BaseTest, unittest.TestCase): self.assertEqual(spec.id, workflow.workflow_spec_id) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowSchema().load(json_data, session=db.session) + workflow = WorkflowModelSchema().load(json_data, session=db.session) self.assertEqual(workflow.id, workflow.id) def test_delete_workflow(self): @@ -71,10 +111,10 @@ class TestStudy(BaseTest, unittest.TestCase): study = db.session.query(StudyModel).first() spec = db.session.query(WorkflowSpecModel).first() rv = self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", - data=json.dumps(WorkflowSpecSchema().dump(spec))) + data=json.dumps(WorkflowSpecModelSchema().dump(spec))) self.assertEqual(1, db.session.query(WorkflowModel).count()) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowSchema().load(json_data, session=db.session) + workflow = WorkflowModelSchema().load(json_data, session=db.session) rv = self.app.delete('/v1.0/workflow/%i' % workflow.id) self.assert_success(rv) self.assertEqual(0, db.session.query(WorkflowModel).count()) @@ -84,7 +124,7 @@ class TestStudy(BaseTest, unittest.TestCase): study = db.session.query(StudyModel).first() spec = db.session.query(WorkflowSpecModel).filter_by(id='random_fact').first() self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", - data=json.dumps(WorkflowSpecSchema().dump(spec))) + data=json.dumps(WorkflowSpecModelSchema().dump(spec))) rv = self.app.get('/v1.0/workflow/%i/tasks' % study.id, content_type="application/json") self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) @@ -97,9 +137,9 @@ class TestStudy(BaseTest, unittest.TestCase): study = db.session.query(StudyModel).first() spec = db.session.query(WorkflowSpecModel).filter_by(id='two_forms').first() rv = self.app.post('/v1.0/study/%i/workflows' % study.id, content_type="application/json", - data=json.dumps(WorkflowSpecSchema().dump(spec))) + data=json.dumps(WorkflowSpecModelSchema().dump(spec))) json_data = json.loads(rv.get_data(as_text=True)) - workflow = WorkflowSchema().load(json_data, session=db.session) + workflow = WorkflowModelSchema().load(json_data, session=db.session) rv = self.app.get('/v1.0/workflow/%i/tasks' % workflow.id, content_type="application/json") json_data = json.loads(rv.get_data(as_text=True)) diff --git a/tests/test_api_files.py b/tests/test_api_files.py index 414604d9..34184cc7 100644 --- a/tests/test_api_files.py +++ b/tests/test_api_files.py @@ -5,7 +5,7 @@ from datetime import datetime from crc import db from crc.models.workflow import WorkflowSpecModel -from crc.models.file import FileModel, FileType, FileSchema, FileDataModel +from crc.models.file import FileModel, FileType, FileModelSchema, FileDataModel from tests.base_test import BaseTest @@ -20,7 +20,7 @@ class TestApiFiles(BaseTest, unittest.TestCase): self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) self.assertEqual(1, len(json_data)) - file = FileSchema(many=True).load(json_data, session=db.session) + file = FileModelSchema(many=True).load(json_data, session=db.session) self.assertEqual("random_fact.bpmn", file[0].name) def test_list_multiple_files_for_workflow_spec(self): @@ -50,7 +50,7 @@ class TestApiFiles(BaseTest, unittest.TestCase): self.assert_success(rv) self.assertIsNotNone(rv.get_data()) json_data = json.loads(rv.get_data(as_text=True)) - file = FileSchema().load(json_data, session=db.session) + file = FileModelSchema().load(json_data, session=db.session) self.assertEqual(1, file.version) self.assertEqual(FileType.svg, file.type) self.assertFalse(file.primary) @@ -60,7 +60,7 @@ class TestApiFiles(BaseTest, unittest.TestCase): rv = self.app.get('/v1.0/file/%i' % file.id) self.assert_success(rv) json_data = json.loads(rv.get_data(as_text=True)) - file2 = FileSchema().load(json_data, session=db.session) + file2 = FileModelSchema().load(json_data, session=db.session) self.assertEqual(file, file2) def test_update_file(self): @@ -77,7 +77,7 @@ class TestApiFiles(BaseTest, unittest.TestCase): self.assert_success(rv) self.assertIsNotNone(rv.get_data()) json_data = json.loads(rv.get_data(as_text=True)) - file = FileSchema().load(json_data, session=db.session) + file = FileModelSchema().load(json_data, session=db.session) self.assertEqual(2, file.version) self.assertEqual(FileType.bpmn, file.type) self.assertTrue(file.primary)