diff --git a/jira/client.py b/jira/client.py index ba430c65b..108542d44 100644 --- a/jira/client.py +++ b/jira/client.py @@ -293,7 +293,7 @@ class JIRA: "context_path": "/", "rest_path": "api", "rest_api_version": "2", - "agile_rest_path": GreenHopperResource.GREENHOPPER_REST_PATH, + "agile_rest_path": GreenHopperResource.AGILE_BASE_REST_PATH, "agile_rest_api_version": "1.0", "verify": True, "resilient": True, @@ -4669,14 +4669,36 @@ def add_issues_to_epic( url = self._get_url(f"epics/{epic_id}/add", base=self.AGILE_BASE_URL) return self._session.put(url, data=json.dumps(data)) - # TODO(ssbarnea): Both GreenHopper and new Jira Agile API support moving more than one issue. - def rank(self, issue: str, next_issue: str) -> Response: - """Rank an issue before another using the default Ranking field, the one named 'Rank'. + # TODO(ssbarnea): Jira Agile API supports moving more than one issue. + def rank( + self, + issue: str, + next_issue: Optional[str] = None, + prev_issue: Optional[str] = None, + ) -> Response: + """Rank an issue before/after another using the default Ranking field, the one named 'Rank'. + + Pass only ONE of `next_issue` or `prev_issue`. Args: - issue (str): issue key of the issue to be ranked before the second one. - next_issue (str): issue key of the second issue. + issue (str): issue key of the issue to be ranked before/after the second one. + next_issue (str): issue key that the first issue is to be ranked before. + prev_issue (str): issue key that the first issue is to be ranked after. """ + + if next_issue is None and prev_issue is None: + raise ValueError("One of 'next_issue' or 'prev_issue' must be specified") + elif next_issue is not None and prev_issue is not None: + raise ValueError( + "Only one of 'next_issue' or 'prev_issue' may be specified" + ) + if next_issue is not None: + before_or_after = "Before" + other_issue = next_issue + elif prev_issue is not None: + before_or_after = "After" + other_issue = prev_issue + if not self._rank: for field in self.fields(): if field["name"] == "Rank": @@ -4693,38 +4715,21 @@ def rank(self, issue: str, next_issue: str) -> Response: # Obsolete since Jira v6.3.13.1 self._rank = field["schema"]["customId"] - if self._options["agile_rest_path"] == GreenHopperResource.AGILE_BASE_REST_PATH: - url = self._get_url("issue/rank", base=self.AGILE_BASE_URL) - payload = { - "issues": [issue], - "rankBeforeIssue": next_issue, - "rankCustomFieldId": self._rank, - } - try: - return self._session.put(url, data=json.dumps(payload)) - except JIRAError as e: - if e.status_code == 404: - warnings.warn( - "Status code 404 may mean, that too old Jira Agile version is installed." - " At least version 6.7.10 is required." - ) - raise - elif ( - self._options["agile_rest_path"] - == GreenHopperResource.GREENHOPPER_REST_PATH - ): - data = { - "issueKeys": [issue], - "rankBeforeKey": next_issue, - "customFieldId": self._rank, - } - url = self._get_url("rank", base=self.AGILE_BASE_URL) - return self._session.put(url, data=json.dumps(data)) - else: - raise NotImplementedError( - 'No API for ranking issues for agile_rest_path="%s"' - % self._options["agile_rest_path"] - ) + url = self._get_url("issue/rank", base=self.AGILE_BASE_URL) + payload = { + "issues": [issue], + f"rank{before_or_after}Issue": other_issue, + "rankCustomFieldId": self._rank, + } + try: + return self._session.put(url, data=json.dumps(payload)) + except JIRAError as e: + if e.status_code == 404: + warnings.warn( + "Status code 404 may mean, that too old Jira Agile version is installed." + " At least version 6.7.10 is required." + ) + raise def move_to_backlog(self, issue_keys: str) -> Response: """Move issues in ``issue_keys`` to the backlog, removing them from all sprints that have not been completed. diff --git a/tests/resources/test_issue.py b/tests/resources/test_issue.py index ca7bcbcd0..4b7ca4e28 100644 --- a/tests/resources/test_issue.py +++ b/tests/resources/test_issue.py @@ -446,6 +446,25 @@ def test_transitioning(self): # self.assertEqual(issue.fields.assignee.name, self.test_manager.CI_JIRA_USER) # self.assertEqual(issue.fields.status.id, transition_id) + def test_rank(self): + def get_issues_ordered_by_rank(): + """Search for the issues, returned in the order determined by their rank.""" + return self.jira.search_issues( + f"key in ({self.issue_1},{self.issue_2}) ORDER BY Rank ASC" + ) + + self.jira.rank(self.issue_1, next_issue=self.issue_2) + issues = get_issues_ordered_by_rank() + assert (issues[0].key, issues[1].key) == (self.issue_1, self.issue_2) + + self.jira.rank(self.issue_2, next_issue=self.issue_1) + issues = get_issues_ordered_by_rank() + assert (issues[0].key, issues[1].key) == (self.issue_2, self.issue_1) + + self.jira.rank(self.issue_2, prev_issue=self.issue_1) + issues = get_issues_ordered_by_rank() + assert (issues[0].key, issues[1].key) == (self.issue_1, self.issue_2) + @broken_test( reason="Greenhopper API doesn't work on standalone docker image with JIRA Server 8.9.0" )