diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c40c9313..cc9a9f88 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,11 @@ Version History =============== +v7.0.2 +------ + +* Add accumulation of time lost in Jira comments + v7.0.1 ------ diff --git a/manager/api/tests/test_jira.py b/manager/api/tests/test_jira.py index a428714c..51d827d1 100644 --- a/manager/api/tests/test_jira.py +++ b/manager/api/tests/test_jira.py @@ -24,9 +24,16 @@ from unittest.mock import patch import requests +import rest_framework from django.test import TestCase, override_settings -from manager.utils import handle_jira_payload, jira_comment, jira_ticket +from manager.utils import ( + TIME_LOST_FIELD, + handle_jira_payload, + jira_comment, + jira_ticket, + update_time_lost, +) OLE_JIRA_OBS_COMPONENTS_FIELDS = [ "AuxTel", @@ -311,6 +318,36 @@ def test_needed_parameters(self): mock_jira_patcher.stop() + def test_update_time_lost(self): + """Test call to update_time_lost and verify field was updated""" + mock_jira_patcher = patch("requests.get") + mock_jira_get = mock_jira_patcher.start() + response = requests.Response() + response.status_code = 200 + response.json = lambda: {TIME_LOST_FIELD: 13.6} + mock_jira_get.return_value = response + + put_patcher = patch("requests.put") + mock_jira_put = put_patcher.start() + response.status_code = 204 + mock_jira_put.return_value = response + + # call update time lost + jira_response = update_time_lost(1, 3.4) + assert jira_response.status_code == 200 + assert jira_response.data["ack"] == "Jira time_lost field updated" + + jira_response = update_time_lost(93827, 1.23) + assert jira_response.status_code == 200 + assert jira_response.data["ack"] == "Jira time_lost field updated" + + response.status_code = 400 + mock_jira_put.return_value = response + + jira_response = update_time_lost(12, 97.01) + assert jira_response.status_code == 400 + assert jira_response.data["ack"] == "Jira time_lost field could not be updated" + def test_add_comment(self): """Test call to jira_comment function with all needed parameters""" mock_jira_patcher = patch("requests.post") @@ -323,10 +360,41 @@ def test_add_comment(self): assert jira_response.status_code == 200 assert jira_response.data["ack"] == "Jira comment created" + mock_time_lost_patcher = patch("manager.utils.update_time_lost") + mock_time_lost_client = mock_time_lost_patcher.start() + time_lost_response = rest_framework.response.Response() + time_lost_response.status_code = 200 + time_lost_response.data = { + "ack": "Jira time_lost field updated", + } + mock_time_lost_client.return_value = time_lost_response + jira_response = jira_comment(self.jira_request_narrative_full_jira_comment.data) assert jira_response.status_code == 200 assert jira_response.data["ack"] == "Jira comment created" + def test_add_comment_fail(self): + """Test jira_comment() return value when update_time_lost() + fails during jira_comment()""" + mock_jira_patcher = patch("requests.post") + mock_jira_client = mock_jira_patcher.start() + response = requests.Response() + response.status_code = 201 + mock_jira_client.return_value = response + + mock_time_lost_patcher = patch("manager.utils.update_time_lost") + mock_time_lost_client = mock_time_lost_patcher.start() + time_lost_response = rest_framework.response.Response() + time_lost_response.status_code = 400 + time_lost_response.data = { + "ack": "Jira time_lost field could not be updated", + } + mock_time_lost_client.return_value = time_lost_response + + resp = jira_comment(self.jira_request_narrative_full_jira_comment.data) + assert resp.status_code == 400 + assert resp.data["ack"] == "Jira time_lost field could not be updated" + @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 @@ -339,6 +407,12 @@ def test_handle_narrative_jira_payload(self): response.json = lambda: {"key": "LOVE-XX"} mock_jira_client.return_value = response + mock_time_lost_patcher = patch("manager.utils.update_time_lost") + mock_time_lost_client = mock_time_lost_patcher.start() + time_response = requests.Response() + time_response.status_code = 200 + mock_time_lost_client.return_value = time_response + jira_response = handle_jira_payload(self.jira_request_narrative_full_jira_new) assert jira_response.status_code == 200 assert jira_response.data["ack"] == "Jira ticket created" diff --git a/manager/manager/utils.py b/manager/manager/utils.py index f3994760..3d0147c6 100644 --- a/manager/manager/utils.py +++ b/manager/manager/utils.py @@ -40,6 +40,9 @@ # 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" class LocationPermission(BasePermission): @@ -449,16 +452,16 @@ def jira_ticket(request_data): # "on" if int(request_data.get("level", 0)) >= 100 # else "off" # ), - "customfield_10106": float(request_data.get("time_lost", 0)), + TIME_LOST_FIELD: float(request_data.get("time_lost", 0)), # Default values of the following fields are set to -1 - "customfield_10107": { + PRIMARY_SOFTWARE_COMPONENTS_IDS: { "id": ( str(primary_software_components_ids[0]) if primary_software_components_ids else "-1" ) }, - "customfield_10196": { + PRIMARY_HARDWARE_COMPONENTS_IDS: { "id": ( str(primary_hardware_components_ids[0]) if primary_hardware_components_ids @@ -505,6 +508,57 @@ def jira_ticket(request_data): ) +def update_time_lost(jira_id: int, add_time_lost: float = 0.0) -> Response: + """Connect to the Rubin Observatory JIRA Cloud REST API to + update time_lost field in a given jira ticket + + Params + ------ + jira_id: int + Jira ID + add_time_lost: float + time value given from comment + + Returns + ------- + Response + The response and status code of the request to the JIRA API + 200 if the time_lost field was successfully updated + 400 if the time_lost field was not updated + """ + headers = { + "Authorization": f"Basic {os.environ.get('JIRA_API_TOKEN')}", + "content-type": "application/json", + } + url = f"https://{os.environ.get('JIRA_API_HOSTNAME')}/rest/api/latest/issue/{jira_id}/" + response = requests.get(url, headers=headers) + existent_time_lost = ( + response.json().get(TIME_LOST_FIELD, 0.0) + if response.status_code == 200 + else 0.0 + ) + jira_payload = { + "fields": { + TIME_LOST_FIELD: float(existent_time_lost + add_time_lost), + }, + } + response = requests.put(url, json=jira_payload, headers=headers) + if response.status_code == 204: + return Response( + { + "ack": "Jira time_lost field updated", + "url": f"https://{os.environ.get('JIRA_API_HOSTNAME')}/browse/{jira_id}", + }, + status=200, + ) + return Response( + { + "ack": "Jira time_lost field could not be updated", + }, + status=400, + ) + + def jira_comment(request_data): """Connect to the Rubin Observatory JIRA Cloud REST API to make a comment on a previously created ticket. @@ -547,6 +601,13 @@ def jira_comment(request_data): } url = f"https://{os.environ.get('JIRA_API_HOSTNAME')}/rest/api/latest/issue/{jira_id}/comment" response = requests.post(url, json=jira_payload, headers=headers) + if "time_lost" in request_data: + timelost_response = update_time_lost( + jira_id=jira_id, add_time_lost=request_data.get("time_lost", 0.0) + ) + if timelost_response.status_code != 200: + return timelost_response + if response.status_code == 201: return Response( { @@ -618,8 +679,8 @@ def get_jira_obs_report(request_data): "key": issue["key"], "summary": issue["fields"]["summary"], "time_lost": ( - issue["fields"]["customfield_10106"] - if issue["fields"]["customfield_10106"] is not None + issue["fields"][TIME_LOST_FIELD] + if issue["fields"][TIME_LOST_FIELD] is not None else 0.0 ), "reporter": issue["fields"]["creator"]["displayName"],