diff --git a/crc/api/file.py b/crc/api/file.py index 911b2996..5d03bf2f 100644 --- a/crc/api/file.py +++ b/crc/api/file.py @@ -57,7 +57,7 @@ def get_document_directory(study_id, workflow_id=None): if file.irb_doc_code in doc_dict: doc_code = doc_dict[file.irb_doc_code] else: - doc_code = {'category1': "Unknown", 'category2': None, 'category3': None} + doc_code = {'category1': "Unknown", 'category2': '', 'category3': ''} if workflow_id: expand = file.workflow_id == int(workflow_id) else: diff --git a/crc/api/workflow.py b/crc/api/workflow.py index fcba4d26..4782f833 100644 --- a/crc/api/workflow.py +++ b/crc/api/workflow.py @@ -244,6 +244,7 @@ def __update_task(processor, task, data, user): here because we need to do it multiple times when completing all tasks in a multi-instance task""" task.update_data(data) + WorkflowService.post_process_form(task) # some properties may update the data store. processor.complete_task(task) processor.do_engine_steps() processor.save() diff --git a/crc/models/api_models.py b/crc/models/api_models.py index 308823fc..90b56136 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -26,7 +26,8 @@ class Task(object): PROP_EXTENSIONS_TITLE = "display_name" - # Autocomplete field + # Field Types + FIELD_TYPE_FILE = "file" FIELD_TYPE_AUTO_COMPLETE = "autocomplete" FIELD_PROP_AUTO_COMPLETE_MAX = "autocomplete_num" # Not used directly, passed in from the front end. @@ -59,6 +60,10 @@ class Task(object): FIELD_PROP_REPLEAT_TITLE = "repeat_title" FIELD_PROP_REPLEAT_BUTTON = "repeat_button_label" + # File specific field properties + FIELD_PROP_DOC_CODE = "doc_code" # to associate a file upload field with a doc code + FIELD_PROP_FILE_DATA = "file_data" # to associate a bit of data with a specific file upload file. + # Additional properties FIELD_PROP_ENUM_TYPE = "enum_type" FIELD_PROP_TEXT_AREA_ROWS = "rows" diff --git a/crc/models/file.py b/crc/models/file.py index 9f3073e3..54ccd325 100644 --- a/crc/models/file.py +++ b/crc/models/file.py @@ -90,7 +90,7 @@ class FileModel(db.Model): # it instead, hide it in the interface. is_review = db.Column(db.Boolean, default=False, nullable=True) archived = db.Column(db.Boolean, default=False, nullable=False) - tags = relationship("DataStoreModel", cascade="all,delete", backref="file") + data_stores = relationship("DataStoreModel", cascade="all,delete", backref="file") class File(object): @classmethod @@ -123,6 +123,11 @@ class File(object): else: instance.last_modified = None instance.latest_version = None + + instance.data_store = {} + for ds in model.data_stores: + instance.data_store[ds.key] = ds.value + return instance @@ -142,7 +147,7 @@ class FileSchema(Schema): fields = ["id", "name", "is_status", "is_reference", "content_type", "primary", "primary_process_id", "workflow_spec_id", "workflow_id", "irb_doc_code", "last_modified", "latest_version", "type", "categories", - "description", "category", "description", "download_name", "size"] + "description", "category", "download_name", "size", "data_store"] unknown = INCLUDE type = EnumField(FileType) diff --git a/crc/services/file_service.py b/crc/services/file_service.py index 3d8a1e39..1efd93b9 100644 --- a/crc/services/file_service.py +++ b/crc/services/file_service.py @@ -100,16 +100,6 @@ class FileService(object): @staticmethod def add_workflow_file(workflow_id, irb_doc_code, name, content_type, binary_data): - """Create a new file and associate it with the workflow - Please note that the irb_doc_code MUST be a known file in the irb_documents.xslx reference document.""" - if not FileService.is_allowed_document(irb_doc_code): - raise ApiError("invalid_form_field_key", - "When uploading files, the form field id must match a known document in the " - "irb_docunents.xslx reference file. This code is not found in that file '%s'" % irb_doc_code) - - """Assure this is unique to the workflow, task, and document code AND the Name - Because we will allow users to upload multiple files for the same form field - in some cases """ file_model = session.query(FileModel)\ .filter(FileModel.workflow_id == workflow_id)\ .filter(FileModel.name == name)\ diff --git a/crc/services/workflow_service.py b/crc/services/workflow_service.py index ee37c688..7ae48ee3 100644 --- a/crc/services/workflow_service.py +++ b/crc/services/workflow_service.py @@ -22,6 +22,7 @@ from jinja2 import Template from crc import db, app from crc.api.common import ApiError from crc.models.api_models import Task, MultiInstanceType, WorkflowApi +from crc.models.data_store import DataStoreModel from crc.models.file import LookupDataModel, FileModel from crc.models.study import StudyModel from crc.models.task_event import TaskEventModel @@ -242,6 +243,25 @@ class WorkflowService(object): f'The field {field.id} contains an unsupported ' f'property: {name}', task=task) + + @staticmethod + def post_process_form(task): + """Looks through the fields in a submitted form, acting on any properties.""" + for field in task.task_spec.form.fields: + if field.has_property(Task.FIELD_PROP_DOC_CODE) and \ + field.type == Task.FIELD_TYPE_FILE: + file_id = task.data[field.id] + file = db.session.query(FileModel).filter(FileModel.id == file_id).first() + doc_code = WorkflowService.evaluate_property(Task.FIELD_PROP_DOC_CODE, field, task) + file.irb_doc_code = doc_code + db.session.commit() + # Set the doc code on the file. + if field.has_property(Task.FIELD_PROP_FILE_DATA) and \ + field.get_property(Task.FIELD_PROP_FILE_DATA) in task.data: + file_id = task.data[field.get_property(Task.FIELD_PROP_FILE_DATA)] + data_store = DataStoreModel(file_id=file_id, key=field.id, value=task.data[field.id]) + db.session.add(data_store) + @staticmethod def evaluate_property(property_name, field, task): expression = field.get_property(property_name) diff --git a/tests/data/file_upload_form/file_upload_form.bpmn b/tests/data/file_upload_form/file_upload_form.bpmn index f76db85d..0a58a601 100644 --- a/tests/data/file_upload_form/file_upload_form.bpmn +++ b/tests/data/file_upload_form/file_upload_form.bpmn @@ -1,13 +1,14 @@ - + SequenceFlow_0ea9hvd - SequenceFlow_1h0d349 + Flow_0t55959 - + + #### Non-Funded Executed Agreement @@ -15,40 +16,55 @@ OGC will upload the Non-Funded Executed Agreement after it has been negotiated by OSP contract negotiator. - + + + + + - - + + + + + + + + + + + + + + SequenceFlow_0ea9hvd - SequenceFlow_1h0d349 + Flow_0t55959 - - + + + + + + + + + - + - - + + - - - - - - - - diff --git a/tests/files/test_files_api.py b/tests/files/test_files_api.py index cce55fb5..74129ebb 100644 --- a/tests/files/test_files_api.py +++ b/tests/files/test_files_api.py @@ -8,6 +8,7 @@ from crc.models.file import FileModel, FileType, FileSchema, FileModelSchema from crc.models.workflow import WorkflowSpecModel from crc.services.file_service import FileService from crc.services.workflow_processor import WorkflowProcessor +from crc.models.data_store import DataStoreModel from example_data import ExampleDataLoader @@ -232,6 +233,17 @@ class TestFilesApi(BaseTest): self.assertEqual("text/xml; charset=utf-8", rv.content_type) self.assertTrue(rv.content_length > 1) + def test_get_file_contains_data_store_elements(self): + self.load_example_data() + spec = session.query(WorkflowSpecModel).first() + file = session.query(FileModel).filter_by(workflow_spec_id=spec.id).first() + ds = DataStoreModel(key="my_key", value="my_value", file_id=file.id); + db.session.add(ds) + rv = self.app.get('/v1.0/file/%i' % file.id, headers=self.logged_in_headers()) + self.assert_success(rv) + json_data = json.loads(rv.get_data(as_text=True)) + self.assertEqual("my_value", json_data['data_store']['my_key']) + def test_get_files_for_form_field_returns_only_those_files(self): self.create_reference_document() workflow = self.create_workflow('file_upload_form')