Widget system Phase 4b: per-widget gear-icon config dialogs
Replaces the Photos/Calendar/Whiteboard tabs with a single Layout page
(now the frame's landing route) where each widget gets a gear icon
opening a dialog scoped to that specific widget's own settings. This
was the missing piece for genuinely independent same-type widgets --
"the Calendar tab" never made sense once a frame could hold more than
one calendar widget with different settings.
Data layer: FrameCalendar re-keyed from frame_id to widget_id, so each
calendar widget has its own independent included-calendars set. The
rekey runs as an unconditional post-startup step (like the existing
widget backfill), not a numbered migration -- it depends on calendar
widgets already existing, which themselves come from that same
backfill step, not from schema migration. Registering it as a numbered
migration would have run it first during a real upgrade, silently
dropping every row; caught by a new test that exercises the raw-SQL
upgrade path instead of the fresh-install create_all() shortcut every
other migration test takes.
API layer: every endpoint that used to assume "the frame's widget of
this type" (photo queue/thumbnail/preview, calendar select/color/
tasks/weather, whiteboard source/browse/preview) moved into
api_widgets.py under /api/frames/{id}/widgets/{widget_id}/..., with a
new require_widget_view/control dependency pair mirroring the existing
frame-level ones. Device status (battery/last-seen/firmware) got its
own frame-level /status endpoint, split out of the old photo-specific
/queue it used to piggyback on -- fixes the status bar going silently
blank on any frame without a photo widget.
UI layer: each widget type's existing settings markup/JS was ported
into a dialog partial + an explicit init/close function pair (the
content is now fetched and injected on demand, not loaded at page load
time). window.FRAME_API is repointed to the open dialog's widget-scoped
API base for its duration and restored on close; a separate
window.FRAME_BASE_API stays stable for the always-present header/
status-bar scripts.
Caught during manual browser testing: the consolidated config-save
endpoint initially expected a JSON body while the copied-over dialog JS
posts form-urlencoded data (the old convention) -- fixed to match, with
new HTTP-level test coverage that would have caught it immediately.
This commit is contained in:
@@ -17,12 +17,15 @@ from app.models import (
|
||||
CalendarWidgetConfig,
|
||||
Frame,
|
||||
FrameButtonAction,
|
||||
FrameCalendar,
|
||||
PhotoWidgetConfig,
|
||||
ServerSettings,
|
||||
Widget,
|
||||
WhiteboardWidgetConfig,
|
||||
)
|
||||
|
||||
from .conftest import make_user
|
||||
|
||||
|
||||
def test_migrations_list_is_sequential_and_unique():
|
||||
versions = [v for v, _ in MIGRATIONS]
|
||||
@@ -170,11 +173,29 @@ def test_migration_16_raw_sql_path_applies_to_an_existing_pre_widget_database(db
|
||||
"""Exercises _migration_16's actual CREATE TABLE statements (the real
|
||||
"existing production database upgrading past this migration"
|
||||
scenario) rather than the fresh-install create_all() shortcut, which
|
||||
every other test in this file goes through instead."""
|
||||
every other test in this file goes through instead. frame_calendars
|
||||
also gets rebuilt back to its pre-rekey (frame_id-keyed) shape,
|
||||
matching what _migration_9/_migration_11 originally produced, so
|
||||
_ensure_frame_calendars_rekeyed has a real frame_id-shaped table to
|
||||
migrate."""
|
||||
with db_module.engine.begin() as conn:
|
||||
for table in ("frame_button_actions", "whiteboard_widget_configs",
|
||||
"calendar_widget_configs", "photo_widget_configs", "widgets"):
|
||||
conn.execute(text(f"DROP TABLE {table}"))
|
||||
conn.execute(text("DROP TABLE frame_calendars"))
|
||||
conn.execute(text(
|
||||
"CREATE TABLE frame_calendars ("
|
||||
"id INTEGER PRIMARY KEY, "
|
||||
"frame_id INTEGER NOT NULL REFERENCES frames(id) ON DELETE CASCADE, "
|
||||
"user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, "
|
||||
"calendar_key TEXT NOT NULL, "
|
||||
"calendar_label TEXT NOT NULL DEFAULT '', "
|
||||
"included INTEGER NOT NULL DEFAULT 1, "
|
||||
"color_index INTEGER)"
|
||||
))
|
||||
conn.execute(text(
|
||||
"CREATE UNIQUE INDEX ix_frame_calendars_unique ON frame_calendars (frame_id, user_id, calendar_key)"
|
||||
))
|
||||
conn.execute(text("UPDATE schema_version SET version = 15"))
|
||||
|
||||
frame = Frame(
|
||||
@@ -183,8 +204,20 @@ def test_migration_16_raw_sql_path_applies_to_an_existing_pre_widget_database(db
|
||||
queue=["legacy-asset", "next-asset"], created_at=time.time(),
|
||||
)
|
||||
db_session.add(frame)
|
||||
db_session.flush()
|
||||
user = make_user(db_session, "legacy-owner")
|
||||
db_session.commit()
|
||||
frame_id = frame.id
|
||||
frame_id, user_id = frame.id, user.id
|
||||
|
||||
# An orphaned frame_calendars row (this frame's mode was never
|
||||
# "calendar", so it has no calendar widget for _ensure_frame_
|
||||
# calendars_rekeyed to attach it to) -- exercises that it's dropped
|
||||
# cleanly rather than raising.
|
||||
with db_module.engine.begin() as conn:
|
||||
conn.execute(text(
|
||||
"INSERT INTO frame_calendars (frame_id, user_id, calendar_key, calendar_label, included) "
|
||||
"VALUES (:frame_id, :user_id, 'ics', 'Legacy Cal', 1)"
|
||||
), {"frame_id": frame_id, "user_id": user_id})
|
||||
|
||||
run_migrations()
|
||||
|
||||
@@ -198,3 +231,61 @@ def test_migration_16_raw_sql_path_applies_to_an_existing_pre_widget_database(db
|
||||
assert config.album_id == "legacy-album"
|
||||
assert config.current_asset_id == "legacy-asset"
|
||||
assert config.queue == ["legacy-asset", "next-asset"]
|
||||
|
||||
|
||||
def test_frame_calendars_rekey_attaches_existing_rows_to_their_calendar_widget(db_session):
|
||||
"""A frame whose mode was "calendar" (not "photos") gets a calendar
|
||||
widget from _ensure_widgets_backfilled -- _ensure_frame_calendars_
|
||||
rekeyed should then attach the pre-existing frame_id-keyed
|
||||
FrameCalendar row inserted below to that widget's id, not drop it.
|
||||
This is the regression case for the ordering bug the two migration
|
||||
tests above's setup was designed to catch: the rekey must run AFTER
|
||||
the widget backfill, not as a numbered migration racing ahead of
|
||||
it (see _ensure_frame_calendars_rekeyed's own docstring)."""
|
||||
with db_module.engine.begin() as conn:
|
||||
for table in ("frame_button_actions", "whiteboard_widget_configs",
|
||||
"calendar_widget_configs", "photo_widget_configs", "widgets"):
|
||||
conn.execute(text(f"DROP TABLE {table}"))
|
||||
conn.execute(text("DROP TABLE frame_calendars"))
|
||||
conn.execute(text(
|
||||
"CREATE TABLE frame_calendars ("
|
||||
"id INTEGER PRIMARY KEY, "
|
||||
"frame_id INTEGER NOT NULL REFERENCES frames(id) ON DELETE CASCADE, "
|
||||
"user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, "
|
||||
"calendar_key TEXT NOT NULL, "
|
||||
"calendar_label TEXT NOT NULL DEFAULT '', "
|
||||
"included INTEGER NOT NULL DEFAULT 1, "
|
||||
"color_index INTEGER)"
|
||||
))
|
||||
conn.execute(text(
|
||||
"CREATE UNIQUE INDEX ix_frame_calendars_unique ON frame_calendars (frame_id, user_id, calendar_key)"
|
||||
))
|
||||
conn.execute(text("UPDATE schema_version SET version = 15"))
|
||||
|
||||
frame = Frame(
|
||||
name="Calendar Frame", device_token="tok-cal", manage_token="mtok-cal",
|
||||
mode="calendar", created_at=time.time(),
|
||||
)
|
||||
db_session.add(frame)
|
||||
db_session.flush()
|
||||
user = make_user(db_session, "cal-owner")
|
||||
db_session.commit()
|
||||
frame_id, user_id = frame.id, user.id
|
||||
|
||||
with db_module.engine.begin() as conn:
|
||||
conn.execute(text(
|
||||
"INSERT INTO frame_calendars (frame_id, user_id, calendar_key, calendar_label, included, color_index) "
|
||||
"VALUES (:frame_id, :user_id, 'ics', 'Legacy Cal', 1, 3)"
|
||||
), {"frame_id": frame_id, "user_id": user_id})
|
||||
|
||||
run_migrations()
|
||||
|
||||
widget = db_session.scalars(
|
||||
select(Widget).where(Widget.frame_id == frame_id, Widget.widget_type == "calendar")
|
||||
).one()
|
||||
row = db_session.scalars(select(FrameCalendar).where(FrameCalendar.widget_id == widget.id)).one()
|
||||
assert row.user_id == user_id
|
||||
assert row.calendar_key == "ics"
|
||||
assert row.calendar_label == "Legacy Cal"
|
||||
assert row.included is True
|
||||
assert row.color_index == 3
|
||||
|
||||
Reference in New Issue
Block a user