Skip to content

Commit

Permalink
Issue Field and Issue Type object based methods (#1784)
Browse files Browse the repository at this point in the history
* Provide page-aware access to createmeta issuetypes

Version 3.5.0 of the client library introduced the
createmeta_issuetypes() and createmeta_fieldtypes() client member
functions to replace the deprecated form of the createmeta
Jira endpoint. However, these functions return the raw JSON of a
single response, and do not handle pagination that may be applied
to the endpoints, such as when an issue type within a project has
more than 50 associated fields. I recently encountered a Jira
deployment where this case occurred, rendering
createmeta_fieldtypes() unuseful.

Because the functions added in 3.5.0 have a different return type
than these new functions, instead of changing the behavior of those
functions this commit creates two new client member functions:
project_issue_types()  and project_issue_fields().

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add Field test case

* Enable 'hard' delete in Cloud test framework

* Replace previous implementation and tests

* typo

---------

Co-authored-by: Dominic Delabruere <ddelabru@redhat.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 26, 2023
1 parent 686de78 commit cce214e
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 85 deletions.
137 changes: 72 additions & 65 deletions jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
Customer,
CustomFieldOption,
Dashboard,
Field,
Filter,
Group,
Issue,
Expand Down Expand Up @@ -1732,78 +1733,21 @@ def create_customer_request(
else:
return Issue(self._options, self._session, raw=raw_issue_json)

def createmeta_issuetypes(
self,
projectIdOrKey: str | int,
startAt: int = 0,
maxResults: int = 50,
) -> dict[str, Any]:
"""Get the issue types metadata for a given project, required to create issues.
This API was introduced in JIRA Server / DC 8.4 as a replacement for the more general purpose API 'createmeta'.
For details see: https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
def _check_createmeta_issuetypes(self) -> None:
"""Check whether Jira deployment supports the createmeta issuetypes endpoint.
Args:
projectIdOrKey (Union[str, int]): id or key of the project for which to get the metadata.
startAt (int): Index of the first issue to return. (Default: ``0``)
maxResults (int): Maximum number of issues to return.
Total number of results is available in the ``total`` attribute of the returned :class:`ResultList`.
If maxResults evaluates to False, it will try to get all issues in batches. (Default: ``50``)
Returns:
Dict[str, Any]
"""
if self._is_cloud or self._version < (8, 4, 0):
raise JIRAError(
f"Unsupported JIRA deployment type: {self.deploymentType} or version: {self._version}. "
"Use 'createmeta' instead."
)

return self._get_json(
f"issue/createmeta/{projectIdOrKey}/issuetypes",
params={
"startAt": startAt,
"maxResults": maxResults,
},
)

def createmeta_fieldtypes(
self,
projectIdOrKey: str | int,
issueTypeId: str | int,
startAt: int = 0,
maxResults: int = 50,
) -> dict[str, Any]:
"""Get the field metadata for a given project and issue type, required to create issues.
This API was introduced in JIRA Server / DC 8.4 as a replacement for the more general purpose API 'createmeta'.
For details see: https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
Args:
projectIdOrKey (Union[str, int]): id or key of the project for which to get the metadata.
issueTypeId (Union[str, int]): id of the issue type for which to get the metadata.
startAt (int): Index of the first issue to return. (Default: ``0``)
maxResults (int): Maximum number of issues to return.
Total number of results is available in the ``total`` attribute of the returned :class:`ResultList`.
If maxResults evaluates to False, it will try to get all issues in batches. (Default: ``50``)
Raises:
JIRAError: If the deployment does not support the API endpoint.
Returns:
Dict[str, Any]
None
"""
if self._is_cloud or self._version < (8, 4, 0):
raise JIRAError(
f"Unsupported JIRA deployment type: {self.deploymentType} or version: {self._version}. "
"Use 'createmeta' instead."
)

return self._get_json(
f"issue/createmeta/{projectIdOrKey}/issuetypes/{issueTypeId}",
params={
"startAt": startAt,
"maxResults": maxResults,
},
)

def createmeta(
self,
projectKeys: tuple[str, str] | str | None = None,
Expand Down Expand Up @@ -2665,6 +2609,66 @@ def issue_types(self) -> list[IssueType]:
]
return issue_types

def project_issue_types(
self,
project: str,
startAt: int = 0,
maxResults: int = 50,
) -> ResultList[IssueType]:
"""Get a list of issue type Resources available in a given project from the server.
This API was introduced in JIRA Server / DC 8.4 as a replacement for the more general purpose API 'createmeta'.
For details see: https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
Args:
project (str): ID or key of the project to query issue types from.
startAt (int): Index of first issue type to return. (Default: ``0``)
maxResults (int): Maximum number of issue types to return. (Default: ``50``)
Returns:
ResultList[IssueType]
"""
self._check_createmeta_issuetypes()
issue_types = self._fetch_pages(
IssueType,
"values",
f"issue/createmeta/{project}/issuetypes",
startAt=startAt,
maxResults=maxResults,
)
return issue_types

def project_issue_fields(
self,
project: str,
issue_type: str,
startAt: int = 0,
maxResults: int = 50,
) -> ResultList[Field]:
"""Get a list of field type Resources available for a project and issue type from the server.
This API was introduced in JIRA Server / DC 8.4 as a replacement for the more general purpose API 'createmeta'.
For details see: https://confluence.atlassian.com/jiracore/createmeta-rest-endpoint-to-be-removed-975040986.html
Args:
project (str): ID or key of the project to query field types from.
issue_type (str): ID of the issue type to query field types from.
startAt (int): Index of first issue type to return. (Default: ``0``)
maxResults (int): Maximum number of issue types to return. (Default: ``50``)
Returns:
ResultList[Field]
"""
self._check_createmeta_issuetypes()
fields = self._fetch_pages(
Field,
"values",
f"issue/createmeta/{project}/issuetypes/{issue_type}",
startAt=startAt,
maxResults=maxResults,
)
return fields

def issue_type(self, id: str) -> IssueType:
"""Get an issue type Resource from the server.
Expand Down Expand Up @@ -4285,11 +4289,14 @@ def current_user(self, field: str | None = None) -> str:

return self._myself[field]

def delete_project(self, pid: str | Project) -> bool | None:
def delete_project(
self, pid: str | Project, enable_undo: bool = True
) -> bool | None:
"""Delete project from Jira.
Args:
pid (Union[str, Project]): Jira projectID or Project or slug
pid (Union[str, Project]): Jira projectID or Project or slug.
enable_undo (bool): Jira Cloud only. True moves to 'Trash'. False permanently deletes.
Raises:
JIRAError: If project not found or not enough permissions
Expand All @@ -4303,7 +4310,7 @@ def delete_project(self, pid: str | Project) -> bool | None:
pid = str(pid.id)

url = self._get_url(f"project/{pid}")
r = self._session.delete(url)
r = self._session.delete(url, params={"enableUndo": enable_undo})
if r.status_code == 403:
raise JIRAError("Not enough permissions to delete project")
if r.status_code == 404:
Expand Down
18 changes: 18 additions & 0 deletions jira/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,24 @@ def __init__(
self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)


class Field(Resource):
"""An issue field.
A field cannot be fetched from the Jira API individually, but paginated lists of fields are returned by some endpoints.
"""

def __init__(
self,
options: dict[str, str],
session: ResilientSession,
raw: dict[str, Any] = None,
):
Resource.__init__(self, "field/{0}", options, session)
if raw:
self._parse_raw(raw)
self.raw: dict[str, Any] = cast(Dict[str, Any], self.raw)


class Filter(Resource):
"""An issue navigator filter."""

Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def _remove_project(self, project_key):
# https://jira.atlassian.com/browse/JRA-39153
if self._project_exists(project_key):
try:
self.jira_admin.delete_project(project_key)
self.jira_admin.delete_project(project_key, enable_undo=False)
except Exception:
LOGGER.exception("Failed to delete '%s'.", project_key)

Expand Down
25 changes: 25 additions & 0 deletions tests/resources/test_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

from jira.resources import Field
from tests.conftest import JiraTestCase


class FieldsTest(JiraTestCase):
def setUp(self) -> None:
super().setUp()
self.issue_1 = self.test_manager.project_b_issue1
self.issue_1_obj = self.test_manager.project_b_issue1_obj

def test_field(self):
issue_fields = self.test_manager.jira_admin.project_issue_fields(
project=self.project_a, issue_type=self.issue_1_obj.fields.issuetype.id
)
assert isinstance(issue_fields[0], Field)

def test_field_pagination(self):
issue_fields = self.test_manager.jira_admin.project_issue_fields(
project=self.project_a,
issue_type=self.issue_1_obj.fields.issuetype.id,
startAt=50,
)
assert len(issue_fields) == 0
21 changes: 21 additions & 0 deletions tests/resources/test_issue_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from jira.resources import IssueType
from tests.conftest import JiraTestCase


class IssueTypeTest(JiraTestCase):
def setUp(self) -> None:
super().setUp()

def test_issue_type(self):
issue_types = self.test_manager.jira_admin.project_issue_types(
project=self.project_a
)
assert isinstance(issue_types[0], IssueType)

def test_issue_type_pagination(self):
issue_types = self.test_manager.jira_admin.project_issue_types(
project=self.project_a, startAt=50
)
assert len(issue_types) == 0
21 changes: 2 additions & 19 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ def cl_normal(test_manager: JiraTestManager) -> jira.client.JIRA:

@pytest.fixture(scope="function")
def slug(request, cl_admin):
"""Project slug."""

def remove_by_slug():
try:
cl_admin.delete_project(slug)
Expand Down Expand Up @@ -250,22 +252,3 @@ def test_cookie_auth_retry():
)
# THEN: We don't get a RecursionError and only call the reset_function once
mock_reset_func.assert_called_once()


def test_createmeta_issuetypes_pagination(cl_normal, slug):
"""Test createmeta_issuetypes pagination kwargs"""
issue_types_resp = cl_normal.createmeta_issuetypes(slug, startAt=50, maxResults=100)
assert issue_types_resp["startAt"] == 50
assert issue_types_resp["maxResults"] == 100


def test_createmeta_fieldtypes_pagination(cl_normal, slug):
"""Test createmeta_fieldtypes pagination kwargs"""
issue_types = cl_normal.createmeta_issuetypes(slug)
assert issue_types["total"]
issue_type_id = issue_types["values"][-1]["id"]
field_types_resp = cl_normal.createmeta_fieldtypes(
projectIdOrKey=slug, issueTypeId=issue_type_id, startAt=50, maxResults=100
)
assert field_types_resp["startAt"] == 50
assert field_types_resp["maxResults"] == 100

0 comments on commit cce214e

Please sign in to comment.