diff --git a/jira/client.py b/jira/client.py index 4f5aac7d7..8a86bbaa6 100644 --- a/jira/client.py +++ b/jira/client.py @@ -60,6 +60,7 @@ Customer, CustomFieldOption, Dashboard, + Field, Filter, Group, Issue, @@ -1732,63 +1733,14 @@ 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( @@ -1796,14 +1748,6 @@ def createmeta_fieldtypes( "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, @@ -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. @@ -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 @@ -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: diff --git a/jira/resources.py b/jira/resources.py index 2c92da81c..57ec31bbc 100644 --- a/jira/resources.py +++ b/jira/resources.py @@ -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.""" diff --git a/tests/conftest.py b/tests/conftest.py index 8894dbdbc..e493e7c30 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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) diff --git a/tests/resources/test_field.py b/tests/resources/test_field.py new file mode 100644 index 000000000..7f8764655 --- /dev/null +++ b/tests/resources/test_field.py @@ -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 diff --git a/tests/resources/test_issue_type.py b/tests/resources/test_issue_type.py new file mode 100644 index 000000000..2b8c3079c --- /dev/null +++ b/tests/resources/test_issue_type.py @@ -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 diff --git a/tests/test_client.py b/tests/test_client.py index 8edb0bef7..2f088b1e0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -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) @@ -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