Feature/guest form submission (#447)
* WIP: some initial code to allow anonymous users get a task w/ burnettk * added scripts to get the url for a given human task w/ burnettk * users can complete a task anonymously * pyl * fixed up login flow and added submission confirmation message for guest tasks w/ burnettk * added only_guest_task_completion to guest token so we can remove items from the ui with it * renamed anonymous to guest * force logout guest users when verifying the token if certain criteria are met and do not do it random controller methods * also allow saving draft data to use guest users w/ burnettk * updated bpmn-js-spiffworkflow and added test to test allow guest * pyl * fix typo and remove bad file * remove allow_guest column and moved allow guest check to TaskModel * removed unnecessary comment * missing import * do not allow guest users to see completed tasks and remove save and close button for guest users w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com> Co-authored-by: burnettk <burnettk@users.noreply.github.com>
This commit is contained in:
parent
80ad92a0c3
commit
ffe2a18ce9
|
@ -17,6 +17,16 @@ paths:
|
|||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: process_instance_id
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- name: task_guid
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
summary: redirect to open id authentication server
|
||||
operationId: spiffworkflow_backend.routes.user.login
|
||||
|
|
|
@ -13,7 +13,8 @@ if TYPE_CHECKING:
|
|||
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel # noqa: F401
|
||||
|
||||
|
||||
SPIFF_NO_AUTH_ANONYMOUS_GROUP = "spiff_anonymous_group"
|
||||
SPIFF_NO_AUTH_GROUP = "spiff_no_auth_group"
|
||||
SPIFF_GUEST_GROUP = "spiff_guest_group"
|
||||
|
||||
|
||||
class GroupNotFoundError(Exception):
|
||||
|
|
|
@ -98,6 +98,27 @@ class TaskModel(SpiffworkflowBaseDBModel):
|
|||
task_model: TaskModel = self.__class__.query.filter_by(guid=self.properties_json["parent"]).first()
|
||||
return task_model
|
||||
|
||||
# this will redirect to login if the task does not allow guest access.
|
||||
# so if you already completed the task, and you are not signed in, you will get sent to a login page.
|
||||
def allows_guest(self, process_instance_id: int) -> bool:
|
||||
properties_json = self.task_definition.properties_json
|
||||
if (
|
||||
"extensions" in properties_json
|
||||
and "allowGuest" in properties_json["extensions"]
|
||||
and properties_json["extensions"]["allowGuest"] == "true"
|
||||
and self.process_instance_id == int(process_instance_id)
|
||||
and self.state != "COMPLETED"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def task_guid_allows_guest(cls, task_guid: str, process_instance_id: int) -> bool:
|
||||
task_model = cls.query.filter_by(guid=task_guid).first()
|
||||
if task_model is not None and task_model.allows_guest(process_instance_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class Task:
|
||||
HUMAN_TASK_TYPES = ["User Task", "Manual Task"]
|
||||
|
|
|
@ -13,7 +13,8 @@ from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
|
|||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
|
||||
SPIFF_NO_AUTH_ANONYMOUS_USER = "spiff_anonymous_user"
|
||||
SPIFF_NO_AUTH_USER = "spiff_no_auth_guest_user"
|
||||
SPIFF_GUEST_USER = "spiff_guest_user"
|
||||
|
||||
|
||||
class UserNotFoundError(Exception):
|
||||
|
|
|
@ -333,6 +333,7 @@ def task_show(
|
|||
task_model.typename = task_definition.typename
|
||||
task_model.can_complete = can_complete
|
||||
task_model.name_for_display = TaskService.get_name_for_display(task_definition)
|
||||
extensions = TaskService.get_extensions_from_task_model(task_model)
|
||||
|
||||
if with_form_data:
|
||||
task_process_identifier = task_model.bpmn_process.bpmn_process_definition.bpmn_identifier
|
||||
|
@ -353,7 +354,6 @@ def task_show(
|
|||
|
||||
form_schema_file_name = ""
|
||||
form_ui_schema_file_name = ""
|
||||
extensions = TaskService.get_extensions_from_task_model(task_model)
|
||||
task_model.signal_buttons = TaskService.get_ready_signals_with_button_labels(
|
||||
process_instance_id, task_model.guid
|
||||
)
|
||||
|
@ -409,7 +409,8 @@ def task_show(
|
|||
|
||||
_munge_form_ui_schema_based_on_hidden_fields_in_task_data(task_model)
|
||||
JinjaService.render_instructions_for_end_user(task_model, extensions)
|
||||
task_model.extensions = extensions
|
||||
|
||||
task_model.extensions = extensions
|
||||
|
||||
return make_response(jsonify(task_model), 200)
|
||||
|
||||
|
@ -736,7 +737,13 @@ def _task_submit_shared(
|
|||
)
|
||||
if next_human_task_assigned_to_me:
|
||||
return make_response(jsonify(HumanTaskModel.to_task(next_human_task_assigned_to_me)), 200)
|
||||
elif processor.next_task():
|
||||
|
||||
if "guestConfirmation" in spiff_task.task_spec.extensions:
|
||||
return make_response(
|
||||
jsonify({"guest_confirmation": spiff_task.task_spec.extensions["guestConfirmation"]}), 200
|
||||
)
|
||||
|
||||
if processor.next_task():
|
||||
task = ProcessInstanceService.spiff_task_to_api_task(processor, processor.next_task())
|
||||
return make_response(jsonify(task), 200)
|
||||
|
||||
|
|
|
@ -16,16 +16,16 @@ from werkzeug.wrappers import Response
|
|||
|
||||
from spiffworkflow_backend.exceptions.api_error import ApiError
|
||||
from spiffworkflow_backend.helpers.api_version import V1_API_PATH_PREFIX
|
||||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.group import SPIFF_NO_AUTH_ANONYMOUS_GROUP
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
from spiffworkflow_backend.models.user import SPIFF_NO_AUTH_ANONYMOUS_USER
|
||||
from spiffworkflow_backend.models.group import SPIFF_GUEST_GROUP
|
||||
from spiffworkflow_backend.models.group import SPIFF_NO_AUTH_GROUP
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
||||
from spiffworkflow_backend.models.user import SPIFF_NO_AUTH_USER
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.authentication_service import AuthenticationService
|
||||
from spiffworkflow_backend.services.authentication_service import MissingAccessTokenError
|
||||
from spiffworkflow_backend.services.authentication_service import TokenExpiredError
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.group_service import GroupService
|
||||
from spiffworkflow_backend.services.user_service import UserService
|
||||
|
||||
"""
|
||||
|
@ -82,19 +82,8 @@ def verify_token(token: str | None = None, force_run: bool | None = False) -> No
|
|||
f"Exception in verify_token getting user from decoded internal token. {e}"
|
||||
)
|
||||
|
||||
# if the user is the anonymous user and we have auth enabled then make sure we clean up the anonymouse user
|
||||
if (
|
||||
user_model
|
||||
and not current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED")
|
||||
and user_model.username == SPIFF_NO_AUTH_ANONYMOUS_USER
|
||||
and user_model.service_id == "spiff_anonymous_service_id"
|
||||
):
|
||||
group_model = GroupModel.query.filter_by(identifier=SPIFF_NO_AUTH_ANONYMOUS_GROUP).first()
|
||||
db.session.delete(group_model)
|
||||
db.session.delete(user_model)
|
||||
db.session.commit()
|
||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||
tld.user_has_logged_out = True
|
||||
# if the user is forced logged out then stop processing the token
|
||||
if _force_logout_user_if_necessary(user_model):
|
||||
return None
|
||||
|
||||
elif "iss" in decoded_token.keys():
|
||||
|
@ -211,20 +200,22 @@ def set_new_access_token_in_cookie(
|
|||
return response
|
||||
|
||||
|
||||
def login(redirect_url: str = "/") -> Response:
|
||||
def login(redirect_url: str = "/", process_instance_id: int | None = None, task_guid: str | None = None) -> Response:
|
||||
if current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED"):
|
||||
user = UserModel.query.filter_by(username=SPIFF_NO_AUTH_ANONYMOUS_USER).first()
|
||||
if user is None:
|
||||
user = UserService.create_user(
|
||||
SPIFF_NO_AUTH_ANONYMOUS_USER, "spiff_anonymous_service", "spiff_anonymous_service_id"
|
||||
)
|
||||
GroupService.add_user_to_group_or_add_to_waiting(user.username, SPIFF_NO_AUTH_ANONYMOUS_GROUP)
|
||||
AuthorizationService.add_permission_from_uri_or_macro(SPIFF_NO_AUTH_ANONYMOUS_GROUP, "all", "/*")
|
||||
g.user = user
|
||||
g.token = user.encode_auth_token({"authentication_disabled": True})
|
||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||
tld.new_access_token = g.token
|
||||
tld.new_id_token = g.token
|
||||
AuthorizationService.create_guest_token(
|
||||
username=SPIFF_NO_AUTH_USER,
|
||||
group_identifier=SPIFF_NO_AUTH_GROUP,
|
||||
permission_target="/*",
|
||||
auth_token_properties={"authentication_disabled": True},
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
|
||||
if process_instance_id and task_guid and TaskModel.task_guid_allows_guest(task_guid, process_instance_id):
|
||||
AuthorizationService.create_guest_token(
|
||||
username=SPIFF_GUEST_USER,
|
||||
group_identifier=SPIFF_GUEST_GROUP,
|
||||
auth_token_properties={"only_guest_task_completion": True},
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
|
||||
state = AuthenticationService.generate_state(redirect_url)
|
||||
|
@ -375,3 +366,25 @@ def _clear_auth_tokens_from_thread_local_data() -> None:
|
|||
delattr(tld, "new_id_token")
|
||||
if hasattr(tld, "user_has_logged_out"):
|
||||
delattr(tld, "user_has_logged_out")
|
||||
|
||||
|
||||
def _force_logout_user_if_necessary(user_model: UserModel | None = None) -> bool:
|
||||
"""Logs out a guest user if certain criteria gets met.
|
||||
|
||||
* if the user is a no auth guest and we have auth enabled
|
||||
* if the user is a guest and goes somewhere else that does not allow guests
|
||||
"""
|
||||
if user_model is not None:
|
||||
if (
|
||||
not current_app.config.get("SPIFFWORKFLOW_BACKEND_AUTHENTICATION_DISABLED")
|
||||
and user_model.username == SPIFF_NO_AUTH_USER
|
||||
and user_model.service_id == "spiff_guest_service_id"
|
||||
) or (
|
||||
user_model.username == SPIFF_GUEST_USER
|
||||
and user_model.service_id == "spiff_guest_service_id"
|
||||
and not AuthorizationService.request_allows_guest_access()
|
||||
):
|
||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||
tld.user_has_logged_out = True
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
from typing import Any
|
||||
|
||||
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
|
||||
from spiffworkflow_backend.scripts.script import Script
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
|
||||
|
||||
class GetCurrentTaskInfo(Script):
|
||||
@staticmethod
|
||||
def requires_privileged_permissions() -> bool:
|
||||
"""We have deemed this function safe to run without elevated permissions."""
|
||||
return False
|
||||
|
||||
def get_description(self) -> str:
|
||||
return """Returns the information about the current task."""
|
||||
|
||||
def run(self, script_attributes_context: ScriptAttributesContext, *_args: Any, **kwargs: Any) -> Any:
|
||||
task_dict = ProcessInstanceProcessor._serializer.task_to_dict(script_attributes_context.task)
|
||||
task_dict.pop("data")
|
||||
return task_dict
|
|
@ -0,0 +1,46 @@
|
|||
from typing import Any
|
||||
|
||||
from flask import current_app
|
||||
from spiffworkflow_backend.models.script_attributes_context import ScriptAttributesContext
|
||||
from spiffworkflow_backend.scripts.script import Script
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
|
||||
|
||||
class GetUrlForTaskWithBpmnIdentifier(Script):
|
||||
@staticmethod
|
||||
def requires_privileged_permissions() -> bool:
|
||||
"""We have deemed this function safe to run without elevated permissions."""
|
||||
return False
|
||||
|
||||
def get_description(self) -> str:
|
||||
return (
|
||||
"Returns the url to the task show page for a task with the given bpmn identifier. The script task calling"
|
||||
" this MUST be in the same process as the desired task and should be next to each other in the diagram."
|
||||
)
|
||||
|
||||
def run(self, script_attributes_context: ScriptAttributesContext, *args: Any, **kwargs: Any) -> Any:
|
||||
bpmn_identifier = args[0]
|
||||
if bpmn_identifier is None:
|
||||
raise Exception("Bpmn identifier is required for get_url_for_task_with_bpmn_identifier")
|
||||
|
||||
spiff_task = script_attributes_context.task
|
||||
if spiff_task is None:
|
||||
raise Exception("Initial spiff task not given to get_url_for_task_with_bpmn_identifier")
|
||||
|
||||
desired_spiff_task = ProcessInstanceProcessor.get_task_by_bpmn_identifier(bpmn_identifier, spiff_task.workflow)
|
||||
if desired_spiff_task is None:
|
||||
raise Exception(
|
||||
f"Could not find a task with bpmn identifier '{bpmn_identifier}' in"
|
||||
" get_url_for_task_with_bpmn_identifier"
|
||||
)
|
||||
|
||||
if not desired_spiff_task.task_spec.manual:
|
||||
raise Exception(
|
||||
f"Given bpmn identifier ({bpmn_identifier}) represents a task that cannot be completed by people and"
|
||||
" therefore it does not have a url to retrieve"
|
||||
)
|
||||
|
||||
guid = str(desired_spiff_task.id)
|
||||
fe_url = current_app.config["SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND"]
|
||||
url = f"{fe_url}/tasks/{script_attributes_context.process_instance_id}/{guid}"
|
||||
return url
|
|
@ -20,6 +20,7 @@ from spiffworkflow_backend.models.permission_assignment import PermissionAssignm
|
|||
from spiffworkflow_backend.models.permission_target import PermissionTargetModel
|
||||
from spiffworkflow_backend.models.principal import MissingPrincipalError
|
||||
from spiffworkflow_backend.models.principal import PrincipalModel
|
||||
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.models.user_group_assignment import UserGroupAssignmentModel
|
||||
from spiffworkflow_backend.routes.openid_blueprint import openid_blueprint
|
||||
|
@ -132,6 +133,24 @@ class AuthorizationService:
|
|||
"unauthorized",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create_guest_token(
|
||||
cls,
|
||||
username: str,
|
||||
group_identifier: str,
|
||||
permission_target: str | None = None,
|
||||
permission: str = "all",
|
||||
auth_token_properties: dict | None = None,
|
||||
) -> None:
|
||||
guest_user = GroupService.find_or_create_guest_user(username=username, group_identifier=group_identifier)
|
||||
if permission_target is not None:
|
||||
cls.add_permission_from_uri_or_macro(group_identifier, permission=permission, target=permission_target)
|
||||
g.user = guest_user
|
||||
g.token = guest_user.encode_auth_token(auth_token_properties)
|
||||
tld = current_app.config["THREAD_LOCAL_DATA"]
|
||||
tld.new_access_token = g.token
|
||||
tld.new_id_token = g.token
|
||||
|
||||
@classmethod
|
||||
def has_permission(cls, principals: list[PrincipalModel], permission: str, target_uri: str) -> bool:
|
||||
principal_ids = [p.id for p in principals]
|
||||
|
@ -297,15 +316,15 @@ class AuthorizationService:
|
|||
if cls.should_disable_auth_for_request():
|
||||
return None
|
||||
|
||||
authorization_exclusion_list = ["permissions_check"]
|
||||
|
||||
if not hasattr(g, "user"):
|
||||
raise UserNotLoggedInError(
|
||||
"User is not logged in. Please log in",
|
||||
)
|
||||
|
||||
api_view_function = current_app.view_functions[request.endpoint]
|
||||
if api_view_function and api_view_function.__name__ in authorization_exclusion_list:
|
||||
if cls.request_is_excluded_from_permission_check():
|
||||
return None
|
||||
|
||||
if cls.request_allows_guest_access():
|
||||
return None
|
||||
|
||||
permission_string = cls.get_permission_from_http_method(request.method)
|
||||
|
@ -325,6 +344,27 @@ class AuthorizationService:
|
|||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def request_is_excluded_from_permission_check(cls) -> bool:
|
||||
authorization_exclusion_list = ["permissions_check"]
|
||||
api_view_function = current_app.view_functions[request.endpoint]
|
||||
if api_view_function and api_view_function.__name__ in authorization_exclusion_list:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def request_allows_guest_access(cls) -> bool:
|
||||
if cls.request_is_excluded_from_permission_check():
|
||||
return True
|
||||
|
||||
api_view_function = current_app.view_functions[request.endpoint]
|
||||
if api_view_function.__name__ in ["task_show", "task_submit", "task_save_draft"]:
|
||||
process_instance_id = int(request.path.split("/")[3])
|
||||
task_guid = request.path.split("/")[4]
|
||||
if TaskModel.task_guid_allows_guest(task_guid, process_instance_id):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def decode_auth_token(auth_token: str) -> dict[str, str | None]:
|
||||
secret_key = current_app.config.get("SECRET_KEY")
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from spiffworkflow_backend.models.db import db
|
||||
from spiffworkflow_backend.models.group import SPIFF_GUEST_GROUP
|
||||
from spiffworkflow_backend.models.group import GroupModel
|
||||
from spiffworkflow_backend.models.user import SPIFF_GUEST_USER
|
||||
from spiffworkflow_backend.models.user import UserModel
|
||||
from spiffworkflow_backend.services.user_service import UserService
|
||||
|
||||
|
@ -23,3 +25,16 @@ class GroupService:
|
|||
UserService.add_user_to_group(user, group)
|
||||
else:
|
||||
UserService.add_waiting_group_assignment(username, group)
|
||||
|
||||
@classmethod
|
||||
def find_or_create_guest_user(
|
||||
cls, username: str = SPIFF_GUEST_USER, group_identifier: str = SPIFF_GUEST_GROUP
|
||||
) -> UserModel:
|
||||
guest_user: UserModel | None = UserModel.query.filter_by(
|
||||
username=username, service="spiff_guest_service", service_id="spiff_guest_service_id"
|
||||
).first()
|
||||
if guest_user is None:
|
||||
guest_user = UserService.create_user(username, "spiff_guest_service", "spiff_guest_service_id")
|
||||
GroupService.add_user_to_group_or_add_to_waiting(guest_user.username, group_identifier)
|
||||
|
||||
return guest_user
|
||||
|
|
|
@ -72,6 +72,7 @@ from spiffworkflow_backend.scripts.script import Script
|
|||
from spiffworkflow_backend.services.custom_parser import MyCustomParser
|
||||
from spiffworkflow_backend.services.element_units_service import ElementUnitsService
|
||||
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||
from spiffworkflow_backend.services.group_service import GroupService
|
||||
from spiffworkflow_backend.services.jinja_service import JinjaHelpers
|
||||
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService
|
||||
from spiffworkflow_backend.services.process_instance_tmp_service import ProcessInstanceTmpService
|
||||
|
@ -780,7 +781,11 @@ class ProcessInstanceProcessor:
|
|||
|
||||
potential_owner_ids = []
|
||||
lane_assignment_id = None
|
||||
if re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
|
||||
|
||||
if "allowGuest" in task.task_spec.extensions and task.task_spec.extensions["allowGuest"] == "true":
|
||||
guest_user = GroupService.find_or_create_guest_user()
|
||||
potential_owner_ids = [guest_user.id]
|
||||
elif re.match(r"(process.?)initiator", task_lane, re.IGNORECASE):
|
||||
potential_owner_ids = [self.process_instance_model.process_initiator_id]
|
||||
else:
|
||||
group_model = GroupModel.query.filter_by(identifier=task_lane).first()
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:process id="Process_czdgvu1" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_0xsrhef</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0xsrhef" sourceRef="StartEvent_1" targetRef="manual_task_one" />
|
||||
<bpmn:endEvent id="Event_1qsae34">
|
||||
<bpmn:incoming>Flow_02dvhev</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_0l1pg29" sourceRef="manual_task_one" targetRef="script_task" />
|
||||
<bpmn:sequenceFlow id="Flow_02dvhev" sourceRef="manual_task_two" targetRef="Event_1qsae34" />
|
||||
<bpmn:manualTask id="manual_task_two">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:allowGuest>true</spiffworkflow:allowGuest>
|
||||
<spiffworkflow:guestConfirmation>You have completed the task.</spiffworkflow:guestConfirmation>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_14w7df0</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_02dvhev</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:manualTask id="manual_task_one">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:allowGuest>true</spiffworkflow:allowGuest>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_0xsrhef</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0l1pg29</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_14w7df0" sourceRef="script_task" targetRef="manual_task_two" />
|
||||
<bpmn:scriptTask id="script_task">
|
||||
<bpmn:incoming>Flow_0l1pg29</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_14w7df0</bpmn:outgoing>
|
||||
<bpmn:script>a = 1</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_czdgvu1">
|
||||
<bpmndi:BPMNShape id="Event_1qsae34_di" bpmnElement="Event_1qsae34">
|
||||
<dc:Bounds x="642" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_12fbatl_di" bpmnElement="manual_task_two">
|
||||
<dc:Bounds x="500" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_05mndd3_di" bpmnElement="manual_task_one">
|
||||
<dc:Bounds x="230" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="162" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0otxewg_di" bpmnElement="script_task">
|
||||
<dc:Bounds x="380" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_0xsrhef_di" bpmnElement="Flow_0xsrhef">
|
||||
<di:waypoint x="198" y="177" />
|
||||
<di:waypoint x="230" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0l1pg29_di" bpmnElement="Flow_0l1pg29">
|
||||
<di:waypoint x="330" y="177" />
|
||||
<di:waypoint x="380" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_02dvhev_di" bpmnElement="Flow_02dvhev">
|
||||
<di:waypoint x="600" y="177" />
|
||||
<di:waypoint x="642" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_14w7df0_di" bpmnElement="Flow_14w7df0">
|
||||
<di:waypoint x="480" y="177" />
|
||||
<di:waypoint x="500" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"description": "",
|
||||
"display_name": "hey",
|
||||
"exception_notification_addresses": [],
|
||||
"fault_or_suspend_on_exception": "fault",
|
||||
"metadata_extraction_paths": null,
|
||||
"primary_file_name": "test-get-current-task-info-script.bpmn",
|
||||
"primary_process_id": "Process_vfh8kgr"
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:process id="Process_vfh8kgr" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1jnpyt6</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1jnpyt6" sourceRef="StartEvent_1" targetRef="get_task_info" />
|
||||
<bpmn:endEvent id="Event_1py5403">
|
||||
<bpmn:incoming>Flow_0gcbbpt</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1hi0iix" sourceRef="get_task_info" targetRef="manual_task" />
|
||||
<bpmn:scriptTask id="get_task_info">
|
||||
<bpmn:incoming>Flow_1jnpyt6</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1hi0iix</bpmn:outgoing>
|
||||
<bpmn:script>script_task_info = get_current_task_info()</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:sequenceFlow id="Flow_0gcbbpt" sourceRef="manual_task" targetRef="Event_1py5403" />
|
||||
<bpmn:manualTask id="manual_task">
|
||||
<bpmn:extensionElements>
|
||||
<spiffworkflow:postScript />
|
||||
<spiffworkflow:preScript>manual_task_info = get_current_task_info()</spiffworkflow:preScript>
|
||||
</bpmn:extensionElements>
|
||||
<bpmn:incoming>Flow_1hi0iix</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_0gcbbpt</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_vfh8kgr">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_18p9gvm_di" bpmnElement="get_task_info">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1py5403_di" bpmnElement="Event_1py5403">
|
||||
<dc:Bounds x="592" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0n1z5ct_di" bpmnElement="manual_task">
|
||||
<dc:Bounds x="420" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_1jnpyt6_di" bpmnElement="Flow_1jnpyt6">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1hi0iix_di" bpmnElement="Flow_1hi0iix">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="420" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_0gcbbpt_di" bpmnElement="Flow_0gcbbpt">
|
||||
<di:waypoint x="520" y="177" />
|
||||
<di:waypoint x="592" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"description": "",
|
||||
"display_name": "test-get-url-for-task",
|
||||
"exception_notification_addresses": [],
|
||||
"fault_or_suspend_on_exception": "fault",
|
||||
"metadata_extraction_paths": null,
|
||||
"primary_file_name": "test-get-url-for-task.bpmn",
|
||||
"primary_process_id": "Process_2jd03k0"
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||
<bpmn:process id="Process_2jd03k0" isExecutable="true">
|
||||
<bpmn:startEvent id="StartEvent_1">
|
||||
<bpmn:outgoing>Flow_1sk03xw</bpmn:outgoing>
|
||||
</bpmn:startEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1sk03xw" sourceRef="StartEvent_1" targetRef="script_task" />
|
||||
<bpmn:endEvent id="Event_1skunad">
|
||||
<bpmn:incoming>Flow_1xw30wr</bpmn:incoming>
|
||||
</bpmn:endEvent>
|
||||
<bpmn:sequenceFlow id="Flow_1ckjs49" sourceRef="script_task" targetRef="manual_task" />
|
||||
<bpmn:scriptTask id="script_task">
|
||||
<bpmn:incoming>Flow_1sk03xw</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1ckjs49</bpmn:outgoing>
|
||||
<bpmn:script>url = get_url_for_task_with_bpmn_identifier("manual_task")</bpmn:script>
|
||||
</bpmn:scriptTask>
|
||||
<bpmn:manualTask id="manual_task">
|
||||
<bpmn:incoming>Flow_1ckjs49</bpmn:incoming>
|
||||
<bpmn:outgoing>Flow_1xw30wr</bpmn:outgoing>
|
||||
</bpmn:manualTask>
|
||||
<bpmn:sequenceFlow id="Flow_1xw30wr" sourceRef="manual_task" targetRef="Event_1skunad" />
|
||||
</bpmn:process>
|
||||
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_2jd03k0">
|
||||
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_15lnnd2_di" bpmnElement="script_task">
|
||||
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Event_1skunad_di" bpmnElement="Event_1skunad">
|
||||
<dc:Bounds x="552" y="159" width="36" height="36" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="Activity_0ozc1ka_di" bpmnElement="manual_task">
|
||||
<dc:Bounds x="410" y="137" width="100" height="80" />
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="Flow_1sk03xw_di" bpmnElement="Flow_1sk03xw">
|
||||
<di:waypoint x="215" y="177" />
|
||||
<di:waypoint x="270" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1ckjs49_di" bpmnElement="Flow_1ckjs49">
|
||||
<di:waypoint x="370" y="177" />
|
||||
<di:waypoint x="410" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="Flow_1xw30wr_di" bpmnElement="Flow_1xw30wr">
|
||||
<di:waypoint x="510" y="177" />
|
||||
<di:waypoint x="552" y="177" />
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</bpmn:definitions>
|
|
@ -31,7 +31,6 @@ class TestTasksController(BaseTest):
|
|||
with_super_admin_user,
|
||||
process_group_id=process_group_id,
|
||||
process_model_id=process_model_id,
|
||||
# bpmn_file_name=bpmn_file_name,
|
||||
bpmn_file_location=bpmn_file_location,
|
||||
)
|
||||
|
||||
|
@ -387,3 +386,83 @@ class TestTasksController(BaseTest):
|
|||
assert response.json is not None
|
||||
assert response.json["saved_form_data"] is None
|
||||
assert response.json["data"]["HEY"] == draft_data["HEY"]
|
||||
|
||||
def test_can_complete_complete_a_guest_task(
|
||||
self,
|
||||
app: Flask,
|
||||
client: FlaskClient,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
with_super_admin_user: UserModel,
|
||||
) -> None:
|
||||
process_group_id = "my_process_group"
|
||||
process_model_id = "test-allow-guest"
|
||||
bpmn_file_location = "test-allow-guest"
|
||||
process_model = self.create_group_and_model_with_bpmn(
|
||||
client,
|
||||
with_super_admin_user,
|
||||
process_group_id=process_group_id,
|
||||
process_model_id=process_model_id,
|
||||
bpmn_file_location=bpmn_file_location,
|
||||
)
|
||||
|
||||
headers = self.logged_in_headers(with_super_admin_user)
|
||||
response = self.create_process_instance_from_process_model_id_with_api(client, process_model.id, headers)
|
||||
assert response.json is not None
|
||||
process_instance_id = response.json["id"]
|
||||
|
||||
response = client.post(
|
||||
f"/v1.0/process-instances/{self.modify_process_identifier_for_path_param(process_model.id)}/{process_instance_id}/run",
|
||||
headers=self.logged_in_headers(with_super_admin_user),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
assert "next_task" in response.json
|
||||
task_guid = response.json["next_task"]["id"]
|
||||
assert task_guid is not None
|
||||
|
||||
# log in a guest user to complete the tasks
|
||||
redirect_url = "/test-redirect-dne"
|
||||
response = client.get(
|
||||
f"/v1.0/login?process_instance_id={process_instance_id}&task_guid={task_guid}&redirect_url={redirect_url}",
|
||||
)
|
||||
assert response.status_code == 302
|
||||
assert response.location == redirect_url
|
||||
headers_dict = dict(response.headers)
|
||||
assert "Set-Cookie" in headers_dict
|
||||
cookie = headers_dict["Set-Cookie"]
|
||||
access_token = cookie.split(";")[0].split("=")[1]
|
||||
assert access_token is not None
|
||||
|
||||
# ensure guest user can get and complete both guest manual tasks
|
||||
response = client.get(
|
||||
f"/v1.0/tasks/{process_instance_id}/{task_guid}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
response = client.put(
|
||||
f"/v1.0/tasks/{process_instance_id}/{task_guid}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
task_guid = response.json["id"]
|
||||
assert task_guid is not None
|
||||
response = client.put(
|
||||
f"/v1.0/tasks/{process_instance_id}/{task_guid}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json is not None
|
||||
assert "guest_confirmation" in response.json
|
||||
|
||||
# ensure user gets logged out when they try to go anywhere else
|
||||
response = client.get(
|
||||
"/v1.0/tasks",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
headers_dict = dict(response.headers)
|
||||
assert "Set-Cookie" in headers_dict
|
||||
cookie = headers_dict["Set-Cookie"]
|
||||
access_token = cookie.split(";")[0].split("=")[1]
|
||||
assert access_token == ""
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
from flask.app import Flask
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
|
||||
class TestGetCurrentTaskInfo(BaseTest):
|
||||
def test_get_current_task_info_works(
|
||||
self,
|
||||
app: Flask,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
initiator_user = self.find_or_create_user("initiator_user")
|
||||
assert initiator_user.principal is not None
|
||||
AuthorizationService.import_permissions_from_yaml_file()
|
||||
|
||||
process_model = load_test_spec(
|
||||
process_model_id="misc/test-get-current-task-info-script",
|
||||
process_model_source_directory="test-get-current-task-info-script",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(
|
||||
process_model=process_model, user=initiator_user
|
||||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True)
|
||||
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
human_task = process_instance.active_human_tasks[0]
|
||||
assert len(human_task.potential_owners) == 1
|
||||
assert human_task.potential_owners[0] == initiator_user
|
||||
|
||||
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
|
||||
human_task.task_name, processor.bpmn_process_instance
|
||||
)
|
||||
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, initiator_user, human_task)
|
||||
assert process_instance.status == ProcessInstanceStatus.complete.value
|
||||
assert spiff_task is not None
|
||||
assert "script_task_info" in spiff_task.data
|
||||
assert spiff_task.data["script_task_info"]["id"] is not None
|
||||
assert "manual_task_info" in spiff_task.data
|
||||
assert spiff_task.data["manual_task_info"]["id"] is not None
|
||||
assert isinstance(spiff_task.data["manual_task_info"]["id"], str)
|
|
@ -0,0 +1,46 @@
|
|||
from flask.app import Flask
|
||||
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
|
||||
from spiffworkflow_backend.services.authorization_service import AuthorizationService
|
||||
from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor
|
||||
from spiffworkflow_backend.services.process_instance_service import ProcessInstanceService
|
||||
|
||||
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||
from tests.spiffworkflow_backend.helpers.test_data import load_test_spec
|
||||
|
||||
|
||||
class TestGetUrlForTaskWithBpmnIdentifier(BaseTest):
|
||||
def test_get_url_for_task_works(
|
||||
self,
|
||||
app: Flask,
|
||||
with_db_and_bpmn_file_cleanup: None,
|
||||
) -> None:
|
||||
initiator_user = self.find_or_create_user("initiator_user")
|
||||
assert initiator_user.principal is not None
|
||||
AuthorizationService.import_permissions_from_yaml_file()
|
||||
|
||||
process_model = load_test_spec(
|
||||
process_model_id="misc/test-get-url-for-task-with-bpmn-identifier",
|
||||
process_model_source_directory="test-get-url-for-task-with-bpmn-identifier",
|
||||
)
|
||||
process_instance = self.create_process_instance_from_process_model(
|
||||
process_model=process_model, user=initiator_user
|
||||
)
|
||||
processor = ProcessInstanceProcessor(process_instance)
|
||||
processor.do_engine_steps(save=True)
|
||||
|
||||
assert len(process_instance.active_human_tasks) == 1
|
||||
human_task = process_instance.active_human_tasks[0]
|
||||
assert len(human_task.potential_owners) == 1
|
||||
assert human_task.potential_owners[0] == initiator_user
|
||||
|
||||
spiff_task = processor.__class__.get_task_by_bpmn_identifier(
|
||||
human_task.task_name, processor.bpmn_process_instance
|
||||
)
|
||||
ProcessInstanceService.complete_form_task(processor, spiff_task, {}, initiator_user, human_task)
|
||||
assert process_instance.status == ProcessInstanceStatus.complete.value
|
||||
assert spiff_task is not None
|
||||
assert "url" in spiff_task.data
|
||||
|
||||
fe_url = app.config["SPIFFWORKFLOW_BACKEND_URL_FOR_FRONTEND"]
|
||||
expected_url = f"{fe_url}/tasks/{process_instance.id}/{str(spiff_task.id)}"
|
||||
assert spiff_task.data["url"] == expected_url
|
|
@ -63,8 +63,13 @@ export default function NavigationBar() {
|
|||
[targetUris.messageInstanceListPath]: ['GET'],
|
||||
[targetUris.secretListPath]: ['GET'],
|
||||
[targetUris.dataStoreListPath]: ['GET'],
|
||||
[targetUris.extensionListPath]: ['GET'],
|
||||
[targetUris.processInstanceListForMePath]: ['POST'],
|
||||
[targetUris.processGroupListPath]: ['GET'],
|
||||
};
|
||||
const { ability } = usePermissionFetcher(permissionRequestData);
|
||||
const { ability, permissionsLoaded } = usePermissionFetcher(
|
||||
permissionRequestData
|
||||
);
|
||||
|
||||
// default to readthedocs and let someone specify an environment variable to override:
|
||||
//
|
||||
|
@ -97,7 +102,12 @@ export default function NavigationBar() {
|
|||
setActiveKey(newActiveKey);
|
||||
}, [location]);
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
useEffect(() => {
|
||||
if (!permissionsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const processExtensionResult = (processModels: ProcessModel[]) => {
|
||||
const eni: UiSchemaNavItem[] = processModels
|
||||
.map((processModel: ProcessModel) => {
|
||||
|
@ -126,11 +136,13 @@ export default function NavigationBar() {
|
|||
}
|
||||
};
|
||||
|
||||
HttpService.makeCallToBackend({
|
||||
path: targetUris.extensionListPath,
|
||||
successCallback: processExtensionResult,
|
||||
});
|
||||
}, [targetUris.extensionListPath]);
|
||||
if (ability.can('GET', targetUris.extensionListPath)) {
|
||||
HttpService.makeCallToBackend({
|
||||
path: targetUris.extensionListPath,
|
||||
successCallback: processExtensionResult,
|
||||
});
|
||||
}
|
||||
}, [targetUris.extensionListPath, permissionsLoaded, ability]);
|
||||
|
||||
const isActivePage = (menuItemPath: string) => {
|
||||
return activeKey === menuItemPath;
|
||||
|
@ -278,19 +290,27 @@ export default function NavigationBar() {
|
|||
<HeaderMenuItem href="/" isCurrentPage={isActivePage('/')}>
|
||||
Home
|
||||
</HeaderMenuItem>
|
||||
<HeaderMenuItem
|
||||
href="/admin/process-groups"
|
||||
isCurrentPage={isActivePage('/admin/process-groups')}
|
||||
data-qa="header-nav-processes"
|
||||
<Can I="GET" a={targetUris.processGroupListPath} ability={ability}>
|
||||
<HeaderMenuItem
|
||||
href="/admin/process-groups"
|
||||
isCurrentPage={isActivePage('/admin/process-groups')}
|
||||
data-qa="header-nav-processes"
|
||||
>
|
||||
Processes
|
||||
</HeaderMenuItem>
|
||||
</Can>
|
||||
<Can
|
||||
I="POST"
|
||||
a={targetUris.processInstanceListForMePath}
|
||||
ability={ability}
|
||||
>
|
||||
Processes
|
||||
</HeaderMenuItem>
|
||||
<HeaderMenuItem
|
||||
href="/admin/process-instances"
|
||||
isCurrentPage={isActivePage('/admin/process-instances')}
|
||||
>
|
||||
Process Instances
|
||||
</HeaderMenuItem>
|
||||
<HeaderMenuItem
|
||||
href="/admin/process-instances"
|
||||
isCurrentPage={isActivePage('/admin/process-instances')}
|
||||
>
|
||||
Process Instances
|
||||
</HeaderMenuItem>
|
||||
</Can>
|
||||
<Can I="GET" a={targetUris.messageInstanceListPath} ability={ability}>
|
||||
<HeaderMenuItem
|
||||
href="/admin/messages"
|
||||
|
@ -313,7 +333,7 @@ export default function NavigationBar() {
|
|||
);
|
||||
};
|
||||
|
||||
if (activeKey && ability) {
|
||||
if (activeKey && ability && !UserService.onlyGuestTaskCompletion()) {
|
||||
return (
|
||||
<HeaderContainer
|
||||
render={({ isSideNavExpanded, onClickSideNavExpand }: any) => (
|
||||
|
|
|
@ -24,6 +24,7 @@ export default function ProcessBreadcrumb({ hotCrumbs }: OwnProps) {
|
|||
if ('entityToExplode' in crumb) {
|
||||
const { entityToExplode, entityType } = crumb;
|
||||
if (entityType === 'process-model-id') {
|
||||
console.log('crumb', crumb);
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/process-models/${modifyProcessIdentifierForPathParam(
|
||||
entityToExplode as string
|
||||
|
|
|
@ -70,6 +70,8 @@ export interface BasicTask {
|
|||
process_model_identifier: string;
|
||||
name_for_display: string;
|
||||
can_complete: boolean;
|
||||
|
||||
extensions?: any;
|
||||
}
|
||||
|
||||
// TODO: merge with ProcessInstanceTask
|
||||
|
|
|
@ -11,14 +11,17 @@ export default function OnboardingView() {
|
|||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname.match(/^\/tasks\/\d+\//)) {
|
||||
return;
|
||||
}
|
||||
HttpService.makeCallToBackend({
|
||||
path: `/onboarding`,
|
||||
successCallback: setOnboarding,
|
||||
});
|
||||
}, [setOnboarding]);
|
||||
}, [setOnboarding, location.pathname]);
|
||||
|
||||
const onboardingElement = () => {
|
||||
if (location.pathname.match(/^\/tasks\/\d+\/\b/)) {
|
||||
if (location.pathname.match(/^\/tasks\/\d+\//)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||
import { Grid, Column, Button, ButtonSet, Loading } from '@carbon/react';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import MDEditor from '@uiw/react-md-editor';
|
||||
import HttpService from '../services/HttpService';
|
||||
import useAPIError from '../hooks/UseApiError';
|
||||
import {
|
||||
|
@ -12,17 +13,26 @@ import {
|
|||
recursivelyChangeNullAndUndefined,
|
||||
} from '../helpers';
|
||||
import { BasicTask, EventDefinition, Task } from '../interfaces';
|
||||
import CustomForm from '../components/CustomForm';
|
||||
import ProcessBreadcrumb from '../components/ProcessBreadcrumb';
|
||||
import InstructionsForEndUser from '../components/InstructionsForEndUser';
|
||||
import CustomForm from '../components/CustomForm';
|
||||
import UserService from '../services/UserService';
|
||||
|
||||
export default function TaskShow() {
|
||||
// get a basic task which doesn't get the form data so we can load
|
||||
// the basic form and structure of the page without waiting for form data.
|
||||
// this was mainly to help with loading form data with large files attached to it
|
||||
const [basicTask, setBasicTask] = useState<BasicTask | null>(null);
|
||||
const [taskWithTaskData, setTaskWithTaskData] = useState<Task | null>(null);
|
||||
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const [formButtonsDisabled, setFormButtonsDisabled] = useState(false);
|
||||
|
||||
const [guestConfirmationText, setGuestConfirmationText] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
|
||||
const [taskData, setTaskData] = useState<any>(null);
|
||||
const [autosaveOnFormChanges, setAutosaveOnFormChanges] =
|
||||
useState<boolean>(true);
|
||||
|
@ -32,11 +42,15 @@ export default function TaskShow() {
|
|||
// if a user can complete a task then the for-me page should
|
||||
// always work for them so use that since it will work in all cases
|
||||
const navigateToInterstitial = (myTask: BasicTask) => {
|
||||
navigate(
|
||||
`/admin/process-instances/for-me/${modifyProcessIdentifierForPathParam(
|
||||
myTask.process_model_identifier
|
||||
)}/${myTask.process_instance_id}/interstitial`
|
||||
);
|
||||
if (UserService.onlyGuestTaskCompletion()) {
|
||||
setGuestConfirmationText('Thank you!');
|
||||
} else {
|
||||
navigate(
|
||||
`/admin/process-instances/for-me/${modifyProcessIdentifierForPathParam(
|
||||
myTask.process_model_identifier
|
||||
)}/${myTask.process_instance_id}/interstitial`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -92,13 +106,18 @@ export default function TaskShow() {
|
|||
};
|
||||
|
||||
const sendAutosaveEvent = (eventDetails?: any) => {
|
||||
(document.getElementById('hidden-form-for-autosave') as any).dispatchEvent(
|
||||
new CustomEvent('submit', {
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
detail: eventDetails,
|
||||
})
|
||||
const elementToDispath: any = document.getElementById(
|
||||
'hidden-form-for-autosave'
|
||||
);
|
||||
if (elementToDispath) {
|
||||
elementToDispath.dispatchEvent(
|
||||
new CustomEvent('submit', {
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
detail: eventDetails,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const addDebouncedTaskDataAutoSave = useDebouncedCallback(
|
||||
|
@ -115,6 +134,8 @@ export default function TaskShow() {
|
|||
removeError();
|
||||
if (result.ok) {
|
||||
navigate(`/tasks`);
|
||||
} else if (result.guest_confirmation) {
|
||||
setGuestConfirmationText(result.guest_confirmation);
|
||||
} else if (result.process_instance_id) {
|
||||
if (result.can_complete) {
|
||||
navigate(`/tasks/${result.process_instance_id}/${result.id}`);
|
||||
|
@ -239,7 +260,10 @@ export default function TaskShow() {
|
|||
let closeButton = null;
|
||||
if (taskWithTaskData.typename === 'ManualTask') {
|
||||
submitButtonText = 'Continue';
|
||||
} else if (taskWithTaskData.typename === 'UserTask') {
|
||||
} else if (
|
||||
taskWithTaskData.typename === 'UserTask' &&
|
||||
!UserService.onlyGuestTaskCompletion()
|
||||
) {
|
||||
closeButton = (
|
||||
<Button
|
||||
id="close-button"
|
||||
|
@ -248,7 +272,7 @@ export default function TaskShow() {
|
|||
kind="secondary"
|
||||
title="Save data as draft and close the form."
|
||||
>
|
||||
Save and Close
|
||||
Save and close
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -328,27 +352,39 @@ export default function TaskShow() {
|
|||
statusString = ` ${basicTask.state}`;
|
||||
}
|
||||
|
||||
pageElements.push(
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
[
|
||||
`Process Instance Id: ${params.process_instance_id}`,
|
||||
`/admin/process-instances/for-me/${modifyProcessIdentifierForPathParam(
|
||||
basicTask.process_model_identifier
|
||||
)}/${params.process_instance_id}`,
|
||||
],
|
||||
[`Task: ${basicTask.name_for_display || basicTask.id}`],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
pageElements.push(
|
||||
<h1>
|
||||
Task: {basicTask.name_for_display} (
|
||||
{basicTask.process_model_display_name}){statusString}
|
||||
</h1>
|
||||
);
|
||||
if (
|
||||
!('allowGuest' in basicTask.extensions) ||
|
||||
basicTask.extensions.allowGuest !== 'true'
|
||||
) {
|
||||
pageElements.push(
|
||||
<ProcessBreadcrumb
|
||||
hotCrumbs={[
|
||||
[
|
||||
`Process Instance Id: ${params.process_instance_id}`,
|
||||
`/admin/process-instances/for-me/${modifyProcessIdentifierForPathParam(
|
||||
basicTask.process_model_identifier
|
||||
)}/${params.process_instance_id}`,
|
||||
],
|
||||
[`Task: ${basicTask.name_for_display || basicTask.id}`],
|
||||
]}
|
||||
/>
|
||||
);
|
||||
pageElements.push(
|
||||
<h3>
|
||||
Task: {basicTask.name_for_display} (
|
||||
{basicTask.process_model_display_name}){statusString}
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (basicTask && taskData) {
|
||||
|
||||
if (guestConfirmationText) {
|
||||
pageElements.push(
|
||||
<div data-color-mode="light">
|
||||
<MDEditor.Markdown linkTarget="_blank" source={guestConfirmationText} />
|
||||
</div>
|
||||
);
|
||||
} else if (basicTask && taskData) {
|
||||
pageElements.push(<InstructionsForEndUser task={taskWithTaskData} />);
|
||||
pageElements.push(formElement());
|
||||
} else {
|
||||
|
|
|
@ -29,8 +29,28 @@ const getCurrentLocation = (queryParams: string = window.location.search) => {
|
|||
);
|
||||
};
|
||||
|
||||
const checkPathForTaskShowParams = () => {
|
||||
// expected url pattern:
|
||||
// /tasks/[process_instance_id]/[task_guid]
|
||||
const pathSegments = window.location.pathname.match(
|
||||
/^\/tasks\/(\d+)\/([0-9a-z]{8}-([0-9a-z]{4}-){3}[0-9a-z]{12})$/
|
||||
);
|
||||
if (pathSegments) {
|
||||
return { process_instance_id: pathSegments[1], task_guid: pathSegments[2] };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const doLogin = () => {
|
||||
const url = `${BACKEND_BASE_URL}/login?redirect_url=${getCurrentLocation()}`;
|
||||
const taskShowParams = checkPathForTaskShowParams();
|
||||
const loginParams = [`redirect_url=${getCurrentLocation()}`];
|
||||
if (taskShowParams) {
|
||||
loginParams.push(
|
||||
`process_instance_id=${taskShowParams.process_instance_id}`
|
||||
);
|
||||
loginParams.push(`task_guid=${taskShowParams.task_guid}`);
|
||||
}
|
||||
const url = `${BACKEND_BASE_URL}/login?${loginParams.join('&')}`;
|
||||
window.location.href = url;
|
||||
};
|
||||
|
||||
|
@ -71,6 +91,15 @@ const authenticationDisabled = () => {
|
|||
return false;
|
||||
};
|
||||
|
||||
const onlyGuestTaskCompletion = () => {
|
||||
const idToken = getIdToken();
|
||||
if (idToken) {
|
||||
const idObject = jwt(idToken);
|
||||
return (idObject as any).only_guest_task_completion;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getPreferredUsername = () => {
|
||||
const idToken = getIdToken();
|
||||
if (idToken) {
|
||||
|
@ -100,6 +129,7 @@ const UserService = {
|
|||
hasRole,
|
||||
isLoggedIn,
|
||||
loginIfNeeded,
|
||||
onlyGuestTaskCompletion,
|
||||
};
|
||||
|
||||
export default UserService;
|
||||
|
|
Loading…
Reference in New Issue