From 60664f47510edd5ff330b72bfe68a01dde604a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Mon, 14 Apr 2025 16:59:34 +0200 Subject: [PATCH 1/7] add custom operation event for docs --- csfunctions/events/__init__.py | 5 +++ csfunctions/events/base.py | 1 + csfunctions/events/custom_operations.py | 21 ++++++++++ json_schemas/request.json | 55 +++++++++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 csfunctions/events/custom_operations.py diff --git a/csfunctions/events/__init__.py b/csfunctions/events/__init__.py index 216a3c1..bf0f739 100644 --- a/csfunctions/events/__init__.py +++ b/csfunctions/events/__init__.py @@ -2,6 +2,7 @@ from pydantic import Field +from .custom_operations import CustomOperationDocumentData, CustomOperationDocumentEvent from .dialog_data import DocumentReleasedDialogData, PartReleasedDialogData from .document_create_check import DocumentCreateCheckData, DocumentCreateCheckEvent from .document_field_calculation import DocumentFieldCalculationData, DocumentFieldCalculationEvent @@ -36,6 +37,7 @@ DocumentModifyCheckEvent, PartCreateCheckEvent, PartModifyCheckEvent, + CustomOperationDocumentEvent, ], Field(discriminator="name"), ] @@ -55,6 +57,7 @@ DocumentModifyCheckData, PartCreateCheckData, PartModifyCheckData, + CustomOperationDocumentData, ] __all__ = [ @@ -90,4 +93,6 @@ "PartCreateCheckEvent", "PartModifyCheckData", "PartModifyCheckEvent", + "CustomOperationDocumentData", + "CustomOperationDocumentEvent", ] diff --git a/csfunctions/events/base.py b/csfunctions/events/base.py index c14ac62..151cc23 100644 --- a/csfunctions/events/base.py +++ b/csfunctions/events/base.py @@ -19,6 +19,7 @@ class EventNames(str, Enum): DOCUMENT_MODIFY_CHECK = "document_modify_check" PART_CREATE_CHECK = "part_create_check" PART_MODIFY_CHECK = "part_modify_check" + CUSTOM_OPERATION_DOCUMENT = "custom_operation_document" class BaseEvent(BaseModel): diff --git a/csfunctions/events/custom_operations.py b/csfunctions/events/custom_operations.py new file mode 100644 index 0000000..7d39080 --- /dev/null +++ b/csfunctions/events/custom_operations.py @@ -0,0 +1,21 @@ +from typing import Literal + +from pydantic import BaseModel, Field + +from csfunctions.objects import Document, Part + +from .base import BaseEvent, EventNames + + +class CustomOperationDocumentData(BaseModel): + documents: list[Document] = Field(..., description="List of documents that the custom operation was called on") + parts: list[Part] = Field(..., description="List of parts that belong to the documents") + + +class CustomOperationDocumentEvent(BaseEvent): + """ + Event triggered when a custom operation is called on a document. + """ + + name: Literal[EventNames.CUSTOM_OPERATION_DOCUMENT] = EventNames.CUSTOM_OPERATION_DOCUMENT + data: CustomOperationDocumentData diff --git a/json_schemas/request.json b/json_schemas/request.json index 27979c8..ae2e24f 100644 --- a/json_schemas/request.json +++ b/json_schemas/request.json @@ -85,6 +85,57 @@ "title": "Briefcase", "type": "object" }, + "CustomOperationDocumentData": { + "properties": { + "documents": { + "description": "List of documents that the custom operation was called on", + "items": { + "$ref": "#/$defs/Document" + }, + "title": "Documents", + "type": "array" + }, + "parts": { + "description": "List of parts that belong to the documents", + "items": { + "$ref": "#/$defs/Part" + }, + "title": "Parts", + "type": "array" + } + }, + "required": [ + "documents", + "parts" + ], + "title": "CustomOperationDocumentData", + "type": "object" + }, + "CustomOperationDocumentEvent": { + "description": "Event triggered when a custom operation is called on a document.", + "properties": { + "name": { + "const": "custom_operation_document", + "default": "custom_operation_document", + "title": "Name", + "type": "string" + }, + "event_id": { + "description": "unique identifier", + "title": "Event Id", + "type": "string" + }, + "data": { + "$ref": "#/$defs/CustomOperationDocumentData" + } + }, + "required": [ + "event_id", + "data" + ], + "title": "CustomOperationDocumentEvent", + "type": "object" + }, "Document": { "description": "Normal Document that doesn't contain a CAD-Model.", "properties": { @@ -3068,6 +3119,7 @@ "event": { "discriminator": { "mapping": { + "custom_operation_document": "#/$defs/CustomOperationDocumentEvent", "document_create_check": "#/$defs/DocumentCreateCheckEvent", "document_field_calculation": "#/$defs/DocumentFieldCalculationEvent", "document_modify_check": "#/$defs/DocumentModifyCheckEvent", @@ -3131,6 +3183,9 @@ }, { "$ref": "#/$defs/PartModifyCheckEvent" + }, + { + "$ref": "#/$defs/CustomOperationDocumentEvent" } ], "title": "Event" From 848c3bdbaaf906d38f23b8744c0ed403eb513ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Mon, 14 Apr 2025 17:01:14 +0200 Subject: [PATCH 2/7] add custom operation for part --- csfunctions/events/__init__.py | 11 ++++- csfunctions/events/base.py | 1 + csfunctions/events/custom_operations.py | 18 ++++++++ json_schemas/request.json | 55 +++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/csfunctions/events/__init__.py b/csfunctions/events/__init__.py index bf0f739..dcdc51b 100644 --- a/csfunctions/events/__init__.py +++ b/csfunctions/events/__init__.py @@ -2,7 +2,12 @@ from pydantic import Field -from .custom_operations import CustomOperationDocumentData, CustomOperationDocumentEvent +from .custom_operations import ( + CustomOperationDocumentData, + CustomOperationDocumentEvent, + CustomOperationPartData, + CustomOperationPartEvent, +) from .dialog_data import DocumentReleasedDialogData, PartReleasedDialogData from .document_create_check import DocumentCreateCheckData, DocumentCreateCheckEvent from .document_field_calculation import DocumentFieldCalculationData, DocumentFieldCalculationEvent @@ -38,6 +43,7 @@ PartCreateCheckEvent, PartModifyCheckEvent, CustomOperationDocumentEvent, + CustomOperationPartEvent, ], Field(discriminator="name"), ] @@ -58,6 +64,7 @@ PartCreateCheckData, PartModifyCheckData, CustomOperationDocumentData, + CustomOperationPartData, ] __all__ = [ @@ -95,4 +102,6 @@ "PartModifyCheckEvent", "CustomOperationDocumentData", "CustomOperationDocumentEvent", + "CustomOperationPartData", + "CustomOperationPartEvent", ] diff --git a/csfunctions/events/base.py b/csfunctions/events/base.py index 151cc23..78e72b4 100644 --- a/csfunctions/events/base.py +++ b/csfunctions/events/base.py @@ -20,6 +20,7 @@ class EventNames(str, Enum): PART_CREATE_CHECK = "part_create_check" PART_MODIFY_CHECK = "part_modify_check" CUSTOM_OPERATION_DOCUMENT = "custom_operation_document" + CUSTOM_OPERATION_PART = "custom_operation_part" class BaseEvent(BaseModel): diff --git a/csfunctions/events/custom_operations.py b/csfunctions/events/custom_operations.py index 7d39080..1738fd8 100644 --- a/csfunctions/events/custom_operations.py +++ b/csfunctions/events/custom_operations.py @@ -7,6 +7,7 @@ from .base import BaseEvent, EventNames +# ----------- DOCUMENTS ----------- class CustomOperationDocumentData(BaseModel): documents: list[Document] = Field(..., description="List of documents that the custom operation was called on") parts: list[Part] = Field(..., description="List of parts that belong to the documents") @@ -19,3 +20,20 @@ class CustomOperationDocumentEvent(BaseEvent): name: Literal[EventNames.CUSTOM_OPERATION_DOCUMENT] = EventNames.CUSTOM_OPERATION_DOCUMENT data: CustomOperationDocumentData + + +# ----------- PARTS ----------- + + +class CustomOperationPartData(BaseModel): + parts: list[Part] = Field(..., description="List of parts that the custom operation was called on") + documents: list[Document] = Field(..., description="List of documents that belong to the parts") + + +class CustomOperationPartEvent(BaseEvent): + """ + Event triggered when a custom operation is called on a part. + """ + + name: Literal[EventNames.CUSTOM_OPERATION_PART] = EventNames.CUSTOM_OPERATION_PART + data: CustomOperationPartData diff --git a/json_schemas/request.json b/json_schemas/request.json index ae2e24f..c419d21 100644 --- a/json_schemas/request.json +++ b/json_schemas/request.json @@ -136,6 +136,57 @@ "title": "CustomOperationDocumentEvent", "type": "object" }, + "CustomOperationPartData": { + "properties": { + "parts": { + "description": "List of parts that the custom operation was called on", + "items": { + "$ref": "#/$defs/Part" + }, + "title": "Parts", + "type": "array" + }, + "documents": { + "description": "List of documents that belong to the parts", + "items": { + "$ref": "#/$defs/Document" + }, + "title": "Documents", + "type": "array" + } + }, + "required": [ + "parts", + "documents" + ], + "title": "CustomOperationPartData", + "type": "object" + }, + "CustomOperationPartEvent": { + "description": "Event triggered when a custom operation is called on a part.", + "properties": { + "name": { + "const": "custom_operation_part", + "default": "custom_operation_part", + "title": "Name", + "type": "string" + }, + "event_id": { + "description": "unique identifier", + "title": "Event Id", + "type": "string" + }, + "data": { + "$ref": "#/$defs/CustomOperationPartData" + } + }, + "required": [ + "event_id", + "data" + ], + "title": "CustomOperationPartEvent", + "type": "object" + }, "Document": { "description": "Normal Document that doesn't contain a CAD-Model.", "properties": { @@ -3120,6 +3171,7 @@ "discriminator": { "mapping": { "custom_operation_document": "#/$defs/CustomOperationDocumentEvent", + "custom_operation_part": "#/$defs/CustomOperationPartEvent", "document_create_check": "#/$defs/DocumentCreateCheckEvent", "document_field_calculation": "#/$defs/DocumentFieldCalculationEvent", "document_modify_check": "#/$defs/DocumentModifyCheckEvent", @@ -3186,6 +3238,9 @@ }, { "$ref": "#/$defs/CustomOperationDocumentEvent" + }, + { + "$ref": "#/$defs/CustomOperationPartEvent" } ], "title": "Event" From b0a335318f96290e5abec20f66255be71e67e190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Thu, 27 Nov 2025 15:23:11 +0100 Subject: [PATCH 3/7] feat: Add CustomOperationDocumentEvent and CustomOperationPartEvent to event documentation --- docs/reference/events.md | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/reference/events.md b/docs/reference/events.md index 09dd464..452e395 100644 --- a/docs/reference/events.md +++ b/docs/reference/events.md @@ -317,3 +317,44 @@ This event is fired when a user tries to modify an engineering change's status. | target_status | int | The status the engineering change will be set to | | documents | list[[Document](objects.md#document)] | List of documents attached to the engineering change | | parts | list[[Part](objects.md#part)] | List of parts attached to the engineering change | + + +## CustomOperationDocumentEvent +`csfunctions.events.CustomOperationDocumentEvent` + + +This event is triggered when a custom operation is called on one or more documents. + +**Supported actions:** + +- [StartWorkflowAction](actions.md#startworkflowaction) +- [AbortAndShowErrorAction](actions.md#abortandshowerroraction) + +**CustomOperationDocumentEvent.name:** custom_operation_document + +**CustomOperationDocumentEvent.data:** + +| Attribute | Type | Description | +| --------- | ------------------------------------- | ---------------------------------------------------------- | +| documents | list[[Document](objects.md#document)] | List of documents that the custom operation was called on. | +| parts | list[[Part](objects.md#part)] | List of parts that belong to the documents. | + +## CustomOperationPartEvent +`csfunctions.events.CustomOperationPartEvent` + + +This event is triggered when a custom operation is called on one or more parts. + +**Supported actions:** + +- [StartWorkflowAction](actions.md#startworkflowaction) +- [AbortAndShowErrorAction](actions.md#abortandshowerroraction) + +**CustomOperationPartEvent.name:** custom_operation_part + +**CustomOperationPartEvent.data:** + +| Attribute | Type | Description | +| --------- | ------------------------------------- | ------------------------------------------------------ | +| parts | list[[Part](objects.md#part)] | List of parts that the custom operation was called on. | +| documents | list[[Document](objects.md#document)] | List of documents that belong to the parts. | From 61066f6e0a2e9732afa59279b037f53f35cedcd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Tue, 2 Dec 2025 11:00:02 +0100 Subject: [PATCH 4/7] feat: Add example for generating a basic report using custom operations --- docs/examples/basic_report.md | 114 ++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 115 insertions(+) create mode 100644 docs/examples/basic_report.md diff --git a/docs/examples/basic_report.md b/docs/examples/basic_report.md new file mode 100644 index 0000000..df2b42c --- /dev/null +++ b/docs/examples/basic_report.md @@ -0,0 +1,114 @@ +# Basic Report + +This example shows, how you can use custom operations to generate a basic report on a document and attach that report to the document. + +The example uses [python-docx](https://python-docx.readthedocs.io/en/latest/) to generate a Word file. +To install the library in your Function, you need to add it to the `requirements.txt`: + +```requirements.txt +contactsoftware-functions +python-docx +``` + +```python +import os +import tempfile +from datetime import datetime + +import requests +from docx import Document as DocxDocument + +from csfunctions import MetaData, Service +from csfunctions.events import CustomOperationDocumentEvent +from csfunctions.objects import Document + + +def simple_report(metadata: MetaData, event: CustomOperationDocumentEvent, service: Service): + """ + Generates a simple report for each document the custom operation is called on. + The report contains basic information about the document and is saved as a new file + named "myreport.docx" within the document. + """ + + for document in event.data.documents: + # generate a report for each document + report = _create_report(document, metadata) + + temp_file_path = None + try: + # we need to use a tempfile, because the rest of the filesystem is read-only + with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp: + temp_file_path = tmp.name + report.save("test.docx") + + # check if the document already has a report file, so we can overwrite it + file_name = "myreport.docx" + existing_file = next((file for file in document.files if file.cdbf_name == file_name), None) + + with open(temp_file_path, "rb") as file_stream: + if existing_file: + # overwrite the existing report file + service.file_upload.upload_file_content( + file_object_id=existing_file.cdb_object_id, stream=file_stream, check_access=False + ) + else: + # create a new one + service.file_upload.upload_new_file( + parent_object_id=document.cdb_object_id, # type: ignore + filename=file_name, + stream=file_stream, + check_access=False, + ) + finally: + if temp_file_path: + # Clean up temp file + os.unlink(temp_file_path) + + +def _fetch_person_name(persno: str, metadata: MetaData) -> str | None: + """Fetches the name of a person given their personnel number via GraphQL.""" + graphql_url = str(metadata.db_service_url).rstrip("/") + "/graphql/v1" + headers = {"Authorization": f"Bearer {metadata.service_token}"} + + query = f""" + {{ + persons(personalnummer: \"{persno}\", max_rows: 1) {{ + name + }} + }} + """ + response = requests.post( + graphql_url, + headers=headers, + json={"query": query}, + ) + response.raise_for_status() + data = response.json() + persons = data["data"]["persons"] + if persons: + return persons[0]["name"] + return None + + +def _create_report(document: Document, metadata: MetaData) -> DocxDocument: + """Creates a simple Word report for the given document.""" + doc = DocxDocument() + + doc.add_heading("Simple Report", 0) + + report_time_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + doc.add_paragraph(f"Report generated on: {report_time_string}") + + # add some basic information about the document + doc.add_heading("Document Information", level=1) + doc.add_paragraph(f"Document ID: {document.z_nummer}@{document.z_index}") + doc.add_paragraph(f"Title: {document.titel}") + doc.add_paragraph(f"Created On: {document.cdb_cdate}") + + # Fetch the name of the person who created the document via GraphQL + person_name = _fetch_person_name(document.cdb_cpersno, metadata) + doc.add_paragraph(f"Created By: {person_name or document.cdb_cpersno}") + + return doc + +``` diff --git a/mkdocs.yml b/mkdocs.yml index a48dd2e..67f559f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -46,6 +46,7 @@ nav: - examples/enforce_field_rules.md - examples/field_calculation.md - examples/workflows.md + - examples/basic_report.md - Reference: - reference/events.md - reference/objects.md From d27fe48cd9b80221ea2cb4522e48c74887d43832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Tue, 2 Dec 2025 11:13:13 +0100 Subject: [PATCH 5/7] Update docs/examples/basic_report.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/examples/basic_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/basic_report.md b/docs/examples/basic_report.md index df2b42c..7f78f38 100644 --- a/docs/examples/basic_report.md +++ b/docs/examples/basic_report.md @@ -39,7 +39,7 @@ def simple_report(metadata: MetaData, event: CustomOperationDocumentEvent, servi # we need to use a tempfile, because the rest of the filesystem is read-only with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp: temp_file_path = tmp.name - report.save("test.docx") + report.save(temp_file_path) # check if the document already has a report file, so we can overwrite it file_name = "myreport.docx" From b78654c270bf6aed8da46a14c7f20bdd29bbd53f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Tue, 2 Dec 2025 11:14:28 +0100 Subject: [PATCH 6/7] Update docs/examples/basic_report.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/examples/basic_report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples/basic_report.md b/docs/examples/basic_report.md index 7f78f38..5a25c84 100644 --- a/docs/examples/basic_report.md +++ b/docs/examples/basic_report.md @@ -1,6 +1,6 @@ # Basic Report -This example shows, how you can use custom operations to generate a basic report on a document and attach that report to the document. +This example shows how you can use custom operations to generate a basic report on a document and attach that report to the document. The example uses [python-docx](https://python-docx.readthedocs.io/en/latest/) to generate a Word file. To install the library in your Function, you need to add it to the `requirements.txt`: From aad2cd55fdf34b1b738ccc6e20fddba3cd7a2efc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jens=20K=C3=BCrten?= Date: Tue, 2 Dec 2025 11:16:33 +0100 Subject: [PATCH 7/7] clarify why check_access is false --- docs/examples/basic_report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/examples/basic_report.md b/docs/examples/basic_report.md index 5a25c84..1f40e9c 100644 --- a/docs/examples/basic_report.md +++ b/docs/examples/basic_report.md @@ -48,11 +48,13 @@ def simple_report(metadata: MetaData, event: CustomOperationDocumentEvent, servi with open(temp_file_path, "rb") as file_stream: if existing_file: # overwrite the existing report file + # we set check_access to false to allow attaching reports to released documents service.file_upload.upload_file_content( file_object_id=existing_file.cdb_object_id, stream=file_stream, check_access=False ) else: # create a new one + # we set check_access to false to allow attaching reports to released documents service.file_upload.upload_new_file( parent_object_id=document.cdb_object_id, # type: ignore filename=file_name,