Add due date and description to Google Tasks (#104654)
* Add tests for config validation function * Add Google Tasks due date and description * Revert test timezone * Update changes after upstream * Update homeassistant/components/google_tasks/todo.py Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Add google tasks tests for creating --------- Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
55bafc260d
commit
655b067277
3 changed files with 114 additions and 68 deletions
|
@ -1,7 +1,7 @@
|
||||||
"""Google Tasks todo platform."""
|
"""Google Tasks todo platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from homeassistant.components.todo import (
|
from homeassistant.components.todo import (
|
||||||
|
@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
from .api import AsyncConfigEntryAuth
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
@ -35,9 +36,31 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
|
||||||
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]
|
||||||
|
if (due := item.due) is not None:
|
||||||
|
# due API field is a timestamp string, but with only date resolution
|
||||||
|
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
||||||
|
if (description := item.description) is not None:
|
||||||
|
result["notes"] = description
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
||||||
|
"""Convert tasks API items into a TodoItem."""
|
||||||
|
due: date | None = None
|
||||||
|
if (due_str := item.get("due")) is not None:
|
||||||
|
due = datetime.fromisoformat(due_str).date()
|
||||||
|
return TodoItem(
|
||||||
|
summary=item["title"],
|
||||||
|
uid=item["id"],
|
||||||
|
status=TODO_STATUS_MAP.get(
|
||||||
|
item.get("status", ""),
|
||||||
|
TodoItemStatus.NEEDS_ACTION,
|
||||||
|
),
|
||||||
|
due=due,
|
||||||
|
description=item.get("notes"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -68,6 +91,8 @@ class GoogleTaskTodoListEntity(
|
||||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
|
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||||
|
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -88,17 +113,7 @@ class GoogleTaskTodoListEntity(
|
||||||
"""Get the current set of To-do items."""
|
"""Get the current set of To-do items."""
|
||||||
if self.coordinator.data is None:
|
if self.coordinator.data is None:
|
||||||
return None
|
return None
|
||||||
return [
|
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
|
||||||
TodoItem(
|
|
||||||
summary=item["title"],
|
|
||||||
uid=item["id"],
|
|
||||||
status=TODO_STATUS_MAP.get(
|
|
||||||
item.get("status"), # type: ignore[arg-type]
|
|
||||||
TodoItemStatus.NEEDS_ACTION,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for item in _order_tasks(self.coordinator.data)
|
|
||||||
]
|
|
||||||
|
|
||||||
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."""
|
||||||
|
|
|
@ -1,11 +1,29 @@
|
||||||
# serializer version: 1
|
# serializer version: 1
|
||||||
# name: test_create_todo_list_item[api_responses0]
|
# name: test_create_todo_list_item[description]
|
||||||
tuple(
|
tuple(
|
||||||
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json',
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json',
|
||||||
'POST',
|
'POST',
|
||||||
)
|
)
|
||||||
# ---
|
# ---
|
||||||
# name: test_create_todo_list_item[api_responses0].1
|
# name: test_create_todo_list_item[description].1
|
||||||
|
'{"title": "Soda", "status": "needsAction", "notes": "6-pack"}'
|
||||||
|
# ---
|
||||||
|
# name: test_create_todo_list_item[due]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json',
|
||||||
|
'POST',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_create_todo_list_item[due].1
|
||||||
|
'{"title": "Soda", "status": "needsAction", "due": "2023-11-18T00:00:00-08:00"}'
|
||||||
|
# ---
|
||||||
|
# name: test_create_todo_list_item[summary]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json',
|
||||||
|
'POST',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_create_todo_list_item[summary].1
|
||||||
'{"title": "Soda", "status": "needsAction"}'
|
'{"title": "Soda", "status": "needsAction"}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_delete_todo_list_item[_handler]
|
# name: test_delete_todo_list_item[_handler]
|
||||||
|
@ -38,6 +56,33 @@
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_partial_update[description]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[description].1
|
||||||
|
'{"notes": "6-pack"}'
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[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[due_date].1
|
||||||
|
'{"due": "2023-11-18T00:00:00-08:00"}'
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[rename]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
'PATCH',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
|
# name: test_partial_update[rename].1
|
||||||
|
'{"title": "Soda"}'
|
||||||
|
# ---
|
||||||
# name: test_partial_update_status[api_responses0]
|
# name: test_partial_update_status[api_responses0]
|
||||||
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',
|
||||||
|
@ -47,15 +92,6 @@
|
||||||
# name: test_partial_update_status[api_responses0].1
|
# name: test_partial_update_status[api_responses0].1
|
||||||
'{"status": "needsAction"}'
|
'{"status": "needsAction"}'
|
||||||
# ---
|
# ---
|
||||||
# name: test_partial_update_title[api_responses0]
|
|
||||||
tuple(
|
|
||||||
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
|
||||||
'PATCH',
|
|
||||||
)
|
|
||||||
# ---
|
|
||||||
# name: test_partial_update_title[api_responses0].1
|
|
||||||
'{"title": "Soda"}'
|
|
||||||
# ---
|
|
||||||
# name: test_update_todo_list_item[api_responses0]
|
# name: test_update_todo_list_item[api_responses0]
|
||||||
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',
|
||||||
|
|
|
@ -19,13 +19,12 @@ from homeassistant.exceptions import HomeAssistantError
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
ENTITY_ID = "todo.my_tasks"
|
ENTITY_ID = "todo.my_tasks"
|
||||||
|
ITEM = {
|
||||||
|
"id": "task-list-id-1",
|
||||||
|
"title": "My tasks",
|
||||||
|
}
|
||||||
LIST_TASK_LIST_RESPONSE = {
|
LIST_TASK_LIST_RESPONSE = {
|
||||||
"items": [
|
"items": [ITEM],
|
||||||
{
|
|
||||||
"id": "task-list-id-1",
|
|
||||||
"title": "My tasks",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
EMPTY_RESPONSE = {}
|
EMPTY_RESPONSE = {}
|
||||||
LIST_TASKS_RESPONSE = {
|
LIST_TASKS_RESPONSE = {
|
||||||
|
@ -76,6 +75,20 @@ LIST_TASKS_RESPONSE_MULTIPLE = {
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# API responses when testing update methods
|
||||||
|
UPDATE_API_RESPONSES = [
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_WATER,
|
||||||
|
EMPTY_RESPONSE, # update
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after update
|
||||||
|
]
|
||||||
|
CREATE_API_RESPONSES = [
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE,
|
||||||
|
EMPTY_RESPONSE, # create
|
||||||
|
LIST_TASKS_RESPONSE, # refresh
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def platforms() -> list[str]:
|
def platforms() -> list[str]:
|
||||||
|
@ -207,12 +220,14 @@ def mock_http_response(response_handler: list | Callable) -> Mock:
|
||||||
"title": "Task 1",
|
"title": "Task 1",
|
||||||
"status": "needsAction",
|
"status": "needsAction",
|
||||||
"position": "0000000000000001",
|
"position": "0000000000000001",
|
||||||
|
"due": "2023-11-18T00:00:00+00:00",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "task-2",
|
"id": "task-2",
|
||||||
"title": "Task 2",
|
"title": "Task 2",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"position": "0000000000000002",
|
"position": "0000000000000002",
|
||||||
|
"notes": "long description",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -238,11 +253,13 @@ async def test_get_items(
|
||||||
"uid": "task-1",
|
"uid": "task-1",
|
||||||
"summary": "Task 1",
|
"summary": "Task 1",
|
||||||
"status": "needs_action",
|
"status": "needs_action",
|
||||||
|
"due": "2023-11-18",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uid": "task-2",
|
"uid": "task-2",
|
||||||
"summary": "Task 2",
|
"summary": "Task 2",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
|
"description": "long description",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -333,21 +350,20 @@ async def test_task_items_error_response(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"api_responses",
|
("api_responses", "item_data"),
|
||||||
[
|
[
|
||||||
[
|
(CREATE_API_RESPONSES, {}),
|
||||||
LIST_TASK_LIST_RESPONSE,
|
(CREATE_API_RESPONSES, {"due_date": "2023-11-18"}),
|
||||||
LIST_TASKS_RESPONSE,
|
(CREATE_API_RESPONSES, {"description": "6-pack"}),
|
||||||
EMPTY_RESPONSE, # create
|
|
||||||
LIST_TASKS_RESPONSE, # refresh after delete
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
|
ids=["summary", "due", "description"],
|
||||||
)
|
)
|
||||||
async def test_create_todo_list_item(
|
async def test_create_todo_list_item(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
integration_setup: Callable[[], Awaitable[bool]],
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
mock_http_response: Mock,
|
mock_http_response: Mock,
|
||||||
|
item_data: dict[str, Any],
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test for creating a To-do Item."""
|
"""Test for creating a To-do Item."""
|
||||||
|
@ -361,7 +377,7 @@ async def test_create_todo_list_item(
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
TODO_DOMAIN,
|
TODO_DOMAIN,
|
||||||
"add_item",
|
"add_item",
|
||||||
{"item": "Soda"},
|
{"item": "Soda", **item_data},
|
||||||
target={"entity_id": "todo.my_tasks"},
|
target={"entity_id": "todo.my_tasks"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
@ -407,17 +423,7 @@ async def test_create_todo_list_item_error(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES])
|
||||||
"api_responses",
|
|
||||||
[
|
|
||||||
[
|
|
||||||
LIST_TASK_LIST_RESPONSE,
|
|
||||||
LIST_TASKS_RESPONSE_WATER,
|
|
||||||
EMPTY_RESPONSE, # update
|
|
||||||
LIST_TASKS_RESPONSE, # refresh after update
|
|
||||||
]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_update_todo_list_item(
|
async def test_update_todo_list_item(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
|
@ -483,21 +489,20 @@ async def test_update_todo_list_item_error(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"api_responses",
|
("api_responses", "item_data"),
|
||||||
[
|
[
|
||||||
[
|
(UPDATE_API_RESPONSES, {"rename": "Soda"}),
|
||||||
LIST_TASK_LIST_RESPONSE,
|
(UPDATE_API_RESPONSES, {"due_date": "2023-11-18"}),
|
||||||
LIST_TASKS_RESPONSE_WATER,
|
(UPDATE_API_RESPONSES, {"description": "6-pack"}),
|
||||||
EMPTY_RESPONSE, # update
|
|
||||||
LIST_TASKS_RESPONSE, # refresh after update
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
|
ids=("rename", "due_date", "description"),
|
||||||
)
|
)
|
||||||
async def test_partial_update_title(
|
async def test_partial_update(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
integration_setup: Callable[[], Awaitable[bool]],
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
mock_http_response: Any,
|
mock_http_response: Any,
|
||||||
|
item_data: dict[str, Any],
|
||||||
snapshot: SnapshotAssertion,
|
snapshot: SnapshotAssertion,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test for partial update with title only."""
|
"""Test for partial update with title only."""
|
||||||
|
@ -511,7 +516,7 @@ async def test_partial_update_title(
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
TODO_DOMAIN,
|
TODO_DOMAIN,
|
||||||
"update_item",
|
"update_item",
|
||||||
{"item": "some-task-id", "rename": "Soda"},
|
{"item": "some-task-id", **item_data},
|
||||||
target={"entity_id": "todo.my_tasks"},
|
target={"entity_id": "todo.my_tasks"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
@ -522,17 +527,7 @@ async def test_partial_update_title(
|
||||||
assert call.kwargs.get("body") == snapshot
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES])
|
||||||
"api_responses",
|
|
||||||
[
|
|
||||||
[
|
|
||||||
LIST_TASK_LIST_RESPONSE,
|
|
||||||
LIST_TASKS_RESPONSE_WATER,
|
|
||||||
EMPTY_RESPONSE, # update
|
|
||||||
LIST_TASKS_RESPONSE, # refresh after update
|
|
||||||
]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def test_partial_update_status(
|
async def test_partial_update_status(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue