diff --git a/crc/api.yml b/crc/api.yml index 37fcb147..0bac3afc 100644 --- a/crc/api.yml +++ b/crc/api.yml @@ -67,6 +67,37 @@ paths: type: array items: $ref: "#/components/schemas/User" + # /v1.0/document_directory/{study_id} + /document_directory/{study_id}: + parameters : + - name : study_id + required : true + in : path + description : The unique id of a study. + schema : + type : integer + - name : workflow_id + in : query + required : false + schema : + type : integer + get: + operationId: crc.api.file.get_document_directory + security: + - auth_admin: ['secret'] + summary: Returns a directory of all files for study in a nested structure + tags: + - Document Categories + responses: + '200': + description: All IRB Categories defined in the file document. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/DocumentDirectory" + # /v1.0/study /study: get: @@ -565,6 +596,12 @@ paths: description: The unique key of a workflow task form field. Make sure this matches a document in the irb_documents.xslx reference sheet. schema: type: string + - name: study_id + in: query + required: false + description: The study that the files are related to + schema: + type: integer get: operationId: crc.api.file.get_files summary: Provides a list of files that match the given parameters (such as a spec id) IMPORTANT, only includes metadata, not the file content. @@ -1429,6 +1466,29 @@ components: type: string x-nullable: true example: "27b-6-1212" + DocumentDirectory: + properties: + level: + type: string + x-nullable: true + example: "Ancillary Document" + description: + type: string + x-nullable: true + example: "Documents that are ancillary to the study" + file: + $ref: "#/components/schemas/File" + x-nullable: true + expanded: + type: boolean + example: False + filecount: + type: integer + example: 1 + children: + type: array + items: + $ref: "#/components/schemas/File" DataStore: properties: id: diff --git a/crc/api/file.py b/crc/api/file.py index 50a06124..a7babb33 100644 --- a/crc/api/file.py +++ b/crc/api/file.py @@ -6,24 +6,80 @@ from flask import send_file from crc import session from crc.api.common import ApiError +from crc.models.api_models import DocumentDirectory, DocumentDirectorySchema from crc.models.file import FileSchema, FileModel, File, FileModelSchema, FileDataModel, FileType from crc.models.workflow import WorkflowSpecModel from crc.services.file_service import FileService +def ensure_exists(output, categories, expanded): + """ + This is a recursive function, it expects a list of + levels with a file object at the end (kinda like duck,duck,duck,goose) + + for each level, it makes sure that level is already in the structure and if it is not + it will add it + + function terminates upon getting an entry that is a file object ( or really anything but string) + """ + current_item = categories[0] + found = False + if isinstance(current_item, str): + for item in output: + if item.level == current_item: + found = True + item.filecount = item.filecount + 1 + item.expanded = expanded | item.expanded + ensure_exists(item.children, categories[1:], expanded) + if not found: + new_level = DocumentDirectory(level=current_item) + new_level.filecount = 1 + new_level.expanded = expanded + output.append(new_level) + ensure_exists(new_level.children, categories[1:], expanded) + else: + new_level = DocumentDirectory(file=current_item) + new_level.expanded = expanded + output.append(new_level) + + +def get_document_directory(study_id, workflow_id=None): + """ + return a nested list of files arranged according to the category hirearchy + defined in the doc dictionary + """ + output = [] + doc_dict = FileService.get_doc_dictionary() + file_models = FileService.get_files_for_study(study_id=study_id) + files = (to_file_api(model) for model in file_models) + for file in files: + doc_code = doc_dict[file.irb_doc_code] + if workflow_id: + expand = file.workflow_id == int(workflow_id) + else: + expand = False + print(expand) + categories = [x for x in [doc_code['category1'],doc_code['category2'],doc_code['category3'],file] if x != ''] + ensure_exists(output, categories, expanded=expand) + return DocumentDirectorySchema(many=True).dump(output) + + def to_file_api(file_model): """Converts a FileModel object to something we can return via the api""" return File.from_models(file_model, FileService.get_file_data(file_model.id), FileService.get_doc_dictionary()) -def get_files(workflow_spec_id=None, workflow_id=None, form_field_key=None): - if all(v is None for v in [workflow_spec_id, workflow_id, form_field_key]): +def get_files(workflow_spec_id=None, workflow_id=None, form_field_key=None,study_id=None): + if all(v is None for v in [workflow_spec_id, workflow_id, form_field_key,study_id]): raise ApiError('missing_parameter', 'Please specify either a workflow_spec_id or a ' 'workflow_id with an optional form_field_key') - file_models = FileService.get_files(workflow_spec_id=workflow_spec_id, + if study_id is not None: + file_models = FileService.get_files_for_study(study_id=study_id) + else: + file_models = FileService.get_files(workflow_spec_id=workflow_spec_id, workflow_id=workflow_id, irb_doc_code=form_field_key) diff --git a/crc/models/api_models.py b/crc/models/api_models.py index 74913bef..2bb4abc6 100644 --- a/crc/models/api_models.py +++ b/crc/models/api_models.py @@ -7,7 +7,7 @@ from marshmallow_enum import EnumField from crc import ma from crc.models.workflow import WorkflowStatus - +from crc.models.file import FileSchema class MultiInstanceType(enum.Enum): none = "none" @@ -167,6 +167,27 @@ class NavigationItemSchema(ma.Schema): item.spec_type = spec_type return item +class DocumentDirectorySchema(ma.Schema): + level = marshmallow.fields.String() + file = marshmallow.fields.Nested(FileSchema) + filecount = marshmallow.fields.Integer() + expanded = marshmallow.fields.Boolean() + children = marshmallow.fields.Nested("self",many=True) + + +class DocumentDirectory(object): + def __init__(self, level=None, file=None, children=None): + + self.level = level + self.file = file + self.expanded = False + self.filecount = 0 + if children is None: + self.children = list() + else: + self.children=children + + class WorkflowApi(object): def __init__(self, id, status, next_task, navigation, spec_version, is_latest_spec, workflow_spec_id, total_tasks, completed_tasks, diff --git a/crc/models/file.py b/crc/models/file.py index b1269f78..5be8bccd 100644 --- a/crc/models/file.py +++ b/crc/models/file.py @@ -108,7 +108,7 @@ class File(object): doc_dictionary[model.irb_doc_code]['category2'], doc_dictionary[model.irb_doc_code]['category3']])) instance.description = doc_dictionary[model.irb_doc_code]['description'] - instance.download_name = ".".join([instance.category, model.type.value]) + instance.download_name = "/".join([instance.category, model.name]) else: instance.category = "" instance.description = "" diff --git a/crc/services/file_service.py b/crc/services/file_service.py index 805a8e81..14e1b20e 100644 --- a/crc/services/file_service.py +++ b/crc/services/file_service.py @@ -17,6 +17,21 @@ from crc.api.common import ApiError from crc.models.file import FileType, FileDataModel, FileModel, LookupFileModel, LookupDataModel from crc.models.workflow import WorkflowSpecModel, WorkflowModel, WorkflowSpecDependencyFile from crc.services.cache_service import cache +import re + + +def camel_to_snake(camel): + """ + make a camelcase from a snakecase + with a few things thrown in - we had a case where + we were parsing a spreadsheet and using the headings as keys in an object + one of the headings was "Who Uploads?" + """ + camel = camel.strip() + camel = re.sub(' ', '', camel) + camel = re.sub('?', '', camel) + return re.sub(r'(?