Skip to content

Remove internal user lookup #1874

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +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::
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::

Expand Down Expand Up @@ -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
-----------
Expand Down
71 changes: 19 additions & 52 deletions jira/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -2223,59 +2223,24 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@translate_resource_args

It will be critical for this approach to work to remove this decorator which would have automagically converted the object to a string

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the decorator touches on the User objects, it seems to only manipulate the Issue in this case

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.
None will set it to unassigned. -1 will set it to Automatic.

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

Expand Down Expand Up @@ -2719,36 +2684,38 @@ def watchers(self, issue: str | int) -> Watchers:
return self._find_for_resource(Watchers, issue)

@translate_resource_args
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@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:
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 | User): 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))
if isinstance(watcher, User):
watcher = self.get_user_identifier(watcher)
return self._session.post(url, data=json.dumps(watcher))

@translate_resource_args
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@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:
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}
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

Expand Down
10 changes: 9 additions & 1 deletion tests/resources/test_issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions tests/resources/test_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading