Add support for Todoist sections (#115671)
* Add support for Todoist sections * ServiceValidationError & section name tweaks from PR comments * Remove whitespace Co-authored-by: Erik Montnemery <erik@montnemery.com> * More natural error message Co-authored-by: Erik Montnemery <erik@montnemery.com> --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>pull/123103/head
parent
aa6f0cd55a
commit
6684f61a54
|
@ -21,7 +21,7 @@ from homeassistant.components.calendar import (
|
|||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, CONF_NAME, CONF_TOKEN, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
@ -54,6 +54,7 @@ from .const import (
|
|||
REMINDER_DATE,
|
||||
REMINDER_DATE_LANG,
|
||||
REMINDER_DATE_STRING,
|
||||
SECTION_NAME,
|
||||
SERVICE_NEW_TASK,
|
||||
START,
|
||||
SUMMARY,
|
||||
|
@ -68,6 +69,7 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema(
|
|||
vol.Required(CONTENT): cv.string,
|
||||
vol.Optional(DESCRIPTION): cv.string,
|
||||
vol.Optional(PROJECT_NAME, default="inbox"): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(SECTION_NAME): vol.All(cv.string, vol.Lower),
|
||||
vol.Optional(LABELS): cv.ensure_list_csv,
|
||||
vol.Optional(ASSIGNEE): cv.string,
|
||||
vol.Optional(PRIORITY): vol.All(vol.Coerce(int), vol.Range(min=1, max=4)),
|
||||
|
@ -201,7 +203,7 @@ async def async_setup_platform(
|
|||
async_register_services(hass, coordinator)
|
||||
|
||||
|
||||
def async_register_services(
|
||||
def async_register_services( # noqa: C901
|
||||
hass: HomeAssistant, coordinator: TodoistCoordinator
|
||||
) -> None:
|
||||
"""Register services."""
|
||||
|
@ -211,7 +213,7 @@ def async_register_services(
|
|||
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
async def handle_new_task(call: ServiceCall) -> None:
|
||||
async def handle_new_task(call: ServiceCall) -> None: # noqa: C901
|
||||
"""Call when a user creates a new Todoist Task from Home Assistant."""
|
||||
project_name = call.data[PROJECT_NAME].lower()
|
||||
projects = await coordinator.async_get_projects()
|
||||
|
@ -222,12 +224,35 @@ def async_register_services(
|
|||
if project_id is None:
|
||||
raise HomeAssistantError(f"Invalid project name '{project_name}'")
|
||||
|
||||
# Optional section within project
|
||||
section_id: str | None = None
|
||||
if SECTION_NAME in call.data:
|
||||
section_name = call.data[SECTION_NAME]
|
||||
sections = await coordinator.async_get_sections(project_id)
|
||||
for section in sections:
|
||||
if section_name == section.name.lower():
|
||||
section_id = section.id
|
||||
break
|
||||
if section_id is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="section_invalid",
|
||||
translation_placeholders={
|
||||
"section": section_name,
|
||||
"project": project_name,
|
||||
},
|
||||
)
|
||||
|
||||
# Create the task
|
||||
content = call.data[CONTENT]
|
||||
data: dict[str, Any] = {"project_id": project_id}
|
||||
|
||||
if description := call.data.get(DESCRIPTION):
|
||||
data["description"] = description
|
||||
|
||||
if section_id is not None:
|
||||
data["section_id"] = section_id
|
||||
|
||||
if task_labels := call.data.get(LABELS):
|
||||
data["labels"] = task_labels
|
||||
|
||||
|
|
|
@ -78,6 +78,8 @@ PROJECT_ID: Final = "project_id"
|
|||
PROJECT_NAME: Final = "project"
|
||||
# Todoist API: Fetch all Projects
|
||||
PROJECTS: Final = "projects"
|
||||
# Section Name: What Section of the Project do you want to add the Task to?
|
||||
SECTION_NAME: Final = "section"
|
||||
# Calendar Platform: When does a calendar event start?
|
||||
START: Final = "start"
|
||||
# Calendar Platform: What is the next calendar event about?
|
||||
|
|
|
@ -4,7 +4,7 @@ from datetime import timedelta
|
|||
import logging
|
||||
|
||||
from todoist_api_python.api_async import TodoistAPIAsync
|
||||
from todoist_api_python.models import Label, Project, Task
|
||||
from todoist_api_python.models import Label, Project, Section, Task
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
@ -41,6 +41,10 @@ class TodoistCoordinator(DataUpdateCoordinator[list[Task]]):
|
|||
self._projects = await self.api.get_projects()
|
||||
return self._projects
|
||||
|
||||
async def async_get_sections(self, project_id: str) -> list[Section]:
|
||||
"""Return todoist sections for a given project ID."""
|
||||
return await self.api.get_sections(project_id=project_id)
|
||||
|
||||
async def async_get_labels(self) -> list[Label]:
|
||||
"""Return todoist labels fetched at most once."""
|
||||
if self._labels is None:
|
||||
|
|
|
@ -13,6 +13,10 @@ new_task:
|
|||
default: Inbox
|
||||
selector:
|
||||
text:
|
||||
section:
|
||||
example: Deliveries
|
||||
selector:
|
||||
text:
|
||||
labels:
|
||||
example: Chores,Delivieries
|
||||
selector:
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"section_invalid": {
|
||||
"message": "Project \"{project}\" has no section \"{section}\""
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"new_task": {
|
||||
"name": "New task",
|
||||
|
@ -37,6 +42,10 @@
|
|||
"name": "Project",
|
||||
"description": "The name of the project this task should belong to."
|
||||
},
|
||||
"section": {
|
||||
"name": "Section",
|
||||
"description": "The name of a section within the project to add the task to."
|
||||
},
|
||||
"labels": {
|
||||
"name": "Labels",
|
||||
"description": "Any labels that you want to apply to this task, separated by a comma."
|
||||
|
|
|
@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, patch
|
|||
import pytest
|
||||
from requests.exceptions import HTTPError
|
||||
from requests.models import Response
|
||||
from todoist_api_python.models import Collaborator, Due, Label, Project, Task
|
||||
from todoist_api_python.models import Collaborator, Due, Label, Project, Section, Task
|
||||
|
||||
from homeassistant.components.todoist import DOMAIN
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
|
@ -18,6 +18,7 @@ from homeassistant.util import dt as dt_util
|
|||
from tests.common import MockConfigEntry
|
||||
|
||||
PROJECT_ID = "project-id-1"
|
||||
SECTION_ID = "section-id-1"
|
||||
SUMMARY = "A task"
|
||||
TOKEN = "some-token"
|
||||
TODAY = dt_util.now().strftime("%Y-%m-%d")
|
||||
|
@ -98,6 +99,14 @@ def mock_api(tasks: list[Task]) -> AsyncMock:
|
|||
view_style="list",
|
||||
)
|
||||
]
|
||||
api.get_sections.return_value = [
|
||||
Section(
|
||||
id=SECTION_ID,
|
||||
project_id=PROJECT_ID,
|
||||
name="Section Name",
|
||||
order=1,
|
||||
)
|
||||
]
|
||||
api.get_labels.return_value = [
|
||||
Label(id="1", name="Label1", color="1", order=1, is_favorite=False)
|
||||
]
|
||||
|
|
|
@ -18,6 +18,7 @@ from homeassistant.components.todoist.const import (
|
|||
DOMAIN,
|
||||
LABELS,
|
||||
PROJECT_NAME,
|
||||
SECTION_NAME,
|
||||
SERVICE_NEW_TASK,
|
||||
)
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
|
@ -26,7 +27,7 @@ from homeassistant.helpers import entity_registry as er
|
|||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import PROJECT_ID, SUMMARY
|
||||
from .conftest import PROJECT_ID, SECTION_ID, SUMMARY
|
||||
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
@ -269,6 +270,32 @@ async def test_create_task_service_call(hass: HomeAssistant, api: AsyncMock) ->
|
|||
)
|
||||
|
||||
|
||||
async def test_create_task_service_call_with_section(
|
||||
hass: HomeAssistant, api: AsyncMock
|
||||
) -> None:
|
||||
"""Test api is called correctly when section is included."""
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_NEW_TASK,
|
||||
{
|
||||
ASSIGNEE: "user",
|
||||
CONTENT: "task",
|
||||
LABELS: ["Label1"],
|
||||
PROJECT_NAME: "Name",
|
||||
SECTION_NAME: "Section Name",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
api.add_task.assert_called_with(
|
||||
"task",
|
||||
project_id=PROJECT_ID,
|
||||
section_id=SECTION_ID,
|
||||
labels=["Label1"],
|
||||
assignee_id="1",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("due"),
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue