Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ 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.
- Tests for journal notes.

## [2.1.1] - 2024-04-10

### Fixed
Expand Down Expand Up @@ -94,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
Expand Down
141 changes: 141 additions & 0 deletions itk_dev_shared_components/kmd_nova/nova_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""This module has functions to do with journal note related calls to the KMD Nova api."""

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, 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:
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": approved,
"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.
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.

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)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand Down
72 changes: 72 additions & 0 deletions tests/test_nova_api/test_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Test the part of the API to do with journal notes."""
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, False, 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()