diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py index 0c844766..02ad5fc1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py @@ -4,6 +4,7 @@ from dataclasses import field from datetime import datetime from typing import Optional +import marshmallow from marshmallow import INCLUDE from marshmallow import Schema @@ -61,6 +62,20 @@ CONTENT_TYPES = { } +@dataclass() +class FileReference: + """File Reference Information. + + Includes items such as the process id and name for a BPMN, + or the Decision id and Decision name for a DMN file. There may be more than + one reference that points to a particular file. + """ + + id: str + name: str + type: str # can be 'process', 'decision', or just 'file' + + @dataclass(order=True) class File: """File.""" @@ -70,17 +85,12 @@ class File: content_type: str name: str type: str - document: dict last_modified: datetime size: int - process_instance_id: Optional[int] = None - irb_doc_code: Optional[str] = None - data_store: Optional[dict] = field(default_factory=dict) - user_uid: Optional[str] = None + references: Optional[list[FileReference]] = None file_contents: Optional[bytes] = None process_model_id: Optional[str] = None process_group_id: Optional[str] = None - archived: bool = False def __post_init__(self) -> None: """__post_init__.""" @@ -100,7 +110,6 @@ class File: name=file_name, content_type=content_type, type=file_type.value, - document={}, last_modified=last_modified, size=file_size, ) @@ -118,32 +127,29 @@ class FileSchema(Schema): "id", "name", "content_type", - "process_instance_id", - "irb_doc_code", "last_modified", "type", - "archived", "size", "data_store", - "document", "user_uid", "url", "file_contents", - "process_model_id", + "references", "process_group_id", + "process_model_id", ] unknown = INCLUDE + references = marshmallow.fields.List( + marshmallow.fields.Nested("FileReferenceSchema") + ) - # url = Method("get_url") - # - # def get_url(self, obj): - # token = 'not_available' - # if hasattr(obj, 'id') and obj.id is not None: - # file_url = url_for("/v1_0.crc_api_file_get_file_data_link", file_id=obj.id, _external=True) - # if hasattr(flask.g, 'user'): - # token = flask.g.user.encode_auth_token() - # url = file_url + '?auth_token=' + urllib.parse.quote_plus(token) - # return url - # else: - # return "" - # + +class FileReferenceSchema(Schema): + """FileSchema.""" + + class Meta: + """Meta.""" + + model = FileReference + fields = ["id", "name", "type"] + unknown = INCLUDE diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py index 4ff25a75..f0b9065c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_api_blueprint.py @@ -65,6 +65,7 @@ from spiffworkflow_backend.services.error_handling_service import ErrorHandlingS from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.git_service import GitService from spiffworkflow_backend.services.message_service import MessageService +from spiffworkflow_backend.services.process_instance_processor import MyCustomParser from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) @@ -263,6 +264,10 @@ def process_model_show(process_group_id: str, process_model_id: str) -> Any: process_model = get_process_model(process_model_id, process_group_id) files = sorted(SpecFileService.get_files(process_model)) process_model.files = files + for file in process_model.files: + file.references = SpecFileService.get_references_for_file( + file, process_model, MyCustomParser + ) process_model_json = ProcessModelInfoSchema().dump(process_model) return process_model_json diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index eb2a322f..c6b86d3e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -2,6 +2,7 @@ import os import shutil from datetime import datetime +from typing import Any from typing import List from typing import Optional @@ -14,6 +15,7 @@ from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup from spiffworkflow_backend.models.file import File +from spiffworkflow_backend.models.file import FileReference from spiffworkflow_backend.models.file import FileType from spiffworkflow_backend.models.message_correlation_property import ( MessageCorrelationPropertyModel, @@ -54,6 +56,41 @@ class SpecFileService(FileSystemService): ) return files + @staticmethod + def get_references_for_file( + file: File, process_model_info: ProcessModelInfo, parser_class: Any + ) -> list[FileReference]: + """Uses spiffworkflow to parse BPMN and DMN files to determine how they can be externally referenced. + + Returns a list of Reference objects that contain the type of reference, the id, the name. + Ex. + id = {str} 'Level3' + name = {str} 'Level 3' + type = {str} 'process' + """ + references: list[FileReference] = [] + file_path = SpecFileService.file_path(process_model_info, file.name) + parser = parser_class() + parser_type = None + sub_parser = None + if file.type == FileType.bpmn.value: + parser.add_bpmn_file(file_path) + parser_type = "process" + sub_parsers = list(parser.process_parsers.values()) + elif file.type == FileType.dmn.value: + parser.add_dmn_file(file_path) + sub_parsers = list(parser.dmn_parsers.values()) + parser_type = "decision" + else: + return references + for sub_parser in sub_parsers: + references.append( + FileReference( + id=sub_parser.get_id(), name=sub_parser.get_name(), type=parser_type + ) + ) + return references + @staticmethod def add_file( process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes diff --git a/spiffworkflow-backend/tests/data/call_activity_nested/schema.json b/spiffworkflow-backend/tests/data/call_activity_nested/schema.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/spiffworkflow-backend/tests/data/call_activity_nested/schema.json @@ -0,0 +1 @@ +{} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py index 22f2fb15..c5819587 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_file.py @@ -21,7 +21,6 @@ def create_test_file(type: str, name: str) -> File: type=type, name=name, content_type=type, - document={}, last_modified=datetime.now(), size=1, ) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py index fd882e71..85adb298 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_spec_file_service.py @@ -5,10 +5,13 @@ import pytest from flask import Flask from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db +from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.models.bpmn_process_id_lookup import BpmnProcessIdLookup +from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.spec_file_service import SpecFileService class TestSpecFileService(BaseTest): @@ -95,3 +98,44 @@ class TestSpecFileService(BaseTest): bpmn_process_id_lookups[0].bpmn_file_relative_path == self.call_activity_nested_relative_file_path ) + + def test_load_reference_information( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_load_reference_information. + + When getting files from the spec_file service, each file includes + details about how the file can be referenced -- for instance + it is possible to reference a DMN file with a Decision.id or + a bpmn file with a process.id. These Decisions and processes + can also have human readable display names, which should also be avaiable. + Note that a single bpmn file can contain many processes, and + a DMN file can (theoretically) contain many decisions. So this + is an array. + """ + load_test_spec( + "call_activity_nested", + process_model_source_directory="call_activity_nested", + ) + process_model_info = ProcessModelService().get_process_model( + "call_activity_nested" + ) + files = SpecFileService.get_files(process_model_info) + + file = next(filter(lambda f: f.name == "call_activity_level_3.bpmn", files)) + ca_3 = SpecFileService.get_references_for_file( + file, process_model_info, BpmnDmnParser + ) + assert len(ca_3) == 1 + assert ca_3[0].name == "Level 3" + assert ca_3[0].id == "Level3" + assert ca_3[0].type == "process" + + file = next(filter(lambda f: f.name == "level2c.dmn", files)) + dmn1 = SpecFileService.get_references_for_file( + file, process_model_info, BpmnDmnParser + ) + assert len(dmn1) == 1 + assert dmn1[0].name == "Decision 1" + assert dmn1[0].id == "Decision_0vrtcmk" + assert dmn1[0].type == "decision"