Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a26a470af6 | |||
| 71ace47197 | |||
| 30d3d9f0cf |
@@ -285,28 +285,23 @@ class DeckClient(BaseNextcloudClient):
|
||||
archived: Optional[bool] = None,
|
||||
done: Optional[str] = None,
|
||||
) -> None:
|
||||
# First, get the current card to use existing values for required fields
|
||||
# Deck PUT API is a full replacement - all required fields must be sent.
|
||||
# Fetch current card to preserve values for fields not being updated.
|
||||
current_card = await self.get_card(board_id, stack_id, card_id)
|
||||
|
||||
json_data = {}
|
||||
if title is not None:
|
||||
json_data["title"] = title
|
||||
if description is not None:
|
||||
json_data["description"] = description
|
||||
# Type is required by the API, use provided or keep current
|
||||
json_data["type"] = type if type is not None else current_card.type
|
||||
# Owner is required by the API, use provided or keep current
|
||||
json_data["owner"] = (
|
||||
owner
|
||||
if owner is not None
|
||||
else (
|
||||
current_card.owner
|
||||
if isinstance(current_card.owner, str)
|
||||
else current_card.owner.uid
|
||||
if hasattr(current_card.owner, "uid")
|
||||
else current_card.owner.primaryKey
|
||||
)
|
||||
)
|
||||
# Build payload with required fields always included
|
||||
json_data = {
|
||||
# Title is required by the API
|
||||
"title": title if title is not None else current_card.title,
|
||||
# Type is required by the API
|
||||
"type": type if type is not None else current_card.type,
|
||||
# Owner is required by the API (model validator ensures it's a string)
|
||||
"owner": owner if owner is not None else current_card.owner,
|
||||
# Description must be sent to preserve it (PUT clears omitted fields)
|
||||
"description": description
|
||||
if description is not None
|
||||
else (current_card.description or ""),
|
||||
}
|
||||
if order is not None:
|
||||
json_data["order"] = order
|
||||
if duedate is not None:
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""
|
||||
Integration tests for DeckClient.update_card API behavior.
|
||||
|
||||
These tests define the EXPECTED behavior for partial card updates:
|
||||
- Only fields explicitly passed should be modified
|
||||
- All other fields should be preserved unchanged
|
||||
|
||||
Related issues:
|
||||
- nextcloud-mcp-server #452: DeckClient.update_card partial update bugs
|
||||
- deck #3127: REST API Docs: missing parameter in "update cards"
|
||||
- deck #4106: Provide a working example of API usage to update a cards details
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = [pytest.mark.integration]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def deck_test_card(nc_client):
|
||||
"""Create a board, stack, and card for testing, cleanup after."""
|
||||
board = await nc_client.deck.create_board("Test Update Card API", "FF0000")
|
||||
stack = await nc_client.deck.create_stack(board.id, "Test Stack", 1)
|
||||
card = await nc_client.deck.create_card(
|
||||
board.id,
|
||||
stack.id,
|
||||
"Original Title",
|
||||
type="plain",
|
||||
description="Original description",
|
||||
)
|
||||
|
||||
yield {
|
||||
"board_id": board.id,
|
||||
"stack_id": stack.id,
|
||||
"card_id": card.id,
|
||||
"card": card,
|
||||
}
|
||||
|
||||
# Cleanup
|
||||
await nc_client.deck.delete_board(board.id)
|
||||
|
||||
|
||||
class TestDeckClientUpdateCard:
|
||||
"""
|
||||
Test DeckClient.update_card() partial update behavior.
|
||||
|
||||
Expected: Only explicitly provided fields are updated, all others preserved.
|
||||
"""
|
||||
|
||||
async def test_update_title_only_preserves_description(
|
||||
self, nc_client, deck_test_card
|
||||
):
|
||||
"""Updating only the title should preserve the description."""
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
title="New Title",
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.title == "New Title"
|
||||
assert updated.description == "Original description"
|
||||
|
||||
async def test_update_description_only(self, nc_client, deck_test_card):
|
||||
"""Updating only the description should work and preserve other fields."""
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
description="New description only",
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.title == "Original Title"
|
||||
assert updated.description == "New description only"
|
||||
|
||||
async def test_update_title_and_description(self, nc_client, deck_test_card):
|
||||
"""Updating title and description together should work."""
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
title="New Title",
|
||||
description="New description",
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.title == "New Title"
|
||||
assert updated.description == "New description"
|
||||
|
||||
async def test_update_duedate_only(self, nc_client, deck_test_card):
|
||||
"""Updating only the duedate should work and preserve other fields."""
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
duedate="2025-12-31T23:59:59+00:00",
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.title == "Original Title"
|
||||
assert updated.description == "Original description"
|
||||
assert updated.duedate is not None
|
||||
|
||||
async def test_update_archived_only(self, nc_client, deck_test_card):
|
||||
"""Updating only the archived status should work and preserve other fields."""
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
archived=True,
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.title == "Original Title"
|
||||
assert updated.description == "Original description"
|
||||
assert updated.archived is True
|
||||
|
||||
async def test_update_order_only(self, nc_client, deck_test_card):
|
||||
"""Updating only the order should work and preserve other fields."""
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
order=99,
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.title == "Original Title"
|
||||
assert updated.description == "Original description"
|
||||
assert updated.order == 99
|
||||
|
||||
async def test_update_preserves_type(self, nc_client, deck_test_card):
|
||||
"""Type should be preserved when not explicitly changed."""
|
||||
original = deck_test_card["card"]
|
||||
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
title="Changed Title",
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.type == original.type
|
||||
assert updated.description == "Original description"
|
||||
|
||||
async def test_update_preserves_owner(self, nc_client, deck_test_card):
|
||||
"""Owner should be preserved when not explicitly changed."""
|
||||
original = deck_test_card["card"]
|
||||
|
||||
await nc_client.deck.update_card(
|
||||
board_id=deck_test_card["board_id"],
|
||||
stack_id=deck_test_card["stack_id"],
|
||||
card_id=deck_test_card["card_id"],
|
||||
title="Changed Title",
|
||||
)
|
||||
|
||||
updated = await nc_client.deck.get_card(
|
||||
deck_test_card["board_id"],
|
||||
deck_test_card["stack_id"],
|
||||
deck_test_card["card_id"],
|
||||
)
|
||||
assert updated.owner == original.owner
|
||||
assert updated.description == "Original description"
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
[tool.commitizen]
|
||||
name = "cz_conventional_commits"
|
||||
version = "0.7.2"
|
||||
version = "0.7.0"
|
||||
tag_format = "astrolabe-v$version"
|
||||
version_scheme = "semver"
|
||||
update_changelog_on_bump = true
|
||||
|
||||
Vendored
-14
@@ -25,20 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Requires external MCP server deployment
|
||||
- See documentation for setup: https://github.com/cbcoutinho/nextcloud-mcp-server
|
||||
|
||||
## astrolabe-v0.7.2 (2025-12-30)
|
||||
|
||||
### Fix
|
||||
|
||||
- **astrolabe**: Fix CSS loading for Nextcloud apps
|
||||
|
||||
## astrolabe-v0.7.1 (2025-12-30)
|
||||
|
||||
### Fix
|
||||
|
||||
- **astrolabe**: Fix revoke access button HTTP method mismatch
|
||||
- **oauth**: Enable browser OAuth routes for Management API in hybrid mode
|
||||
- **mcp**: Move all imports to the top of modules
|
||||
|
||||
## astrolabe-v0.7.0 (2025-12-26)
|
||||
|
||||
### Feat
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ Astrolabe connects to a semantic search service that understands the meaning of
|
||||
|
||||
See [documentation](https://github.com/cbcoutinho/nextcloud-mcp-server) for configuration details.
|
||||
]]></description>
|
||||
<version>0.7.2</version>
|
||||
<version>0.7.0</version>
|
||||
<licence>agpl</licence>
|
||||
<author homepage="https://github.com/cbcoutinho">Chris Coutinho</author>
|
||||
<namespace>Astrolabe</namespace>
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "astrolabe",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": "^22.0.0",
|
||||
|
||||
Vendored
+3
-5
@@ -7,10 +7,9 @@ export default defineConfig({
|
||||
build: {
|
||||
outDir: '.',
|
||||
emptyOutDir: false,
|
||||
cssCodeSplit: false, // Bundle all CSS into entry points (Nextcloud doesn't load CSS chunks)
|
||||
rollupOptions: {
|
||||
input: {
|
||||
'astrolabe-main': resolve(__dirname, 'src/main.js'),
|
||||
main: resolve(__dirname, 'src/main.js'),
|
||||
'astrolabe-adminSettings': resolve(__dirname, 'src/adminSettings.js'),
|
||||
'astrolabe-personalSettings': resolve(__dirname, 'src/personalSettings.js'),
|
||||
},
|
||||
@@ -18,10 +17,9 @@ export default defineConfig({
|
||||
entryFileNames: 'js/[name].mjs',
|
||||
chunkFileNames: 'js/[name]-[hash].chunk.mjs',
|
||||
assetFileNames: (assetInfo) => {
|
||||
// With cssCodeSplit:false, all CSS goes to a single file
|
||||
// Name it astrolabe-main.css to match Nextcloud's Util::addStyle expectation
|
||||
// Output CSS to css/ directory, JS/other assets to js/
|
||||
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
||||
return 'css/astrolabe-main.css';
|
||||
return 'css/[name][extname]';
|
||||
}
|
||||
return 'js/[name][extname]';
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user