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:
Allen Porter 2023-11-29 13:37:43 -08:00 committed by GitHub
parent 8e64eff626
commit c8aed06438
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 68 deletions

View file

@ -1,7 +1,7 @@
"""Google Tasks todo platform."""
from __future__ import annotations
from datetime import timedelta
from datetime import date, datetime, timedelta
from typing import Any, cast
from homeassistant.components.todo import (
@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
@ -35,9 +36,31 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
result["title"] = item.summary
if item.status is not None:
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
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(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
@ -68,6 +91,8 @@ class GoogleTaskTodoListEntity(
TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
def __init__(
@ -88,17 +113,7 @@ class GoogleTaskTodoListEntity(
"""Get the current set of To-do items."""
if self.coordinator.data is None:
return None
return [
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)
]
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""

View file

@ -1,11 +1,29 @@
# serializer version: 1
# name: test_create_todo_list_item[api_responses0]
# name: test_create_todo_list_item[description]
tuple(
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks?alt=json',
'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"}'
# ---
# 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]
tuple(
'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
'{"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]
tuple(
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',

View file

@ -19,13 +19,12 @@ from homeassistant.exceptions import HomeAssistantError
from tests.typing import WebSocketGenerator
ENTITY_ID = "todo.my_tasks"
ITEM = {
"id": "task-list-id-1",
"title": "My tasks",
}
LIST_TASK_LIST_RESPONSE = {
"items": [
{
"id": "task-list-id-1",
"title": "My tasks",
},
]
"items": [ITEM],
}
EMPTY_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
def platforms() -> list[str]:
@ -207,12 +220,14 @@ def mock_http_response(response_handler: list | Callable) -> Mock:
"title": "Task 1",
"status": "needsAction",
"position": "0000000000000001",
"due": "2023-11-18T00:00:00+00:00",
},
{
"id": "task-2",
"title": "Task 2",
"status": "completed",
"position": "0000000000000002",
"notes": "long description",
},
],
},
@ -238,11 +253,13 @@ async def test_get_items(
"uid": "task-1",
"summary": "Task 1",
"status": "needs_action",
"due": "2023-11-18",
},
{
"uid": "task-2",
"summary": "Task 2",
"status": "completed",
"description": "long description",
},
]
@ -333,21 +350,20 @@ async def test_task_items_error_response(
@pytest.mark.parametrize(
"api_responses",
("api_responses", "item_data"),
[
[
LIST_TASK_LIST_RESPONSE,
LIST_TASKS_RESPONSE,
EMPTY_RESPONSE, # create
LIST_TASKS_RESPONSE, # refresh after delete
]
(CREATE_API_RESPONSES, {}),
(CREATE_API_RESPONSES, {"due_date": "2023-11-18"}),
(CREATE_API_RESPONSES, {"description": "6-pack"}),
],
ids=["summary", "due", "description"],
)
async def test_create_todo_list_item(
hass: HomeAssistant,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
mock_http_response: Mock,
item_data: dict[str, Any],
snapshot: SnapshotAssertion,
) -> None:
"""Test for creating a To-do Item."""
@ -361,7 +377,7 @@ async def test_create_todo_list_item(
await hass.services.async_call(
TODO_DOMAIN,
"add_item",
{"item": "Soda"},
{"item": "Soda", **item_data},
target={"entity_id": "todo.my_tasks"},
blocking=True,
)
@ -407,17 +423,7 @@ async def test_create_todo_list_item_error(
)
@pytest.mark.parametrize(
"api_responses",
[
[
LIST_TASK_LIST_RESPONSE,
LIST_TASKS_RESPONSE_WATER,
EMPTY_RESPONSE, # update
LIST_TASKS_RESPONSE, # refresh after update
]
],
)
@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES])
async def test_update_todo_list_item(
hass: HomeAssistant,
setup_credentials: None,
@ -483,21 +489,20 @@ async def test_update_todo_list_item_error(
@pytest.mark.parametrize(
"api_responses",
("api_responses", "item_data"),
[
[
LIST_TASK_LIST_RESPONSE,
LIST_TASKS_RESPONSE_WATER,
EMPTY_RESPONSE, # update
LIST_TASKS_RESPONSE, # refresh after update
]
(UPDATE_API_RESPONSES, {"rename": "Soda"}),
(UPDATE_API_RESPONSES, {"due_date": "2023-11-18"}),
(UPDATE_API_RESPONSES, {"description": "6-pack"}),
],
ids=("rename", "due_date", "description"),
)
async def test_partial_update_title(
async def test_partial_update(
hass: HomeAssistant,
setup_credentials: None,
integration_setup: Callable[[], Awaitable[bool]],
mock_http_response: Any,
item_data: dict[str, Any],
snapshot: SnapshotAssertion,
) -> None:
"""Test for partial update with title only."""
@ -511,7 +516,7 @@ async def test_partial_update_title(
await hass.services.async_call(
TODO_DOMAIN,
"update_item",
{"item": "some-task-id", "rename": "Soda"},
{"item": "some-task-id", **item_data},
target={"entity_id": "todo.my_tasks"},
blocking=True,
)
@ -522,17 +527,7 @@ async def test_partial_update_title(
assert call.kwargs.get("body") == snapshot
@pytest.mark.parametrize(
"api_responses",
[
[
LIST_TASK_LIST_RESPONSE,
LIST_TASKS_RESPONSE_WATER,
EMPTY_RESPONSE, # update
LIST_TASKS_RESPONSE, # refresh after update
]
],
)
@pytest.mark.parametrize("api_responses", [UPDATE_API_RESPONSES])
async def test_partial_update_status(
hass: HomeAssistant,
setup_credentials: None,