mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
fix(projects): carry tags, due date and priority in the list payload (#2536)
The edit dialog is shared between the projects list and the project detail page and seeds itself from whichever project object it is handed. The list payload never carried tags, due_date or priority, so editing from the list showed a blank tags field -- and, unreported, submitted the dialog's default priority over a stored high/urgent one. The component read those fields through a cast, so the compiler never flagged that they were always absent. Put them on ProjectListResponse and ProjectListItem, drop the casts, and let an explicit null clear tags and due date the way it already clears budget and url -- an emptied field was previously sent as undefined and silently reverted. The template list was missing target_parts_count, which the same dialog edits.
This commit is contained in:
@@ -1298,3 +1298,98 @@ class TestProjectExportImport:
|
||||
assert response.status_code == 200, response.text
|
||||
data = response.json()
|
||||
assert data["name"] == "nested-ok"
|
||||
|
||||
|
||||
class TestProjectListEditableFields:
|
||||
"""Tests for #2536 — the project list payload must carry every field the
|
||||
shared edit dialog renders. The dialog is opened from both the project list
|
||||
and the project detail page and seeds itself from whichever project object it
|
||||
is handed, so a field missing from the list payload shows up blank there and
|
||||
is saved back over the stored value."""
|
||||
|
||||
@pytest.fixture
|
||||
async def project_factory(self, db_session):
|
||||
async def _create(**kwargs):
|
||||
from backend.app.models.project import Project
|
||||
|
||||
defaults = {"name": "Editable Fields Project", "color": "#123456"}
|
||||
defaults.update(kwargs)
|
||||
project = Project(**defaults)
|
||||
db_session.add(project)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(project)
|
||||
return project
|
||||
|
||||
return _create
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_carries_the_fields_the_edit_dialog_renders(self, async_client: AsyncClient, project_factory):
|
||||
"""The list view is where the reporter saw an empty tags field."""
|
||||
from datetime import datetime
|
||||
|
||||
await project_factory(
|
||||
name="Tagged Project",
|
||||
tags="prototype,client-work",
|
||||
due_date=datetime(2026, 8, 1, 12, 0, 0),
|
||||
priority="high",
|
||||
target_parts_count=7,
|
||||
)
|
||||
|
||||
response = await async_client.get("/api/v1/projects/")
|
||||
assert response.status_code == 200
|
||||
item = next(p for p in response.json() if p["name"] == "Tagged Project")
|
||||
|
||||
assert item["tags"] == "prototype,client-work"
|
||||
assert item["due_date"].startswith("2026-08-01")
|
||||
assert item["priority"] == "high"
|
||||
assert item["target_parts_count"] == 7
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_template_list_carries_them_too(self, async_client: AsyncClient, project_factory):
|
||||
"""Templates feed the same dialog, so they need the same payload."""
|
||||
await project_factory(
|
||||
name="Tagged Template",
|
||||
is_template=True,
|
||||
tags="reusable",
|
||||
priority="urgent",
|
||||
target_parts_count=3,
|
||||
)
|
||||
|
||||
response = await async_client.get("/api/v1/projects/templates")
|
||||
assert response.status_code == 200
|
||||
item = next(p for p in response.json() if p["name"] == "Tagged Template")
|
||||
|
||||
assert item["tags"] == "reusable"
|
||||
assert item["priority"] == "urgent"
|
||||
assert item["target_parts_count"] == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_priority_survives_an_edit_that_does_not_touch_it(self, async_client: AsyncClient, project_factory):
|
||||
"""A save from the list view used to submit the default priority over a
|
||||
stored 'high' — the dialog never received the real one."""
|
||||
project = await project_factory(name="Important", priority="high", tags="keep-me")
|
||||
|
||||
response = await async_client.patch(f"/api/v1/projects/{project.id}", json={"name": "Still Important"})
|
||||
assert response.status_code == 200
|
||||
|
||||
result = response.json()
|
||||
assert result["priority"] == "high"
|
||||
assert result["tags"] == "keep-me"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_explicit_null_clears_tags_and_due_date(self, async_client: AsyncClient, project_factory):
|
||||
"""Emptying the field in the dialog has to actually remove the value."""
|
||||
from datetime import datetime
|
||||
|
||||
project = await project_factory(name="Clearable", tags="obsolete", due_date=datetime(2026, 8, 1, 12, 0, 0))
|
||||
|
||||
response = await async_client.patch(f"/api/v1/projects/{project.id}", json={"tags": None, "due_date": None})
|
||||
assert response.status_code == 200
|
||||
|
||||
result = response.json()
|
||||
assert result["tags"] is None
|
||||
assert result["due_date"] is None
|
||||
|
||||
Reference in New Issue
Block a user