Split the tasks feature out of the calendar widget into its own widget type
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.
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""Everything scoped to one specific widget rather than "the frame":
|
||||
placement CRUD (backing the Layout tab's canvas, static/frame_layout.js)
|
||||
plus every setting/action that used to assume a frame had at most one
|
||||
widget of a given type -- photo queue, calendar inclusion/color/tasks,
|
||||
whiteboard source, and their preview endpoints. Split out of
|
||||
widget of a given type -- photo queue, calendar inclusion/color, tasks
|
||||
source, whiteboard source, and their preview endpoints. Split out of
|
||||
api_frames.py (which keeps frame-wide settings: orientation, quiet
|
||||
hours, palette, firmware, stats) once a frame could hold more than one
|
||||
widget of the same type, at which point "the frame's calendar settings"
|
||||
@@ -35,6 +35,7 @@ from ..models import (
|
||||
Frame,
|
||||
FrameCalendar,
|
||||
PhotoWidgetConfig,
|
||||
TaskWidgetConfig,
|
||||
WhiteboardWidgetConfig,
|
||||
WIDGET_CONFIG_MODELS,
|
||||
Widget,
|
||||
@@ -254,7 +255,6 @@ def api_widget_config_save(
|
||||
calendar_week_start_offset: int | None = Form(None),
|
||||
calendar_weather_enabled: bool | None = Form(None),
|
||||
calendar_weather_units: str | None = Form(None),
|
||||
calendar_tasks_enabled: bool | None = Form(None),
|
||||
):
|
||||
"""Every field optional -- same partial-update, form-urlencoded
|
||||
convention as the old frame-level api_config_save, now scoped to one
|
||||
@@ -317,8 +317,6 @@ def api_widget_config_save(
|
||||
# new unit label.
|
||||
ccfg.weather_checked_at = 0.0
|
||||
ccfg.weather_units = calendar_weather_units
|
||||
if calendar_tasks_enabled is not None:
|
||||
ccfg.tasks_enabled = calendar_tasks_enabled
|
||||
with frame_locked(db, frame.id) as cfg:
|
||||
cfg.stats_config_saves += 1
|
||||
return {"status": "saved"}
|
||||
@@ -506,7 +504,7 @@ def api_widget_preview_rendered(
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
|
||||
# --- Calendar: inclusion/color/tasks/weather/preview --------------------
|
||||
# --- Calendar: inclusion/color/weather/preview ---------------------------
|
||||
|
||||
class CalendarSelectRequest(BaseModel):
|
||||
user_id: int
|
||||
@@ -609,21 +607,37 @@ def api_widget_preview_calendar(
|
||||
ccfg = db.get(CalendarWidgetConfig, widget.id)
|
||||
events, summary = get_or_refresh_calendar_events_for_widget(db, frame, widget)
|
||||
weather_cities = get_or_refresh_weather_for_widget(db, frame, widget) if ccfg.weather_enabled else None
|
||||
tasks = (
|
||||
get_or_refresh_tasks_for_widget(db, frame, widget)
|
||||
if (ccfg.view == "week" and ccfg.tasks_enabled) else None
|
||||
)
|
||||
png = calendar_render.render_calendar_preview_png(
|
||||
events, view=ccfg.view, browse_offset=ccfg.browse_offset, orientation=frame.orientation,
|
||||
palette_rgb=frame.palette_rgb, timezone=frame.timezone, fetch_summary=summary,
|
||||
week_start=ccfg.week_start,
|
||||
weather_cities=weather_cities, weather_units=ccfg.weather_units,
|
||||
week_days=ccfg.week_days, week_layout=ccfg.week_layout, tasks=tasks,
|
||||
week_days=ccfg.week_days, week_layout=ccfg.week_layout,
|
||||
week_start_offset=ccfg.week_start_offset,
|
||||
)
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
|
||||
# --- Tasks: source/preview ------------------------------------------------
|
||||
|
||||
@router.get("/api/frames/{frame_id}/widgets/{widget_id}/preview/tasks")
|
||||
def api_widget_preview_tasks(
|
||||
frame_widget: tuple[Frame, Widget] = Depends(require_widget_view), db: Session = Depends(get_db)
|
||||
):
|
||||
"""The same cached task list a live device render would use, same
|
||||
"reflects what's currently saved" convention as the other preview
|
||||
endpoints."""
|
||||
frame, widget = frame_widget
|
||||
_require_widget_type(widget, "tasks")
|
||||
tcfg = db.get(TaskWidgetConfig, widget.id)
|
||||
if not tcfg.calendar_key or not tcfg.user_id:
|
||||
raise HTTPException(400, "No task list configured on this widget yet")
|
||||
tasks = get_or_refresh_tasks_for_widget(db, frame, widget)
|
||||
png = calendar_render.render_tasks_preview_png(tasks, orientation=frame.orientation,
|
||||
palette_rgb=frame.palette_rgb)
|
||||
return Response(content=png, media_type="image/png")
|
||||
|
||||
|
||||
class TasksSourceRequest(BaseModel):
|
||||
calendar_key: str | None # None clears the source
|
||||
|
||||
@@ -634,27 +648,27 @@ def api_widget_tasks_source(
|
||||
frame_widget: tuple[Frame, Widget] = Depends(require_widget_view),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Points this widget's week-view task list at one of the calling
|
||||
user's own CalDAV calendars -- same owner-controls-their-own-data
|
||||
permission split as calendar-select's included=True, since this is
|
||||
volunteering personal calendar data, not a display setting a
|
||||
controller should get to pick on someone else's behalf. None clears
|
||||
the source; clearing (unlike setting) isn't ownership-gated -- like
|
||||
muting a shared calendar, anyone linked to the frame can turn off a
|
||||
task list they'd rather not see, but only its owner can point the
|
||||
widget at one of their calendars to begin with."""
|
||||
"""Points this tasks widget at one of the calling user's own CalDAV
|
||||
calendars -- same owner-controls-their-own-data permission split as
|
||||
calendar-select's included=True, since this is volunteering personal
|
||||
calendar data, not a display setting a controller should get to pick
|
||||
on someone else's behalf. None clears the source; clearing (unlike
|
||||
setting) isn't ownership-gated -- like muting a shared calendar,
|
||||
anyone linked to the frame can turn off a task list they'd rather
|
||||
not see, but only its owner can point the widget at one of their
|
||||
calendars to begin with."""
|
||||
frame, widget = frame_widget
|
||||
_require_widget_type(widget, "calendar")
|
||||
_require_widget_type(widget, "tasks")
|
||||
user = require_user_api(request, db)
|
||||
with widget_locked(db, frame.id, widget.id) as (_, _, cfg):
|
||||
if body.calendar_key is None:
|
||||
cfg.tasks_user_id = None
|
||||
cfg.tasks_calendar_key = None
|
||||
cfg.tasks_cached = None
|
||||
cfg.user_id = None
|
||||
cfg.calendar_key = None
|
||||
cfg.cached = None
|
||||
else:
|
||||
cfg.tasks_user_id = user.id
|
||||
cfg.tasks_calendar_key = body.calendar_key
|
||||
cfg.tasks_checked_at = 0.0 # pick up the change promptly
|
||||
cfg.user_id = user.id
|
||||
cfg.calendar_key = body.calendar_key
|
||||
cfg.checked_at = 0.0 # pick up the change promptly
|
||||
return {"status": "saved", "calendar_key": body.calendar_key}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user