From 803a16b914c5e164aabfbf24cd027aeb18547969 Mon Sep 17 00:00:00 2001 From: auslin-aot Date: Thu, 12 Sep 2024 18:17:28 +0530 Subject: [PATCH 1/3] FWF-3584: [Feature] Added Publish endpoint --- .../d4618d0e45ca_added_prompt_new_version.py | 28 ++++++ .../src/formsflow_api/constants/__init__.py | 76 ++++++++++++++++ .../models/form_process_mapper.py | 1 + .../src/formsflow_api/models/process.py | 2 +- .../resources/form_process_mapper.py | 59 +++++++++++++ .../services/external/base_bpm.py | 14 +-- .../formsflow_api/services/external/bpm.py | 9 ++ .../services/form_process_mapper.py | 86 ++++++++++++++++++- 8 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py diff --git a/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py b/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py new file mode 100644 index 0000000000..bc4ddcd411 --- /dev/null +++ b/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py @@ -0,0 +1,28 @@ +"""Added prompt_new_version column in form_process_mapper + +Revision ID: d4618d0e45ca +Revises: 9929f234cef0 +Create Date: 2024-09-12 15:57:52.395411 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd4618d0e45ca' +down_revision = '9929f234cef0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('form_process_mapper', sa.Column('prompt_new_version', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('form_process_mapper', 'prompt_new_version') + # ### end Alembic commands ### diff --git a/forms-flow-api/src/formsflow_api/constants/__init__.py b/forms-flow-api/src/formsflow_api/constants/__init__.py index abf20caa1b..8086178a97 100644 --- a/forms-flow-api/src/formsflow_api/constants/__init__.py +++ b/forms-flow-api/src/formsflow_api/constants/__init__.py @@ -94,3 +94,79 @@ def message(self): def status_code(self): """Return status code.""" return self._value + + +def default_flow_xml_data(name="Defaultflow"): + """Xml data for default flow.""" + return f""" + + + + Flow_09rbji4 + + + + + execution.setVariable('applicationStatus', 'Completed'); + + + + ["applicationId","applicationStatus"] + + + + + Flow_09rbji4 + Flow_0klorcg + + + + + execution.setVariable('applicationStatus', 'New'); + + + + + + Flow_0klorcg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ diff --git a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py index d93f7cbc90..dde0eb2881 100644 --- a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py @@ -52,6 +52,7 @@ class FormProcessMapper(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model) task_variable = db.Column(JSON, nullable=True) version = db.Column(db.Integer, nullable=False, default=1) description = db.Column(db.String, nullable=True) + prompt_new_version = db.Column(db.Boolean, nullable=True, default=False) __table_args__ = ( UniqueConstraint("form_id", "version", "tenant", name="_form_version_uc"), diff --git a/forms-flow-api/src/formsflow_api/models/process.py b/forms-flow-api/src/formsflow_api/models/process.py index b99a038f4a..19324158e9 100644 --- a/forms-flow-api/src/formsflow_api/models/process.py +++ b/forms-flow-api/src/formsflow_api/models/process.py @@ -130,7 +130,7 @@ def find_all_process( def get_latest_version(cls, process_name): """Get latest version of process.""" query = ( - cls.query.filter(cls.name == process_name) + cls.auth_query(cls.query.filter(cls.name == process_name)) .order_by(cls.major_version.desc(), cls.minor_version.desc()) .first() ) diff --git a/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py b/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py index c43d5dfeab..b83f03ad53 100644 --- a/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py @@ -702,3 +702,62 @@ def get(): """ response = FormProcessMapperService.validate_form_name_path_title(request) return response, HTTPStatus.OK + + +@cors_preflight("POST,OPTIONS") +@API.route("//publish", methods=["POST", "OPTIONS"]) +class PublishResource(Resource): + """Resource to support publish.""" + + @staticmethod + @auth.has_one_of_roles([CREATE_DESIGNS]) + @profiletime + @API.response(200, "OK:- Successful request.") + @API.response( + 400, + "BAD_REQUEST:- Invalid request.", + ) + @API.response( + 401, + "UNAUTHORIZED:- Authorization header not provided or an invalid token passed.", + ) + @API.response( + 403, + "FORBIDDEN:- Authorization will not help.", + ) + def post(mapper_id: int): + """Publish by mapper_id.""" + form_service = FormProcessMapperService() + return ( + form_service.publish(mapper_id, request), + HTTPStatus.OK, + ) + + +@cors_preflight("POST,OPTIONS") +@API.route("//unpublish", methods=["POST", "OPTIONS"]) +class UnpublishResource(Resource): + """Resource to support unpublish.""" + + @staticmethod + @auth.has_one_of_roles([CREATE_DESIGNS]) + @profiletime + @API.response(200, "OK:- Successful request.") + @API.response( + 400, + "BAD_REQUEST:- Invalid request.", + ) + @API.response( + 401, + "UNAUTHORIZED:- Authorization header not provided or an invalid token passed.", + ) + @API.response( + 403, + "FORBIDDEN:- Authorization will not help.", + ) + def get(mapper_id: int): + """Unpublish by mapper_id.""" + return ( + FormProcessMapperService.unpublish(mapper_id), + HTTPStatus.OK, + ) diff --git a/forms-flow-api/src/formsflow_api/services/external/base_bpm.py b/forms-flow-api/src/formsflow_api/services/external/base_bpm.py index 26bf9dc791..5260c31965 100644 --- a/forms-flow-api/src/formsflow_api/services/external/base_bpm.py +++ b/forms-flow-api/src/formsflow_api/services/external/base_bpm.py @@ -35,11 +35,13 @@ def get_request(cls, url, token): return data @classmethod - def post_request(cls, url, token, payload=None, tenant_key=None): + def post_request(cls, url, token, payload=None, tenant_key=None, files=None): """Post HTTP request to BPM API with auth header.""" - headers = cls._get_headers_(token, tenant_key) - payload = json.dumps(payload) - response = requests.post(url, data=payload, headers=headers, timeout=120) + headers = cls._get_headers_(token, tenant_key, files) + payload = payload if files else json.dumps(payload) + response = requests.post( + url, data=payload, headers=headers, timeout=120, files=files + ) current_app.logger.debug( "POST URL : %s, Response Code : %s", url, response.status_code ) @@ -61,7 +63,7 @@ def post_request(cls, url, token, payload=None, tenant_key=None): return data @classmethod - def _get_headers_(cls, token, tenant_key=None): + def _get_headers_(cls, token, tenant_key=None, files=None): """Generate headers.""" bpm_token_api = current_app.config.get("BPM_TOKEN_API") bpm_client_id = current_app.config.get("BPM_CLIENT_ID") @@ -77,6 +79,8 @@ def _get_headers_(cls, token, tenant_key=None): "grant_type": bpm_grant_type, } if token: + if files: + return {"Authorization": token} return {"Authorization": token, "content-type": "application/json"} response = requests.post( diff --git a/forms-flow-api/src/formsflow_api/services/external/bpm.py b/forms-flow-api/src/formsflow_api/services/external/bpm.py index 5005fe3b4d..aa40082f0c 100644 --- a/forms-flow-api/src/formsflow_api/services/external/bpm.py +++ b/forms-flow-api/src/formsflow_api/services/external/bpm.py @@ -18,6 +18,7 @@ class BPMEndpointType(IntEnum): FORM_AUTH_DETAILS = 2 MESSAGE_EVENT = 3 DECISION_DEFINITION = 4 + DEPLOYMENT = 5 class BPMService(BaseBPMService): @@ -109,6 +110,12 @@ def decision_definition_xml(cls, decision_key, token, tenant_key): url += f"?tenantId={tenant_key}" return cls.get_request(url, token) + @classmethod + def post_deployment(cls, token, payload, tenant_key, files): + """Create new deployment.""" + url = f"{cls._get_url_(BPMEndpointType.DEPLOYMENT)}" + return cls.post_request(url, token, payload, tenant_key, files) + @classmethod def _get_url_(cls, endpoint_type: BPMEndpointType): """Get Url.""" @@ -123,6 +130,8 @@ def _get_url_(cls, endpoint_type: BPMEndpointType): url = f"{bpm_api_base}/engine-rest-ext/v1/message" elif endpoint_type == BPMEndpointType.DECISION_DEFINITION: url = f"{bpm_api_base}/engine-rest-ext/v1/decision-definition" + elif endpoint_type == BPMEndpointType.DEPLOYMENT: + url = f"{bpm_api_base}/engine-rest-ext/v1/deployment/create" return url except BaseException as e: # pylint: disable=broad-except diff --git a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py index 91bffa89e7..2671b216d1 100644 --- a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py @@ -10,12 +10,14 @@ from formsflow_api_utils.utils.enums import FormProcessMapperStatus from formsflow_api_utils.utils.user_context import UserContext, user_context -from formsflow_api.constants import BusinessErrorCode +from formsflow_api.constants import BusinessErrorCode, default_flow_xml_data from formsflow_api.models import ( Authorization, AuthType, Draft, + FormHistory, FormProcessMapper, + Process, ) from formsflow_api.schemas import FormProcessMapperSchema from formsflow_api.services.external.bpm import BPMService @@ -574,3 +576,85 @@ def validate_form_name_path_title(request): raise BusinessException(BusinessErrorCode.FORM_EXISTS) # If no results, the form name is valid return {} + + def deploy_process(self, process_name, process_data, tenant_key, token): + """Deploy process.""" + file_path = f"{process_name}.bpmn" + process_data = ( + process_data.encode("utf-8") + if process_data + else default_flow_xml_data(process_name).encode("utf-8") + ) + print(process_data, "process data", type(process_data)) + + # Prepare the parameters for the deployment + payload = { + "deployment-name": process_name, + "enable-duplicate-filtering": "true", + "deploy-changed-only": "false", + "deployment-source": "Camunda Modeler", + "tenant-id": tenant_key, + } + files = {"upload": (file_path, process_data, "text/bpmn")} + BPMService.post_deployment(token, payload, tenant_key, files) + + @user_context + def publish(self, mapper_id, request, **kwargs): + """Publish by mapper_id.""" + user: UserContext = kwargs["user"] + tenant_key = user.tenant_key + token = user.bearer_token + data = request.get_json() + mapper = FormProcessMapper.find_form_by_id(form_process_mapper_id=mapper_id) + if not mapper: + raise BusinessException(BusinessErrorCode.INVALID_FORM_PROCESS_MAPPER_ID) + + # Check tenant authentication + if tenant_key and mapper.tenant != tenant_key: + raise PermissionError("Tenant authentication failed.") + process_data = data.get("processData") + process_name = mapper.process_key + # Deploy process + self.deploy_process(process_name, process_data, tenant_key, token) + + # Update status in process table. + process = Process.get_latest_version(process_name) + if not process: + # create entry in process & publish + process = Process( + name=process_name, + process_type="LOWCODE", + process_data="[]", + form_process_mapper_id=mapper_id, + tenant=tenant_key, + major_version=1, + minor_version=0, + ) + process.status = "PUBLISHED" + process.save() + + # Capture publish status in form history table. + major_version, minor_version = 1, 0 + latest_form_history = FormHistory.get_latest_version(mapper.parent_form_id) + if latest_form_history: + major_version, minor_version = ( + latest_form_history.major_version, + latest_form_history.minor_version, + ) + FormHistory( + created_by=user.user_name, + parent_form_id=mapper.parent_form_id, + change_log={"status": "Publish"}, + status=True, + major_version=major_version, + minor_version=minor_version, + ) + + # Update status in mapper table - discuss & do + mapper.prompt_new_version = False + mapper.save() + + @staticmethod + def unpublish(mapper_id: int): + """Publish by mapper_id.""" + pass From 09143414b2fa2748b8c35a5a9c92bed5ba3ab955 Mon Sep 17 00:00:00 2001 From: auslin-aot Date: Thu, 12 Sep 2024 18:17:28 +0530 Subject: [PATCH 2/3] FWF-3584: [Feature] Added Publish endpoint --- .../d4618d0e45ca_added_prompt_new_version.py | 28 ++++++ .../src/formsflow_api/constants/__init__.py | 76 ++++++++++++++++ .../models/form_process_mapper.py | 1 + .../src/formsflow_api/models/process.py | 2 +- .../resources/form_process_mapper.py | 59 +++++++++++++ .../services/external/base_bpm.py | 14 +-- .../formsflow_api/services/external/bpm.py | 9 ++ .../services/form_process_mapper.py | 86 ++++++++++++++++++- 8 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py diff --git a/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py b/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py new file mode 100644 index 0000000000..bc4ddcd411 --- /dev/null +++ b/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py @@ -0,0 +1,28 @@ +"""Added prompt_new_version column in form_process_mapper + +Revision ID: d4618d0e45ca +Revises: 9929f234cef0 +Create Date: 2024-09-12 15:57:52.395411 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd4618d0e45ca' +down_revision = '9929f234cef0' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('form_process_mapper', sa.Column('prompt_new_version', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('form_process_mapper', 'prompt_new_version') + # ### end Alembic commands ### diff --git a/forms-flow-api/src/formsflow_api/constants/__init__.py b/forms-flow-api/src/formsflow_api/constants/__init__.py index abf20caa1b..8086178a97 100644 --- a/forms-flow-api/src/formsflow_api/constants/__init__.py +++ b/forms-flow-api/src/formsflow_api/constants/__init__.py @@ -94,3 +94,79 @@ def message(self): def status_code(self): """Return status code.""" return self._value + + +def default_flow_xml_data(name="Defaultflow"): + """Xml data for default flow.""" + return f""" + + + + Flow_09rbji4 + + + + + execution.setVariable('applicationStatus', 'Completed'); + + + + ["applicationId","applicationStatus"] + + + + + Flow_09rbji4 + Flow_0klorcg + + + + + execution.setVariable('applicationStatus', 'New'); + + + + + + Flow_0klorcg + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """ diff --git a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py index d93f7cbc90..dde0eb2881 100644 --- a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py @@ -52,6 +52,7 @@ class FormProcessMapper(AuditDateTimeMixin, AuditUserMixin, BaseModel, db.Model) task_variable = db.Column(JSON, nullable=True) version = db.Column(db.Integer, nullable=False, default=1) description = db.Column(db.String, nullable=True) + prompt_new_version = db.Column(db.Boolean, nullable=True, default=False) __table_args__ = ( UniqueConstraint("form_id", "version", "tenant", name="_form_version_uc"), diff --git a/forms-flow-api/src/formsflow_api/models/process.py b/forms-flow-api/src/formsflow_api/models/process.py index c3e761fb36..732663bd46 100644 --- a/forms-flow-api/src/formsflow_api/models/process.py +++ b/forms-flow-api/src/formsflow_api/models/process.py @@ -131,7 +131,7 @@ def find_all_process( def get_latest_version(cls, process_name): """Get latest version of process.""" query = ( - cls.query.filter(cls.name == process_name) + cls.auth_query(cls.query.filter(cls.name == process_name)) .order_by(cls.major_version.desc(), cls.minor_version.desc()) .first() ) diff --git a/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py b/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py index c43d5dfeab..b83f03ad53 100644 --- a/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py @@ -702,3 +702,62 @@ def get(): """ response = FormProcessMapperService.validate_form_name_path_title(request) return response, HTTPStatus.OK + + +@cors_preflight("POST,OPTIONS") +@API.route("//publish", methods=["POST", "OPTIONS"]) +class PublishResource(Resource): + """Resource to support publish.""" + + @staticmethod + @auth.has_one_of_roles([CREATE_DESIGNS]) + @profiletime + @API.response(200, "OK:- Successful request.") + @API.response( + 400, + "BAD_REQUEST:- Invalid request.", + ) + @API.response( + 401, + "UNAUTHORIZED:- Authorization header not provided or an invalid token passed.", + ) + @API.response( + 403, + "FORBIDDEN:- Authorization will not help.", + ) + def post(mapper_id: int): + """Publish by mapper_id.""" + form_service = FormProcessMapperService() + return ( + form_service.publish(mapper_id, request), + HTTPStatus.OK, + ) + + +@cors_preflight("POST,OPTIONS") +@API.route("//unpublish", methods=["POST", "OPTIONS"]) +class UnpublishResource(Resource): + """Resource to support unpublish.""" + + @staticmethod + @auth.has_one_of_roles([CREATE_DESIGNS]) + @profiletime + @API.response(200, "OK:- Successful request.") + @API.response( + 400, + "BAD_REQUEST:- Invalid request.", + ) + @API.response( + 401, + "UNAUTHORIZED:- Authorization header not provided or an invalid token passed.", + ) + @API.response( + 403, + "FORBIDDEN:- Authorization will not help.", + ) + def get(mapper_id: int): + """Unpublish by mapper_id.""" + return ( + FormProcessMapperService.unpublish(mapper_id), + HTTPStatus.OK, + ) diff --git a/forms-flow-api/src/formsflow_api/services/external/base_bpm.py b/forms-flow-api/src/formsflow_api/services/external/base_bpm.py index 26bf9dc791..5260c31965 100644 --- a/forms-flow-api/src/formsflow_api/services/external/base_bpm.py +++ b/forms-flow-api/src/formsflow_api/services/external/base_bpm.py @@ -35,11 +35,13 @@ def get_request(cls, url, token): return data @classmethod - def post_request(cls, url, token, payload=None, tenant_key=None): + def post_request(cls, url, token, payload=None, tenant_key=None, files=None): """Post HTTP request to BPM API with auth header.""" - headers = cls._get_headers_(token, tenant_key) - payload = json.dumps(payload) - response = requests.post(url, data=payload, headers=headers, timeout=120) + headers = cls._get_headers_(token, tenant_key, files) + payload = payload if files else json.dumps(payload) + response = requests.post( + url, data=payload, headers=headers, timeout=120, files=files + ) current_app.logger.debug( "POST URL : %s, Response Code : %s", url, response.status_code ) @@ -61,7 +63,7 @@ def post_request(cls, url, token, payload=None, tenant_key=None): return data @classmethod - def _get_headers_(cls, token, tenant_key=None): + def _get_headers_(cls, token, tenant_key=None, files=None): """Generate headers.""" bpm_token_api = current_app.config.get("BPM_TOKEN_API") bpm_client_id = current_app.config.get("BPM_CLIENT_ID") @@ -77,6 +79,8 @@ def _get_headers_(cls, token, tenant_key=None): "grant_type": bpm_grant_type, } if token: + if files: + return {"Authorization": token} return {"Authorization": token, "content-type": "application/json"} response = requests.post( diff --git a/forms-flow-api/src/formsflow_api/services/external/bpm.py b/forms-flow-api/src/formsflow_api/services/external/bpm.py index 5005fe3b4d..aa40082f0c 100644 --- a/forms-flow-api/src/formsflow_api/services/external/bpm.py +++ b/forms-flow-api/src/formsflow_api/services/external/bpm.py @@ -18,6 +18,7 @@ class BPMEndpointType(IntEnum): FORM_AUTH_DETAILS = 2 MESSAGE_EVENT = 3 DECISION_DEFINITION = 4 + DEPLOYMENT = 5 class BPMService(BaseBPMService): @@ -109,6 +110,12 @@ def decision_definition_xml(cls, decision_key, token, tenant_key): url += f"?tenantId={tenant_key}" return cls.get_request(url, token) + @classmethod + def post_deployment(cls, token, payload, tenant_key, files): + """Create new deployment.""" + url = f"{cls._get_url_(BPMEndpointType.DEPLOYMENT)}" + return cls.post_request(url, token, payload, tenant_key, files) + @classmethod def _get_url_(cls, endpoint_type: BPMEndpointType): """Get Url.""" @@ -123,6 +130,8 @@ def _get_url_(cls, endpoint_type: BPMEndpointType): url = f"{bpm_api_base}/engine-rest-ext/v1/message" elif endpoint_type == BPMEndpointType.DECISION_DEFINITION: url = f"{bpm_api_base}/engine-rest-ext/v1/decision-definition" + elif endpoint_type == BPMEndpointType.DEPLOYMENT: + url = f"{bpm_api_base}/engine-rest-ext/v1/deployment/create" return url except BaseException as e: # pylint: disable=broad-except diff --git a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py index 91bffa89e7..2671b216d1 100644 --- a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py @@ -10,12 +10,14 @@ from formsflow_api_utils.utils.enums import FormProcessMapperStatus from formsflow_api_utils.utils.user_context import UserContext, user_context -from formsflow_api.constants import BusinessErrorCode +from formsflow_api.constants import BusinessErrorCode, default_flow_xml_data from formsflow_api.models import ( Authorization, AuthType, Draft, + FormHistory, FormProcessMapper, + Process, ) from formsflow_api.schemas import FormProcessMapperSchema from formsflow_api.services.external.bpm import BPMService @@ -574,3 +576,85 @@ def validate_form_name_path_title(request): raise BusinessException(BusinessErrorCode.FORM_EXISTS) # If no results, the form name is valid return {} + + def deploy_process(self, process_name, process_data, tenant_key, token): + """Deploy process.""" + file_path = f"{process_name}.bpmn" + process_data = ( + process_data.encode("utf-8") + if process_data + else default_flow_xml_data(process_name).encode("utf-8") + ) + print(process_data, "process data", type(process_data)) + + # Prepare the parameters for the deployment + payload = { + "deployment-name": process_name, + "enable-duplicate-filtering": "true", + "deploy-changed-only": "false", + "deployment-source": "Camunda Modeler", + "tenant-id": tenant_key, + } + files = {"upload": (file_path, process_data, "text/bpmn")} + BPMService.post_deployment(token, payload, tenant_key, files) + + @user_context + def publish(self, mapper_id, request, **kwargs): + """Publish by mapper_id.""" + user: UserContext = kwargs["user"] + tenant_key = user.tenant_key + token = user.bearer_token + data = request.get_json() + mapper = FormProcessMapper.find_form_by_id(form_process_mapper_id=mapper_id) + if not mapper: + raise BusinessException(BusinessErrorCode.INVALID_FORM_PROCESS_MAPPER_ID) + + # Check tenant authentication + if tenant_key and mapper.tenant != tenant_key: + raise PermissionError("Tenant authentication failed.") + process_data = data.get("processData") + process_name = mapper.process_key + # Deploy process + self.deploy_process(process_name, process_data, tenant_key, token) + + # Update status in process table. + process = Process.get_latest_version(process_name) + if not process: + # create entry in process & publish + process = Process( + name=process_name, + process_type="LOWCODE", + process_data="[]", + form_process_mapper_id=mapper_id, + tenant=tenant_key, + major_version=1, + minor_version=0, + ) + process.status = "PUBLISHED" + process.save() + + # Capture publish status in form history table. + major_version, minor_version = 1, 0 + latest_form_history = FormHistory.get_latest_version(mapper.parent_form_id) + if latest_form_history: + major_version, minor_version = ( + latest_form_history.major_version, + latest_form_history.minor_version, + ) + FormHistory( + created_by=user.user_name, + parent_form_id=mapper.parent_form_id, + change_log={"status": "Publish"}, + status=True, + major_version=major_version, + minor_version=minor_version, + ) + + # Update status in mapper table - discuss & do + mapper.prompt_new_version = False + mapper.save() + + @staticmethod + def unpublish(mapper_id: int): + """Publish by mapper_id.""" + pass From 6d8221b2fa1125f1387026267d07c289295bc169 Mon Sep 17 00:00:00 2001 From: auslin-aot <99173163+auslin-aot@users.noreply.github.com> Date: Tue, 17 Sep 2024 19:04:21 +0530 Subject: [PATCH 3/3] FWF-3584: [Feature] Added Publish & unpublish endpoint --- .../d4618d0e45ca_added_prompt_new_version.py | 2 +- .../models/form_process_mapper.py | 1 + .../src/formsflow_api/models/process.py | 7 +- .../resources/form_process_mapper.py | 7 +- .../schemas/form_process_mapper.py | 1 + .../services/external/base_bpm.py | 2 +- .../services/form_process_mapper.py | 109 ++++++++++-------- .../unit/api/test_form_process_mapper.py | 48 ++++++++ 8 files changed, 122 insertions(+), 55 deletions(-) diff --git a/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py b/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py index bc4ddcd411..7eaa02bc11 100644 --- a/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py +++ b/forms-flow-api/migrations/versions/d4618d0e45ca_added_prompt_new_version.py @@ -18,7 +18,7 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.add_column('form_process_mapper', sa.Column('prompt_new_version', sa.Boolean(), nullable=True)) + op.add_column('form_process_mapper', sa.Column('prompt_new_version', sa.Boolean(), nullable=True, server_default='false')) # ### end Alembic commands ### diff --git a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py index dde0eb2881..f856c6fde9 100644 --- a/forms-flow-api/src/formsflow_api/models/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/models/form_process_mapper.py @@ -106,6 +106,7 @@ def update(self, mapper_info: dict): "task_variable", "process_tenant", "description", + "prompt_new_version", ], mapper_info, ) diff --git a/forms-flow-api/src/formsflow_api/models/process.py b/forms-flow-api/src/formsflow_api/models/process.py index 732663bd46..5e723fe053 100644 --- a/forms-flow-api/src/formsflow_api/models/process.py +++ b/forms-flow-api/src/formsflow_api/models/process.py @@ -143,11 +143,8 @@ def fetch_histories_by_process_name(cls, process_name: str) -> List[Process]: """Fetch all versions (histories) of a process by process_name.""" assert process_name is not None - query = ( - cls.auth_query( - cls.query.filter(cls.name == process_name) - ) - .order_by(desc(cls.major_version), desc(cls.minor_version)) + query = cls.auth_query(cls.query.filter(cls.name == process_name)).order_by( + desc(cls.major_version), desc(cls.minor_version) ) return query.all() diff --git a/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py b/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py index b83f03ad53..aa4b0fa672 100644 --- a/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/resources/form_process_mapper.py @@ -729,7 +729,7 @@ def post(mapper_id: int): """Publish by mapper_id.""" form_service = FormProcessMapperService() return ( - form_service.publish(mapper_id, request), + form_service.publish(mapper_id), HTTPStatus.OK, ) @@ -755,9 +755,10 @@ class UnpublishResource(Resource): 403, "FORBIDDEN:- Authorization will not help.", ) - def get(mapper_id: int): + def post(mapper_id: int): """Unpublish by mapper_id.""" + form_service = FormProcessMapperService() return ( - FormProcessMapperService.unpublish(mapper_id), + form_service.unpublish(mapper_id), HTTPStatus.OK, ) diff --git a/forms-flow-api/src/formsflow_api/schemas/form_process_mapper.py b/forms-flow-api/src/formsflow_api/schemas/form_process_mapper.py index 093a8d6331..e7c21829fa 100644 --- a/forms-flow-api/src/formsflow_api/schemas/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/schemas/form_process_mapper.py @@ -31,6 +31,7 @@ class Meta: # pylint: disable=too-few-public-methods process_tenant = fields.Str(data_key="processTenant") deleted = fields.Boolean(data_key="deleted") description = fields.Str(data_key="description") + prompt_new_version = fields.Bool(data_key="promptNewVersion", dump_only=True) class FormProcessMapperListReqSchema(Schema): diff --git a/forms-flow-api/src/formsflow_api/services/external/base_bpm.py b/forms-flow-api/src/formsflow_api/services/external/base_bpm.py index 5260c31965..2fa0beb5a3 100644 --- a/forms-flow-api/src/formsflow_api/services/external/base_bpm.py +++ b/forms-flow-api/src/formsflow_api/services/external/base_bpm.py @@ -35,7 +35,7 @@ def get_request(cls, url, token): return data @classmethod - def post_request(cls, url, token, payload=None, tenant_key=None, files=None): + def post_request(cls, url, token, payload=None, tenant_key=None, files=None): # pylint: disable=too-many-arguments """Post HTTP request to BPM API with auth header.""" headers = cls._get_headers_(token, tenant_key, files) payload = payload if files else json.dumps(payload) diff --git a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py index 2671b216d1..8ebfb6a155 100644 --- a/forms-flow-api/src/formsflow_api/services/form_process_mapper.py +++ b/forms-flow-api/src/formsflow_api/services/form_process_mapper.py @@ -25,7 +25,7 @@ from .form_history_logs import FormHistoryService -class FormProcessMapperService: +class FormProcessMapperService: # pylint: disable=too-many-public-methods """This class manages form process mapper service.""" @staticmethod @@ -580,12 +580,12 @@ def validate_form_name_path_title(request): def deploy_process(self, process_name, process_data, tenant_key, token): """Deploy process.""" file_path = f"{process_name}.bpmn" - process_data = ( - process_data.encode("utf-8") - if process_data - else default_flow_xml_data(process_name).encode("utf-8") - ) - print(process_data, "process data", type(process_data)) + # If process data empty deploy default workflow data. + if process_data: + if isinstance(process_data, str): + process_data = process_data.encode("utf-8") + else: + process_data = default_flow_xml_data(process_name).encode("utf-8") # Prepare the parameters for the deployment payload = { @@ -598,42 +598,19 @@ def deploy_process(self, process_name, process_data, tenant_key, token): files = {"upload": (file_path, process_data, "text/bpmn")} BPMService.post_deployment(token, payload, tenant_key, files) - @user_context - def publish(self, mapper_id, request, **kwargs): - """Publish by mapper_id.""" - user: UserContext = kwargs["user"] - tenant_key = user.tenant_key - token = user.bearer_token - data = request.get_json() + def validate_mapper(self, mapper_id, tenant_key): + """Validate mapper by mapper Id.""" mapper = FormProcessMapper.find_form_by_id(form_process_mapper_id=mapper_id) if not mapper: raise BusinessException(BusinessErrorCode.INVALID_FORM_PROCESS_MAPPER_ID) # Check tenant authentication if tenant_key and mapper.tenant != tenant_key: - raise PermissionError("Tenant authentication failed.") - process_data = data.get("processData") - process_name = mapper.process_key - # Deploy process - self.deploy_process(process_name, process_data, tenant_key, token) - - # Update status in process table. - process = Process.get_latest_version(process_name) - if not process: - # create entry in process & publish - process = Process( - name=process_name, - process_type="LOWCODE", - process_data="[]", - form_process_mapper_id=mapper_id, - tenant=tenant_key, - major_version=1, - minor_version=0, - ) - process.status = "PUBLISHED" - process.save() + raise PermissionError(BusinessErrorCode.PERMISSION_DENIED) + return mapper - # Capture publish status in form history table. + def capture_form_history(self, mapper, data, user_name): + """Capture form history.""" major_version, minor_version = 1, 0 latest_form_history = FormHistory.get_latest_version(mapper.parent_form_id) if latest_form_history: @@ -642,19 +619,61 @@ def publish(self, mapper_id, request, **kwargs): latest_form_history.minor_version, ) FormHistory( - created_by=user.user_name, + created_by=user_name, parent_form_id=mapper.parent_form_id, - change_log={"status": "Publish"}, + form_id=mapper.form_id, + change_log=data, status=True, major_version=major_version, minor_version=minor_version, - ) + ).save() + + @user_context + def publish(self, mapper_id, **kwargs): + """Publish by mapper_id.""" + user: UserContext = kwargs["user"] + tenant_key = user.tenant_key + token = user.bearer_token + user_name = user.user_name + mapper = self.validate_mapper(mapper_id, tenant_key) + process_name = mapper.process_key + + # Fetch process data from process table + process = Process.get_latest_version(process_name) + process_data = process.process_data if process else None + # Deploy process + self.deploy_process(process_name, process_data, tenant_key, token) + if not process: + # create entry in process with default flow. + process = Process( + name=process_name, + process_type="BPMN", + process_data=default_flow_xml_data(process_name).encode("utf-8"), + form_process_mapper_id=mapper_id, + tenant=tenant_key, + major_version=1, + minor_version=0, + created_by=user_name, + ) + # Update process status + process.status = "PUBLISHED" + process.save() - # Update status in mapper table - discuss & do - mapper.prompt_new_version = False - mapper.save() + # Capture publish(active) status in form history table. + self.capture_form_history(mapper, {"status": "active"}, user_name) + # Update status in mapper table + mapper.update({"status": str(FormProcessMapperStatus.ACTIVE.value), "prompt_new_version": False}) + return {} - @staticmethod - def unpublish(mapper_id: int): + @user_context + def unpublish(self, mapper_id: int, **kwargs): """Publish by mapper_id.""" - pass + user: UserContext = kwargs["user"] + user_name = user.user_name + tenant_key = user.tenant_key + mapper = self.validate_mapper(mapper_id, tenant_key) + # Capture publish status in form history table. + self.capture_form_history(mapper, {"status": "inactive"}, user_name) + # Update status(inactive) in mapper table + mapper.update({"status": str(FormProcessMapperStatus.INACTIVE.value), "prompt_new_version": True}) + return {} diff --git a/forms-flow-api/tests/unit/api/test_form_process_mapper.py b/forms-flow-api/tests/unit/api/test_form_process_mapper.py index ee24e73a58..adc579d8d1 100644 --- a/forms-flow-api/tests/unit/api/test_form_process_mapper.py +++ b/forms-flow-api/tests/unit/api/test_form_process_mapper.py @@ -639,3 +639,51 @@ def test_form_history(app, client, session, jwt, mock_redis_client): assert response.json[1]["minorVersion"] == 0 assert response.json[1]["formId"] == form_id assert response.json[1]["version"] == "1.0" + + +def test_publish(app, client, session, jwt, mock_redis_client): + """Testing publish endpoint.""" + token = get_token(jwt, role=CREATE_DESIGNS, username="designer") + headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} + response = client.post( + "/form", + headers=headers, + json=get_form_request_payload(), + ) + assert response.status_code == 201 + mapper_id = response.json.get("id") + rv = client.get(f"/form/{mapper_id}", headers=headers) + assert rv.status_code == 200 + assert rv.json.get("id") == mapper_id + # Test publish endpoint with valid response. + with patch("requests.post") as mock_post: + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = '{}' + mock_post.return_value = mock_response + response = client.post(f"/form/{mapper_id}/publish", headers=headers) + assert response.status_code == 200 + + +def test_unpublish(app, client, session, jwt, mock_redis_client): + """Testing unpublish endpoint.""" + token = get_token(jwt, role=CREATE_DESIGNS, username="designer") + headers = {"Authorization": f"Bearer {token}", "content-type": "application/json"} + response = client.post( + "/form", + headers=headers, + json=get_form_request_payload(), + ) + assert response.status_code == 201 + mapper_id = response.json.get("id") + rv = client.get(f"/form/{mapper_id}", headers=headers) + assert rv.status_code == 200 + assert rv.json.get("id") == mapper_id + # Test unpublish endpoint with valid response. + with patch("requests.post") as mock_post: + mock_response = MagicMock() + mock_response.text = '{}' + mock_response.status_code = 200 + mock_post.return_value = mock_response + response = client.post(f"/form/{mapper_id}/unpublish", headers=headers) + assert response.status_code == 200