Skip to content

Commit 30a8644

Browse files
authored
Merge pull request #109 from itk-dev-rpa/release/2.12.0
Release/2.12.0
2 parents d73b6fb + 4de4808 commit 30a8644

File tree

9 files changed

+241
-6
lines changed

9 files changed

+241
-6
lines changed

.pylintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
disable =
33
C0301, # Line too long
44
I1101, E1101, # C-modules members
5-
R0913, R0917 # Too many arguments
5+
R0913, R0917, # Too many arguments
6+
W0223 # Abstract class functions missing

changelog.md

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

88
## [Unreleased]
99

10+
## [2.12.0] - 2025-06-17
11+
12+
### Added
13+
14+
- getorganized: Added functions and tests for using GetOrganized.
15+
1016
## [2.11.1] - 2025-04-28
1117

1218
### Fixed
@@ -220,7 +226,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
220226

221227
- Initial release
222228

223-
[Unreleased]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.11.1...HEAD
229+
[Unreleased]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.12.0...HEAD
230+
[2.12.0]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.12.0
224231
[2.11.1]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.11.1
225232
[2.11.0]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.11.0
226233
[2.10.0]: https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.10.0

itk_dev_shared_components/getorganized/__init__.py

Whitespace-only changes.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Functions for working with the GetOrganized API."""
2+
3+
import json
4+
from urllib.parse import urljoin
5+
from typing import Literal
6+
7+
from requests import Session
8+
from requests_ntlm import HttpNtlmAuth
9+
10+
11+
def create_session(username: str, password: str) -> Session:
12+
"""Create a session for accessing GetOrganized API.
13+
14+
Args:
15+
username: Username for login.
16+
password: Password for login.
17+
18+
Returns:
19+
Return the session object
20+
"""
21+
session = Session()
22+
session.headers.setdefault("Content-Type", "application/json")
23+
session.auth = HttpNtlmAuth(username, password)
24+
return session
25+
26+
27+
def upload_document(*, apiurl: str, file: bytearray, case_id: str, filename: str, agent_name: str | None = None, date_string: str | None = None, session: Session, doc_category: str | None = None, case_type: str = Literal["EMN", "GEO"], overwrite: bool = True) -> tuple[str, Session]:
28+
"""Upload a document to Get Organized.
29+
30+
Args:
31+
apiurl: Base url for API.
32+
session: Session token for request.
33+
file: Bytearray of file to upload.
34+
case_id: Case ID already present in GO.
35+
filename: Name of file when saved in GO.
36+
agent_name: Agent name, used for creating a folder in GO. Defaults to None.
37+
date_string: A date to add as metadata to GetOrganized. Defaults to None.
38+
39+
Returns:
40+
Return response text.
41+
"""
42+
url = apiurl + "/_goapi/Documents/AddToCase"
43+
payload = {
44+
"Bytes": list(file),
45+
"CaseId": case_id,
46+
"SiteUrl": urljoin(apiurl, f"/case{case_type}/{case_id}"),
47+
"ListName": "Dokumenter",
48+
"FolderPath": agent_name,
49+
"FileName": filename,
50+
"Metadata": f"<z:row xmlns:z='#RowsetSchema' ows_Dato='{date_string}' ows_Kategori='{doc_category}'/>",
51+
"Overwrite": overwrite
52+
}
53+
response = session.post(url, data=json.dumps(payload), timeout=60)
54+
response.raise_for_status()
55+
return response.text
56+
57+
58+
def delete_document(apiurl: str, document_id: int, session: Session) -> tuple[str, Session]:
59+
"""Delete a document from GetOrganized.
60+
61+
Args:
62+
apiurl: Url of the GetOrganized API.
63+
session: Session object used for logging in.
64+
document_id: ID of the document to delete.
65+
66+
Returns:
67+
Return the response.
68+
"""
69+
url = urljoin(apiurl, "/_goapi/Documents/ByDocumentId/")
70+
payload = {
71+
"DocId": document_id,
72+
"ForceDelete": True
73+
}
74+
response = session.delete(url, timeout=60, data=json.dumps(payload))
75+
response.raise_for_status()
76+
return response
77+
78+
79+
def create_case(session: Session, apiurl: str, title: str, category: str, department: str, kle: str, case_type: str = Literal["EMN", "GEO"]) -> str:
80+
"""Create a case in GetOrganized.
81+
82+
Args:
83+
apiurl: Url for the GetOrganized API.
84+
session: Session object to access API.
85+
title: Title of the case being created.
86+
category: Case category to create the case for.
87+
department: Department for the case.
88+
kle: KLE number for the case (https://www.kle-online.dk/emneplan/00/)
89+
90+
Returns:
91+
Return the caseID of the created case.
92+
"""
93+
url = urljoin(apiurl, "/_goapi/Cases/")
94+
payload = {
95+
'CaseTypePrefix': case_type,
96+
'MetadataXml': f'''<z:row xmlns:z="#RowsetSchema"
97+
ows_Title="{title}"
98+
ows_CaseStatus="Åben"
99+
ows_CaseCategory="{category}"
100+
ows_Afdeling="{department}"
101+
ows_KLENummer="{kle}"/>''',
102+
'ReturnWhenCaseFullyCreated': False
103+
}
104+
response = session.post(url, data=json.dumps(payload), timeout=60)
105+
response.raise_for_status()
106+
return response.json()["CaseID"]
107+
108+
109+
def get_case_metadata(session: Session, apiurl: str, case_id: str) -> str:
110+
"""Get metadata for a GetOrganized case, to look through parameters and values.
111+
112+
Args:
113+
session: Session token.
114+
apiurl: Base URL for the API.
115+
case_id: Case ID to get metadata on.
116+
117+
Returns:
118+
Return the metadata for the case as an XML string.
119+
"""
120+
url = urljoin(apiurl, f"/_goapi/Cases/Metadata/{case_id}")
121+
response = session.get(url, timeout=60)
122+
response.raise_for_status()
123+
return response.json()["Metadata"]
124+
125+
126+
def find_case(session: Session, apiurl: str, case_title: str, case_type: str = Literal["EMN", "GEO"]) -> list[str]:
127+
"""Search for an existing case in GO with the given case title.
128+
The search finds any case that contains the given title in its title.
129+
130+
Args:
131+
case_title: The title to search for.
132+
session: Session object to access the API.
133+
134+
Returns:
135+
The case id of the found case(s) if any.
136+
"""
137+
url = apiurl + "/_goapi/Cases/FindByCaseProperties"
138+
payload = {
139+
"FieldProperties": [
140+
{
141+
"InternalName": "ows_Title",
142+
"Value": case_title,
143+
"ComparisonType": "Contains",
144+
}
145+
],
146+
"CaseTypePrefixes": [case_type],
147+
"LogicalOperator": "AND",
148+
"ExcludeDeletedCases": True
149+
}
150+
response = session.post(url, data=json.dumps(payload), timeout=60)
151+
response.raise_for_status()
152+
cases = response.json()['CasesInfo']
153+
154+
return [case['CaseID'] for case in cases]

pyproject.toml

Lines changed: 3 additions & 2 deletions
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.11.1"
7+
version = "2.12.0"
88
authors = [
99
{ name="ITK Development", email="itk-rpa@mkb.aarhus.dk" },
1010
]
@@ -22,7 +22,8 @@ dependencies = [
2222
"requests == 2.*",
2323
"beautifulsoup4 == 4.*",
2424
"selenium == 4.*",
25-
"uiautomation == 2.*"
25+
"uiautomation == 2.*",
26+
"requests_ntlm == 1.*"
2627
]
2728

2829
[project.urls]

tests/readme.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,20 @@ Note: The NOVA_CVR_CASE and NOVA_CPR_CASE variables require cases to be created
7474

7575
CVR_CREDS = 'login;password'
7676

77-
### EFLYT
77+
### eFlyt
7878

79-
EFLYT_LOGIN = 'username,password'
79+
EFLYT_LOGIN = 'login,password'
8080
TEST_CPR = 'XXXXXXXXXX'
8181
TEST_CASE = '123456' # A test case with multiple current inhabitants, with relations registered
8282
TEST_CASE_NOONE = '56789' # A test case without any current inhabitants
83+
84+
### GetOrganized
85+
86+
GO_LOGIN = 'login;password'
87+
GO_APIURL = 'https://test.go.aarhuskommune.dk'
88+
GO_CATEGORY="Åben for alle",
89+
GO_DEPARTMENT="916;#Backoffice - Drift og Økonomi",
90+
GO_KLE="318;#25.02.00 Ejendomsbeskatning i almindelighed"
91+
92+
These GO-variables are found by fetching metadata for a case created in the GO interface with the correct setup for a specific process.
93+
Use get_case_metadata on a known case ID with the required setup, and use those when implementing GetOrganized.

tests/test_getorganized/__init__.py

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Tests related to the GetOrganized module."""
2+
3+
import unittest
4+
import os
5+
import re
6+
import json
7+
from uuid import uuid4
8+
9+
from dotenv import load_dotenv
10+
11+
from itk_dev_shared_components.getorganized import go_api
12+
13+
load_dotenv()
14+
15+
16+
class CaseTest(unittest.TestCase):
17+
"""Test the Case functionality of GetOrganized integration"""
18+
test_case = None
19+
20+
@classmethod
21+
def setUpClass(cls):
22+
user, password = os.getenv("GO_LOGIN").split(",")
23+
24+
cls.session = go_api.create_session(user, password)
25+
uuid = uuid4()
26+
cls.test_case = go_api.create_case(session=cls.session,
27+
apiurl=os.getenv("GO_APIURL"),
28+
title=uuid,
29+
case_type="EMN",
30+
category=os.getenv("GO_CATEGORY"),
31+
department=os.getenv("GO_DEPARTMENT"),
32+
kle=os.getenv("GO_KLE")
33+
)
34+
35+
def test_case_created(self):
36+
"""Test case is created."""
37+
self.assertIsNotNone(self.test_case)
38+
39+
def test_document(self):
40+
"""Test upload and delete of a document."""
41+
test_data = bytearray(b"Testdata")
42+
document = go_api.upload_document(session=self.session, apiurl=os.getenv("GO_APIURL"), case_id=self.test_case, filename="Testfil", file=test_data)
43+
self.assertIsNotNone(document)
44+
response = go_api.delete_document(session=self.session, apiurl=os.getenv("GO_APIURL"), document_id=json.loads(document)['DocId'])
45+
self.assertEqual(response.status_code, 200)
46+
47+
def test_find_case(self):
48+
"""Test finding a case and getting metadata."""
49+
metadata = go_api.get_case_metadata(self.session, os.getenv("GO_APIURL"), self.test_case)
50+
self.assertIsNotNone(metadata)
51+
52+
test_case_title = re.match('.*ows_Title="([^"]+)"', metadata)[1]
53+
case_found = go_api.find_case(session=self.session, apiurl=os.getenv("GO_APIURL"), case_title=test_case_title, case_type="EMN")
54+
if isinstance(case_found, list):
55+
self.assertIn(self.test_case, case_found)
56+
else:
57+
self.assertEqual(self.test_case, case_found)
58+
59+
60+
if __name__ == '__main__':
61+
unittest.main()

tests/test_nova_api/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)