Task lists used to be a week-view-only sub-feature bolted onto calendar widgets (CalendarWidgetConfig.tasks_*), so a task list could only exist tied to a calendar's view and only inside its footprint. Tasks are now a standalone widget type (TaskWidgetConfig, app/widgets/tasks.py) that can be placed and sized independently, same as photos/calendar/ whiteboard -- no separate "enabled" flag either, since being on the grid at all is the on/off switch, matching every other widget type. Migration 17 creates task_widget_configs, extracts any existing calendar widget's configured task source into a new sibling tasks widget (auto-placed in open grid space, source dropped+logged if truly none is left), then drops calendar_widget_configs' now-dead tasks_* columns in the same migration -- this project's usual same-migration- drop convention. Also handles the rarer case of a database jumping straight from before the widget system existed to after this split in one boot, via the legacy Frame.calendar_tasks_* columns. Verified live in the browser at desktop and mobile widths: adding a Tasks widget, its own dialog (task-list source picker + preview), and confirming the calendar widget's dialog no longer mentions tasks at all. Full test suite (180 tests, including new coverage for the widget render/actions, the migration's data-extraction path, and the permission-boundary shape for tasks-source) passes.
73 lines
2.9 KiB
Python
73 lines
2.9 KiB
Python
"""app.widgets.tasks -- unit-level, no HTTP: constructs Widget/
|
|
TaskWidgetConfig rows directly and monkeypatches the underlying fetch
|
|
call (get_or_refresh_tasks_for_widget, throttle/CalDAV-fetch logic
|
|
covered separately). Split out of the old calendar widget's week-view-
|
|
only task list -- see models.TaskWidgetConfig."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from app import widgets
|
|
from app.models import Frame, TaskWidgetConfig, Widget
|
|
|
|
from .conftest import make_user
|
|
|
|
|
|
def _make_widget(db_session, **cfg_kwargs) -> tuple[Frame, Widget]:
|
|
frame = db_session.get(Frame, 1)
|
|
widget = Widget(frame_id=frame.id, widget_type="tasks", x=0, y=0, w=2, h=2,
|
|
sort_order=0, created_at=time.time())
|
|
db_session.add(widget)
|
|
db_session.flush()
|
|
db_session.add(TaskWidgetConfig(widget_id=widget.id, **cfg_kwargs))
|
|
db_session.commit()
|
|
return frame, widget
|
|
|
|
|
|
def _make_configured_widget(db_session) -> tuple[Frame, Widget]:
|
|
user = make_user(db_session, "task-owner")
|
|
return _make_widget(db_session, user_id=user.id, calendar_key="caldav:/some/tasks/")
|
|
|
|
|
|
def test_render_unconfigured_widget_returns_correctly_sized_placeholder(db_session):
|
|
frame, widget = _make_widget(db_session) # no user_id/calendar_key set
|
|
img = widgets.tasks.render(db_session, frame, widget, 300, 200)
|
|
assert img.size == (300, 200)
|
|
|
|
|
|
def test_render_configured_widget_produces_a_correctly_sized_image(db_session, monkeypatch):
|
|
frame, widget = _make_configured_widget(db_session)
|
|
monkeypatch.setattr(widgets.tasks, "get_or_refresh_tasks_for_widget",
|
|
lambda db, frame, widget: [{"summary": "Buy milk", "due": None}])
|
|
|
|
img = widgets.tasks.render(db_session, frame, widget, 300, 200)
|
|
assert img.size == (300, 200)
|
|
assert img.mode == "RGB"
|
|
|
|
|
|
def test_render_configured_but_empty_task_list_still_renders(db_session, monkeypatch):
|
|
frame, widget = _make_configured_widget(db_session)
|
|
monkeypatch.setattr(widgets.tasks, "get_or_refresh_tasks_for_widget", lambda db, frame, widget: [])
|
|
|
|
img = widgets.tasks.render(db_session, frame, widget, 300, 200)
|
|
assert img.size == (300, 200)
|
|
|
|
|
|
def test_render_at_minimum_grid_footprint(db_session, monkeypatch):
|
|
"""grid.MIN_FOOTPRINT["tasks"] is (2, 2) cells -- on an 8x5 grid
|
|
against a full 800x480 panel that's a 200x192 box, the smallest a
|
|
tasks widget can actually be placed at."""
|
|
frame, widget = _make_configured_widget(db_session)
|
|
monkeypatch.setattr(widgets.tasks, "get_or_refresh_tasks_for_widget", lambda db, frame, widget: [])
|
|
|
|
img = widgets.tasks.render(db_session, frame, widget, 200, 192)
|
|
assert img.size == (200, 192)
|
|
|
|
|
|
def test_no_button_actions():
|
|
"""A passive checklist on the same throttled-refresh cadence as
|
|
weather -- nothing to advance/back/force."""
|
|
assert widgets.tasks.ACTIONS == {}
|
|
assert widgets.tasks.ACTION_LABELS == {}
|