Skip to content

Commit 8048dec

Browse files
authored
Merge pull request #47 from itk-dev-rpa/release/2.2.0
Release/2.2.0
2 parents 3c01a0c + 68b66f6 commit 8048dec

File tree

4 files changed

+223
-2
lines changed

4 files changed

+223
-2
lines changed

changelog.md

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

88
## [Unreleased]
99

10+
## [2.2.0] - 2024-05-08
11+
12+
### Added
13+
14+
- Module for getting and creating journal notes in Nova.
15+
- Tests for journal notes.
16+
1017
## [2.1.1] - 2024-04-10
1118

1219
### Fixed
@@ -94,7 +101,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
94101

95102
- Initial release
96103

97-
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.1.1...HEAD
104+
[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.2.0...HEAD
105+
[2.2.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.2.0
98106
[2.1.1] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.1
99107
[2.1.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.0
100108
[2.0.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.0.0
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"""This module has functions to do with journal note related calls to the KMD Nova api."""
2+
3+
import base64
4+
import uuid
5+
import urllib.parse
6+
from datetime import datetime
7+
8+
import requests
9+
10+
from itk_dev_shared_components.kmd_nova.authentication import NovaAccess
11+
from itk_dev_shared_components.kmd_nova.nova_objects import JournalNote
12+
13+
14+
def add_text_note(case_uuid: str, note_title: str, note_text: str, approved: bool, nova_access: NovaAccess) -> str:
15+
"""Add a text based journal note to a Nova case.
16+
17+
Args:
18+
case_uuid: The uuid of the case to add the journal note to.
19+
note_title: The title of the note.
20+
note_text: The text content of the note.
21+
approved: Whether the journal note should be marked as approved in Nova.
22+
nova_access: The NovaAccess object used to authenticate.
23+
24+
Returns:
25+
The uuid of the created journal note.
26+
"""
27+
note_uuid = str(uuid.uuid4())
28+
29+
url = urllib.parse.urljoin(nova_access.domain, "api/Case/Update")
30+
params = {"api-version": "1.0-Case"}
31+
32+
payload = {
33+
"common": {
34+
"transactionId": str(uuid.uuid4()),
35+
"uuid": case_uuid
36+
},
37+
"journalNotes": [
38+
{
39+
"uuid": note_uuid,
40+
"approved": approved,
41+
"journalNoteAttributes": {
42+
"journalNoteDate": datetime.today().isoformat(),
43+
"title": note_title,
44+
"journalNoteType": "Bruger",
45+
"format": "Text",
46+
"note": _encode_text(note_text)
47+
}
48+
}
49+
]
50+
}
51+
52+
headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"}
53+
54+
response = requests.patch(url, params=params, headers=headers, json=payload, timeout=60)
55+
response.raise_for_status()
56+
57+
return note_uuid
58+
59+
60+
def _encode_text(string: str) -> str:
61+
"""Encode a string to a base64 string.
62+
Ensure the base64 string doesn't contain padding by inserting spaces at the end of the input string.
63+
There is a bug in the Nova api that corrupts the string if it contains padding.
64+
The extra spaces will not show up in the Nova user interface.
65+
66+
Args:
67+
string: The string to encode.
68+
69+
Returns:
70+
A base64 string containing no padding.
71+
"""
72+
def b64(s: str) -> str:
73+
"""Helper function to convert a string to base64."""
74+
return base64.b64encode(s.encode()).decode()
75+
76+
while (s := b64(string)).endswith("="):
77+
string += ' '
78+
79+
return s
80+
81+
82+
def get_notes(case_uuid: str, nova_access: NovaAccess, offset: int = 0, limit: int = 100) -> tuple[JournalNote, ...]:
83+
"""Get all journal notes from the given case.
84+
85+
Args:
86+
case_uuid: The uuid of the case to get notes from.
87+
nova_access: The NovaAccess object used to authenticate.
88+
offset: The number of journal notes to skip.
89+
limit: The maximum number of journal notes to get (1-500).
90+
91+
Returns:
92+
A tuple of JournalNote objects.
93+
"""
94+
url = urllib.parse.urljoin(nova_access.domain, "api/Case/GetList")
95+
params = {"api-version": "1.0-Case"}
96+
97+
payload = {
98+
"common": {
99+
"transactionId": str(uuid.uuid4()),
100+
"uuid": case_uuid
101+
},
102+
"paging": {
103+
"startRow": offset+1,
104+
"numberOfRows": limit
105+
},
106+
"caseGetOutput": {
107+
"journalNotes": {
108+
"uuid": True,
109+
"approved": True,
110+
"journalNoteAttributes": {
111+
"title": True,
112+
"format": True,
113+
"note": True,
114+
"createdTime": True
115+
}
116+
}
117+
}
118+
}
119+
120+
headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"}
121+
122+
response = requests.put(url, params=params, headers=headers, json=payload, timeout=60)
123+
response.raise_for_status()
124+
125+
note_dicts = response.json()['cases'][0]['journalNotes']['journalNotes']
126+
127+
notes_list = []
128+
129+
for note_dict in note_dicts:
130+
notes_list.append(
131+
JournalNote(
132+
uuid=note_dict['uuid'],
133+
title=note_dict['journalNoteAttributes']['title'],
134+
approved=note_dict['approved'],
135+
journal_date=note_dict['journalNoteAttributes']['createdTime'],
136+
note=note_dict['journalNoteAttributes']['note'],
137+
note_format=note_dict['journalNoteAttributes']['format']
138+
)
139+
)
140+
141+
return tuple(notes_list)

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.1.1"
7+
version = "2.2.0"
88
authors = [
99
{ name="ITK Development", email="itk-rpa@mkb.aarhus.dk" },
1010
]

tests/test_nova_api/test_notes.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"""Test the part of the API to do with journal notes."""
2+
import unittest
3+
import os
4+
from datetime import datetime
5+
import base64
6+
7+
from itk_dev_shared_components.kmd_nova.authentication import NovaAccess
8+
from itk_dev_shared_components.kmd_nova.nova_objects import JournalNote
9+
from itk_dev_shared_components.kmd_nova import nova_notes, nova_cases
10+
11+
12+
class NovaNotesTest(unittest.TestCase):
13+
"""Test the part of the API to do with notes."""
14+
@classmethod
15+
def setUpClass(cls):
16+
credentials = os.getenv('nova_api_credentials')
17+
credentials = credentials.split(',')
18+
cls.nova_access = NovaAccess(client_id=credentials[0], client_secret=credentials[1])
19+
20+
def test_add_note(self):
21+
"""Test adding a text note."""
22+
case = self._get_test_case()
23+
24+
title = f"Test title {datetime.today()}"
25+
text = f"Test note {datetime.today()}"
26+
27+
nova_notes.add_text_note(case.uuid, title, text, False, self.nova_access)
28+
29+
# Get the note back from Nova
30+
notes = nova_notes.get_notes(case.uuid, self.nova_access, limit=10)
31+
32+
nova_note = None
33+
for note in notes:
34+
if note.title == title:
35+
nova_note = note
36+
break
37+
38+
self.assertIsNotNone(nova_note)
39+
self.assertEqual(nova_note.title, title)
40+
self.assertEqual(nova_note.note_format, "Text")
41+
self.assertIsNotNone(nova_note.journal_date)
42+
43+
# Decode note text and remove trailing spaces
44+
nova_text = nova_note.note
45+
nova_text = base64.b64decode(nova_text).decode()
46+
nova_text = nova_text.rstrip()
47+
self.assertEqual(nova_text, text)
48+
49+
def test_get_notes(self):
50+
"""Test getting notes from a case."""
51+
case = self._get_test_case()
52+
notes = nova_notes.get_notes(case.uuid, self.nova_access, limit=10)
53+
self.assertGreater(len(notes), 0)
54+
self.assertIsInstance(notes[0], JournalNote)
55+
56+
def test_encoding(self):
57+
"""Test encoding strings to base 64."""
58+
test_data = (
59+
("Hello", "SGVsbG8g"),
60+
(".", "LiAg"),
61+
("This is a longer test string", "VGhpcyBpcyBhIGxvbmdlciB0ZXN0IHN0cmluZyAg")
62+
)
63+
64+
for string, result in test_data:
65+
self.assertEqual(nova_notes._encode_text(string), result) # pylint: disable=protected-access
66+
67+
def _get_test_case(self):
68+
return nova_cases.get_cases(self.nova_access, case_number="S2023-61078")[0]
69+
70+
71+
if __name__ == '__main__':
72+
unittest.main()

0 commit comments

Comments
 (0)