Add due date and description fields to Todoist To-do entity (#104655)
* Add Todoist Due date and description fields * Update entity features with new names * Make items into walrus * Update due_datetime field * Add additional tests for adding new fields to items * Fix call args in todoist testpull/105135/head
parent
655b067277
commit
db6b804298
|
@ -1,7 +1,8 @@
|
|||
"""A todo platform for Todoist."""
|
||||
|
||||
import asyncio
|
||||
from typing import cast
|
||||
import datetime
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
TodoItem,
|
||||
|
@ -13,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TodoistCoordinator
|
||||
|
@ -30,6 +32,24 @@ async def async_setup_entry(
|
|||
)
|
||||
|
||||
|
||||
def _task_api_data(item: TodoItem) -> dict[str, Any]:
|
||||
"""Convert a TodoItem to the set of add or update arguments."""
|
||||
item_data: dict[str, Any] = {}
|
||||
if summary := item.summary:
|
||||
item_data["content"] = summary
|
||||
if due := item.due:
|
||||
if isinstance(due, datetime.datetime):
|
||||
item_data["due"] = {
|
||||
"date": due.date().isoformat(),
|
||||
"datetime": due.isoformat(),
|
||||
}
|
||||
else:
|
||||
item_data["due"] = {"date": due.isoformat()}
|
||||
if description := item.description:
|
||||
item_data["description"] = description
|
||||
return item_data
|
||||
|
||||
|
||||
class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntity):
|
||||
"""A Todoist TodoListEntity."""
|
||||
|
||||
|
@ -37,6 +57,9 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
|||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(
|
||||
|
@ -66,11 +89,21 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
|||
status = TodoItemStatus.COMPLETED
|
||||
else:
|
||||
status = TodoItemStatus.NEEDS_ACTION
|
||||
due: datetime.date | datetime.datetime | None = None
|
||||
if task_due := task.due:
|
||||
if task_due.datetime:
|
||||
due = dt_util.as_local(
|
||||
datetime.datetime.fromisoformat(task_due.datetime)
|
||||
)
|
||||
elif task_due.date:
|
||||
due = datetime.date.fromisoformat(task_due.date)
|
||||
items.append(
|
||||
TodoItem(
|
||||
summary=task.content,
|
||||
uid=task.id,
|
||||
status=status,
|
||||
due=due,
|
||||
description=task.description or None, # Don't use empty string
|
||||
)
|
||||
)
|
||||
self._attr_todo_items = items
|
||||
|
@ -81,7 +114,7 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
|||
if item.status != TodoItemStatus.NEEDS_ACTION:
|
||||
raise ValueError("Only active tasks may be created.")
|
||||
await self.coordinator.api.add_task(
|
||||
content=item.summary or "",
|
||||
**_task_api_data(item),
|
||||
project_id=self._project_id,
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
@ -89,8 +122,8 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
|||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update a To-do item."""
|
||||
uid: str = cast(str, item.uid)
|
||||
if item.summary:
|
||||
await self.coordinator.api.update_task(task_id=uid, content=item.summary)
|
||||
if update_data := _task_api_data(item):
|
||||
await self.coordinator.api.update_task(task_id=uid, **update_data)
|
||||
if item.status is not None:
|
||||
if item.status == TodoItemStatus.COMPLETED:
|
||||
await self.coordinator.api.close_task(task_id=uid)
|
||||
|
|
|
@ -45,6 +45,7 @@ def make_api_task(
|
|||
is_completed: bool = False,
|
||||
due: Due | None = None,
|
||||
project_id: str | None = None,
|
||||
description: str | None = None,
|
||||
) -> Task:
|
||||
"""Mock a todoist Task instance."""
|
||||
return Task(
|
||||
|
@ -55,8 +56,8 @@ def make_api_task(
|
|||
content=content or SUMMARY,
|
||||
created_at="2021-10-01T00:00:00",
|
||||
creator_id="1",
|
||||
description="A task",
|
||||
due=due or Due(is_recurring=False, date=TODAY, string="today"),
|
||||
description=description,
|
||||
due=due,
|
||||
id=id or "1",
|
||||
labels=["Label1"],
|
||||
order=1,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Unit tests for the Todoist todo platform."""
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from todoist_api_python.models import Due, Task
|
||||
|
||||
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
|
@ -19,6 +21,12 @@ def platforms() -> list[Platform]:
|
|||
return [Platform.TODO]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def set_time_zone(hass: HomeAssistant) -> None:
|
||||
"""Set the time zone for the tests that keesp UTC-6 all year round."""
|
||||
hass.config.set_time_zone("America/Regina")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tasks", "expected_state"),
|
||||
[
|
||||
|
@ -57,11 +65,91 @@ async def test_todo_item_state(
|
|||
assert state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("tasks"), [[]])
|
||||
@pytest.mark.parametrize(
|
||||
("tasks", "item_data", "tasks_after_update", "add_kwargs", "expected_item"),
|
||||
[
|
||||
(
|
||||
[],
|
||||
{},
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
{"content": "Soda"},
|
||||
{"uid": "task-id-1", "summary": "Soda", "status": "needs_action"},
|
||||
),
|
||||
(
|
||||
[],
|
||||
{"due_date": "2023-11-18"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
||||
)
|
||||
],
|
||||
{"due": {"date": "2023-11-18"}},
|
||||
{
|
||||
"uid": "task-id-1",
|
||||
"summary": "Soda",
|
||||
"status": "needs_action",
|
||||
"due": "2023-11-18",
|
||||
},
|
||||
),
|
||||
(
|
||||
[],
|
||||
{"due_datetime": "2023-11-18T06:30:00"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(
|
||||
date="2023-11-18",
|
||||
is_recurring=False,
|
||||
datetime="2023-11-18T12:30:00.000000Z",
|
||||
string="today",
|
||||
),
|
||||
)
|
||||
],
|
||||
{
|
||||
"due": {"date": "2023-11-18", "datetime": "2023-11-18T06:30:00-06:00"},
|
||||
},
|
||||
{
|
||||
"uid": "task-id-1",
|
||||
"summary": "Soda",
|
||||
"status": "needs_action",
|
||||
"due": "2023-11-18T06:30:00-06:00",
|
||||
},
|
||||
),
|
||||
(
|
||||
[],
|
||||
{"description": "6-pack"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
)
|
||||
],
|
||||
{"description": "6-pack"},
|
||||
{
|
||||
"uid": "task-id-1",
|
||||
"summary": "Soda",
|
||||
"status": "needs_action",
|
||||
"description": "6-pack",
|
||||
},
|
||||
),
|
||||
],
|
||||
ids=["summary", "due_date", "due_datetime", "description"],
|
||||
)
|
||||
async def test_add_todo_list_item(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
api: AsyncMock,
|
||||
item_data: dict[str, Any],
|
||||
tasks_after_update: list[Task],
|
||||
add_kwargs: dict[str, Any],
|
||||
expected_item: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test for adding a To-do Item."""
|
||||
|
||||
|
@ -71,28 +159,35 @@ async def test_add_todo_list_item(
|
|||
|
||||
api.add_task = AsyncMock()
|
||||
# Fake API response when state is refreshed after create
|
||||
api.get_tasks.return_value = [
|
||||
make_api_task(id="task-id-1", content="Soda", is_completed=False)
|
||||
]
|
||||
api.get_tasks.return_value = tasks_after_update
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
"add_item",
|
||||
{"item": "Soda"},
|
||||
{"item": "Soda", **item_data},
|
||||
target={"entity_id": "todo.name"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
args = api.add_task.call_args
|
||||
assert args
|
||||
assert args.kwargs.get("content") == "Soda"
|
||||
assert args.kwargs.get("project_id") == PROJECT_ID
|
||||
assert args.kwargs == {"project_id": PROJECT_ID, "content": "Soda", **add_kwargs}
|
||||
|
||||
# Verify state is refreshed
|
||||
state = hass.states.get("todo.name")
|
||||
assert state
|
||||
assert state.state == "1"
|
||||
|
||||
result = await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
"get_items",
|
||||
{},
|
||||
target={"entity_id": "todo.name"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert result == {"todo.name": {"items": [expected_item]}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tasks"), [[make_api_task(id="task-id-1", content="Soda", is_completed=False)]]
|
||||
|
@ -158,12 +253,91 @@ async def test_update_todo_item_status(
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tasks"), [[make_api_task(id="task-id-1", content="Soda", is_completed=False)]]
|
||||
("tasks", "update_data", "tasks_after_update", "update_kwargs", "expected_item"),
|
||||
[
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
{"rename": "Milk"},
|
||||
[make_api_task(id="task-id-1", content="Milk", is_completed=False)],
|
||||
{"task_id": "task-id-1", "content": "Milk"},
|
||||
{"uid": "task-id-1", "summary": "Milk", "status": "needs_action"},
|
||||
),
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
{"due_date": "2023-11-18"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
||||
)
|
||||
],
|
||||
{"task_id": "task-id-1", "due": {"date": "2023-11-18"}},
|
||||
{
|
||||
"uid": "task-id-1",
|
||||
"summary": "Soda",
|
||||
"status": "needs_action",
|
||||
"due": "2023-11-18",
|
||||
},
|
||||
),
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
{"due_datetime": "2023-11-18T06:30:00"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(
|
||||
date="2023-11-18",
|
||||
is_recurring=False,
|
||||
datetime="2023-11-18T12:30:00.000000Z",
|
||||
string="today",
|
||||
),
|
||||
)
|
||||
],
|
||||
{
|
||||
"task_id": "task-id-1",
|
||||
"due": {"date": "2023-11-18", "datetime": "2023-11-18T06:30:00-06:00"},
|
||||
},
|
||||
{
|
||||
"uid": "task-id-1",
|
||||
"summary": "Soda",
|
||||
"status": "needs_action",
|
||||
"due": "2023-11-18T06:30:00-06:00",
|
||||
},
|
||||
),
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
{"description": "6-pack"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
)
|
||||
],
|
||||
{"task_id": "task-id-1", "description": "6-pack"},
|
||||
{
|
||||
"uid": "task-id-1",
|
||||
"summary": "Soda",
|
||||
"status": "needs_action",
|
||||
"description": "6-pack",
|
||||
},
|
||||
),
|
||||
],
|
||||
ids=["rename", "due_date", "due_datetime", "description"],
|
||||
)
|
||||
async def test_update_todo_item_summary(
|
||||
async def test_update_todo_items(
|
||||
hass: HomeAssistant,
|
||||
setup_integration: None,
|
||||
api: AsyncMock,
|
||||
update_data: dict[str, Any],
|
||||
tasks_after_update: list[Task],
|
||||
update_kwargs: dict[str, Any],
|
||||
expected_item: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test for updating a To-do Item that changes the summary."""
|
||||
|
||||
|
@ -174,22 +348,29 @@ async def test_update_todo_item_summary(
|
|||
api.update_task = AsyncMock()
|
||||
|
||||
# Fake API response when state is refreshed after close
|
||||
api.get_tasks.return_value = [
|
||||
make_api_task(id="task-id-1", content="Soda", is_completed=True)
|
||||
]
|
||||
api.get_tasks.return_value = tasks_after_update
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
"update_item",
|
||||
{"item": "task-id-1", "rename": "Milk"},
|
||||
{"item": "task-id-1", **update_data},
|
||||
target={"entity_id": "todo.name"},
|
||||
blocking=True,
|
||||
)
|
||||
assert api.update_task.called
|
||||
args = api.update_task.call_args
|
||||
assert args
|
||||
assert args.kwargs.get("task_id") == "task-id-1"
|
||||
assert args.kwargs.get("content") == "Milk"
|
||||
assert args.kwargs == update_kwargs
|
||||
|
||||
result = await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
"get_items",
|
||||
{},
|
||||
target={"entity_id": "todo.name"},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert result == {"todo.name": {"items": [expected_item]}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
Loading…
Reference in New Issue