Merge pull request #241 from sartography/feature/multiple_editor_users
Feature/multiple editor users
This commit is contained in:
commit
cc9c559b15
|
@ -1245,6 +1245,13 @@ paths:
|
|||
put:
|
||||
operationId: spiffworkflow_backend.routes.process_models_controller.process_model_file_update
|
||||
summary: save the contents to the given file
|
||||
parameters:
|
||||
- name: file_contents_hash
|
||||
in: query
|
||||
required: false
|
||||
description: The hash of the file contents that originally came with the file.
|
||||
schema:
|
||||
type: string
|
||||
tags:
|
||||
- Process Model Files
|
||||
requestBody:
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
"""File."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import marshmallow
|
||||
from marshmallow import INCLUDE
|
||||
from marshmallow import Schema
|
||||
from typing import Any
|
||||
|
||||
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
|
||||
from spiffworkflow_backend.models.spec_reference import SpecReference
|
||||
|
@ -74,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
|
||||
process_group_id: 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__."""
|
||||
|
@ -91,7 +88,7 @@ class File:
|
|||
content_type: str,
|
||||
last_modified: datetime,
|
||||
file_size: int,
|
||||
) -> "File":
|
||||
) -> File:
|
||||
"""From_file_system."""
|
||||
instance = cls(
|
||||
name=file_name,
|
||||
|
@ -102,28 +99,9 @@ class File:
|
|||
)
|
||||
return instance
|
||||
|
||||
|
||||
class FileSchema(Schema):
|
||||
"""FileSchema."""
|
||||
|
||||
class Meta:
|
||||
"""Meta."""
|
||||
|
||||
model = File
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"content_type",
|
||||
"last_modified",
|
||||
"type",
|
||||
"size",
|
||||
"data_store",
|
||||
"user_uid",
|
||||
"url",
|
||||
"file_contents",
|
||||
"references",
|
||||
"process_group_id",
|
||||
"process_model_id",
|
||||
]
|
||||
unknown = INCLUDE
|
||||
references = marshmallow.fields.List(marshmallow.fields.Nested("SpecReferenceSchema"))
|
||||
@property
|
||||
def serialized(self) -> dict[str, Any]:
|
||||
dictionary = self.__dict__
|
||||
if isinstance(self.file_contents, bytes):
|
||||
dictionary["file_contents"] = self.file_contents.decode("utf-8")
|
||||
return dictionary
|
||||
|
|
|
@ -87,7 +87,7 @@ class ProcessModelInfoSchema(Schema):
|
|||
display_order = marshmallow.fields.Integer(allow_none=True)
|
||||
primary_file_name = marshmallow.fields.String(allow_none=True)
|
||||
primary_process_id = marshmallow.fields.String(allow_none=True)
|
||||
files = marshmallow.fields.List(marshmallow.fields.Nested("FileSchema"))
|
||||
files = marshmallow.fields.List(marshmallow.fields.Nested("File"))
|
||||
fault_or_suspend_on_exception = marshmallow.fields.String()
|
||||
exception_notification_addresses = marshmallow.fields.List(marshmallow.fields.String)
|
||||
metadata_extraction_paths = marshmallow.fields.List(
|
||||
|
|
|
@ -298,8 +298,8 @@ def process_instance_report_show(
|
|||
def process_instance_report_column_list(
|
||||
process_model_identifier: Optional[str] = None,
|
||||
) -> flask.wrappers.Response:
|
||||
"""Process_instance_report_column_list."""
|
||||
table_columns = ProcessInstanceReportService.builtin_column_options()
|
||||
system_report_column_options = ProcessInstanceReportService.system_report_column_options()
|
||||
columns_for_metadata_query = (
|
||||
db.session.query(ProcessInstanceMetadataModel.key)
|
||||
.order_by(ProcessInstanceMetadataModel.key)
|
||||
|
@ -315,7 +315,7 @@ def process_instance_report_column_list(
|
|||
columns_for_metadata_strings = [
|
||||
{"Header": i[0], "accessor": i[0], "filterable": True} for i in columns_for_metadata
|
||||
]
|
||||
return make_response(jsonify(table_columns + columns_for_metadata_strings), 200)
|
||||
return make_response(jsonify(table_columns + system_report_column_options + columns_for_metadata_strings), 200)
|
||||
|
||||
|
||||
def process_instance_show_for_me(
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
from hashlib import sha256
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
|
@ -18,7 +19,6 @@ from werkzeug.datastructures import FileStorage
|
|||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.interfaces import IdToProcessGroupMapping
|
||||
from spiffworkflow_backend.models.file import FileSchema
|
||||
from spiffworkflow_backend.models.process_group import ProcessGroup
|
||||
from spiffworkflow_backend.models.process_instance_report import (
|
||||
ProcessInstanceReportModel,
|
||||
|
@ -245,10 +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) -> flask.wrappers.Response:
|
||||
"""Process_model_file_update."""
|
||||
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)
|
||||
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:
|
||||
|
@ -293,24 +296,23 @@ def process_model_file_create(
|
|||
|
||||
|
||||
def process_model_file_show(modified_process_model_identifier: str, file_name: str) -> Any:
|
||||
"""Process_model_file_show."""
|
||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||
process_model = _get_process_model(process_model_identifier)
|
||||
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,
|
||||
)
|
||||
|
||||
file = files[0]
|
||||
file_contents = SpecFileService.get_data(process_model, file.name)
|
||||
file.file_contents = file_contents
|
||||
file_contents_hash = sha256(file_contents).hexdigest()
|
||||
file.file_contents_hash = file_contents_hash
|
||||
file.process_model_id = process_model.id
|
||||
return FileSchema().dump(file)
|
||||
return make_response(jsonify(file), 200)
|
||||
|
||||
|
||||
# {
|
||||
|
@ -477,6 +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] = None,
|
||||
) -> flask.wrappers.Response:
|
||||
"""_create_or_update_process_model_file."""
|
||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||
|
@ -498,6 +501,21 @@ def _create_or_update_process_model_file(
|
|||
status_code=400,
|
||||
)
|
||||
|
||||
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:
|
||||
file = SpecFileService.update_file(process_model, request_file.filename, request_file_contents)
|
||||
|
@ -514,8 +532,4 @@ def _create_or_update_process_model_file(
|
|||
file.process_model_id = process_model.id
|
||||
_commit_and_push_to_git(f"{message_for_git_commit} {process_model_identifier}/{file.name}")
|
||||
|
||||
return Response(
|
||||
json.dumps(FileSchema().dump(file)),
|
||||
status=http_status_to_return,
|
||||
mimetype="application/json",
|
||||
)
|
||||
return make_response(jsonify(file), http_status_to_return)
|
||||
|
|
|
@ -320,7 +320,7 @@ class ProcessInstanceReportService:
|
|||
|
||||
@classmethod
|
||||
def builtin_column_options(cls) -> list[ReportMetadataColumn]:
|
||||
"""Builtin_column_options."""
|
||||
"""Columns that are actually in the process instance table."""
|
||||
return_value: list[ReportMetadataColumn] = [
|
||||
{"Header": "Id", "accessor": "id", "filterable": False},
|
||||
{
|
||||
|
@ -339,6 +339,15 @@ class ProcessInstanceReportService:
|
|||
]
|
||||
return return_value
|
||||
|
||||
@classmethod
|
||||
def system_report_column_options(cls) -> list[ReportMetadataColumn]:
|
||||
"""Columns that are used with certain system reports."""
|
||||
return_value: list[ReportMetadataColumn] = [
|
||||
{"Header": "Task", "accessor": "task_title", "filterable": False},
|
||||
{"Header": "Waiting For", "accessor": "waiting_for", "filterable": False},
|
||||
]
|
||||
return return_value
|
||||
|
||||
@classmethod
|
||||
def get_filter_value(cls, filters: list[FilterValue], filter_key: str) -> Any:
|
||||
for filter in filters:
|
||||
|
|
|
@ -175,7 +175,6 @@ class SpecFileService(FileSystemService):
|
|||
|
||||
@classmethod
|
||||
def update_file(cls, process_model_info: ProcessModelInfo, file_name: str, binary_data: bytes) -> File:
|
||||
"""Update_file."""
|
||||
SpecFileService.assert_valid_file_name(file_name)
|
||||
cls.validate_bpmn_xml(file_name, binary_data)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -120,6 +120,7 @@ export interface ProcessFile {
|
|||
size: number;
|
||||
type: string;
|
||||
file_contents?: string;
|
||||
file_contents_hash?: string;
|
||||
}
|
||||
|
||||
export interface ProcessInstanceMetadata {
|
||||
|
|
|
@ -206,6 +206,9 @@ export default function ProcessModelEditDiagram() {
|
|||
httpMethod = 'POST';
|
||||
} else {
|
||||
url += `/${fileNameWithExtension}`;
|
||||
if (processModelFile && processModelFile.file_contents_hash) {
|
||||
url += `?file_contents_hash=${processModelFile.file_contents_hash}`;
|
||||
}
|
||||
}
|
||||
if (!fileNameWithExtension) {
|
||||
handleShowFileNameEditor();
|
||||
|
|
Loading…
Reference in New Issue