diff --git a/csfunctions/events/__init__.py b/csfunctions/events/__init__.py index d7390b9..d6672ac 100644 --- a/csfunctions/events/__init__.py +++ b/csfunctions/events/__init__.py @@ -2,6 +2,12 @@ from pydantic import Field +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 @@ -41,7 +47,9 @@ | DocumentCreateCheckEvent | DocumentModifyCheckEvent | PartCreateCheckEvent - | PartModifyCheckEvent, + | PartModifyCheckEvent + | CustomOperationDocumentEvent + | CustomOperationPartEvent, Field(discriminator="name"), ] EventData = ( @@ -62,6 +70,8 @@ | DocumentModifyCheckData | PartCreateCheckData | PartModifyCheckData + | CustomOperationDocumentData + | CustomOperationPartData ) __all__ = [ @@ -99,4 +109,8 @@ "PartCreateCheckEvent", "PartModifyCheckData", "PartModifyCheckEvent", + "CustomOperationDocumentData", + "CustomOperationDocumentEvent", + "CustomOperationPartData", + "CustomOperationPartEvent", ] diff --git a/csfunctions/events/base.py b/csfunctions/events/base.py index 262db1b..1d4f717 100644 --- a/csfunctions/events/base.py +++ b/csfunctions/events/base.py @@ -21,6 +21,8 @@ class EventNames(str, Enum): PART_MODIFY_CHECK = "part_modify_check" ENGINEERING_CHANGE_STATUS_CHANGED = "engineering_change_status_changed" ENGINEERING_CHANGE_STATUS_CHANGE_CHECK = "engineering_change_status_change_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 new file mode 100644 index 0000000..1738fd8 --- /dev/null +++ b/csfunctions/events/custom_operations.py @@ -0,0 +1,39 @@ +from typing import Literal + +from pydantic import BaseModel, Field + +from csfunctions.objects import Document, Part + +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") + + +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 + + +# ----------- 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/docs/examples/basic_report.md b/docs/examples/basic_report.md new file mode 100644 index 0000000..1f40e9c --- /dev/null +++ b/docs/examples/basic_report.md @@ -0,0 +1,116 @@ +# 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(temp_file_path) + + # 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 + # 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, + 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/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. | diff --git a/json_schemas/request.json b/json_schemas/request.json index 2040695..3ed5a7d 100644 --- a/json_schemas/request.json +++ b/json_schemas/request.json @@ -85,6 +85,108 @@ "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" + }, + "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": { @@ -7957,6 +8059,8 @@ "event": { "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", @@ -8028,6 +8132,12 @@ }, { "$ref": "#/$defs/PartModifyCheckEvent" + }, + { + "$ref": "#/$defs/CustomOperationDocumentEvent" + }, + { + "$ref": "#/$defs/CustomOperationPartEvent" } ], "title": "Event" 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