diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e679d9a..57a84b43 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Version History =============== +v7.2.0 +------ + +* Refactor jira_ticket method to comply with new OBS systems hierarchy. ``_ + v7.1.7 ------ diff --git a/manager/api/tests/test_jira.py b/manager/api/tests/test_jira.py index 4afd4eef..2aac518e 100644 --- a/manager/api/tests/test_jira.py +++ b/manager/api/tests/test_jira.py @@ -18,7 +18,6 @@ # this program. If not, see . -import math import os import random from unittest.mock import patch @@ -30,7 +29,7 @@ from django.test import TestCase, override_settings from manager.utils import ( - TIME_LOST_FIELD, + OBS_TIME_LOST_FIELD, get_jira_obs_report, handle_jira_payload, jira_comment, @@ -38,41 +37,48 @@ update_time_lost, ) -OLE_JIRA_OBS_COMPONENTS_FIELDS = [ - "AuxTel", - "Calibrations", - "Environmental Monitoring Systems", - "Facilities", - "IT Infrastricture", - "MainTel", - "Observer Remark", - "Other", - "Unknown", -] - -OLE_JIRA_OBS_PRIMARY_SOFTWARE_COMPONENT_FIELDS = [ - "None", - "CSC level", - "Component Level (EUI)", - "Visualization", - "Analysis", - "Other", - "Camera Control Software", -] - -OLE_JIRA_OBS_PRIMARY_HARDWARE_COMPONENT_FIELDS = [ - "None", - "Mount", - "Rotator", - "Hexapod", - "M2", - "Science Cameras", - "M1M3", - "Dome", - "Utilities", - "Calibration", - "Other", -] +JIRA_OBS_SYSTEMS_SELECTION_EXAMPLE = """ +{ + "selection": [ + [ + { + "name": "AuxTel", + "id": "1", + "children": [ + "60", + "61", + "62", + "63", + "64", + "430", + "128" + ] + } + ], + [ + { + "name": "AT: OCS", + "id": "430", + "children": [ + "432", + "433", + "434", + "435", + "436", + "437", + "438" + ] + } + ], + [ + { + "name": "ATScheduler CSC", + "id": "437" + } + ] + ] +} +""" @override_settings(DEBUG=True) @@ -109,40 +115,7 @@ def setUp(self): } request_narrative = { - "components": ",".join( - list( - OLE_JIRA_OBS_COMPONENTS_FIELDS[ - : math.ceil( - random.random() * (len(OLE_JIRA_OBS_COMPONENTS_FIELDS) - 1) - ) - ] - ) - ), - "components_ids": ",".join( - [str(n) for n in range(1, math.ceil(random.random() * 100))] - ), - "primary_software_components": ",".join( - OLE_JIRA_OBS_PRIMARY_SOFTWARE_COMPONENT_FIELDS[ - math.ceil( - random.random() - * (len(OLE_JIRA_OBS_PRIMARY_SOFTWARE_COMPONENT_FIELDS) - 1) - ) - ] - ), - "primary_software_components_ids": ",".join( - [str(math.ceil(random.random() * 100))] - ), - "primary_hardware_components": ",".join( - OLE_JIRA_OBS_PRIMARY_HARDWARE_COMPONENT_FIELDS[ - math.ceil( - random.random() - * (len(OLE_JIRA_OBS_PRIMARY_HARDWARE_COMPONENT_FIELDS) - 1) - ) - ] - ), - "primary_hardware_components_ids": ",".join( - [str(math.ceil(random.random() * 100))] - ), + "jira_obs_selection": JIRA_OBS_SYSTEMS_SELECTION_EXAMPLE, "date_begin": "2022-07-03T19:58:13.00000", "date_end": "2022-07-04T19:25:13.00000", "time_lost": 10, @@ -231,6 +204,13 @@ def setUp(self): data=data_narrative_without_param ) + # narrative with not valid jira_obs_selection json + data_narrative_invalid_jira_obs_selection = {**request_full_narrative} + data_narrative_invalid_jira_obs_selection["jira_obs_selection"] = "invalid_json" + self.jira_request_narrative_invalid_jira_obs_selection = requests.Request( + data=data_narrative_invalid_jira_obs_selection + ) + # all parameters requests self.jira_request_exposure_full = requests.Request(data=request_full_exposure) self.jira_request_narrative_full = requests.Request(data=request_full_narrative) @@ -298,6 +278,13 @@ def test_missing_parameters(self): jira_response = jira_ticket(self.jira_request_narrative_without_param.data) assert "Error creating jira payload" in jira_response.data["ack"] + def test_not_valid_obs_systems_json(self): + """Test call to jira_ticket function with invalid jira_obs_selection""" + jira_response = jira_ticket( + self.jira_request_narrative_invalid_jira_obs_selection.data + ) + assert "Error creating jira payload" in jira_response.data["ack"] + @patch.dict(os.environ, {"JIRA_API_HOSTNAME": "jira.lsstcorp.org"}) def test_needed_parameters(self): """Test call to jira_ticket function with all needed parameters""" @@ -332,7 +319,7 @@ def test_update_time_lost(self): mock_jira_get = mock_jira_patcher.start() response_get = requests.Response() response_get.status_code = 200 - response_get.json = lambda: {"fields": {TIME_LOST_FIELD: 13.6}} + response_get.json = lambda: {"fields": {OBS_TIME_LOST_FIELD: 13.6}} mock_jira_get.return_value = response_get put_patcher = patch("requests.put") @@ -360,13 +347,16 @@ def test_update_time_lost(self): assert jira_response.status_code == 400 assert jira_response.data["ack"] == "Jira time_lost field could not be updated" + mock_jira_patcher.stop() + put_patcher.stop() + def test_update_current_time_lost_none(self): """Test call to update_time_lost with None as current time_lost""" mock_jira_patcher = patch("requests.get") mock_jira_get = mock_jira_patcher.start() response_get = requests.Response() response_get.status_code = 200 - response_get.json = lambda: {"fields": {TIME_LOST_FIELD: None}} + response_get.json = lambda: {"fields": {OBS_TIME_LOST_FIELD: None}} mock_jira_get.return_value = response_get put_patcher = patch("requests.put") @@ -380,6 +370,9 @@ def test_update_current_time_lost_none(self): assert jira_response.status_code == 200 assert jira_response.data["ack"] == "Jira time_lost field updated" + mock_jira_patcher.stop() + put_patcher.stop() + def test_add_comment(self): """Test call to jira_comment function with all needed parameters""" mock_jira_patcher = patch("requests.post") @@ -405,6 +398,9 @@ def test_add_comment(self): assert jira_response.status_code == 200 assert jira_response.data["ack"] == "Jira comment created" + mock_jira_patcher.stop() + mock_time_lost_patcher.stop() + def test_add_comment_fail(self): """Test jira_comment() return value when update_time_lost() fails during jira_comment()""" @@ -427,6 +423,9 @@ def test_add_comment_fail(self): assert resp.status_code == 400 assert resp.data["ack"] == "Jira time_lost field could not be updated" + mock_jira_patcher.stop() + mock_time_lost_patcher.stop() + @patch.dict(os.environ, {"JIRA_API_HOSTNAME": "jira.lsstcorp.org"}) def test_handle_narrative_jira_payload(self): """Test call to function handle_jira_payload with all needed parameters @@ -464,6 +463,7 @@ def test_handle_narrative_jira_payload(self): ) mock_jira_patcher.stop() + mock_time_lost_patcher.stop() def test_handle_exposure_jira_payload(self): """Test call to function handle_jira_payload with all needed parameters @@ -534,7 +534,7 @@ def test_get_jira_obs_report(self): "key": "LOVE-XX", "fields": { "summary": "Issue title", - TIME_LOST_FIELD: 13.6, + OBS_TIME_LOST_FIELD: 13.6, "creator": {"displayName": "user"}, "created": "2024-11-27T12:00:00.00000", }, diff --git a/manager/api/tests/test_ole.py b/manager/api/tests/test_ole.py index 56b1e919..20a1ab4b 100644 --- a/manager/api/tests/test_ole.py +++ b/manager/api/tests/test_ole.py @@ -18,6 +18,7 @@ # this program. If not, see . +import json from unittest.mock import patch import requests @@ -30,6 +31,22 @@ from manager.utils import DATETIME_ISO_FORMAT, get_tai_from_utc +OBS_SYSTEMS_HIERARCHY = """ +{ + "name": "AuxTel", + "children": [ + { + "name": "OCS", + "children": [ + { + "name": "ATScheduler CSC" + } + ] + } + ] +} +""" + @override_settings(DEBUG=True) class OLETestCase(TestCase): @@ -70,9 +87,7 @@ def setUp(self): payload_narrative = { "request_type": "narrative", - "components": "MainTel", - "primary_software_components": "None", - "primary_hardware_components": "None", + "components_json": OBS_SYSTEMS_HIERARCHY, "date_begin": "2024-01-01T00:00:00.000000", "date_end": "2024-01-01T00:10:00.000000", "time_lost": 1, @@ -141,7 +156,7 @@ def test_exposurelog_list(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 0) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_simple_exposurelog_create(self): """Test exposurelog create.""" @@ -162,7 +177,7 @@ def test_simple_exposurelog_create(self): response = self.client.post(url, self.payload_full_exposure) self.assertEqual(response.status_code, 201) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_exposurelog_update(self): """Test exposurelog update.""" @@ -183,7 +198,7 @@ def test_exposurelog_update(self): response = self.client.put(url, self.payload_full_exposure) self.assertEqual(response.status_code, 200) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_exposurelog_create_with_jira(self): """Test exposurelog create with jira.""" @@ -230,8 +245,9 @@ def test_exposurelog_create_with_jira(self): response = self.client.post(url, self.payload_full_exposure_with_jira_comment) self.assertEqual(response.status_code, 201) - mock_jira_ticket_client.stop() - mock_ole_client.stop() + mock_jira_ticket_patcher.stop() + mock_jira_comment_patcher.stop() + mock_ole_patcher.stop() def test_exposurelog_update_with_jira(self): """Test exposurelog update with jira.""" @@ -278,8 +294,9 @@ def test_exposurelog_update_with_jira(self): response = self.client.put(url, self.payload_full_exposure_with_jira_comment) self.assertEqual(response.status_code, 200) - mock_jira_ticket_client.stop() - mock_ole_client.stop() + mock_jira_ticket_patcher.stop() + mock_jira_comment_patcher.stop() + mock_ole_patcher.stop() def test_narrativelog_list(self): """Test narrativelog list.""" @@ -301,7 +318,7 @@ def test_narrativelog_list(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 0) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_simple_narrativelog_create(self): """Test narrativelog create.""" @@ -338,7 +355,7 @@ def test_simple_narrativelog_create(self): assert date_begin_arg == payload_date_begin_formatted assert date_end_arg == payload_date_end_formatted - mock_ole_client.stop() + mock_ole_patcher.stop() def test_narrativelog_update(self): """Test narrativelog update.""" @@ -375,7 +392,7 @@ def test_narrativelog_update(self): assert date_begin_arg == payload_date_begin_formatted assert date_end_arg == payload_date_end_formatted - mock_ole_client.stop() + mock_ole_patcher.stop() def test_narrative_log_create_with_jira(self): """Test narrativelog create with jira.""" @@ -422,8 +439,9 @@ def test_narrative_log_create_with_jira(self): response = self.client.post(url, self.payload_full_narrative_with_jira_comment) self.assertEqual(response.status_code, 201) - mock_jira_ticket_client.stop() - mock_ole_client.stop() + mock_jira_ticket_patcher.stop() + mock_jira_comment_patcher.stop() + mock_ole_patcher.stop() def test_narrative_log_update_with_jira(self): """Test narrativelog update with jira.""" @@ -470,8 +488,23 @@ def test_narrative_log_update_with_jira(self): response = self.client.put(url, self.payload_full_narrative_with_jira_comment) self.assertEqual(response.status_code, 200) - mock_jira_ticket_client.stop() - mock_ole_client.stop() + mock_jira_ticket_patcher.stop() + mock_jira_comment_patcher.stop() + mock_ole_patcher.stop() + + def test_narrativelog_create_with_not_valid_obs_systems_hierarchy(self): + """Test narrativelog create with not valid OBS systems hierarchy.""" + # Arrange: + self.client.credentials( + HTTP_AUTHORIZATION="Token " + self.token_user_normal.key + ) + + # Act: + url = reverse("NarrativeLogs-list") + with self.assertRaises(json.decoder.JSONDecodeError): + self.client.post( + url, {**self.payload_full_narrative, "components_json": "invalid"} + ) @override_settings(DEBUG=True) @@ -543,7 +576,7 @@ def test_nightreport_list(self): self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data), 1) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_simple_nightreport_create(self): """Test nightreport create.""" @@ -564,7 +597,7 @@ def test_simple_nightreport_create(self): response = self.client.post(url, self.payload) self.assertEqual(response.status_code, 201) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_nightreport_update(self): """Test nightreport update.""" @@ -585,7 +618,7 @@ def test_nightreport_update(self): response = self.client.put(url, self.payload) self.assertEqual(response.status_code, 200) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_nightreport_delete(self): """Test nightreport delete.""" @@ -606,7 +639,7 @@ def test_nightreport_delete(self): response = self.client.delete(url) self.assertEqual(response.status_code, 200) - mock_ole_client.stop() + mock_ole_patcher.stop() def test_nightreport_send(self): """Test nightreport send.""" @@ -644,10 +677,10 @@ def test_nightreport_send(self): response = self.client.post(url) self.assertEqual(response.status_code, 200) - mock_ole_client_get.stop() - mock_ole_client_patch.stop() - mock_get_jira_obs_report_client.stop() - mock_send_smtp_email_client.stop() + mock_ole_patcher_get.stop() + mock_ole_patcher_patch.stop() + mock_get_jira_obs_report.stop() + mock_send_smtp_email.stop() def test_nightreport_already_sent(self): """Test nightreport already sent.""" @@ -673,4 +706,4 @@ def test_nightreport_already_sent(self): self.assertEqual(response.status_code, 400) self.assertEqual(response.data, {"error": "Night report already sent"}) - mock_ole_client_get.stop() + mock_ole_patcher_get.stop() diff --git a/manager/api/views.py b/manager/api/views.py index 92640568..01be0c99 100644 --- a/manager/api/views.py +++ b/manager/api/views.py @@ -1319,15 +1319,9 @@ def create(self, request, *args, **kwargs): tai_datetime = get_tai_from_utc(json_data[key]) json_data[key] = tai_datetime.strftime(DATETIME_ISO_FORMAT) - # Split lists of values separated by comma - array_keys = { - "components", - "primary_software_components", - "primary_hardware_components", - } - for key in array_keys: - if key in json_data: - json_data[key] = json_data[key].split(",") + # Transform components_json to a dict + if "components_json" in json_data: + json_data["components_json"] = json.loads(json_data["components_json"]) # Add LFA and JIRA urls to the payload json_data["urls"] = [jira_url, *lfa_urls] @@ -1387,16 +1381,18 @@ def update(self, request, pk=None, *args, **kwargs): tai_datetime = get_tai_from_utc(json_data[key]) json_data[key] = tai_datetime.strftime(DATETIME_ISO_FORMAT) + # Split lists of values separated by comma array_keys = { - "components", - "primary_software_components", - "primary_hardware_components", "urls", } for key in array_keys: if key in json_data: json_data[key] = json_data[key].split(",") + # Transform components_json to a dict + if "components_json" in json_data: + json_data["components_json"] = json.loads(json_data["components_json"]) + # Add LFA and JIRA urls urls to the payload json_data["urls"] = [ jira_url, diff --git a/manager/manager/utils.py b/manager/manager/utils.py index da5689ca..e2381ea6 100644 --- a/manager/manager/utils.py +++ b/manager/manager/utils.py @@ -41,9 +41,10 @@ # Constants JSON_RESPONSE_LOCAL_STORAGE_NOT_ALLOWED = {"error": "Local storage not allowed."} JSON_RESPONSE_ERROR_NOT_VALID_JSON = {"error": "Not a valid JSON response."} -TIME_LOST_FIELD = "customfield_10106" -PRIMARY_SOFTWARE_COMPONENTS_IDS = "customfield_10107" -PRIMARY_HARDWARE_COMPONENTS_IDS = "customfield_10196" + +OBS_ISSUE_TYPE_ID = "10065" +OBS_TIME_LOST_FIELD = "customfield_10106" +OBS_SYSTEMS_FIELD = "customfield_10476" DATETIME_ISO_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" @@ -416,26 +417,12 @@ def jira_ticket(request_data): tags_data = request_data.get("tags").split(",") if request_data.get("tags") else [] - components_ids = ( - request_data.get("components_ids").split(",") - if request_data.get("components_ids") - else [] - ) - primary_software_components_ids = ( - request_data.get("primary_software_components_ids").split(",") - if request_data.get("primary_software_components_ids") - else None - ) - primary_hardware_components_ids = ( - request_data.get("primary_hardware_components_ids").split(",") - if request_data.get("primary_hardware_components_ids") - else None - ) + obs_system_selection = request_data.get("jira_obs_selection") try: jira_payload = { "fields": { - "issuetype": {"id": "10065"}, + "issuetype": {"id": OBS_ISSUE_TYPE_ID}, # If the JIRA_PROJECT_ID environment variable is not set, # the project id is set to the OBS project by default: 10063. # Set it in case the OBS project id has changed for any reason @@ -455,27 +442,11 @@ def jira_ticket(request_data): # "on" if int(request_data.get("level", 0)) >= 100 # else "off" # ), - TIME_LOST_FIELD: float(request_data.get("time_lost", 0)), - # Default values of the following fields are set to -1 - PRIMARY_SOFTWARE_COMPONENTS_IDS: { - "id": ( - str(primary_software_components_ids[0]) - if primary_software_components_ids - else "-1" - ) - }, - PRIMARY_HARDWARE_COMPONENTS_IDS: { - "id": ( - str(primary_hardware_components_ids[0]) - if primary_hardware_components_ids - else "-1" - ) - }, - }, - "update": { - "components": [{"set": [{"id": str(id)} for id in components_ids]}] + OBS_TIME_LOST_FIELD: float(request_data.get("time_lost", 0)), }, } + if obs_system_selection: + jira_payload["fields"][OBS_SYSTEMS_FIELD] = json.loads(obs_system_selection) except Exception as e: return Response( { @@ -490,7 +461,6 @@ def jira_ticket(request_data): "content-type": "application/json", } url = f"https://{os.environ.get('JIRA_API_HOSTNAME')}/rest/api/latest/issue/" - response = requests.post(url, json=jira_payload, headers=headers) response_data = response.json() if response.status_code == 201: @@ -542,11 +512,11 @@ def update_time_lost(jira_id: int, add_time_lost: float = 0.0) -> Response: if response.status_code == 200: jira_ticket_fields = response.json().get("fields", {}) - time_lost_value = jira_ticket_fields.get(TIME_LOST_FIELD, 0.0) + time_lost_value = jira_ticket_fields.get(OBS_TIME_LOST_FIELD, 0.0) existent_time_lost = float(time_lost_value) if time_lost_value else 0.0 jira_payload = { "fields": { - TIME_LOST_FIELD: existent_time_lost + add_time_lost, + OBS_TIME_LOST_FIELD: existent_time_lost + add_time_lost, }, } response = requests.put(url, json=jira_payload, headers=headers) @@ -734,8 +704,8 @@ def get_jira_obs_report(request_data): "key": issue["key"], "summary": issue["fields"]["summary"], "time_lost": ( - issue["fields"][TIME_LOST_FIELD] - if issue["fields"][TIME_LOST_FIELD] is not None + issue["fields"][OBS_TIME_LOST_FIELD] + if issue["fields"][OBS_TIME_LOST_FIELD] is not None else 0.0 ), "reporter": issue["fields"]["creator"]["displayName"],