diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index 0bec6684f..be4d25a9e 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -1248,7 +1248,7 @@ paths: parameters: - name: file_contents_hash in: query - required: true + required: false description: The hash of the file contents that originally came with the file. schema: type: string diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py index 5a29aec1c..3b4d72e23 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/models/file.py @@ -1,9 +1,9 @@ from __future__ import annotations + from dataclasses import dataclass -from typing import Any from dataclasses import field from datetime import datetime -from typing import Optional +from typing import Any from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum from spiffworkflow_backend.models.spec_reference import SpecReference @@ -71,10 +71,10 @@ class File: type: str last_modified: datetime size: int - references: Optional[list[SpecReference]] = None - file_contents: Optional[bytes] = None - process_model_id: Optional[str] = None - file_contents_hash: Optional[str] = None + references: list[SpecReference] | None = None + file_contents: bytes | None = None + process_model_id: str | None = None + file_contents_hash: str | None = None def __post_init__(self) -> None: """__post_init__.""" @@ -88,7 +88,7 @@ class File: content_type: str, last_modified: datetime, file_size: int, - ) -> "File": + ) -> File: """From_file_system.""" instance = cls( name=file_name, @@ -103,5 +103,5 @@ class File: def serialized(self) -> dict[str, Any]: dictionary = self.__dict__ if isinstance(self.file_contents, bytes): - dictionary['file_contents'] = self.file_contents.decode('utf-8') + dictionary["file_contents"] = self.file_contents.decode("utf-8") return dictionary diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index 84376eca2..7d33b67d7 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -1,8 +1,8 @@ """APIs for dealing with process groups, process models, and process instances.""" -from hashlib import sha256 import json import os import re +from hashlib import sha256 from typing import Any from typing import Dict from typing import Optional @@ -245,9 +245,13 @@ def process_model_list( return make_response(jsonify(response_json), 200) -def process_model_file_update(modified_process_model_identifier: str, file_name: str, file_contents_hash: str) -> flask.wrappers.Response: +def process_model_file_update( + modified_process_model_identifier: str, file_name: str, file_contents_hash: str +) -> flask.wrappers.Response: message = f"User: {g.user.username} clicked save for" - return _create_or_update_process_model_file(modified_process_model_identifier, message, 200, file_contents_hash=file_contents_hash) + return _create_or_update_process_model_file( + modified_process_model_identifier, message, 200, file_contents_hash=file_contents_hash + ) def process_model_file_delete(modified_process_model_identifier: str, file_name: str) -> flask.wrappers.Response: @@ -297,10 +301,8 @@ def process_model_file_show(modified_process_model_identifier: str, file_name: s files = SpecFileService.get_files(process_model, file_name) if len(files) == 0: raise ApiError( - error_code="unknown file", - message=( - f"No information exists for file {file_name} it does not exist in workflow {process_model_identifier}." - ), + error_code="process_model_file_not_found", + message=f"File {file_name} not found in workflow {process_model_identifier}.", status_code=404, ) @@ -477,7 +479,7 @@ def _create_or_update_process_model_file( modified_process_model_identifier: str, message_for_git_commit: str, http_status_to_return: int, - file_contents_hash: Optional[str], + file_contents_hash: Optional[str] = None, ) -> flask.wrappers.Response: """_create_or_update_process_model_file.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") @@ -499,15 +501,20 @@ def _create_or_update_process_model_file( status_code=400, ) - current_file_contents_bytes = SpecFileService.get_data(process_model, request_file.filename) - if current_file_contents_bytes and file_contents_hash: - current_file_contents_hash = sha256(current_file_contents_bytes).hexdigest() - if current_file_contents_hash != file_contents_hash: - raise ApiError( - error_code="process_model_file_has_changed", - message=f"Process model file: {request_file.filename} was already changed by someone else. Please reload before making changes.", - status_code=409, - ) + if file_contents_hash is not None: + current_file_contents_bytes = SpecFileService.get_data(process_model, request_file.filename) + if current_file_contents_bytes and file_contents_hash: + current_file_contents_hash = sha256(current_file_contents_bytes).hexdigest() + if current_file_contents_hash != file_contents_hash: + raise ApiError( + error_code="process_model_file_has_changed", + message=( + f"Process model file: {request_file.filename} was already changed by someone else. If you made" + " changes you do not want to lose, click the Download button and make sure your changes are" + " in the resulting file. If you do not need your changes, you can safely reload this page." + ), + status_code=409, + ) file = None try: diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py index d89ed3581..3a5290ed2 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/base_test.py @@ -113,7 +113,6 @@ class BaseTest: process_group_id: str, display_name: str = "", ) -> str: - """Create_process_group.""" process_group = ProcessGroup(id=process_group_id, display_name=display_name, display_order=0, admin=False) response = client.post( "/v1.0/process-groups", @@ -138,7 +137,6 @@ class BaseTest: primary_file_name: Optional[str] = None, user: Optional[UserModel] = None, ) -> TestResponse: - """Create_process_model.""" if process_model_id is not None: # make sure we have a group process_group_id, _ = os.path.split(process_model_id) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py index ce72759b1..2c4aa5067 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_process_api.py @@ -3,6 +3,7 @@ import io import json import os import time +from hashlib import sha256 from typing import Any from typing import Dict @@ -232,7 +233,6 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_primary_process_id_updates_via_xml.""" process_group_id = "test_group" process_model_id = "sample" process_model_identifier = f"{process_group_id}/{process_model_id}" @@ -258,10 +258,11 @@ class TestProcessApi(BaseTest): updated_bpmn_file_data_string = bpmn_file_data_string.replace(old_string, new_string) updated_bpmn_file_data_bytes = bytearray(updated_bpmn_file_data_string, "utf-8") data = {"file": (io.BytesIO(updated_bpmn_file_data_bytes), bpmn_file_name)} + file_contents_hash = sha256(bpmn_file_data_bytes).hexdigest() modified_process_model_id = process_model_identifier.replace("/", ":") response = client.put( - f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}", + f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}", data=data, follow_redirects=True, content_type="multipart/form-data", @@ -780,13 +781,12 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_process_model_file_update.""" process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user) modified_process_model_id = process_model_identifier.replace("/", ":") data = {"key1": "THIS DATA"} response = client.put( - f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg", + f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg?file_contents_hash=does_not_matter", data=data, follow_redirects=True, content_type="multipart/form-data", @@ -803,13 +803,12 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_process_model_file_update.""" process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user) modified_process_model_id = process_model_identifier.replace("/", ":") data = {"file": (io.BytesIO(b""), "random_fact.svg")} response = client.put( - f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg", + f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg?file_contents_hash=does_not_matter", data=data, follow_redirects=True, content_type="multipart/form-data", @@ -827,30 +826,22 @@ class TestProcessApi(BaseTest): with_db_and_bpmn_file_cleanup: None, with_super_admin_user: UserModel, ) -> None: - """Test_process_model_file_update.""" process_group_id = "test_group" - process_group_description = "Test Group" - process_model_id = "random_fact" + process_model_id = "simple_form" process_model_identifier = f"{process_group_id}/{process_model_id}" - self.create_process_group_with_api(client, with_super_admin_user, process_group_id, process_group_description) - self.create_process_model_with_api( - client, + bpmn_file_name = "simple_form.json" + load_test_spec( process_model_id=process_model_identifier, - user=with_super_admin_user, - ) - - bpmn_file_name = "random_fact.bpmn" - original_file = load_test_spec( - process_model_id=process_model_id, - bpmn_file_name=bpmn_file_name, - process_model_source_directory="random_fact", + process_model_source_directory="simple_form", ) + bpmn_file_data_bytes = self.get_test_data_file_contents(bpmn_file_name, process_model_id) + file_contents_hash = sha256(bpmn_file_data_bytes).hexdigest() modified_process_model_id = process_model_identifier.replace("/", ":") new_file_contents = b"THIS_IS_NEW_DATA" - data = {"file": (io.BytesIO(new_file_contents), "random_fact.svg")} + data = {"file": (io.BytesIO(new_file_contents), bpmn_file_name)} response = client.put( - f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg", + f"/v1.0/process-models/{modified_process_model_id}/files/{bpmn_file_name}?file_contents_hash={file_contents_hash}", data=data, follow_redirects=True, content_type="multipart/form-data", @@ -862,12 +853,11 @@ class TestProcessApi(BaseTest): assert response.json["file_contents"] is not None response = client.get( - f"/v1.0/process-models/{modified_process_model_id}/files/random_fact.svg", + f"/v1.0/process-models/{modified_process_model_id}/files/simple_form.json", headers=self.logged_in_headers(with_super_admin_user), ) assert response.status_code == 200 updated_file = json.loads(response.get_data(as_text=True)) - assert original_file != updated_file assert updated_file["file_contents"] == new_file_contents.decode() def test_process_model_file_delete_when_bad_process_model( @@ -879,9 +869,6 @@ class TestProcessApi(BaseTest): ) -> None: """Test_process_model_file_update.""" process_model_identifier = self.create_group_and_model_with_bpmn(client, with_super_admin_user) - # self.create_spec_file(client, user=with_super_admin_user) - - # process_model = load_test_spec("random_fact") bad_process_model_identifier = f"x{process_model_identifier}" modified_bad_process_model_identifier = bad_process_model_identifier.replace("/", ":") response = client.delete( diff --git a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx index f0abcfb84..3b105dde2 100644 --- a/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx +++ b/spiffworkflow-frontend/src/routes/ProcessModelShow.tsx @@ -8,7 +8,6 @@ import { Favorite, Edit, View, - ArrowRight, // @ts-ignore } from '@carbon/icons-react'; import {