From b779888ff8ef520a145b2eb265afcad187e22a71 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Mon, 22 Apr 2024 09:44:57 +0200 Subject: [PATCH 1/8] Added creating and getting nova notes --- .../kmd_nova/nova_notes.py | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 itk_dev_shared_components/kmd_nova/nova_notes.py diff --git a/itk_dev_shared_components/kmd_nova/nova_notes.py b/itk_dev_shared_components/kmd_nova/nova_notes.py new file mode 100644 index 0000000..f33ad8a --- /dev/null +++ b/itk_dev_shared_components/kmd_nova/nova_notes.py @@ -0,0 +1,137 @@ +import base64 +import uuid +import urllib.parse +from datetime import datetime + +import requests + +from itk_dev_shared_components.kmd_nova.authentication import NovaAccess +from itk_dev_shared_components.kmd_nova.nova_objects import JournalNote + + +def add_text_note(case_uuid: str, note_title: str, note_text: str, nova_access: NovaAccess) -> str: + """Add a text based journal note to a Nova case. + + Args: + case_uuid: The uuid of the case to add the journal note to. + note_title: The title of the note. + note_text: The text content of the note. + nova_access: The NovaAccess object used to authenticate. + + Returns: + The uuid of the created journal note. + """ + note_uuid = str(uuid.uuid4()) + + url = urllib.parse.urljoin(nova_access.domain, "api/Case/Update") + params = {"api-version": "1.0-Case"} + + payload = { + "common": { + "transactionId": str(uuid.uuid4()), + "uuid": case_uuid + }, + "journalNotes": [ + { + "uuid": note_uuid, + "approved": False, + "journalNoteAttributes": { + "journalNoteDate": datetime.today().isoformat(), + "title": note_title, + "journalNoteType": "Bruger", + "format": "Text", + "note": _encode_text(note_text) + } + } + ] + } + + headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"} + + response = requests.patch(url, params=params, headers=headers, json=payload, timeout=60) + response.raise_for_status() + + return note_uuid + + +def _encode_text(string: str) -> str: + """Encode a string to a base64 string. + Ensure the base64 string doesn't contain padding by + inserting spaces at the end of the input string. + + Args: + string: The string to encode. + + Returns: + A base64 string containing no padding. + """ + def b64(s: str) -> str: + """Helper function to convert a string to base64.""" + return base64.b64encode(s.encode()).decode() + + while (s := b64(string)).endswith("="): + string += ' ' + + return s + + +def get_notes(case_uuid: str, nova_access: NovaAccess, offset: int = 0, limit: int = 100) -> tuple[JournalNote, ...]: + """Get all journal notes from the given case. + + Args: + case_uuid: The uuid of the case to get notes from. + nova_access: The NovaAccess object used to authenticate. + offset: The number of journal notes to skip. + limit: The maximum number of journal notes to get (1-500). + + Returns: + A tuple of JournalNote objects. + """ + url = urllib.parse.urljoin(nova_access.domain, "api/Case/GetList") + params = {"api-version": "1.0-Case"} + + payload = { + "common": { + "transactionId": str(uuid.uuid4()), + "uuid": case_uuid + }, + "paging": { + "startRow": offset+1, + "numberOfRows": limit + }, + "caseGetOutput": { + "journalNotes": { + "uuid": True, + "approved": True, + "journalNoteAttributes": { + "title": True, + "format": True, + "note": True, + "createdTime": True + } + } + } + } + + headers = {'Content-Type': 'application/json', 'Authorization': f"Bearer {nova_access.get_bearer_token()}"} + + response = requests.put(url, params=params, headers=headers, json=payload, timeout=60) + response.raise_for_status() + + note_dicts = response.json()['cases'][0]['journalNotes']['journalNotes'] + + notes_list = [] + + for note_dict in note_dicts: + notes_list.append( + JournalNote( + uuid=note_dict['uuid'], + title=note_dict['journalNoteAttributes']['title'], + approved=note_dict['approved'], + journal_date=note_dict['journalNoteAttributes']['createdTime'], + note=note_dict['journalNoteAttributes']['note'], + note_format=note_dict['journalNoteAttributes']['format'] + ) + ) + + return tuple(notes_list) From e9b466b81623ae5d0da73c1f9002a22747ee623c Mon Sep 17 00:00:00 2001 From: Mathias G Date: Mon, 22 Apr 2024 09:45:05 +0200 Subject: [PATCH 2/8] Added Nova notes tests --- tests/test_nova_api/test_notes.py | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 tests/test_nova_api/test_notes.py diff --git a/tests/test_nova_api/test_notes.py b/tests/test_nova_api/test_notes.py new file mode 100644 index 0000000..b6e65e4 --- /dev/null +++ b/tests/test_nova_api/test_notes.py @@ -0,0 +1,72 @@ +"""Test the part of the API to do with cases.""" +import unittest +import os +from datetime import datetime +import base64 + +from itk_dev_shared_components.kmd_nova.authentication import NovaAccess +from itk_dev_shared_components.kmd_nova.nova_objects import JournalNote +from itk_dev_shared_components.kmd_nova import nova_notes, nova_cases + + +class NovaNotesTest(unittest.TestCase): + """Test the part of the API to do with notes.""" + @classmethod + def setUpClass(cls): + credentials = os.getenv('nova_api_credentials') + credentials = credentials.split(',') + cls.nova_access = NovaAccess(client_id=credentials[0], client_secret=credentials[1]) + + def test_add_note(self): + """Test adding a text note.""" + case = self._get_test_case() + + title = f"Test title {datetime.today()}" + text = f"Test note {datetime.today()}" + + nova_notes.add_text_note(case.uuid, title, text, self.nova_access) + + # Get the note back from Nova + notes = nova_notes.get_notes(case.uuid, self.nova_access, limit=10) + + nova_note = None + for note in notes: + if note.title == title: + nova_note = note + break + + self.assertIsNotNone(nova_note) + self.assertEqual(nova_note.title, title) + self.assertEqual(nova_note.note_format, "Text") + self.assertIsNotNone(nova_note.journal_date) + + # Decode note text and remove trailing spaces + nova_text = nova_note.note + nova_text = base64.b64decode(nova_text).decode() + nova_text = nova_text.rstrip() + self.assertEqual(nova_text, text) + + def test_get_notes(self): + """Test getting notes from a case.""" + case = self._get_test_case() + notes = nova_notes.get_notes(case.uuid, self.nova_access, limit=10) + self.assertGreater(len(notes), 0) + self.assertIsInstance(notes[0], JournalNote) + + def test_encoding(self): + """Test encoding strings to base 64.""" + test_data = ( + ("Hello", "SGVsbG8g"), + (".", "LiAg"), + ("This is a longer test string", "VGhpcyBpcyBhIGxvbmdlciB0ZXN0IHN0cmluZyAg") + ) + + for string, result in test_data: + self.assertEqual(nova_notes._encode_text(string), result) # pylint: disable=protected-access + + def _get_test_case(self): + return nova_cases.get_cases(self.nova_access, case_number="S2023-61078")[0] + + +if __name__ == '__main__': + unittest.main() From f03d59072b18542507f46fbcc827b42e0e347da9 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Mon, 22 Apr 2024 09:46:02 +0200 Subject: [PATCH 3/8] Updated changelog --- changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 471b2b4..ab6e0bc 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Module for getting and creating journal notes in Nova. +- Tests for journal notes. + ## [2.1.1] - 2024-04-10 ### Fixed From 0dd330c2321dcbbed3489dda211e62242030de89 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Mon, 22 Apr 2024 09:50:10 +0200 Subject: [PATCH 4/8] Updated docstrings --- itk_dev_shared_components/kmd_nova/nova_notes.py | 2 ++ tests/test_nova_api/test_notes.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/itk_dev_shared_components/kmd_nova/nova_notes.py b/itk_dev_shared_components/kmd_nova/nova_notes.py index f33ad8a..a14b0e9 100644 --- a/itk_dev_shared_components/kmd_nova/nova_notes.py +++ b/itk_dev_shared_components/kmd_nova/nova_notes.py @@ -1,3 +1,5 @@ +"""This module has functions to do with journal note related calls to the KMD Nova api.""" + import base64 import uuid import urllib.parse diff --git a/tests/test_nova_api/test_notes.py b/tests/test_nova_api/test_notes.py index b6e65e4..882f5a5 100644 --- a/tests/test_nova_api/test_notes.py +++ b/tests/test_nova_api/test_notes.py @@ -1,4 +1,4 @@ -"""Test the part of the API to do with cases.""" +"""Test the part of the API to do with journal notes.""" import unittest import os from datetime import datetime From 1ddbb0271e3e5aa49e8fcf62abc453961286f684 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Mon, 22 Apr 2024 10:12:48 +0200 Subject: [PATCH 5/8] Updated docstring --- itk_dev_shared_components/kmd_nova/nova_notes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/itk_dev_shared_components/kmd_nova/nova_notes.py b/itk_dev_shared_components/kmd_nova/nova_notes.py index a14b0e9..03366cc 100644 --- a/itk_dev_shared_components/kmd_nova/nova_notes.py +++ b/itk_dev_shared_components/kmd_nova/nova_notes.py @@ -60,6 +60,8 @@ def _encode_text(string: str) -> str: """Encode a string to a base64 string. Ensure the base64 string doesn't contain padding by inserting spaces at the end of the input string. + There is a bug in the Nova api that corrupts the string + if it contains padding. Args: string: The string to encode. From 5da9e4acc659f0983cea5710b18925131a932439 Mon Sep 17 00:00:00 2001 From: Mathias G Date: Mon, 22 Apr 2024 10:19:13 +0200 Subject: [PATCH 6/8] Added 'approved' argument. Added sanity comment --- itk_dev_shared_components/kmd_nova/nova_notes.py | 12 ++++++------ tests/test_nova_api/test_notes.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/itk_dev_shared_components/kmd_nova/nova_notes.py b/itk_dev_shared_components/kmd_nova/nova_notes.py index 03366cc..9e71aaa 100644 --- a/itk_dev_shared_components/kmd_nova/nova_notes.py +++ b/itk_dev_shared_components/kmd_nova/nova_notes.py @@ -11,13 +11,14 @@ from itk_dev_shared_components.kmd_nova.nova_objects import JournalNote -def add_text_note(case_uuid: str, note_title: str, note_text: str, nova_access: NovaAccess) -> str: +def add_text_note(case_uuid: str, note_title: str, note_text: str, approved: bool, nova_access: NovaAccess) -> str: """Add a text based journal note to a Nova case. Args: case_uuid: The uuid of the case to add the journal note to. note_title: The title of the note. note_text: The text content of the note. + approved: Whether the journal note should be marked as approved in Nova. nova_access: The NovaAccess object used to authenticate. Returns: @@ -36,7 +37,7 @@ def add_text_note(case_uuid: str, note_title: str, note_text: str, nova_access: "journalNotes": [ { "uuid": note_uuid, - "approved": False, + "approved": approved, "journalNoteAttributes": { "journalNoteDate": datetime.today().isoformat(), "title": note_title, @@ -58,10 +59,9 @@ def add_text_note(case_uuid: str, note_title: str, note_text: str, nova_access: def _encode_text(string: str) -> str: """Encode a string to a base64 string. - Ensure the base64 string doesn't contain padding by - inserting spaces at the end of the input string. - There is a bug in the Nova api that corrupts the string - if it contains padding. + Ensure the base64 string doesn't contain padding by inserting spaces at the end of the input string. + There is a bug in the Nova api that corrupts the string if it contains padding. + The extra spaces will not show up in the Nova user interface. Args: string: The string to encode. diff --git a/tests/test_nova_api/test_notes.py b/tests/test_nova_api/test_notes.py index 882f5a5..cefe68c 100644 --- a/tests/test_nova_api/test_notes.py +++ b/tests/test_nova_api/test_notes.py @@ -24,7 +24,7 @@ def test_add_note(self): title = f"Test title {datetime.today()}" text = f"Test note {datetime.today()}" - nova_notes.add_text_note(case.uuid, title, text, self.nova_access) + nova_notes.add_text_note(case.uuid, title, text, False, self.nova_access) # Get the note back from Nova notes = nova_notes.get_notes(case.uuid, self.nova_access, limit=10) From 7ad7bb73e95b51a5d5fe28a4f95862a74ead5aec Mon Sep 17 00:00:00 2001 From: ghbm-itk <123645708+ghbm-itk@users.noreply.github.com> Date: Wed, 8 May 2024 10:34:38 +0200 Subject: [PATCH 7/8] Update changelog.md --- changelog.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index ab6e0bc..9b6f7f8 100644 --- a/changelog.md +++ b/changelog.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.2.0] - 2024-05-08 + ### Added - Module for getting and creating journal notes in Nova. @@ -99,7 +101,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release -[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.1.1...HEAD +[Unreleased] https://github.com/itk-dev-rpa/ITK-dev-shared-components/compare/2.2.0...HEAD +[2.2.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.2.0 [2.1.1] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.1 [2.1.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.1.0 [2.0.0] https://github.com/itk-dev-rpa/ITK-dev-shared-components/releases/tag/2.0.0 From 68b66f6500ff7f5663ad71ba476d4aac423b3884 Mon Sep 17 00:00:00 2001 From: ghbm-itk <123645708+ghbm-itk@users.noreply.github.com> Date: Wed, 8 May 2024 10:34:57 +0200 Subject: [PATCH 8/8] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 269e3e0..369b0c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "itk_dev_shared_components" -version = "2.1.1" +version = "2.2.0" authors = [ { name="ITK Development", email="itk-rpa@mkb.aarhus.dk" }, ]