Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 6 additions & 3 deletions atlassian/confluence/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@ def __init__(self, url, *args, **kwargs):
if is_cloud is None:
hostname = urlparse(url).hostname or ""
is_cloud = (
hostname == "atlassian.net" or hostname.endswith(".atlassian.net")
or hostname == "jira.com" or hostname.endswith(".jira.com")
or hostname == "api.atlassian.com" or hostname.endswith(".api.atlassian.com")
hostname == "atlassian.net"
or hostname.endswith(".atlassian.net")
or hostname == "jira.com"
or hostname.endswith(".jira.com")
or hostname == "api.atlassian.com"
or hostname.endswith(".api.atlassian.com")
)
if is_cloud:
impl = ConfluenceCloud(url, *args, **kwargs)
Expand Down
2 changes: 0 additions & 2 deletions atlassian/confluence/cloud/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,3 @@ def __init__(self, url, *args, **kwargs):
:return: nothing
"""
super(ConfluenceCloudBase, self).__init__(url, *args, **kwargs)


8 changes: 6 additions & 2 deletions atlassian/confluence/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,9 @@ def get_all_draft_pages_from_space(self, space_key, **kwargs):

def get_all_draft_blog_posts_from_space(self, space_key, **kwargs):
"""Get all draft blog posts from space."""
return self._get_paged("content", params={"spaceKey": space_key, "type": "blogpost", "status": "draft", **kwargs})
return self._get_paged(
"content", params={"spaceKey": space_key, "type": "blogpost", "status": "draft", **kwargs}
)

# Trash Management
def get_trash_content(self, space_key, **kwargs):
Expand All @@ -310,7 +312,9 @@ def get_all_pages_from_space_trash(self, space_key, **kwargs):

def get_all_blog_posts_from_space_trash(self, space_key, **kwargs):
"""Get all blog posts from space trash."""
return self._get_paged("content", params={"spaceKey": space_key, "type": "blogpost", "status": "trashed", **kwargs})
return self._get_paged(
"content", params={"spaceKey": space_key, "type": "blogpost", "status": "trashed", **kwargs}
)

# Export
def export_content(self, content_id, **kwargs):
Expand Down
2 changes: 0 additions & 2 deletions atlassian/confluence/server/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,3 @@ def __init__(self, url, *args, **kwargs):
:return: nothing
"""
super(ConfluenceServerBase, self).__init__(url, *args, **kwargs)


34 changes: 34 additions & 0 deletions atlassian/jira.py
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,40 @@ def get_issue_changelog(self, issue_key: str, start: Optional[int] = None, limit
url = f"{base_url}/{issue_key}?expand=changelog"
return self._get_response_content(url, fields=[("changelog", params)])

def get_changelogs_bulk(
self,
issue_ids_or_keys: List[str],
fields_by: Optional[str] = None,
next_page_token: Optional[str] = None,
max_results: Optional[int] = None,
) -> T_resp_json:
"""
Returns changelogs for multiple issues in bulk.
Only Jira Cloud platform.

Reference: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-changelog-bulkfetch-post

:param issue_ids_or_keys: List of issue IDs or keys to fetch changelogs for. Required.
:param fields_by: OPTIONAL: Whether to filter changelog entries by field ID or field name.
Valid values: "id", "name".
:param next_page_token: OPTIONAL: Token for the next page of results (pagination).
:param max_results: OPTIONAL: Maximum number of results to return.
:return: Paginated list of changelogs for the given issues.
"""
if not self.cloud:
raise ValueError("``get_changelogs_bulk`` method is only available for Jira Cloud platform")
url = self.resource_url("changelog/bulkfetch", api_version=3)
data: dict = {"issueIdsOrKeys": issue_ids_or_keys}
if fields_by is not None:
if fields_by not in ("id", "name"):
raise ValueError("``fields_by`` must be either 'id' or 'name'")
data["fieldsByKeys"] = fields_by == "name"
if next_page_token is not None:
data["nextPageToken"] = next_page_token
if max_results is not None:
data["maxResults"] = int(max_results)
return self.post(url, data=data)

def issue_add_json_worklog(self, key: str, worklog: Union[dict, str]):
"""

Expand Down
18 changes: 12 additions & 6 deletions atlassian/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,14 @@ def block_code_macro_confluence(code, lang=None):
"""
if not lang:
lang = ""
return ("""\
return (
"""\
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">{lang}</ac:parameter>
<ac:plain-text-body><![CDATA[{code}]]></ac:plain-text-body>
</ac:structured-macro>
""").format(lang=lang, code=code)
"""
).format(lang=lang, code=code)


def html_code__macro_confluence(text):
Expand All @@ -227,11 +229,13 @@ def html_code__macro_confluence(text):
:param text:
:return:
"""
return ("""\
return (
"""\
<ac:structured-macro ac:name="html" ac:schema-version="1">
<ac:plain-text-body><![CDATA[{text}]]></ac:plain-text-body>
</ac:structured-macro>
""").format(text=text)
"""
).format(text=text)


def noformat_code_macro_confluence(text, nopanel=None):
Expand All @@ -243,12 +247,14 @@ def noformat_code_macro_confluence(text, nopanel=None):
"""
if not nopanel:
nopanel = False
return ("""\
return (
"""\
<ac:structured-macro ac:name="noformat" ac:schema-version="1">
<ac:parameter ac:name="nopanel">{nopanel}</ac:parameter>
<ac:plain-text-body><![CDATA[{text}]]></ac:plain-text-body>
</ac:structured-macro>
""").format(nopanel=nopanel, text=text)
"""
).format(nopanel=nopanel, text=text)


def symbol_normalizer(text):
Expand Down
6 changes: 4 additions & 2 deletions examples/jira/jira_admins_confluence_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@

confluence = Confluence(url="http://localhost:8090", username="admin", password="admin")

html = ["""<table>
html = [
"""<table>
<tr>
<th>Project Key</th>
<th>Project Name</th>
<th>Leader</th>
<th>Email</th>
</tr>"""]
</tr>"""
]

for data in jira.project_leaders():
log.info("{project_key} leader is {lead_name} <{lead_email}>".format(**data))
Expand Down
4 changes: 3 additions & 1 deletion examples/jira/jira_project_administrators.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
<td>{project_name}</td>
<td>{lead_name}</td>
<td><a href="mailto:{lead_email}">{lead_email}</a></td>
</tr>""".format(**data)
</tr>""".format(
**data
)

html += "</table>"

Expand Down
6 changes: 4 additions & 2 deletions examples/jira/jira_project_leaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@
jira = Jira(url="http://localhost:8080", username="admin", password="admin")

EMAIL_SUBJECT = quote("Jira access to project {project_key}")
EMAIL_BODY = quote("""I am asking for access to the {project_key} project in Jira.
EMAIL_BODY = quote(
"""I am asking for access to the {project_key} project in Jira.

To give me the appropriate permissions, assign me to a role on the page:
http://localhost:8080/plugins/servlet/project-config/{project_key}/roles

Role:
Users - read-only access + commenting
Developers - work on tasks, editing, etc.
Admin - Change of configuration and the possibility of starting sprints""")
Admin - Change of configuration and the possibility of starting sprints"""
)

MAILTO = '<a href="mailto:{lead_email}?subject={email_subject}&body={email_body}">{lead_name}</a>'

Expand Down
2 changes: 1 addition & 1 deletion tests/confluence/test_confluence_cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ def test_pagination_with_relative_next_link_and_base(self, mock_get, confluence_
assert result == [{"id": "1", "title": "Page 1"}, {"id": "2", "title": "Page 2"}]

assert mock_get.call_count == 2

# Verify the second call used scheme+host from self.url (preserving API gateway routing)
args, kwargs = mock_get.call_args_list[1]
assert args[0] == "https://test.atlassian.net/rest/api/content?cursor=1"
Expand Down