Allow clearing To-do list item extended fields (#106208)
parent
c51ac7171a
commit
2497798b5d
|
@ -90,20 +90,6 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _to_ics_fields(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["summary"] = summary
|
|
||||||
if status := item.status:
|
|
||||||
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
|
||||||
if due := item.due:
|
|
||||||
item_data["due"] = due
|
|
||||||
if description := item.description:
|
|
||||||
item_data["description"] = description
|
|
||||||
return item_data
|
|
||||||
|
|
||||||
|
|
||||||
class WebDavTodoListEntity(TodoListEntity):
|
class WebDavTodoListEntity(TodoListEntity):
|
||||||
"""CalDAV To-do list entity."""
|
"""CalDAV To-do list entity."""
|
||||||
|
|
||||||
|
@ -140,9 +126,18 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||||
|
|
||||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||||
"""Add an item to the To-do list."""
|
"""Add an item to the To-do list."""
|
||||||
|
item_data: dict[str, Any] = {}
|
||||||
|
if summary := item.summary:
|
||||||
|
item_data["summary"] = summary
|
||||||
|
if status := item.status:
|
||||||
|
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
||||||
|
if due := item.due:
|
||||||
|
item_data["due"] = due
|
||||||
|
if description := item.description:
|
||||||
|
item_data["description"] = description
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
partial(self._calendar.save_todo, **_to_ics_fields(item)),
|
partial(self._calendar.save_todo, **item_data),
|
||||||
)
|
)
|
||||||
except (requests.ConnectionError, DAVError) as err:
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||||
|
@ -159,10 +154,17 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||||
except (requests.ConnectionError, DAVError) as err:
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||||
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||||
updated_fields = _to_ics_fields(item)
|
vtodo["SUMMARY"] = item.summary or ""
|
||||||
if "due" in updated_fields:
|
if status := item.status:
|
||||||
todo.set_due(updated_fields.pop("due")) # type: ignore[attr-defined]
|
vtodo["STATUS"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
||||||
vtodo.update(**updated_fields)
|
if due := item.due:
|
||||||
|
todo.set_due(due) # type: ignore[attr-defined]
|
||||||
|
else:
|
||||||
|
vtodo.pop("DUE", None)
|
||||||
|
if description := item.description:
|
||||||
|
vtodo["DESCRIPTION"] = description
|
||||||
|
else:
|
||||||
|
vtodo.pop("DESCRIPTION", None)
|
||||||
try:
|
try:
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
partial(
|
partial(
|
||||||
|
|
|
@ -29,18 +29,20 @@ TODO_STATUS_MAP = {
|
||||||
TODO_STATUS_MAP_INV = {v: k for k, v in TODO_STATUS_MAP.items()}
|
TODO_STATUS_MAP_INV = {v: k for k, v in TODO_STATUS_MAP.items()}
|
||||||
|
|
||||||
|
|
||||||
def _convert_todo_item(item: TodoItem) -> dict[str, str]:
|
def _convert_todo_item(item: TodoItem) -> dict[str, str | None]:
|
||||||
"""Convert TodoItem dataclass items to dictionary of attributes the tasks API."""
|
"""Convert TodoItem dataclass items to dictionary of attributes the tasks API."""
|
||||||
result: dict[str, str] = {}
|
result: dict[str, str | None] = {}
|
||||||
if item.summary is not None:
|
result["title"] = item.summary
|
||||||
result["title"] = item.summary
|
|
||||||
if item.status is not None:
|
if item.status is not None:
|
||||||
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
||||||
|
else:
|
||||||
|
result["status"] = TodoItemStatus.NEEDS_ACTION
|
||||||
if (due := item.due) is not None:
|
if (due := item.due) is not None:
|
||||||
# due API field is a timestamp string, but with only date resolution
|
# due API field is a timestamp string, but with only date resolution
|
||||||
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
||||||
if (description := item.description) is not None:
|
else:
|
||||||
result["notes"] = description
|
result["due"] = None
|
||||||
|
result["notes"] = item.description
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
"""A Local To-do todo platform."""
|
"""A Local To-do todo platform."""
|
||||||
|
|
||||||
from collections.abc import Iterable
|
|
||||||
import dataclasses
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ical.calendar import Calendar
|
from ical.calendar import Calendar
|
||||||
from ical.calendar_stream import IcsCalendarStream
|
from ical.calendar_stream import IcsCalendarStream
|
||||||
from ical.exceptions import CalendarParseError
|
|
||||||
from ical.store import TodoStore
|
from ical.store import TodoStore
|
||||||
from ical.todo import Todo, TodoStatus
|
from ical.todo import Todo, TodoStatus
|
||||||
|
|
||||||
|
@ -59,26 +55,18 @@ async def async_setup_entry(
|
||||||
async_add_entities([entity], True)
|
async_add_entities([entity], True)
|
||||||
|
|
||||||
|
|
||||||
def _todo_dict_factory(obj: Iterable[tuple[str, Any]]) -> dict[str, str]:
|
|
||||||
"""Convert TodoItem dataclass items to dictionary of attributes for ical consumption."""
|
|
||||||
result: dict[str, str] = {}
|
|
||||||
for name, value in obj:
|
|
||||||
if value is None:
|
|
||||||
continue
|
|
||||||
if name == "status":
|
|
||||||
result[name] = ICS_TODO_STATUS_MAP_INV[value]
|
|
||||||
else:
|
|
||||||
result[name] = value
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_item(item: TodoItem) -> Todo:
|
def _convert_item(item: TodoItem) -> Todo:
|
||||||
"""Convert a HomeAssistant TodoItem to an ical Todo."""
|
"""Convert a HomeAssistant TodoItem to an ical Todo."""
|
||||||
try:
|
todo = Todo()
|
||||||
return Todo(**dataclasses.asdict(item, dict_factory=_todo_dict_factory))
|
if item.uid:
|
||||||
except CalendarParseError as err:
|
todo.uid = item.uid
|
||||||
_LOGGER.debug("Error parsing todo input fields: %s (%s)", item, err)
|
if item.summary:
|
||||||
raise HomeAssistantError("Error parsing todo input fields") from err
|
todo.summary = item.summary
|
||||||
|
if item.status:
|
||||||
|
todo.status = ICS_TODO_STATUS_MAP_INV[item.status]
|
||||||
|
todo.due = item.due
|
||||||
|
todo.description = item.description
|
||||||
|
return todo
|
||||||
|
|
||||||
|
|
||||||
class LocalTodoListEntity(TodoListEntity):
|
class LocalTodoListEntity(TodoListEntity):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""A shopping list todo platform."""
|
"""A shopping list todo platform."""
|
||||||
|
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
TodoItem,
|
TodoItem,
|
||||||
|
@ -55,11 +55,10 @@ class ShoppingTodoListEntity(TodoListEntity):
|
||||||
|
|
||||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||||
"""Update an item to the To-do list."""
|
"""Update an item to the To-do list."""
|
||||||
data: dict[str, Any] = {}
|
data = {
|
||||||
if item.summary:
|
"name": item.summary,
|
||||||
data["name"] = item.summary
|
"complete": item.status == TodoItemStatus.COMPLETED,
|
||||||
if item.status:
|
}
|
||||||
data["complete"] = item.status == TodoItemStatus.COMPLETED
|
|
||||||
try:
|
try:
|
||||||
await self._data.async_update(item.uid, data)
|
await self._data.async_update(item.uid, data)
|
||||||
except NoMatchingShoppingListItem as err:
|
except NoMatchingShoppingListItem as err:
|
||||||
|
|
|
@ -74,19 +74,19 @@ class TodoItemFieldDescription:
|
||||||
TODO_ITEM_FIELDS = [
|
TODO_ITEM_FIELDS = [
|
||||||
TodoItemFieldDescription(
|
TodoItemFieldDescription(
|
||||||
service_field=ATTR_DUE_DATE,
|
service_field=ATTR_DUE_DATE,
|
||||||
validation=cv.date,
|
validation=vol.Any(cv.date, None),
|
||||||
todo_item_field=ATTR_DUE,
|
todo_item_field=ATTR_DUE,
|
||||||
required_feature=TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
|
required_feature=TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
|
||||||
),
|
),
|
||||||
TodoItemFieldDescription(
|
TodoItemFieldDescription(
|
||||||
service_field=ATTR_DUE_DATETIME,
|
service_field=ATTR_DUE_DATETIME,
|
||||||
validation=vol.All(cv.datetime, dt_util.as_local),
|
validation=vol.Any(vol.All(cv.datetime, dt_util.as_local), None),
|
||||||
todo_item_field=ATTR_DUE,
|
todo_item_field=ATTR_DUE,
|
||||||
required_feature=TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
|
required_feature=TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
|
||||||
),
|
),
|
||||||
TodoItemFieldDescription(
|
TodoItemFieldDescription(
|
||||||
service_field=ATTR_DESCRIPTION,
|
service_field=ATTR_DESCRIPTION,
|
||||||
validation=cv.string,
|
validation=vol.Any(cv.string, None),
|
||||||
todo_item_field=ATTR_DESCRIPTION,
|
todo_item_field=ATTR_DESCRIPTION,
|
||||||
required_feature=TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
|
required_feature=TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
|
||||||
),
|
),
|
||||||
|
@ -485,18 +485,22 @@ async def _async_update_todo_item(entity: TodoListEntity, call: ServiceCall) ->
|
||||||
|
|
||||||
_validate_supported_features(entity.supported_features, call.data)
|
_validate_supported_features(entity.supported_features, call.data)
|
||||||
|
|
||||||
await entity.async_update_todo_item(
|
# Perform a partial update on the existing entity based on the fields
|
||||||
item=TodoItem(
|
# present in the update. This allows explicitly clearing any of the
|
||||||
uid=found.uid,
|
# extended fields present and set to None.
|
||||||
summary=call.data.get("rename"),
|
updated_data = dataclasses.asdict(found)
|
||||||
status=call.data.get("status"),
|
if summary := call.data.get("rename"):
|
||||||
**{
|
updated_data["summary"] = summary
|
||||||
desc.todo_item_field: call.data[desc.service_field]
|
if status := call.data.get("status"):
|
||||||
for desc in TODO_ITEM_FIELDS
|
updated_data["status"] = status
|
||||||
if desc.service_field in call.data
|
updated_data.update(
|
||||||
},
|
{
|
||||||
)
|
desc.todo_item_field: call.data[desc.service_field]
|
||||||
|
for desc in TODO_ITEM_FIELDS
|
||||||
|
if desc.service_field in call.data
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
await entity.async_update_todo_item(item=TodoItem(**updated_data))
|
||||||
|
|
||||||
|
|
||||||
async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> None:
|
async def _async_remove_todo_items(entity: TodoListEntity, call: ServiceCall) -> None:
|
||||||
|
|
|
@ -34,19 +34,20 @@ async def async_setup_entry(
|
||||||
|
|
||||||
def _task_api_data(item: TodoItem) -> dict[str, Any]:
|
def _task_api_data(item: TodoItem) -> dict[str, Any]:
|
||||||
"""Convert a TodoItem to the set of add or update arguments."""
|
"""Convert a TodoItem to the set of add or update arguments."""
|
||||||
item_data: dict[str, Any] = {}
|
item_data: dict[str, Any] = {
|
||||||
if summary := item.summary:
|
"content": item.summary,
|
||||||
item_data["content"] = summary
|
# Description needs to be empty string to be cleared
|
||||||
|
"description": item.description or "",
|
||||||
|
}
|
||||||
if due := item.due:
|
if due := item.due:
|
||||||
if isinstance(due, datetime.datetime):
|
if isinstance(due, datetime.datetime):
|
||||||
item_data["due"] = {
|
item_data["due_datetime"] = due.isoformat()
|
||||||
"date": due.date().isoformat(),
|
|
||||||
"datetime": due.isoformat(),
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
item_data["due"] = {"date": due.isoformat()}
|
item_data["due_date"] = due.isoformat()
|
||||||
if description := item.description:
|
else:
|
||||||
item_data["description"] = description
|
# Special flag "no date" clears the due date/datetime.
|
||||||
|
# See https://developer.todoist.com/rest/v2/#update-a-task for more.
|
||||||
|
item_data["due_string"] = "no date"
|
||||||
return item_data
|
return item_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,10 +129,16 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||||
if update_data := _task_api_data(item):
|
if update_data := _task_api_data(item):
|
||||||
await self.coordinator.api.update_task(task_id=uid, **update_data)
|
await self.coordinator.api.update_task(task_id=uid, **update_data)
|
||||||
if item.status is not None:
|
if item.status is not None:
|
||||||
if item.status == TodoItemStatus.COMPLETED:
|
# Only update status if changed
|
||||||
await self.coordinator.api.close_task(task_id=uid)
|
for existing_item in self._attr_todo_items or ():
|
||||||
else:
|
if existing_item.uid != item.uid:
|
||||||
await self.coordinator.api.reopen_task(task_id=uid)
|
continue
|
||||||
|
|
||||||
|
if item.status != existing_item.status:
|
||||||
|
if item.status == TodoItemStatus.COMPLETED:
|
||||||
|
await self.coordinator.api.close_task(task_id=uid)
|
||||||
|
else:
|
||||||
|
await self.coordinator.api.reopen_task(task_id=uid)
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||||
|
|
|
@ -69,6 +69,19 @@ STATUS:NEEDS-ACTION
|
||||||
END:VTODO
|
END:VTODO
|
||||||
END:VCALENDAR"""
|
END:VCALENDAR"""
|
||||||
|
|
||||||
|
TODO_ALL_FIELDS = """BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//E-Corp.//CalDAV Client//EN
|
||||||
|
BEGIN:VTODO
|
||||||
|
UID:2
|
||||||
|
DTSTAMP:20171125T000000Z
|
||||||
|
SUMMARY:Cheese
|
||||||
|
DESCRIPTION:Any kind will do
|
||||||
|
STATUS:NEEDS-ACTION
|
||||||
|
DUE:20171126
|
||||||
|
END:VTODO
|
||||||
|
END:VCALENDAR"""
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def platforms() -> list[Platform]:
|
def platforms() -> list[Platform]:
|
||||||
|
@ -132,6 +145,18 @@ async def mock_add_to_hass(
|
||||||
config_entry.add_to_hass(hass)
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
|
||||||
|
IGNORE_COMPONENTS = ["BEGIN", "END", "DTSTAMP", "PRODID", "UID", "VERSION"]
|
||||||
|
|
||||||
|
|
||||||
|
def compact_ics(ics: str) -> list[str]:
|
||||||
|
"""Pull out parts of the rfc5545 content useful for assertions in tests."""
|
||||||
|
return [
|
||||||
|
line
|
||||||
|
for line in ics.split("\n")
|
||||||
|
if line and not any(filter(line.startswith, IGNORE_COMPONENTS))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("todos", "expected_state"),
|
("todos", "expected_state"),
|
||||||
[
|
[
|
||||||
|
@ -292,45 +317,148 @@ async def test_add_item_failure(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{"rename": "Swiss Cheese"},
|
{"rename": "Swiss Cheese"},
|
||||||
["SUMMARY:Swiss Cheese", "STATUS:NEEDS-ACTION"],
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"DUE;VALUE=DATE:20171126",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Swiss Cheese",
|
||||||
|
],
|
||||||
"1",
|
"1",
|
||||||
{**RESULT_ITEM, "summary": "Swiss Cheese"},
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Swiss Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
"due": "2017-11-26",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"status": "needs_action"},
|
{"status": "needs_action"},
|
||||||
["SUMMARY:Cheese", "STATUS:NEEDS-ACTION"],
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"DUE;VALUE=DATE:20171126",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Cheese",
|
||||||
|
],
|
||||||
"1",
|
"1",
|
||||||
RESULT_ITEM,
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
"due": "2017-11-26",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"status": "completed"},
|
{"status": "completed"},
|
||||||
["SUMMARY:Cheese", "STATUS:COMPLETED"],
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"DUE;VALUE=DATE:20171126",
|
||||||
|
"STATUS:COMPLETED",
|
||||||
|
"SUMMARY:Cheese",
|
||||||
|
],
|
||||||
"0",
|
"0",
|
||||||
{**RESULT_ITEM, "status": "completed"},
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
"due": "2017-11-26",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"rename": "Swiss Cheese", "status": "needs_action"},
|
{"rename": "Swiss Cheese", "status": "needs_action"},
|
||||||
["SUMMARY:Swiss Cheese", "STATUS:NEEDS-ACTION"],
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"DUE;VALUE=DATE:20171126",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Swiss Cheese",
|
||||||
|
],
|
||||||
"1",
|
"1",
|
||||||
{**RESULT_ITEM, "summary": "Swiss Cheese"},
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Swiss Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
"due": "2017-11-26",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"due_date": "2023-11-18"},
|
{"due_date": "2023-11-18"},
|
||||||
["SUMMARY:Cheese", "DUE;VALUE=DATE:20231118"],
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"DUE;VALUE=DATE:20231118",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Cheese",
|
||||||
|
],
|
||||||
"1",
|
"1",
|
||||||
{**RESULT_ITEM, "due": "2023-11-18"},
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
"due": "2023-11-18",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"due_datetime": "2023-11-18T08:30:00-06:00"},
|
{"due_datetime": "2023-11-18T08:30:00-06:00"},
|
||||||
["SUMMARY:Cheese", "DUE;TZID=America/Regina:20231118T083000"],
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"DUE;TZID=America/Regina:20231118T083000",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Cheese",
|
||||||
|
],
|
||||||
"1",
|
"1",
|
||||||
{**RESULT_ITEM, "due": "2023-11-18T08:30:00-06:00"},
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
"due": "2023-11-18T08:30:00-06:00",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"due_datetime": None},
|
||||||
|
[
|
||||||
|
"DESCRIPTION:Any kind will do",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Cheese",
|
||||||
|
],
|
||||||
|
"1",
|
||||||
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Any kind will do",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"description": "Make sure to get Swiss"},
|
{"description": "Make sure to get Swiss"},
|
||||||
["SUMMARY:Cheese", "DESCRIPTION:Make sure to get Swiss"],
|
[
|
||||||
|
"DESCRIPTION:Make sure to get Swiss",
|
||||||
|
"DUE;VALUE=DATE:20171126",
|
||||||
|
"STATUS:NEEDS-ACTION",
|
||||||
|
"SUMMARY:Cheese",
|
||||||
|
],
|
||||||
"1",
|
"1",
|
||||||
{**RESULT_ITEM, "description": "Make sure to get Swiss"},
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"due": "2017-11-26",
|
||||||
|
"description": "Make sure to get Swiss",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"description": None},
|
||||||
|
["DUE;VALUE=DATE:20171126", "STATUS:NEEDS-ACTION", "SUMMARY:Cheese"],
|
||||||
|
"1",
|
||||||
|
{
|
||||||
|
"uid": "2",
|
||||||
|
"summary": "Cheese",
|
||||||
|
"status": "needs_action",
|
||||||
|
"due": "2017-11-26",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
ids=[
|
ids=[
|
||||||
|
@ -340,7 +468,9 @@ async def test_add_item_failure(
|
||||||
"rename_status",
|
"rename_status",
|
||||||
"due_date",
|
"due_date",
|
||||||
"due_datetime",
|
"due_datetime",
|
||||||
|
"clear_due_date",
|
||||||
"description",
|
"description",
|
||||||
|
"clear_description",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_update_item(
|
async def test_update_item(
|
||||||
|
@ -355,7 +485,7 @@ async def test_update_item(
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test updating an item on the list."""
|
"""Test updating an item on the list."""
|
||||||
|
|
||||||
item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2")
|
item = Todo(dav_client, None, TODO_ALL_FIELDS, calendar, "2")
|
||||||
calendar.search = MagicMock(return_value=[item])
|
calendar.search = MagicMock(return_value=[item])
|
||||||
|
|
||||||
await config_entry.async_setup(hass)
|
await config_entry.async_setup(hass)
|
||||||
|
@ -381,8 +511,7 @@ async def test_update_item(
|
||||||
|
|
||||||
assert dav_client.put.call_args
|
assert dav_client.put.call_args
|
||||||
ics = dav_client.put.call_args.args[1]
|
ics = dav_client.put.call_args.args[1]
|
||||||
for expected in expected_ics:
|
assert compact_ics(ics) == expected_ics
|
||||||
assert expected in ics
|
|
||||||
|
|
||||||
state = hass.states.get(TEST_ENTITY)
|
state = hass.states.get(TEST_ENTITY)
|
||||||
assert state
|
assert state
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_create_todo_list_item[description].1
|
# name: test_create_todo_list_item[description].1
|
||||||
'{"title": "Soda", "status": "needsAction", "notes": "6-pack"}'
|
'{"title": "Soda", "status": "needsAction", "due": null, "notes": "6-pack"}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_create_todo_list_item[due]
|
# name: test_create_todo_list_item[due]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_create_todo_list_item[due].1
|
# name: test_create_todo_list_item[due].1
|
||||||
'{"title": "Soda", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00"}'
|
'{"title": "Soda", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00", "notes": null}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_create_todo_list_item[summary]
|
# name: test_create_todo_list_item[summary]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_create_todo_list_item[summary].1
|
# name: test_create_todo_list_item[summary].1
|
||||||
'{"title": "Soda", "status": "needsAction"}'
|
'{"title": "Soda", "status": "needsAction", "due": null, "notes": null}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_delete_todo_list_item[_handler]
|
# name: test_delete_todo_list_item[_handler]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -106,6 +106,24 @@
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_partial_update[clear_description]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[clear_description].1
|
||||||
|
'{"title": "Water", "status": "needsAction", "due": null, "notes": null}'
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[clear_due_date]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[clear_due_date].1
|
||||||
|
'{"title": "Water", "status": "needsAction", "due": null, "notes": null}'
|
||||||
|
# ---
|
||||||
# name: test_partial_update[description]
|
# name: test_partial_update[description]
|
||||||
tuple(
|
tuple(
|
||||||
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
@ -113,7 +131,7 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update[description].1
|
# name: test_partial_update[description].1
|
||||||
'{"notes": "6-pack"}'
|
'{"title": "Water", "status": "needsAction", "due": null, "notes": "At least one gallon"}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update[due_date]
|
# name: test_partial_update[due_date]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -122,7 +140,16 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update[due_date].1
|
# name: test_partial_update[due_date].1
|
||||||
'{"due": "2023-11-18T00:00:00-08:00"}'
|
'{"title": "Water", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00", "notes": null}'
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[empty_description]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[empty_description].1
|
||||||
|
'{"title": "Water", "status": "needsAction", "due": null, "notes": ""}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update[rename]
|
# name: test_partial_update[rename]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -131,7 +158,7 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update[rename].1
|
# name: test_partial_update[rename].1
|
||||||
'{"title": "Soda"}'
|
'{"title": "Soda", "status": "needsAction", "due": null, "notes": null}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update_status[api_responses0]
|
# name: test_partial_update_status[api_responses0]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -140,7 +167,7 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update_status[api_responses0].1
|
# name: test_partial_update_status[api_responses0].1
|
||||||
'{"status": "needsAction"}'
|
'{"title": "Water", "status": "needsAction", "due": null, "notes": null}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_update_todo_list_item[api_responses0]
|
# name: test_update_todo_list_item[api_responses0]
|
||||||
tuple(
|
tuple(
|
||||||
|
@ -149,5 +176,5 @@
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_update_todo_list_item[api_responses0].1
|
# name: test_update_todo_list_item[api_responses0].1
|
||||||
'{"title": "Soda", "status": "completed"}'
|
'{"title": "Soda", "status": "completed", "due": null, "notes": null}'
|
||||||
# ---
|
# ---
|
||||||
|
|
|
@ -48,6 +48,7 @@ LIST_TASKS_RESPONSE_WATER = {
|
||||||
"id": "some-task-id",
|
"id": "some-task-id",
|
||||||
"title": "Water",
|
"title": "Water",
|
||||||
"status": "needsAction",
|
"status": "needsAction",
|
||||||
|
"description": "Any size is ok",
|
||||||
"position": "00000000000000000001",
|
"position": "00000000000000000001",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -516,9 +517,19 @@ async def test_update_todo_list_item_error(
|
||||||
[
|
[
|
||||||
(UPDATE_API_RESPONSES, {"rename": "Soda"}),
|
(UPDATE_API_RESPONSES, {"rename": "Soda"}),
|
||||||
(UPDATE_API_RESPONSES, {"due_date": "2023-11-18"}),
|
(UPDATE_API_RESPONSES, {"due_date": "2023-11-18"}),
|
||||||
(UPDATE_API_RESPONSES, {"description": "6-pack"}),
|
(UPDATE_API_RESPONSES, {"due_date": None}),
|
||||||
|
(UPDATE_API_RESPONSES, {"description": "At least one gallon"}),
|
||||||
|
(UPDATE_API_RESPONSES, {"description": ""}),
|
||||||
|
(UPDATE_API_RESPONSES, {"description": None}),
|
||||||
],
|
],
|
||||||
ids=("rename", "due_date", "description"),
|
ids=(
|
||||||
|
"rename",
|
||||||
|
"due_date",
|
||||||
|
"clear_due_date",
|
||||||
|
"description",
|
||||||
|
"empty_description",
|
||||||
|
"clear_description",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def test_partial_update(
|
async def test_partial_update(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
|
@ -65,16 +65,27 @@ def set_time_zone(hass: HomeAssistant) -> None:
|
||||||
hass.config.set_time_zone("America/Regina")
|
hass.config.set_time_zone("America/Regina")
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_ADD_ITEM = {
|
||||||
|
"status": "needs_action",
|
||||||
|
"summary": "replace batteries",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("item_data", "expected_item_data"),
|
("item_data", "expected_item_data"),
|
||||||
[
|
[
|
||||||
({}, {}),
|
({}, EXPECTED_ADD_ITEM),
|
||||||
({"due_date": "2023-11-17"}, {"due": "2023-11-17"}),
|
({"due_date": "2023-11-17"}, {**EXPECTED_ADD_ITEM, "due": "2023-11-17"}),
|
||||||
(
|
(
|
||||||
{"due_datetime": "2023-11-17T11:30:00+00:00"},
|
{"due_datetime": "2023-11-17T11:30:00+00:00"},
|
||||||
{"due": "2023-11-17T05:30:00-06:00"},
|
{**EXPECTED_ADD_ITEM, "due": "2023-11-17T05:30:00-06:00"},
|
||||||
),
|
),
|
||||||
({"description": "Additional detail"}, {"description": "Additional detail"}),
|
(
|
||||||
|
{"description": "Additional detail"},
|
||||||
|
{**EXPECTED_ADD_ITEM, "description": "Additional detail"},
|
||||||
|
),
|
||||||
|
({"description": ""}, {**EXPECTED_ADD_ITEM, "description": ""}),
|
||||||
|
({"description": None}, EXPECTED_ADD_ITEM),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_add_item(
|
async def test_add_item(
|
||||||
|
@ -101,11 +112,10 @@ async def test_add_item(
|
||||||
|
|
||||||
items = await ws_get_items()
|
items = await ws_get_items()
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
assert items[0]["summary"] == "replace batteries"
|
item_data = items[0]
|
||||||
assert items[0]["status"] == "needs_action"
|
assert "uid" in item_data
|
||||||
for k, v in expected_item_data.items():
|
del item_data["uid"]
|
||||||
assert items[0][k] == v
|
assert item_data == expected_item_data
|
||||||
assert "uid" in items[0]
|
|
||||||
|
|
||||||
state = hass.states.get(TEST_ENTITY)
|
state = hass.states.get(TEST_ENTITY)
|
||||||
assert state
|
assert state
|
||||||
|
@ -207,19 +217,29 @@ async def test_bulk_remove(
|
||||||
assert state.state == "0"
|
assert state.state == "0"
|
||||||
|
|
||||||
|
|
||||||
|
EXPECTED_UPDATE_ITEM = {
|
||||||
|
"status": "needs_action",
|
||||||
|
"summary": "soda",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("item_data", "expected_item_data", "expected_state"),
|
("item_data", "expected_item_data", "expected_state"),
|
||||||
[
|
[
|
||||||
({"status": "completed"}, {"status": "completed"}, "0"),
|
({"status": "completed"}, {**EXPECTED_UPDATE_ITEM, "status": "completed"}, "0"),
|
||||||
({"due_date": "2023-11-17"}, {"due": "2023-11-17"}, "1"),
|
(
|
||||||
|
{"due_date": "2023-11-17"},
|
||||||
|
{**EXPECTED_UPDATE_ITEM, "due": "2023-11-17"},
|
||||||
|
"1",
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{"due_datetime": "2023-11-17T11:30:00+00:00"},
|
{"due_datetime": "2023-11-17T11:30:00+00:00"},
|
||||||
{"due": "2023-11-17T05:30:00-06:00"},
|
{**EXPECTED_UPDATE_ITEM, "due": "2023-11-17T05:30:00-06:00"},
|
||||||
"1",
|
"1",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"description": "Additional detail"},
|
{"description": "Additional detail"},
|
||||||
{"description": "Additional detail"},
|
{**EXPECTED_UPDATE_ITEM, "description": "Additional detail"},
|
||||||
"1",
|
"1",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -246,6 +266,7 @@ async def test_update_item(
|
||||||
# Fetch item
|
# Fetch item
|
||||||
items = await ws_get_items()
|
items = await ws_get_items()
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
|
|
||||||
item = items[0]
|
item = items[0]
|
||||||
assert item["summary"] == "soda"
|
assert item["summary"] == "soda"
|
||||||
assert item["status"] == "needs_action"
|
assert item["status"] == "needs_action"
|
||||||
|
@ -254,7 +275,7 @@ async def test_update_item(
|
||||||
assert state
|
assert state
|
||||||
assert state.state == "1"
|
assert state.state == "1"
|
||||||
|
|
||||||
# Mark item completed
|
# Update item
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
TODO_DOMAIN,
|
TODO_DOMAIN,
|
||||||
"update_item",
|
"update_item",
|
||||||
|
@ -268,14 +289,130 @@ async def test_update_item(
|
||||||
assert len(items) == 1
|
assert len(items) == 1
|
||||||
item = items[0]
|
item = items[0]
|
||||||
assert item["summary"] == "soda"
|
assert item["summary"] == "soda"
|
||||||
for k, v in expected_item_data.items():
|
assert "uid" in item
|
||||||
assert items[0][k] == v
|
del item["uid"]
|
||||||
|
assert item == expected_item_data
|
||||||
|
|
||||||
state = hass.states.get(TEST_ENTITY)
|
state = hass.states.get(TEST_ENTITY)
|
||||||
assert state
|
assert state
|
||||||
assert state.state == expected_state
|
assert state.state == expected_state
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("item_data", "expected_item_data"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"status": "completed"},
|
||||||
|
{
|
||||||
|
"summary": "soda",
|
||||||
|
"status": "completed",
|
||||||
|
"description": "Additional detail",
|
||||||
|
"due": "2024-01-01",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"due_date": "2024-01-02"},
|
||||||
|
{
|
||||||
|
"summary": "soda",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Additional detail",
|
||||||
|
"due": "2024-01-02",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"due_date": None},
|
||||||
|
{
|
||||||
|
"summary": "soda",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Additional detail",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"due_datetime": "2024-01-01 10:30:00"},
|
||||||
|
{
|
||||||
|
"summary": "soda",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Additional detail",
|
||||||
|
"due": "2024-01-01T10:30:00-06:00",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"due_datetime": None},
|
||||||
|
{
|
||||||
|
"summary": "soda",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "Additional detail",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"description": "updated description"},
|
||||||
|
{
|
||||||
|
"summary": "soda",
|
||||||
|
"status": "needs_action",
|
||||||
|
"due": "2024-01-01",
|
||||||
|
"description": "updated description",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{"description": None},
|
||||||
|
{"summary": "soda", "status": "needs_action", "due": "2024-01-01"},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"status",
|
||||||
|
"due_date",
|
||||||
|
"clear_due_date",
|
||||||
|
"due_datetime",
|
||||||
|
"clear_due_datetime",
|
||||||
|
"description",
|
||||||
|
"clear_description",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_existing_field(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: None,
|
||||||
|
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
|
||||||
|
item_data: dict[str, Any],
|
||||||
|
expected_item_data: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Test updating a todo item."""
|
||||||
|
|
||||||
|
# Create new item
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"add_item",
|
||||||
|
{"item": "soda", "description": "Additional detail", "due_date": "2024-01-01"},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch item
|
||||||
|
items = await ws_get_items()
|
||||||
|
assert len(items) == 1
|
||||||
|
|
||||||
|
item = items[0]
|
||||||
|
assert item["summary"] == "soda"
|
||||||
|
assert item["status"] == "needs_action"
|
||||||
|
|
||||||
|
# Perform update
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{"item": item["uid"], **item_data},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify item is updated
|
||||||
|
items = await ws_get_items()
|
||||||
|
assert len(items) == 1
|
||||||
|
item = items[0]
|
||||||
|
assert item["summary"] == "soda"
|
||||||
|
assert "uid" in item
|
||||||
|
del item["uid"]
|
||||||
|
assert item == expected_item_data
|
||||||
|
|
||||||
|
|
||||||
async def test_rename(
|
async def test_rename(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
setup_integration: None,
|
setup_integration: None,
|
||||||
|
|
|
@ -146,15 +146,19 @@ async def create_mock_platform(
|
||||||
return config_entry
|
return config_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="test_entity_items")
|
||||||
|
def mock_test_entity_items() -> list[TodoItem]:
|
||||||
|
"""Fixture that creates the items returned by the test entity."""
|
||||||
|
return [
|
||||||
|
TodoItem(summary="Item #1", uid="1", status=TodoItemStatus.NEEDS_ACTION),
|
||||||
|
TodoItem(summary="Item #2", uid="2", status=TodoItemStatus.COMPLETED),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="test_entity")
|
@pytest.fixture(name="test_entity")
|
||||||
def mock_test_entity() -> TodoListEntity:
|
def mock_test_entity(test_entity_items: list[TodoItem]) -> TodoListEntity:
|
||||||
"""Fixture that creates a test TodoList entity with mock service calls."""
|
"""Fixture that creates a test TodoList entity with mock service calls."""
|
||||||
entity1 = MockTodoListEntity(
|
entity1 = MockTodoListEntity(test_entity_items)
|
||||||
[
|
|
||||||
TodoItem(summary="Item #1", uid="1", status=TodoItemStatus.NEEDS_ACTION),
|
|
||||||
TodoItem(summary="Item #2", uid="2", status=TodoItemStatus.COMPLETED),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
entity1.entity_id = "todo.entity1"
|
entity1.entity_id = "todo.entity1"
|
||||||
entity1._attr_supported_features = (
|
entity1._attr_supported_features = (
|
||||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||||
|
@ -504,7 +508,7 @@ async def test_update_todo_item_service_by_id_status_only(
|
||||||
item = args.kwargs.get("item")
|
item = args.kwargs.get("item")
|
||||||
assert item
|
assert item
|
||||||
assert item.uid == "1"
|
assert item.uid == "1"
|
||||||
assert item.summary is None
|
assert item.summary == "Item #1"
|
||||||
assert item.status == TodoItemStatus.COMPLETED
|
assert item.status == TodoItemStatus.COMPLETED
|
||||||
|
|
||||||
|
|
||||||
|
@ -530,7 +534,7 @@ async def test_update_todo_item_service_by_id_rename(
|
||||||
assert item
|
assert item
|
||||||
assert item.uid == "1"
|
assert item.uid == "1"
|
||||||
assert item.summary == "Updated item"
|
assert item.summary == "Updated item"
|
||||||
assert item.status is None
|
assert item.status == TodoItemStatus.NEEDS_ACTION
|
||||||
|
|
||||||
|
|
||||||
async def test_update_todo_item_service_raises(
|
async def test_update_todo_item_service_raises(
|
||||||
|
@ -607,7 +611,7 @@ async def test_update_todo_item_service_by_summary_only_status(
|
||||||
assert item
|
assert item
|
||||||
assert item.uid == "1"
|
assert item.uid == "1"
|
||||||
assert item.summary == "Something else"
|
assert item.summary == "Something else"
|
||||||
assert item.status is None
|
assert item.status == TodoItemStatus.NEEDS_ACTION
|
||||||
|
|
||||||
|
|
||||||
async def test_update_todo_item_service_by_summary_not_found(
|
async def test_update_todo_item_service_by_summary_not_found(
|
||||||
|
@ -693,20 +697,32 @@ async def test_update_todo_item_field_unsupported(
|
||||||
(
|
(
|
||||||
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
|
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
|
||||||
{"due_date": "2023-11-13"},
|
{"due_date": "2023-11-13"},
|
||||||
TodoItem(uid="1", due=datetime.date(2023, 11, 13)),
|
TodoItem(
|
||||||
|
uid="1",
|
||||||
|
summary="Item #1",
|
||||||
|
status=TodoItemStatus.NEEDS_ACTION,
|
||||||
|
due=datetime.date(2023, 11, 13),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
|
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
|
||||||
{"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"},
|
{"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"},
|
||||||
TodoItem(
|
TodoItem(
|
||||||
uid="1",
|
uid="1",
|
||||||
|
summary="Item #1",
|
||||||
|
status=TodoItemStatus.NEEDS_ACTION,
|
||||||
due=datetime.datetime(2023, 11, 13, 17, 0, 0, tzinfo=TEST_TIMEZONE),
|
due=datetime.datetime(2023, 11, 13, 17, 0, 0, tzinfo=TEST_TIMEZONE),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
|
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
|
||||||
{"description": "Submit revised draft"},
|
{"description": "Submit revised draft"},
|
||||||
TodoItem(uid="1", description="Submit revised draft"),
|
TodoItem(
|
||||||
|
uid="1",
|
||||||
|
summary="Item #1",
|
||||||
|
status=TodoItemStatus.NEEDS_ACTION,
|
||||||
|
description="Submit revised draft",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -736,6 +752,96 @@ async def test_update_todo_item_extended_fields(
|
||||||
assert item == expected_update
|
assert item == expected_update
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("test_entity_items", "update_data", "expected_update"),
|
||||||
|
(
|
||||||
|
(
|
||||||
|
[TodoItem(uid="1", summary="Summary", description="description")],
|
||||||
|
{"description": "Submit revised draft"},
|
||||||
|
TodoItem(uid="1", summary="Summary", description="Submit revised draft"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TodoItem(uid="1", summary="Summary", description="description")],
|
||||||
|
{"description": ""},
|
||||||
|
TodoItem(uid="1", summary="Summary", description=""),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TodoItem(uid="1", summary="Summary", description="description")],
|
||||||
|
{"description": None},
|
||||||
|
TodoItem(uid="1", summary="Summary"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 1))],
|
||||||
|
{"due_date": datetime.date(2024, 1, 2)},
|
||||||
|
TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 2)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 1))],
|
||||||
|
{"due_date": None},
|
||||||
|
TodoItem(uid="1", summary="Summary"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[TodoItem(uid="1", summary="Summary", due=datetime.date(2024, 1, 1))],
|
||||||
|
{"due_datetime": datetime.datetime(2024, 1, 1, 10, 0, 0)},
|
||||||
|
TodoItem(
|
||||||
|
uid="1",
|
||||||
|
summary="Summary",
|
||||||
|
due=datetime.datetime(
|
||||||
|
2024, 1, 1, 10, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="America/Regina")
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
TodoItem(
|
||||||
|
uid="1",
|
||||||
|
summary="Summary",
|
||||||
|
due=datetime.datetime(2024, 1, 1, 10, 0, 0),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
{"due_datetime": None},
|
||||||
|
TodoItem(uid="1", summary="Summary"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ids=[
|
||||||
|
"overwrite_description",
|
||||||
|
"overwrite_empty_description",
|
||||||
|
"clear_description",
|
||||||
|
"overwrite_due_date",
|
||||||
|
"clear_due_date",
|
||||||
|
"overwrite_due_date_with_time",
|
||||||
|
"clear_due_date_time",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_todo_item_extended_fields_overwrite_existing_values(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
test_entity: TodoListEntity,
|
||||||
|
update_data: dict[str, Any],
|
||||||
|
expected_update: TodoItem,
|
||||||
|
) -> None:
|
||||||
|
"""Test updating an item in a To-do list."""
|
||||||
|
|
||||||
|
test_entity._attr_supported_features |= (
|
||||||
|
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||||
|
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||||
|
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||||
|
)
|
||||||
|
await create_mock_platform(hass, [test_entity])
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{"item": "1", **update_data},
|
||||||
|
target={"entity_id": "todo.entity1"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
args = test_entity.async_update_todo_item.call_args
|
||||||
|
assert args
|
||||||
|
item = args.kwargs.get("item")
|
||||||
|
assert item == expected_update
|
||||||
|
|
||||||
|
|
||||||
async def test_remove_todo_item_service_by_id(
|
async def test_remove_todo_item_service_by_id(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
test_entity: TodoListEntity,
|
test_entity: TodoListEntity,
|
||||||
|
|
|
@ -80,7 +80,7 @@ async def test_todo_item_state(
|
||||||
[],
|
[],
|
||||||
{},
|
{},
|
||||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||||
{"content": "Soda"},
|
{"content": "Soda", "due_string": "no date", "description": ""},
|
||||||
{"uid": "task-id-1", "summary": "Soda", "status": "needs_action"},
|
{"uid": "task-id-1", "summary": "Soda", "status": "needs_action"},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -94,7 +94,7 @@ async def test_todo_item_state(
|
||||||
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{"due": {"date": "2023-11-18"}},
|
{"description": "", "due_date": "2023-11-18"},
|
||||||
{
|
{
|
||||||
"uid": "task-id-1",
|
"uid": "task-id-1",
|
||||||
"summary": "Soda",
|
"summary": "Soda",
|
||||||
|
@ -119,7 +119,8 @@ async def test_todo_item_state(
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"due": {"date": "2023-11-18", "datetime": "2023-11-18T06:30:00-06:00"},
|
"description": "",
|
||||||
|
"due_datetime": "2023-11-18T06:30:00-06:00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "task-id-1",
|
"uid": "task-id-1",
|
||||||
|
@ -139,7 +140,7 @@ async def test_todo_item_state(
|
||||||
is_completed=False,
|
is_completed=False,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{"description": "6-pack"},
|
{"description": "6-pack", "due_string": "no date"},
|
||||||
{
|
{
|
||||||
"uid": "task-id-1",
|
"uid": "task-id-1",
|
||||||
"summary": "Soda",
|
"summary": "Soda",
|
||||||
|
@ -264,11 +265,35 @@ async def test_update_todo_item_status(
|
||||||
("tasks", "update_data", "tasks_after_update", "update_kwargs", "expected_item"),
|
("tasks", "update_data", "tasks_after_update", "update_kwargs", "expected_item"),
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
[
|
||||||
|
make_api_task(
|
||||||
|
id="task-id-1",
|
||||||
|
content="Soda",
|
||||||
|
is_completed=False,
|
||||||
|
description="desc",
|
||||||
|
)
|
||||||
|
],
|
||||||
{"rename": "Milk"},
|
{"rename": "Milk"},
|
||||||
[make_api_task(id="task-id-1", content="Milk", is_completed=False)],
|
[
|
||||||
{"task_id": "task-id-1", "content": "Milk"},
|
make_api_task(
|
||||||
{"uid": "task-id-1", "summary": "Milk", "status": "needs_action"},
|
id="task-id-1",
|
||||||
|
content="Milk",
|
||||||
|
is_completed=False,
|
||||||
|
description="desc",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"task_id": "task-id-1",
|
||||||
|
"content": "Milk",
|
||||||
|
"description": "desc",
|
||||||
|
"due_string": "no date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "task-id-1",
|
||||||
|
"summary": "Milk",
|
||||||
|
"status": "needs_action",
|
||||||
|
"description": "desc",
|
||||||
|
},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||||
|
@ -281,7 +306,12 @@ async def test_update_todo_item_status(
|
||||||
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{"task_id": "task-id-1", "due": {"date": "2023-11-18"}},
|
{
|
||||||
|
"task_id": "task-id-1",
|
||||||
|
"content": "Soda",
|
||||||
|
"due_date": "2023-11-18",
|
||||||
|
"description": "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"uid": "task-id-1",
|
"uid": "task-id-1",
|
||||||
"summary": "Soda",
|
"summary": "Soda",
|
||||||
|
@ -307,7 +337,9 @@ async def test_update_todo_item_status(
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
"task_id": "task-id-1",
|
"task_id": "task-id-1",
|
||||||
"due": {"date": "2023-11-18", "datetime": "2023-11-18T06:30:00-06:00"},
|
"content": "Soda",
|
||||||
|
"due_datetime": "2023-11-18T06:30:00-06:00",
|
||||||
|
"description": "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "task-id-1",
|
"uid": "task-id-1",
|
||||||
|
@ -327,7 +359,12 @@ async def test_update_todo_item_status(
|
||||||
is_completed=False,
|
is_completed=False,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{"task_id": "task-id-1", "description": "6-pack"},
|
{
|
||||||
|
"task_id": "task-id-1",
|
||||||
|
"content": "Soda",
|
||||||
|
"description": "6-pack",
|
||||||
|
"due_string": "no date",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"uid": "task-id-1",
|
"uid": "task-id-1",
|
||||||
"summary": "Soda",
|
"summary": "Soda",
|
||||||
|
@ -335,8 +372,38 @@ async def test_update_todo_item_status(
|
||||||
"description": "6-pack",
|
"description": "6-pack",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
make_api_task(
|
||||||
|
id="task-id-1",
|
||||||
|
content="Soda",
|
||||||
|
description="6-pack",
|
||||||
|
is_completed=False,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
{"description": None},
|
||||||
|
[
|
||||||
|
make_api_task(
|
||||||
|
id="task-id-1",
|
||||||
|
content="Soda",
|
||||||
|
is_completed=False,
|
||||||
|
description="",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
{
|
||||||
|
"task_id": "task-id-1",
|
||||||
|
"content": "Soda",
|
||||||
|
"description": "",
|
||||||
|
"due_string": "no date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "task-id-1",
|
||||||
|
"summary": "Soda",
|
||||||
|
"status": "needs_action",
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
ids=["rename", "due_date", "due_datetime", "description"],
|
ids=["rename", "due_date", "due_datetime", "description", "clear_description"],
|
||||||
)
|
)
|
||||||
async def test_update_todo_items(
|
async def test_update_todo_items(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|
Loading…
Reference in New Issue