From 48cd70b3430a9fa5c3d90571df631957f36e8374 Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:07:47 +0100 Subject: [PATCH 01/10] Remove lookup from watcher methods --- jira/client.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/jira/client.py b/jira/client.py index ff311bcd6..f1206bdfc 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2724,15 +2724,13 @@ def add_watcher(self, issue: str | int, watcher: str) -> Response: Args: issue (Union[str, int]): ID or key of the issue affected - watcher (str): name of the user to add to the watchers list + watcher (str): username (for hosted) or account ID (for cloud) of the user to add to the watchers list Returns: Response """ url = self._get_url("issue/" + str(issue) + "/watchers") - # Use user_id when adding watcher - watcher_id = self._get_user_id(watcher) - return self._session.post(url, data=json.dumps(watcher_id)) + return self._session.post(url, data=json.dumps(watcher)) @translate_resource_args def remove_watcher(self, issue: str | int, watcher: str) -> Response: @@ -2740,15 +2738,14 @@ def remove_watcher(self, issue: str | int, watcher: str) -> Response: Args: issue (Union[str, int]): ID or key of the issue affected - watcher (str): name of the user to remove from the watchers list + watcher (str): username (for hosted) or account ID (for cloud) of the user to add to the watchers list Returns: Response """ url = self._get_url("issue/" + str(issue) + "/watchers") # https://docs.atlassian.com/software/jira/docs/api/REST/8.13.6/#api/2/issue-removeWatcher - user_id = self._get_user_id(watcher) - payload = {"accountId": user_id} if self._is_cloud else {"username": user_id} + payload = {"accountId": watcher} if self._is_cloud else {"username": watcher} result = self._session.delete(url, params=payload) return result From c12a221a95d64515d206a2ac5533d4fd54ad35f5 Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:37:04 +0100 Subject: [PATCH 02/10] Remove internal lookup from assign_issue Also allow the user to provide a User object --- jira/client.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/jira/client.py b/jira/client.py index f1206bdfc..84a3c0af3 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2263,19 +2263,21 @@ def _get_user_id(self, user: str | None) -> str | None: # non-resource @translate_resource_args - def assign_issue(self, issue: int | str, assignee: str | None) -> bool: + def assign_issue(self, issue: int | str, assignee: str | None | User) -> bool: """Assign an issue to a user. Args: issue (Union[int, str]): the issue ID or key to assign - assignee (str): the user to assign the issue to. None will set it to unassigned. -1 will set it to Automatic. + assignee (Union[str, User]): username (for hosted) or account ID (for cloud) of the user to add to the + watchers list. Alternatively, you can provide the User object itself. Returns: bool """ url = self._get_latest_url(f"issue/{issue}/assignee") - user_id = self._get_user_id(assignee) - payload = {"accountId": user_id} if self._is_cloud else {"name": user_id} + if isinstance(assignee, User): + assignee = self.get_user_identifier(assignee) + payload = {"accountId": assignee} if self._is_cloud else {"name": assignee} self._session.put(url, data=json.dumps(payload)) return True From b4e4aee2ea7dcfd21b246f0120a49036f0ae4d30 Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:38:50 +0100 Subject: [PATCH 03/10] Update relevant code - make the `get_user_identifier` method public. - remove the no longer needed `_get_user_id` --- jira/client.py | 40 +--------------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/jira/client.py b/jira/client.py index 84a3c0af3..faece9d67 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2209,7 +2209,7 @@ def createmeta( params["expand"] = expand return self._get_json("issue/createmeta", params) - def _get_user_identifier(self, user: User) -> str: + def get_user_identifier(self, user: User) -> str: """Get the unique identifier depending on the deployment type. - Cloud: 'accountId' @@ -2223,44 +2223,6 @@ def _get_user_identifier(self, user: User) -> str: """ return user.accountId if self._is_cloud else user.name - def _get_user_id(self, user: str | None) -> str | None: - """Internal method for translating a user search (str) to an id. - - Return None and -1 unchanged. - - This function uses :py:meth:`JIRA.search_users` to find the user and then using :py:meth:`JIRA._get_user_identifier` extracts - the relevant identifier property depending on whether the instance is a Cloud or self-hosted Instance. - - Args: - user (Optional[str]): The search term used for finding a user. None, '-1' and -1 are equivalent to 'Unassigned'. - - Raises: - JIRAError: If any error occurs. - - Returns: - Optional[str]: The Jira user's identifier. Or "-1" and None unchanged. - """ - if user in (None, -1, "-1"): - return user - try: - user_obj: User - if self._is_cloud: - users = self.search_users(query=user, maxResults=20) - else: - users = self.search_users(user=user, maxResults=20) - - if len(users) < 1: - raise JIRAError(f"No matching user found for: '{user}'") - - matches = [] - if len(users) > 1: - matches = [u for u in users if self._get_user_identifier(u) == user] - user_obj = matches[0] if matches else users[0] - - except Exception as e: - raise JIRAError(str(e)) - return self._get_user_identifier(user_obj) - # non-resource @translate_resource_args def assign_issue(self, issue: int | str, assignee: str | None | User) -> bool: From 59d2376cba273b036ac4b1f4ef879c4bcccbd5f9 Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Fri, 12 Jul 2024 14:45:34 +0100 Subject: [PATCH 04/10] Allow User as param for watcher methods This commit allows users to pass a User object to the `add_watcher` and `remove_watcher` methods --- jira/client.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jira/client.py b/jira/client.py index faece9d67..9db06912e 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2683,7 +2683,7 @@ def watchers(self, issue: str | int) -> Watchers: return self._find_for_resource(Watchers, issue) @translate_resource_args - def add_watcher(self, issue: str | int, watcher: str) -> Response: + def add_watcher(self, issue: str | int, watcher: str | User) -> Response: """Add a user to an issue's watchers list. Args: @@ -2694,10 +2694,12 @@ def add_watcher(self, issue: str | int, watcher: str) -> Response: Response """ url = self._get_url("issue/" + str(issue) + "/watchers") + if isinstance(watcher, User): + watcher = self.get_user_identifier(watcher) return self._session.post(url, data=json.dumps(watcher)) @translate_resource_args - def remove_watcher(self, issue: str | int, watcher: str) -> Response: + def remove_watcher(self, issue: str | int, watcher: str | User) -> Response: """Remove a user from an issue's watch list. Args: @@ -2709,6 +2711,8 @@ def remove_watcher(self, issue: str | int, watcher: str) -> Response: """ url = self._get_url("issue/" + str(issue) + "/watchers") # https://docs.atlassian.com/software/jira/docs/api/REST/8.13.6/#api/2/issue-removeWatcher + if isinstance(watcher, User): + watcher = self.get_user_identifier(watcher) payload = {"accountId": watcher} if self._is_cloud else {"username": watcher} result = self._session.delete(url, params=payload) return result From 77d348033e43569171a09fae92a9b8870e3daec6 Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:05:59 +0100 Subject: [PATCH 05/10] Test cases --- tests/resources/test_issue.py | 10 +++++++++- tests/resources/test_watchers.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/resources/test_issue.py b/tests/resources/test_issue.py index bd30c95a3..97898b2ca 100644 --- a/tests/resources/test_issue.py +++ b/tests/resources/test_issue.py @@ -429,12 +429,20 @@ def test_createmeta_expand(self): ) self.assertTrue("fields" in meta["projects"][0]["issuetypes"][0]) - def test_assign_issue(self): + def test_assign_issue_username(self): + # Assign issue via username self.assertTrue(self.jira.assign_issue(self.issue_1, self.user_normal.name)) self.assertEqual( self.jira.issue(self.issue_1).fields.assignee.name, self.user_normal.name ) + def test_assign_issue_user_obj(self): + # Assign issue via User object + self.assertTrue(self.jira.assign_issue(self.issue_1, self.user_normal)) + self.assertEqual( + self.jira.issue(self.issue_1).fields.assignee.name, self.user_normal.name + ) + def test_assign_issue_with_issue_obj(self): issue = self.jira.issue(self.issue_1) x = self.jira.assign_issue(issue, self.user_normal.name) diff --git a/tests/resources/test_watchers.py b/tests/resources/test_watchers.py index 7e7c991be..28549fa07 100644 --- a/tests/resources/test_watchers.py +++ b/tests/resources/test_watchers.py @@ -21,3 +21,12 @@ def test_add_remove_watcher(self): self.jira.remove_watcher(self.issue_1, self.test_manager.user_normal.name) new_watchers = self.jira.watchers(self.issue_1).watchCount self.assertEqual(init_watchers, new_watchers) + + # verify passing the user object also words + self.jira.add_watcher(self.issue_1, self.test_manager.user_normal) + self.assertEqual(self.jira.watchers(self.issue_1).watchCount, init_watchers + 1) + + # same, but for removing + self.jira.remove_watcher(self.issue_1, self.test_manager.user_normal) + new_watchers = self.jira.watchers(self.issue_1).watchCount + self.assertEqual(init_watchers, new_watchers) From d6e15cecc2c91d88050a13de1397eccb00d2b3c7 Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:18:06 +0100 Subject: [PATCH 06/10] Update examples.rst --- docs/examples.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index d4e6e1cfa..87e3aee52 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -185,9 +185,15 @@ If you only want a few specific fields, save time by asking for them explicitly: issue = jira.issue('JRA-1330', fields='summary,comment') Reassign an issue:: + Reassigning an issue can be done either by providing the username/accountId (hosted/cloud) or the User Resource object itself:: - # requires issue assign permission, which is different from issue editing permission! - jira.assign_issue(issue, 'newassignee') + # requires issue assign permission, which is different from issue editing permission! + # via username, for hosted instances + jira.assign_issue(issue, 'newassignee') + # or accountId, for cloud instances + jira.assign_issue(issue, 'gweg3:5fr23r23r-3041-4342-23f2-2g3g232c2e1234:8008sdg3-441a-12f2-9sg1-erbwer3q3r3') + # or via the User retrieved from search_users() + jira.assign_issue(issue, user_resource) If you want to unassign it again, just do:: @@ -421,15 +427,17 @@ Watchers are objects, represented by :class:`jira.resources.Watchers`:: # watcher is instance of jira.resources.User: print(watcher.emailAddress) -You can add users to watchers by their name:: +You can add users to watchers by their name (hosted) / accountId (cloud) or the User resource itself:: jira.add_watcher(issue, 'username') jira.add_watcher(issue, user_resource.name) + jira.add_watcher(issue, user_resource) And of course you can remove users from watcher:: jira.remove_watcher(issue, 'username') jira.remove_watcher(issue, user_resource.name) + jira.remove_watcher(issue, user_resource) Attachments ----------- From b0b552ec52ba83ab621f0c90e46190fb067eeb8c Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Thu, 15 Aug 2024 09:08:38 +0200 Subject: [PATCH 07/10] Docs & doc string fixes --- docs/examples.rst | 20 ++++++++++---------- jira/client.py | 4 +++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 87e3aee52..bba30bf75 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -184,16 +184,16 @@ If you only want a few specific fields, save time by asking for them explicitly: issue = jira.issue('JRA-1330', fields='summary,comment') -Reassign an issue:: - Reassigning an issue can be done either by providing the username/accountId (hosted/cloud) or the User Resource object itself:: - - # requires issue assign permission, which is different from issue editing permission! - # via username, for hosted instances - jira.assign_issue(issue, 'newassignee') - # or accountId, for cloud instances - jira.assign_issue(issue, 'gweg3:5fr23r23r-3041-4342-23f2-2g3g232c2e1234:8008sdg3-441a-12f2-9sg1-erbwer3q3r3') - # or via the User retrieved from search_users() - jira.assign_issue(issue, user_resource) +Reassign an issue: +Reassigning an issue can be done either by providing the username/accountId (hosted/cloud) or the User Resource object itself:: + + # requires issue assign permission, which is different from issue editing permission! + # via username, for hosted instances + jira.assign_issue(issue, 'newassignee') + # or accountId, for cloud instances + jira.assign_issue(issue, 'gweg3:5fr23r23r-3041-4342-23f2-2g3g232c2e1234:8008sdg3-441a-12f2-9sg1-erbwer3q3r3') + # or via the User retrieved from search_users() + jira.assign_issue(issue, user_resource) If you want to unassign it again, just do:: diff --git a/jira/client.py b/jira/client.py index 9db06912e..99088b26d 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2232,6 +2232,7 @@ def assign_issue(self, issue: int | str, assignee: str | None | User) -> bool: issue (Union[int, str]): the issue ID or key to assign assignee (Union[str, User]): username (for hosted) or account ID (for cloud) of the user to add to the watchers list. Alternatively, you can provide the User object itself. + None will set it to unassigned. -1 will set it to Automatic. Returns: bool @@ -2688,7 +2689,8 @@ def add_watcher(self, issue: str | int, watcher: str | User) -> Response: Args: issue (Union[str, int]): ID or key of the issue affected - watcher (str): username (for hosted) or account ID (for cloud) of the user to add to the watchers list + watcher (str | User): username (for hosted) or account ID (for cloud) of the user to add to the watchers + list Returns: Response From 790732d0d0df29e8e1bb4b476a60e5297abb556c Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:55:18 +0100 Subject: [PATCH 08/10] Make auto lookup optional - Introduce a new flag at init called 'with_lookup', by default set to True - If set to True, it will emit a warning to the end user at initialization - If set to True, automatic user lookup will be performed for the 'add_watcher', 'remove_watcher' and 'assign_issue' methods --- jira/client.py | 75 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/jira/client.py b/jira/client.py index b89a55595..1d0739ae7 100644 --- a/jira/client.py +++ b/jira/client.py @@ -473,6 +473,7 @@ def __init__( kerberos_options: dict[str, Any] | None = None, validate=False, get_server_info: bool = True, + with_lookup: bool = True, async_: bool = False, async_workers: int = 5, logging: bool = True, @@ -665,6 +666,12 @@ def __init__( else: self._version = (0, 0, 0) + self.with_lookup = with_lookup + if with_lookup: + warnings.warn(f"Auto-lookup (with_lookup) has been set to True, the library will look-up the provided user " + f"for its 'add_watcher', 'remove_watcher' and 'assign_issue' methods; this functionality " + f"will be deprecated in future releases") + if self._options["check_update"] and not JIRA.checked_version: self._check_update_() JIRA.checked_version = True @@ -2210,7 +2217,7 @@ def createmeta( params["expand"] = expand return self._get_json("issue/createmeta", params) - def get_user_identifier(self, user: User) -> str: + def _get_user_identifier(self, user: User) -> str: """Get the unique identifier depending on the deployment type. - Cloud: 'accountId' @@ -2224,9 +2231,47 @@ def get_user_identifier(self, user: User) -> str: """ return user.accountId if self._is_cloud else user.name + def _get_user_id(self, user: str | None) -> str | None: + """Internal method for translating a user search (str) to an id. + + Return None and -1 unchanged. + + This function uses :py:meth:`JIRA.search_users` to find the user and then using :py:meth:`JIRA._get_user_identifier` extracts + the relevant identifier property depending on whether the instance is a Cloud or self-hosted Instance. + + Args: + user (Optional[str]): The search term used for finding a user. None, '-1' and -1 are equivalent to 'Unassigned'. + + Raises: + JIRAError: If any error occurs. + + Returns: + Optional[str]: The Jira user's identifier. Or "-1" and None unchanged. + """ + if user in (None, -1, "-1"): + return user + try: + user_obj: User + if self._is_cloud: + users = self.search_users(query=user, maxResults=20) + else: + users = self.search_users(user=user, maxResults=20) + + if len(users) < 1: + raise JIRAError(f"No matching user found for: '{user}'") + + matches = [] + if len(users) > 1: + matches = [u for u in users if self._get_user_identifier(u) == user] + user_obj = matches[0] if matches else users[0] + + except Exception as e: + raise JIRAError(str(e)) + return self._get_user_identifier(user_obj) + # non-resource @translate_resource_args - def assign_issue(self, issue: int | str, assignee: str | None | User) -> bool: + def assign_issue(self, issue: int | str, assignee: str | User | None) -> bool: """Assign an issue to a user. Args: @@ -2240,8 +2285,12 @@ def assign_issue(self, issue: int | str, assignee: str | None | User) -> bool: """ url = self._get_latest_url(f"issue/{issue}/assignee") if isinstance(assignee, User): - assignee = self.get_user_identifier(assignee) - payload = {"accountId": assignee} if self._is_cloud else {"name": assignee} + user_id = self._get_user_identifier(assignee) + else: + user_id = assignee + if self.with_lookup: + user_id = self._get_user_id(user_id) + payload = {"accountId": user_id} if self._is_cloud else {"name": user_id} self._session.put(url, data=json.dumps(payload)) return True @@ -2698,8 +2747,12 @@ def add_watcher(self, issue: str | int, watcher: str | User) -> Response: """ url = self._get_url("issue/" + str(issue) + "/watchers") if isinstance(watcher, User): - watcher = self.get_user_identifier(watcher) - return self._session.post(url, data=json.dumps(watcher)) + watcher_id = self._get_user_identifier(watcher) + else: + watcher_id = watcher + if self.with_lookup: + watcher_id = self._get_user_id(watcher_id) + return self._session.post(url, data=json.dumps(watcher_id)) @translate_resource_args def remove_watcher(self, issue: str | int, watcher: str | User) -> Response: @@ -2707,7 +2760,7 @@ def remove_watcher(self, issue: str | int, watcher: str | User) -> Response: Args: issue (Union[str, int]): ID or key of the issue affected - watcher (str): username (for hosted) or account ID (for cloud) of the user to add to the watchers list + watcher (str): name of the user to remove from the watchers list Returns: Response @@ -2715,8 +2768,12 @@ def remove_watcher(self, issue: str | int, watcher: str | User) -> Response: url = self._get_url("issue/" + str(issue) + "/watchers") # https://docs.atlassian.com/software/jira/docs/api/REST/8.13.6/#api/2/issue-removeWatcher if isinstance(watcher, User): - watcher = self.get_user_identifier(watcher) - payload = {"accountId": watcher} if self._is_cloud else {"username": watcher} + watcher_id = self._get_user_identifier(watcher) + else: + watcher_id = watcher + if self.with_lookup: + watcher_id = self._get_user_id(watcher_id) + payload = {"accountId": watcher_id} if self._is_cloud else {"username": watcher_id} result = self._session.delete(url, params=payload) return result From 6397dceebd0107103c9802705630921255ef1ef0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 09:52:13 +0000 Subject: [PATCH 09/10] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- jira/client.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/jira/client.py b/jira/client.py index 1d0739ae7..7c38e5920 100644 --- a/jira/client.py +++ b/jira/client.py @@ -668,9 +668,11 @@ def __init__( self.with_lookup = with_lookup if with_lookup: - warnings.warn(f"Auto-lookup (with_lookup) has been set to True, the library will look-up the provided user " - f"for its 'add_watcher', 'remove_watcher' and 'assign_issue' methods; this functionality " - f"will be deprecated in future releases") + warnings.warn( + "Auto-lookup (with_lookup) has been set to True, the library will look-up the provided user " + "for its 'add_watcher', 'remove_watcher' and 'assign_issue' methods; this functionality " + "will be deprecated in future releases" + ) if self._options["check_update"] and not JIRA.checked_version: self._check_update_() @@ -2773,7 +2775,9 @@ def remove_watcher(self, issue: str | int, watcher: str | User) -> Response: watcher_id = watcher if self.with_lookup: watcher_id = self._get_user_id(watcher_id) - payload = {"accountId": watcher_id} if self._is_cloud else {"username": watcher_id} + payload = ( + {"accountId": watcher_id} if self._is_cloud else {"username": watcher_id} + ) result = self._session.delete(url, params=payload) return result From f1c46cfa6e3cd06fbd2f5a7ef6cc35b5f4244a1f Mon Sep 17 00:00:00 2001 From: Dimitar Ivanov <61866827+dimitarOnGithub@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:26:18 +0000 Subject: [PATCH 10/10] Fix linter errors --- jira/client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/jira/client.py b/jira/client.py index 7c38e5920..d4af4698c 100644 --- a/jira/client.py +++ b/jira/client.py @@ -2287,7 +2287,7 @@ def assign_issue(self, issue: int | str, assignee: str | User | None) -> bool: """ url = self._get_latest_url(f"issue/{issue}/assignee") if isinstance(assignee, User): - user_id = self._get_user_identifier(assignee) + user_id: str | None = self._get_user_identifier(assignee) else: user_id = assignee if self.with_lookup: @@ -2749,7 +2749,7 @@ def add_watcher(self, issue: str | int, watcher: str | User) -> Response: """ url = self._get_url("issue/" + str(issue) + "/watchers") if isinstance(watcher, User): - watcher_id = self._get_user_identifier(watcher) + watcher_id: str | None = self._get_user_identifier(watcher) else: watcher_id = watcher if self.with_lookup: @@ -2770,7 +2770,7 @@ def remove_watcher(self, issue: str | int, watcher: str | User) -> Response: url = self._get_url("issue/" + str(issue) + "/watchers") # https://docs.atlassian.com/software/jira/docs/api/REST/8.13.6/#api/2/issue-removeWatcher if isinstance(watcher, User): - watcher_id = self._get_user_identifier(watcher) + watcher_id: str | None = self._get_user_identifier(watcher) else: watcher_id = watcher if self.with_lookup: