Skip to content

Commit 5cec7f5

Browse files
authored
Merge pull request #97 from itk-dev-rpa/release/2.9
Release/2.9
2 parents 9a47626 + 8b56d52 commit 5cec7f5

File tree

14 files changed

+222
-85
lines changed

14 files changed

+222
-85
lines changed

changelog.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.9.0] - 2025-02-19
11+
12+
### Added
13+
14+
- kmd_nova: Added support for case worker groups/units.
15+
16+
### Fixed
17+
18+
- Child folders in Graph are now found, even with more than 10 child folders to go through.
19+
- Approving eflyt cases when the person is marked as "Afsluttet" now works.
20+
1021
## [2.8.1] - 2024-12-02
1122

1223
### Fixed
@@ -187,7 +198,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
187198

188199
- Initial release
189200

190-
[Unreleased]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.8.1...HEAD
201+
[Unreleased]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.9.0...HEAD
202+
[2.9.0]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.9.0
191203
[2.8.1]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.8.1
192204
[2.8.0]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.8.0
193205
[2.7.1]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.7.1

itk_dev_shared_components/eflyt/eflyt_case.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def check_all_approved(browser: webdriver.Chrome) -> bool:
171171

172172
for row in rows:
173173
row_status = row.find_element(By.XPATH, "td[6]").text
174-
if row_status != "Godkendt":
174+
if row_status not in ("Godkendt", "Afsluttet"):
175175
return False
176176
return True
177177

itk_dev_shared_components/graph/mail.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,33 @@ def get_folder_id_from_path(user: str, folder_path: str, graph_access: GraphAcce
122122
# Get child folders
123123
for child_folder in child_folders:
124124
endpoint = f"https://graph.microsoft.com/v1.0/users/{user}/mailFolders/{folder_id}/childFolders"
125-
response = get_request(endpoint, graph_access).json()
126-
folder_id = _find_folder(response, child_folder)
125+
folder_id = recursive_find_folder(endpoint, graph_access, child_folder)
127126
if folder_id is None:
128127
raise ValueError(f"Child folder '{child_folder}' not found under '{main_folder}' for user '{user}'.")
129128

130129
return folder_id
131130

132131

132+
def recursive_find_folder(endpoint: str, graph_access: GraphAccess, target_folder: str) -> str:
133+
"""Look for target folder at endpoint, recursively going through pagination if available and necessary.
134+
135+
Args:
136+
endpoint: Graph endpoint to lookup folder.
137+
graph_access: Access token for Graph API.
138+
target_folder: ID of target folder.
139+
140+
Returns:
141+
Return folder ID.
142+
"""
143+
response = get_request(endpoint, graph_access).json()
144+
folder_id = _find_folder(response, target_folder)
145+
146+
if folder_id is None and '@odata.nextLink' in response:
147+
return recursive_find_folder(response['@odata.nextLink'], graph_access, target_folder)
148+
149+
return folder_id
150+
151+
133152
def list_email_attachments(email: Email, graph_access: GraphAccess) -> tuple[Attachment]:
134153
"""List all attachments of the given email. This function only gets the id, name and size
135154
of the attachment. Use get_attachment_data to get the actual data of an attachment.

itk_dev_shared_components/kmd_nova/nova_cases.py

Lines changed: 21 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22
to the KMD Nova api."""
33

44
import uuid
5-
import base64
65
import urllib.parse
76

87
import requests
98

109
from itk_dev_shared_components.kmd_nova.authentication import NovaAccess
11-
from itk_dev_shared_components.kmd_nova.nova_objects import NovaCase, CaseParty, JournalNote, Caseworker, Department
12-
from itk_dev_shared_components.kmd_nova.util import datetime_from_iso_string
10+
from itk_dev_shared_components.kmd_nova.nova_objects import NovaCase, CaseParty, Department
11+
from itk_dev_shared_components.kmd_nova.util import datetime_from_iso_string, extract_caseworker
1312

1413

1514
def get_case(case_uuid: str, nova_access: NovaAccess) -> NovaCase:
@@ -125,7 +124,7 @@ def _get_nova_cases(nova_access: NovaAccess, payload: dict) -> list[NovaCase]:
125124
kle_number = case_dict['caseClassification']['kleNumber']['code'],
126125
proceeding_facet = case_dict['caseClassification']['proceedingFacet']['code'],
127126
sensitivity = case_dict["sensitivity"]["sensitivity"],
128-
caseworker = _extract_case_worker(case_dict),
127+
caseworker = extract_caseworker(case_dict),
129128
security_unit=security_unit,
130129
responsible_department=responsible_department
131130
)
@@ -189,6 +188,11 @@ def _create_payload(*, case_uuid: str = None, identification: str = None, identi
189188
"novaUserId": True,
190189
"fullName": True,
191190
"racfId": True
191+
},
192+
"losIdentity": {
193+
"novaUnitId": True,
194+
"fullName": True,
195+
"administrativeUnitId": True
192196
}
193197
},
194198
"responsibleDepartment": {
@@ -235,29 +239,6 @@ def _extract_departments(case_dict: dict) -> tuple[Department, Department]:
235239
return security_unit, responsible_department
236240

237241

238-
def _extract_case_worker(case_dict: dict) -> Caseworker | None:
239-
"""Extract the case worker from a HTTP request response.
240-
If the case worker is in a unexpected format, None is returned.
241-
242-
Args:
243-
case_dict: The dictionary describing the case.
244-
245-
Returns:
246-
A case worker object describing the case worker if any.
247-
"""
248-
if 'caseworker' in case_dict:
249-
try:
250-
return Caseworker(
251-
uuid = case_dict['caseworker']['kspIdentity']['novaUserId'],
252-
name = case_dict['caseworker']['kspIdentity']['fullName'],
253-
ident = case_dict['caseworker']['kspIdentity']['racfId']
254-
)
255-
except KeyError:
256-
return None
257-
258-
return None
259-
260-
261242
def _extract_case_parties(case_dict: dict) -> list[CaseParty]:
262243
"""Extract the case parties from a HTTP request response.
263244
@@ -281,29 +262,6 @@ def _extract_case_parties(case_dict: dict) -> list[CaseParty]:
281262
return parties
282263

283264

284-
def _extract_journal_notes(case_dict: dict) -> list:
285-
"""Extract the journal notes from a HTTP request response.
286-
287-
Args:
288-
case_dict: The dictionary describing the journal note.
289-
290-
Returns:
291-
A journal note object describing the journal note.
292-
"""
293-
notes = []
294-
for note_dict in case_dict['journalNotes']['journalNotes']:
295-
note = JournalNote(
296-
uuid = note_dict['uuid'],
297-
title = note_dict['journalNoteAttributes']['title'],
298-
journal_date = note_dict['journalNoteAttributes']['journalNoteDate'],
299-
note_format = note_dict['journalNoteAttributes']['format'],
300-
note = base64.b64decode(note_dict['journalNoteAttributes']['note']),
301-
approved = note_dict['journalNoteAttributes'].get('approved', False)
302-
)
303-
notes.append(note)
304-
return notes
305-
306-
307265
def add_case(case: NovaCase, nova_access: NovaAccess):
308266
"""Add a case to KMD Nova. The case will be created as 'Active'.
309267
@@ -371,12 +329,20 @@ def add_case(case: NovaCase, nova_access: NovaAccess):
371329
}
372330

373331
if case.caseworker:
374-
payload['caseworker'] = {
375-
"kspIdentity": {
376-
"racfId": case.caseworker.ident,
377-
"fullName": case.caseworker.name
332+
if case.caseworker.type == 'user':
333+
payload['caseworker'] = {
334+
"kspIdentity": {
335+
"racfId": case.caseworker.ident,
336+
"fullName": case.caseworker.name
337+
}
338+
}
339+
elif case.caseworker.type == 'group':
340+
payload['caseworker'] = {
341+
"losIdentity": {
342+
"administrativeUnitId": case.caseworker.ident,
343+
"fullName": case.caseworker.name
344+
}
378345
}
379-
}
380346

381347
headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"}
382348

itk_dev_shared_components/kmd_nova/nova_documents.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import requests
1111

1212
from itk_dev_shared_components.kmd_nova.authentication import NovaAccess
13-
from itk_dev_shared_components.kmd_nova.nova_objects import Document, Caseworker
14-
from itk_dev_shared_components.kmd_nova. util import datetime_from_iso_string
13+
from itk_dev_shared_components.kmd_nova.nova_objects import Document
14+
from itk_dev_shared_components.kmd_nova.util import datetime_from_iso_string, extract_caseworker
1515

1616

1717
def get_documents(case_uuid: str, nova_access: NovaAccess) -> list[Document]:
@@ -56,16 +56,6 @@ def get_documents(case_uuid: str, nova_access: NovaAccess) -> list[Document]:
5656

5757
documents = []
5858
for document_dict in response.json()['documents']:
59-
60-
if 'caseworker' in document_dict:
61-
caseworker = Caseworker(
62-
uuid = document_dict['caseworker']['kspIdentity']['novaUserId'],
63-
name = document_dict['caseworker']['kspIdentity']['fullName'],
64-
ident = document_dict['caseworker']['kspIdentity']['racfId']
65-
)
66-
else:
67-
caseworker = None
68-
6959
doc = Document(
7060
uuid = document_dict['documentUuid'],
7161
document_number = document_dict['documentNumber'],
@@ -78,7 +68,7 @@ def get_documents(case_uuid: str, nova_access: NovaAccess) -> list[Document]:
7868
file_extension = document_dict['fileExtension'],
7969
category_name = document_dict.get('documentCategoryName'),
8070
category_uuid = document_dict.get('documentCategoryUuid'),
81-
caseworker=caseworker
71+
caseworker=extract_caseworker(document_dict)
8272
)
8373
documents.append(doc)
8474

@@ -197,12 +187,20 @@ def attach_document_to_case(case_uuid: str, document: Document, nova_access: Nov
197187
}
198188

199189
if document.caseworker:
200-
payload['caseworker'] = {
201-
"kspIdentity": {
202-
"racfId": document.caseworker.ident,
203-
"fullName": document.caseworker.name
190+
if document.caseworker.type == 'user':
191+
payload['caseworker'] = {
192+
"kspIdentity": {
193+
"racfId": document.caseworker.ident,
194+
"fullName": document.caseworker.name
195+
}
196+
}
197+
elif document.caseworker.type == 'group':
198+
payload['caseworker'] = {
199+
"losIdentity": {
200+
"administrativeUnitId": document.caseworker.ident,
201+
"fullName": document.caseworker.name
202+
}
204203
}
205-
}
206204

207205
headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"}
208206
response = requests.post(url, params=params, headers=headers, json=payload, timeout=60)

itk_dev_shared_components/kmd_nova/nova_notes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def add_text_note(case_uuid: str, note_title: str, note_text: str, caseworker: C
1818
case_uuid: The uuid of the case to add the journal note to.
1919
note_title: The title of the note.
2020
note_text: The text content of the note.
21+
caseworker: The author of the note.
2122
approved: Whether the journal note should be marked as approved in Nova.
2223
nova_access: The NovaAccess object used to authenticate.
2324

itk_dev_shared_components/kmd_nova/nova_objects.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Caseworker:
2222
uuid: str
2323
name: str
2424
ident: str
25+
type: Literal['user', 'group'] = 'user'
2526

2627

2728
@dataclass(slots=True, kw_only=True)

itk_dev_shared_components/kmd_nova/nova_tasks.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,18 @@ def attach_task_to_case(case_uuid: str, task: Task, nova_access: NovaAccess) ->
3535
"caseUuid": case_uuid,
3636
"title": task.title,
3737
"description": task.description, # Optional
38-
"caseworkerPersonId": task.caseworker.uuid, # Optional
3938
"statusCode": task.status_code,
4039
"deadline": datetime_to_iso_string(task.deadline),
4140
"startDate": datetime_to_iso_string(task.started_date), # Optional
4241
"closeDate": datetime_to_iso_string(task.closed_date), # Optional
4342
"taskTypeName": "Aktivitet"
4443
}
4544

45+
if task.caseworker.type == 'user':
46+
payload["caseworkerPersonId"] = task.caseworker.uuid
47+
elif task.caseworker.type == 'group':
48+
payload["caseworkerGroupId"] = task.caseworker.uuid
49+
4650
headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"}
4751
response = requests.post(url, params=params, headers=headers, json=payload, timeout=60)
4852
response.raise_for_status()
@@ -114,7 +118,16 @@ def _extract_caseworker(task_dict: dict) -> Caseworker | None:
114118
return Caseworker(
115119
uuid = task_dict['caseWorker']['id'],
116120
ident = task_dict['caseWorker']['ident'],
117-
name = task_dict['caseWorker']['name']
121+
name = task_dict['caseWorker']['name'],
122+
type='user'
123+
)
124+
125+
if 'caseWorkerGroup' in task_dict:
126+
return Caseworker(
127+
uuid = task_dict['caseWorkerGroup']['id'],
128+
ident = None,
129+
name = task_dict['caseWorkerGroup']['name'],
130+
type='group'
118131
)
119132

120133
return None
@@ -143,14 +156,18 @@ def update_task(task: Task, case_uuid: str, nova_access: NovaAccess):
143156
"caseUuid": case_uuid,
144157
"title": task.title,
145158
"description": task.description,
146-
"caseworkerPersonId": task.caseworker.uuid,
147159
"statusCode": task.status_code,
148160
"deadline": datetime_to_iso_string(task.deadline),
149161
"startDate": datetime_to_iso_string(task.started_date),
150162
"closeDate": datetime_to_iso_string(task.closed_date),
151163
"taskType": "Aktivitet"
152164
}
153165

166+
if task.caseworker.type == 'user':
167+
payload["caseworkerPersonId"] = task.caseworker.uuid
168+
elif task.caseworker.type == 'group':
169+
payload["caseworkerGroupId"] = task.caseworker.uuid
170+
154171
headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"}
155172
response = requests.put(url, params=params, headers=headers, json=payload, timeout=60)
156173
response.raise_for_status()

itk_dev_shared_components/kmd_nova/util.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from datetime import datetime
44
from typing import Optional
55

6+
from itk_dev_shared_components.kmd_nova.nova_objects import Caseworker
7+
68

79
def datetime_from_iso_string(date_string: Optional[str]) -> Optional[datetime]:
810
"""A helper function to convert an ISO date string to a datetime.
@@ -34,3 +36,37 @@ def datetime_to_iso_string(datetime_: Optional[datetime]) -> Optional[str]:
3436
return datetime_.isoformat()
3537

3638
return None
39+
40+
41+
def extract_caseworker(response_dict: dict) -> Caseworker | None:
42+
"""Extract the case worker from a HTTP request response.
43+
If the case worker is in a unexpected format, None is returned.
44+
45+
Args:
46+
response_dict: The dictionary describing the response object.
47+
48+
Returns:
49+
A case worker object describing the case worker if any.
50+
"""
51+
try:
52+
if 'caseworker' in response_dict:
53+
if 'kspIdentity' in response_dict['caseworker']:
54+
return Caseworker(
55+
uuid = response_dict['caseworker']['kspIdentity']['novaUserId'],
56+
name = response_dict['caseworker']['kspIdentity']['fullName'],
57+
ident = response_dict['caseworker']['kspIdentity']['racfId'],
58+
type='user'
59+
)
60+
61+
if 'losIdentity' in response_dict['caseworker']:
62+
return Caseworker(
63+
uuid = response_dict['caseworker']['losIdentity']['novaUnitId'],
64+
name = response_dict['caseworker']['losIdentity']['fullName'],
65+
ident = str(response_dict['caseworker']['losIdentity']['administrativeUnitId']),
66+
type='group'
67+
)
68+
69+
except KeyError:
70+
pass
71+
72+
return None

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "itk_dev_shared_components"
7-
version = "2.8.1"
7+
version = "2.9.0"
88
authors = [
99
{ name="ITK Development", email="[email protected]" },
1010
]

0 commit comments

Comments
 (0)