Skip to content

Commit e7e6576

Browse files
jens-kuertenJens KürtenCopilot
authored
feat: Custom operations on Documents and Parts (#40)
* add custom operation event for docs * add custom operation for part * feat: Add CustomOperationDocumentEvent and CustomOperationPartEvent to event documentation * feat: Add example for generating a basic report using custom operations * Update docs/examples/basic_report.md Co-authored-by: Copilot <[email protected]> * Update docs/examples/basic_report.md Co-authored-by: Copilot <[email protected]> * clarify why check_access is false --------- Co-authored-by: Jens Kürten <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 9b981c4 commit e7e6576

File tree

7 files changed

+324
-1
lines changed

7 files changed

+324
-1
lines changed

csfunctions/events/__init__.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
from pydantic import Field
44

5+
from .custom_operations import (
6+
CustomOperationDocumentData,
7+
CustomOperationDocumentEvent,
8+
CustomOperationPartData,
9+
CustomOperationPartEvent,
10+
)
511
from .dialog_data import DocumentReleasedDialogData, PartReleasedDialogData
612
from .document_create_check import DocumentCreateCheckData, DocumentCreateCheckEvent
713
from .document_field_calculation import DocumentFieldCalculationData, DocumentFieldCalculationEvent
@@ -41,7 +47,9 @@
4147
| DocumentCreateCheckEvent
4248
| DocumentModifyCheckEvent
4349
| PartCreateCheckEvent
44-
| PartModifyCheckEvent,
50+
| PartModifyCheckEvent
51+
| CustomOperationDocumentEvent
52+
| CustomOperationPartEvent,
4553
Field(discriminator="name"),
4654
]
4755
EventData = (
@@ -62,6 +70,8 @@
6270
| DocumentModifyCheckData
6371
| PartCreateCheckData
6472
| PartModifyCheckData
73+
| CustomOperationDocumentData
74+
| CustomOperationPartData
6575
)
6676

6777
__all__ = [
@@ -99,4 +109,8 @@
99109
"PartCreateCheckEvent",
100110
"PartModifyCheckData",
101111
"PartModifyCheckEvent",
112+
"CustomOperationDocumentData",
113+
"CustomOperationDocumentEvent",
114+
"CustomOperationPartData",
115+
"CustomOperationPartEvent",
102116
]

csfunctions/events/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class EventNames(str, Enum):
2121
PART_MODIFY_CHECK = "part_modify_check"
2222
ENGINEERING_CHANGE_STATUS_CHANGED = "engineering_change_status_changed"
2323
ENGINEERING_CHANGE_STATUS_CHANGE_CHECK = "engineering_change_status_change_check"
24+
CUSTOM_OPERATION_DOCUMENT = "custom_operation_document"
25+
CUSTOM_OPERATION_PART = "custom_operation_part"
2426

2527

2628
class BaseEvent(BaseModel):
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from typing import Literal
2+
3+
from pydantic import BaseModel, Field
4+
5+
from csfunctions.objects import Document, Part
6+
7+
from .base import BaseEvent, EventNames
8+
9+
10+
# ----------- DOCUMENTS -----------
11+
class CustomOperationDocumentData(BaseModel):
12+
documents: list[Document] = Field(..., description="List of documents that the custom operation was called on")
13+
parts: list[Part] = Field(..., description="List of parts that belong to the documents")
14+
15+
16+
class CustomOperationDocumentEvent(BaseEvent):
17+
"""
18+
Event triggered when a custom operation is called on a document.
19+
"""
20+
21+
name: Literal[EventNames.CUSTOM_OPERATION_DOCUMENT] = EventNames.CUSTOM_OPERATION_DOCUMENT
22+
data: CustomOperationDocumentData
23+
24+
25+
# ----------- PARTS -----------
26+
27+
28+
class CustomOperationPartData(BaseModel):
29+
parts: list[Part] = Field(..., description="List of parts that the custom operation was called on")
30+
documents: list[Document] = Field(..., description="List of documents that belong to the parts")
31+
32+
33+
class CustomOperationPartEvent(BaseEvent):
34+
"""
35+
Event triggered when a custom operation is called on a part.
36+
"""
37+
38+
name: Literal[EventNames.CUSTOM_OPERATION_PART] = EventNames.CUSTOM_OPERATION_PART
39+
data: CustomOperationPartData

docs/examples/basic_report.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Basic Report
2+
3+
This example shows how you can use custom operations to generate a basic report on a document and attach that report to the document.
4+
5+
The example uses [python-docx](https://python-docx.readthedocs.io/en/latest/) to generate a Word file.
6+
To install the library in your Function, you need to add it to the `requirements.txt`:
7+
8+
```requirements.txt
9+
contactsoftware-functions
10+
python-docx
11+
```
12+
13+
```python
14+
import os
15+
import tempfile
16+
from datetime import datetime
17+
18+
import requests
19+
from docx import Document as DocxDocument
20+
21+
from csfunctions import MetaData, Service
22+
from csfunctions.events import CustomOperationDocumentEvent
23+
from csfunctions.objects import Document
24+
25+
26+
def simple_report(metadata: MetaData, event: CustomOperationDocumentEvent, service: Service):
27+
"""
28+
Generates a simple report for each document the custom operation is called on.
29+
The report contains basic information about the document and is saved as a new file
30+
named "myreport.docx" within the document.
31+
"""
32+
33+
for document in event.data.documents:
34+
# generate a report for each document
35+
report = _create_report(document, metadata)
36+
37+
temp_file_path = None
38+
try:
39+
# we need to use a tempfile, because the rest of the filesystem is read-only
40+
with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp:
41+
temp_file_path = tmp.name
42+
report.save(temp_file_path)
43+
44+
# check if the document already has a report file, so we can overwrite it
45+
file_name = "myreport.docx"
46+
existing_file = next((file for file in document.files if file.cdbf_name == file_name), None)
47+
48+
with open(temp_file_path, "rb") as file_stream:
49+
if existing_file:
50+
# overwrite the existing report file
51+
# we set check_access to false to allow attaching reports to released documents
52+
service.file_upload.upload_file_content(
53+
file_object_id=existing_file.cdb_object_id, stream=file_stream, check_access=False
54+
)
55+
else:
56+
# create a new one
57+
# we set check_access to false to allow attaching reports to released documents
58+
service.file_upload.upload_new_file(
59+
parent_object_id=document.cdb_object_id, # type: ignore
60+
filename=file_name,
61+
stream=file_stream,
62+
check_access=False,
63+
)
64+
finally:
65+
if temp_file_path:
66+
# Clean up temp file
67+
os.unlink(temp_file_path)
68+
69+
70+
def _fetch_person_name(persno: str, metadata: MetaData) -> str | None:
71+
"""Fetches the name of a person given their personnel number via GraphQL."""
72+
graphql_url = str(metadata.db_service_url).rstrip("/") + "/graphql/v1"
73+
headers = {"Authorization": f"Bearer {metadata.service_token}"}
74+
75+
query = f"""
76+
{{
77+
persons(personalnummer: \"{persno}\", max_rows: 1) {{
78+
name
79+
}}
80+
}}
81+
"""
82+
response = requests.post(
83+
graphql_url,
84+
headers=headers,
85+
json={"query": query},
86+
)
87+
response.raise_for_status()
88+
data = response.json()
89+
persons = data["data"]["persons"]
90+
if persons:
91+
return persons[0]["name"]
92+
return None
93+
94+
95+
def _create_report(document: Document, metadata: MetaData) -> DocxDocument:
96+
"""Creates a simple Word report for the given document."""
97+
doc = DocxDocument()
98+
99+
doc.add_heading("Simple Report", 0)
100+
101+
report_time_string = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
102+
doc.add_paragraph(f"Report generated on: {report_time_string}")
103+
104+
# add some basic information about the document
105+
doc.add_heading("Document Information", level=1)
106+
doc.add_paragraph(f"Document ID: {document.z_nummer}@{document.z_index}")
107+
doc.add_paragraph(f"Title: {document.titel}")
108+
doc.add_paragraph(f"Created On: {document.cdb_cdate}")
109+
110+
# Fetch the name of the person who created the document via GraphQL
111+
person_name = _fetch_person_name(document.cdb_cpersno, metadata)
112+
doc.add_paragraph(f"Created By: {person_name or document.cdb_cpersno}")
113+
114+
return doc
115+
116+
```

docs/reference/events.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,44 @@ This event is fired when a user tries to modify an engineering change's status.
317317
| target_status | int | The status the engineering change will be set to |
318318
| documents | list[[Document](objects.md#document)] | List of documents attached to the engineering change |
319319
| parts | list[[Part](objects.md#part)] | List of parts attached to the engineering change |
320+
321+
322+
## CustomOperationDocumentEvent
323+
`csfunctions.events.CustomOperationDocumentEvent`
324+
325+
326+
This event is triggered when a custom operation is called on one or more documents.
327+
328+
**Supported actions:**
329+
330+
- [StartWorkflowAction](actions.md#startworkflowaction)
331+
- [AbortAndShowErrorAction](actions.md#abortandshowerroraction)
332+
333+
**CustomOperationDocumentEvent.name:** custom_operation_document
334+
335+
**CustomOperationDocumentEvent.data:**
336+
337+
| Attribute | Type | Description |
338+
| --------- | ------------------------------------- | ---------------------------------------------------------- |
339+
| documents | list[[Document](objects.md#document)] | List of documents that the custom operation was called on. |
340+
| parts | list[[Part](objects.md#part)] | List of parts that belong to the documents. |
341+
342+
## CustomOperationPartEvent
343+
`csfunctions.events.CustomOperationPartEvent`
344+
345+
346+
This event is triggered when a custom operation is called on one or more parts.
347+
348+
**Supported actions:**
349+
350+
- [StartWorkflowAction](actions.md#startworkflowaction)
351+
- [AbortAndShowErrorAction](actions.md#abortandshowerroraction)
352+
353+
**CustomOperationPartEvent.name:** custom_operation_part
354+
355+
**CustomOperationPartEvent.data:**
356+
357+
| Attribute | Type | Description |
358+
| --------- | ------------------------------------- | ------------------------------------------------------ |
359+
| parts | list[[Part](objects.md#part)] | List of parts that the custom operation was called on. |
360+
| documents | list[[Document](objects.md#document)] | List of documents that belong to the parts. |

json_schemas/request.json

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,108 @@
8585
"title": "Briefcase",
8686
"type": "object"
8787
},
88+
"CustomOperationDocumentData": {
89+
"properties": {
90+
"documents": {
91+
"description": "List of documents that the custom operation was called on",
92+
"items": {
93+
"$ref": "#/$defs/Document"
94+
},
95+
"title": "Documents",
96+
"type": "array"
97+
},
98+
"parts": {
99+
"description": "List of parts that belong to the documents",
100+
"items": {
101+
"$ref": "#/$defs/Part"
102+
},
103+
"title": "Parts",
104+
"type": "array"
105+
}
106+
},
107+
"required": [
108+
"documents",
109+
"parts"
110+
],
111+
"title": "CustomOperationDocumentData",
112+
"type": "object"
113+
},
114+
"CustomOperationDocumentEvent": {
115+
"description": "Event triggered when a custom operation is called on a document.",
116+
"properties": {
117+
"name": {
118+
"const": "custom_operation_document",
119+
"default": "custom_operation_document",
120+
"title": "Name",
121+
"type": "string"
122+
},
123+
"event_id": {
124+
"description": "unique identifier",
125+
"title": "Event Id",
126+
"type": "string"
127+
},
128+
"data": {
129+
"$ref": "#/$defs/CustomOperationDocumentData"
130+
}
131+
},
132+
"required": [
133+
"event_id",
134+
"data"
135+
],
136+
"title": "CustomOperationDocumentEvent",
137+
"type": "object"
138+
},
139+
"CustomOperationPartData": {
140+
"properties": {
141+
"parts": {
142+
"description": "List of parts that the custom operation was called on",
143+
"items": {
144+
"$ref": "#/$defs/Part"
145+
},
146+
"title": "Parts",
147+
"type": "array"
148+
},
149+
"documents": {
150+
"description": "List of documents that belong to the parts",
151+
"items": {
152+
"$ref": "#/$defs/Document"
153+
},
154+
"title": "Documents",
155+
"type": "array"
156+
}
157+
},
158+
"required": [
159+
"parts",
160+
"documents"
161+
],
162+
"title": "CustomOperationPartData",
163+
"type": "object"
164+
},
165+
"CustomOperationPartEvent": {
166+
"description": "Event triggered when a custom operation is called on a part.",
167+
"properties": {
168+
"name": {
169+
"const": "custom_operation_part",
170+
"default": "custom_operation_part",
171+
"title": "Name",
172+
"type": "string"
173+
},
174+
"event_id": {
175+
"description": "unique identifier",
176+
"title": "Event Id",
177+
"type": "string"
178+
},
179+
"data": {
180+
"$ref": "#/$defs/CustomOperationPartData"
181+
}
182+
},
183+
"required": [
184+
"event_id",
185+
"data"
186+
],
187+
"title": "CustomOperationPartEvent",
188+
"type": "object"
189+
},
88190
"Document": {
89191
"description": "Normal Document that doesn't contain a CAD-Model.",
90192
"properties": {
@@ -7957,6 +8059,8 @@
79578059
"event": {
79588060
"discriminator": {
79598061
"mapping": {
8062+
"custom_operation_document": "#/$defs/CustomOperationDocumentEvent",
8063+
"custom_operation_part": "#/$defs/CustomOperationPartEvent",
79608064
"document_create_check": "#/$defs/DocumentCreateCheckEvent",
79618065
"document_field_calculation": "#/$defs/DocumentFieldCalculationEvent",
79628066
"document_modify_check": "#/$defs/DocumentModifyCheckEvent",
@@ -8028,6 +8132,12 @@
80288132
},
80298133
{
80308134
"$ref": "#/$defs/PartModifyCheckEvent"
8135+
},
8136+
{
8137+
"$ref": "#/$defs/CustomOperationDocumentEvent"
8138+
},
8139+
{
8140+
"$ref": "#/$defs/CustomOperationPartEvent"
80318141
}
80328142
],
80338143
"title": "Event"

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nav:
4646
- examples/enforce_field_rules.md
4747
- examples/field_calculation.md
4848
- examples/workflows.md
49+
- examples/basic_report.md
4950
- Reference:
5051
- reference/events.md
5152
- reference/objects.md

0 commit comments

Comments
 (0)