From 4c5746d6ed5499767859ff9ddfab023615da9bf2 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 17 Apr 2023 05:21:49 -0700 Subject: [PATCH] Update Todoist all day event handling following best practices (#90491) --- homeassistant/components/todoist/calendar.py | 44 ++++--- tests/components/todoist/test_calendar.py | 130 ++++++++++++++++++- 2 files changed, 154 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index ea5aab15344..d61d2248262 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -9,7 +9,7 @@ import uuid from todoist_api_python.api_async import TodoistAPIAsync from todoist_api_python.endpoints import get_sync_url from todoist_api_python.headers import create_headers -from todoist_api_python.models import Label, Task +from todoist_api_python.models import Due, Label, Task import voluptuous as vol from homeassistant.components.calendar import ( @@ -590,25 +590,19 @@ class TodoistProjectData: for task in project_task_data: if task.due is None: continue - due_date = dt.parse_datetime( - task.due.datetime if task.due.datetime else task.due.date - ) - if not due_date: + start = get_start(task.due) + if start is None: continue - due_date = dt.as_utc(due_date) - if start_date < due_date < end_date: - due_date_value: datetime | date = due_date - midnight = dt.start_of_local_day(due_date) - if due_date == midnight: - # If the due date has no time data, return just the date so that it - # will render correctly as an all day event on a calendar. - due_date_value = due_date.date() - event = CalendarEvent( - summary=task.content, - start=due_date_value, - end=due_date_value + timedelta(days=1), - ) - events.append(event) + event = CalendarEvent( + summary=task.content, + start=start, + end=start + timedelta(days=1), + ) + if event.start_datetime_local >= end_date: + continue + if event.end_datetime_local < start_date: + continue + events.append(event) return events async def async_update(self) -> None: @@ -663,3 +657,15 @@ class TodoistProjectData: return self.event = event _LOGGER.debug("Updated %s", self._name) + + +def get_start(due: Due) -> datetime | date | None: + """Return the task due date as a start date or date time.""" + if due.datetime: + start = dt.parse_datetime(due.datetime) + if not start: + return None + return dt.as_local(start) + if due.date: + return dt.parse_date(due.date) + return None diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 7009e99ed35..75c07be7ec3 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -228,8 +228,39 @@ async def test_calendar_custom_project_unique_id( "2023-03-28T00:00:00.000Z", "2023-04-01T00:00:00.000Z", [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], - ) + ), + ( + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-30T06:00:00.000Z", + "2023-03-31T06:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-29T08:00:00.000Z", + "2023-03-30T08:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-30T08:00:00.000Z", + "2023-03-31T08:00:00.000Z", + [get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})], + ), + ( + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-31T08:00:00.000Z", + "2023-04-01T08:00:00.000Z", + [], + ), + ( + Due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-29T06:00:00.000Z", + "2023-03-30T06:00:00.000Z", + [], + ), ], + ids=("included", "exact", "overlap_start", "overlap_end", "after", "before"), ) async def test_all_day_event( hass: HomeAssistant, @@ -259,3 +290,100 @@ async def test_create_task_service_call(hass: HomeAssistant, api: AsyncMock) -> api.add_task.assert_called_with( "task", project_id="12345", labels=["Label1"], assignee_id="1" ) + + +@pytest.mark.parametrize( + ("due"), + [ + # These are all equivalent due dates for the same time in different + # timezone formats. + Due( + date="2023-03-30", + is_recurring=False, + string="Mar 30 6:00 PM", + datetime="2023-03-31T00:00:00Z", + timezone="America/Regina", + ), + Due( + date="2023-03-30", + is_recurring=False, + string="Mar 30 7:00 PM", + datetime="2023-03-31T00:00:00Z", + timezone="America/Los_Angeles", + ), + Due( + date="2023-03-30", + is_recurring=False, + string="Mar 30 6:00 PM", + datetime="2023-03-30T18:00:00", + ), + ], + ids=("in_local_timezone", "in_other_timezone", "floating"), +) +async def test_task_due_datetime( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> None: + """Test for task due at a specific time, using different time formats.""" + client = await hass_client() + + has_task_response = [ + get_events_response( + {"dateTime": "2023-03-30T18:00:00-06:00"}, + {"dateTime": "2023-03-31T18:00:00-06:00"}, + ) + ] + + # Completely includes the start/end of the task + response = await client.get( + get_events_url( + "calendar.name", "2023-03-30T08:00:00.000Z", "2023-03-31T08:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == has_task_response + + # Overlap with the start of the event + response = await client.get( + get_events_url( + "calendar.name", "2023-03-29T20:00:00.000Z", "2023-03-31T02:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == has_task_response + + # Overlap with the end of the event + response = await client.get( + get_events_url( + "calendar.name", "2023-03-31T20:00:00.000Z", "2023-04-01T02:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == has_task_response + + # Task is active, but range does not include start/end + response = await client.get( + get_events_url( + "calendar.name", "2023-03-31T10:00:00.000Z", "2023-03-31T11:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == has_task_response + + # Query is before the task starts (no results) + response = await client.get( + get_events_url( + "calendar.name", "2023-03-28T00:00:00.000Z", "2023-03-29T00:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == [] + + # Query is after the task ends (no results) + response = await client.get( + get_events_url( + "calendar.name", "2023-04-01T07:00:00.000Z", "2023-04-02T07:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == []