Skip to content
Merged
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
25 changes: 18 additions & 7 deletions atlassian/confluence/cloud/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,12 +1154,16 @@ def add_page_label(self, page_id: str, label: str) -> Dict[str, Any]:
"""
Adds a label to a page.

The Confluence Cloud v2 API only exposes labels for reading, so this call is
made against the v1 endpoint.

Args:
page_id: The ID of the page
label: The label to add

Returns:
The created label object
The response of the v1 label endpoint, holding the created labels in
its ``results`` list

Raises:
HTTPError: If the API call fails
Expand All @@ -1168,9 +1172,9 @@ def add_page_label(self, page_id: str, label: str) -> Dict[str, Any]:
if not label:
raise ValueError("Label cannot be empty")

endpoint = self.get_endpoint("page_labels", id=page_id)
endpoint = self.get_endpoint("page_labels_write", id=page_id)

data = {"name": label}
data = [{"prefix": "global", "name": label}]

try:
return self.post(endpoint, data=data)
Expand All @@ -1182,12 +1186,16 @@ def add_page_labels(self, page_id: str, labels: List[str]) -> List[Dict[str, Any
"""
Adds multiple labels to a page.

The Confluence Cloud v2 API only exposes labels for reading, so this call is
made against the v1 endpoint.

Args:
page_id: The ID of the page
labels: List of labels to add

Returns:
List of created label objects
The response of the v1 label endpoint, holding the created labels in
its ``results`` list

Raises:
HTTPError: If the API call fails
Expand All @@ -1196,9 +1204,9 @@ def add_page_labels(self, page_id: str, labels: List[str]) -> List[Dict[str, Any
if not labels:
raise ValueError("Labels list cannot be empty")

endpoint = self.get_endpoint("page_labels", id=page_id)
endpoint = self.get_endpoint("page_labels_write", id=page_id)

data = [{"name": label} for label in labels]
data = [{"prefix": "global", "name": label} for label in labels]

try:
return self.post(endpoint, data=data)
Expand All @@ -1210,6 +1218,9 @@ def delete_page_label(self, page_id: str, label: str) -> bool:
"""
Deletes a label from a page.

The Confluence Cloud v2 API only exposes labels for reading, so this call is
made against the v1 endpoint.

Args:
page_id: The ID of the page
label: The label to delete
Expand All @@ -1223,7 +1234,7 @@ def delete_page_label(self, page_id: str, label: str) -> bool:
if not label:
raise ValueError("Label cannot be empty")

endpoint = self.get_endpoint("page_labels", id=page_id)
endpoint = self.get_endpoint("page_labels_write", id=page_id)
params = {"name": label}

try:
Expand Down
3 changes: 3 additions & 0 deletions atlassian/confluence_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class ConfluenceEndpoints:
"page_properties": "api/v2/pages/{id}/properties",
"page_property_by_key": "api/v2/pages/{id}/properties/{key}",
"page_labels": "api/v2/pages/{id}/labels",
# The v2 label endpoints are read-only (only GET is defined in the Confluence
# Cloud v2 OpenAPI spec), so adding and deleting labels stays on the v1 API.
"page_labels_write": "rest/api/content/{id}/label",
"space_labels": "api/v2/spaces/{id}/labels",
# Comment endpoints for V2 API
"page_footer_comments": "api/v2/pages/{id}/footer-comments",
Expand Down
3 changes: 3 additions & 0 deletions docs/confluence_v2_migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,9 @@ response = confluence.delete_custom_content(content_id)

### Labels

The v2 API only exposes labels for reading, so adding and deleting them is served
by the v1 endpoint `rest/api/content/{id}/label`.

```python
# Get page labels
labels = confluence.get_page_labels(page_id)
Expand Down
14 changes: 7 additions & 7 deletions tests/test_confluence_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,9 +838,9 @@ def test_add_page_label(self, mock_post):
# Call the method
response = self.confluence_v2.add_page_label("PAGE123", "test-label")

# Assertions
expected_data = {"name": "test-label"}
mock_post.assert_called_once_with("api/v2/pages/PAGE123/labels", data=expected_data)
# Assertions: v2 has no write endpoint for labels, so v1 is used
expected_data = [{"prefix": "global", "name": "test-label"}]
mock_post.assert_called_once_with("rest/api/content/PAGE123/label", data=expected_data)
self.assertEqual(response, mock_response)

def test_add_page_label_empty(self):
Expand All @@ -857,9 +857,9 @@ def test_add_page_labels(self, mock_post):
# Call the method
response = self.confluence_v2.add_page_labels("PAGE123", ["label1", "label2"])

# Assertions
expected_data = [{"name": "label1"}, {"name": "label2"}]
mock_post.assert_called_once_with("api/v2/pages/PAGE123/labels", data=expected_data)
# Assertions: v2 has no write endpoint for labels, so v1 is used
expected_data = [{"prefix": "global", "name": "label1"}, {"prefix": "global", "name": "label2"}]
mock_post.assert_called_once_with("rest/api/content/PAGE123/label", data=expected_data)
self.assertEqual(response, mock_response)

def test_add_page_labels_empty(self):
Expand All @@ -876,7 +876,7 @@ def test_delete_page_label(self, mock_delete):
result = self.confluence_v2.delete_page_label("PAGE123", "test-label")

# Assertions
mock_delete.assert_called_once_with("api/v2/pages/PAGE123/labels", params={"name": "test-label"})
mock_delete.assert_called_once_with("rest/api/content/PAGE123/label", params={"name": "test-label"})
self.assertTrue(result)

def test_delete_page_label_empty(self):
Expand Down
3 changes: 2 additions & 1 deletion tests/test_confluence_v2_with_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ def test_add_page_label(self):
"""Test adding a label to a page."""
page_id = "123456"
label = "test-label"
endpoint = f"api/v2/pages/{page_id}/labels"
# v2 has no write endpoint for labels, so the v1 endpoint is used
endpoint = f"rest/api/content/{page_id}/label"

# Mock the response
expected_data = self.mock_response_for_endpoint(endpoint, mock_data=LABEL_MOCK)
Expand Down