Widget system Phase 2: full cutover to widget-based rendering
device.py's mode-keyed dispatch is replaced by a real compositor: load a frame's widgets, compute pixel rects via app/grid.py, render each through its widget module, and composite with render_panel. Physical NEXT/BACK buttons now execute each frame's assigned FrameButtonAction rows instead of one hardcoded per-mode action. api_frames.py, manage.py, and common.py's build_manage_content are repointed to read/write the frame's widget config rows instead of the old Frame columns, and every settings page (Photos/Calendar/ Whiteboard tabs) now pre-fills its form from the same widget config the write endpoints actually save to -- previously the read and write sides would have silently diverged. The old mode selector and photo-inlay checkbox are removed along with their now-inert wiring; arbitrary widget placement subsumes what the fixed inlay split did. Ships together with Phase 1 (per-type render/action modules) since splitting the read/write cutover across deploys would have left settings changes with no visible effect.
This commit is contained in:
@@ -6,7 +6,16 @@ not just a helper function's logic."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.models import Frame, FrameCalendar, User
|
||||
import time
|
||||
|
||||
from app.models import (
|
||||
CalendarWidgetConfig,
|
||||
Frame,
|
||||
FrameCalendar,
|
||||
User,
|
||||
Widget,
|
||||
WhiteboardWidgetConfig,
|
||||
)
|
||||
|
||||
from .conftest import csrf_headers, link_user, login, make_user
|
||||
|
||||
@@ -21,26 +30,53 @@ def _setup_two_linked_users(client, db_session) -> Frame:
|
||||
return frame
|
||||
|
||||
|
||||
def _add_whiteboard_widget(db_session, frame: Frame) -> Widget:
|
||||
"""Frame #1's auto-migrated widget is a photos widget (its mode was
|
||||
"photos" before the widget system existed) -- these tests need a
|
||||
whiteboard widget too, which nothing creates yet until the
|
||||
widget-placement UI (a later phase) ships, so it's added directly
|
||||
here the same way the widget unit tests do."""
|
||||
widget = Widget(frame_id=frame.id, widget_type="whiteboard", x=0, y=0, w=8, h=5,
|
||||
sort_order=1, created_at=time.time())
|
||||
db_session.add(widget)
|
||||
db_session.flush()
|
||||
db_session.add(WhiteboardWidgetConfig(widget_id=widget.id))
|
||||
db_session.commit()
|
||||
return widget
|
||||
|
||||
|
||||
def _add_calendar_widget(db_session, frame: Frame) -> Widget:
|
||||
widget = Widget(frame_id=frame.id, widget_type="calendar", x=0, y=0, w=8, h=5,
|
||||
sort_order=1, created_at=time.time())
|
||||
db_session.add(widget)
|
||||
db_session.flush()
|
||||
db_session.add(CalendarWidgetConfig(widget_id=widget.id))
|
||||
db_session.commit()
|
||||
return widget
|
||||
|
||||
|
||||
# --- whiteboard-source ---
|
||||
|
||||
def test_whiteboard_source_owner_can_set_it(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
widget = _add_whiteboard_widget(db_session, frame)
|
||||
# alice is still logged in from /setup
|
||||
resp = client.post("/api/frames/1/whiteboard-source", json={
|
||||
"url": "https://cloud.example.com/dav/files/alice/board.whiteboard",
|
||||
}, headers=csrf_headers(client))
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
frame = db_session.get(Frame, 1)
|
||||
assert frame.whiteboard_user_id is not None
|
||||
assert frame.whiteboard_url == "https://cloud.example.com/dav/files/alice/board.whiteboard"
|
||||
cfg = db_session.get(WhiteboardWidgetConfig, widget.id)
|
||||
assert cfg.user_id is not None
|
||||
assert cfg.url == "https://cloud.example.com/dav/files/alice/board.whiteboard"
|
||||
|
||||
|
||||
def test_whiteboard_source_set_always_targets_the_caller(client, db_session):
|
||||
"""bob has no way to point the frame at someone else's account --
|
||||
there's no target-user field in the request at all, so a "set" call
|
||||
from bob always attaches to bob, even if he pastes alice's URL."""
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
widget = _add_whiteboard_widget(db_session, frame)
|
||||
client.cookies.clear()
|
||||
login(client, "bob")
|
||||
|
||||
@@ -49,13 +85,14 @@ def test_whiteboard_source_set_always_targets_the_caller(client, db_session):
|
||||
}, headers=csrf_headers(client))
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
frame = db_session.get(Frame, 1)
|
||||
bob_row = db_session.query(User).filter_by(username="bob").one()
|
||||
assert frame.whiteboard_user_id == bob_row.id
|
||||
cfg = db_session.get(WhiteboardWidgetConfig, widget.id)
|
||||
assert cfg.user_id == bob_row.id
|
||||
|
||||
|
||||
def test_whiteboard_source_anyone_linked_can_clear(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
widget = _add_whiteboard_widget(db_session, frame)
|
||||
client.post("/api/frames/1/whiteboard-source", json={"url": "https://cloud.example.com/board.whiteboard"},
|
||||
headers=csrf_headers(client))
|
||||
|
||||
@@ -64,20 +101,22 @@ def test_whiteboard_source_anyone_linked_can_clear(client, db_session):
|
||||
resp = client.post("/api/frames/1/whiteboard-source", json={"url": None}, headers=csrf_headers(client))
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
frame = db_session.get(Frame, 1)
|
||||
assert frame.whiteboard_url == ""
|
||||
assert frame.whiteboard_user_id is None
|
||||
cfg = db_session.get(WhiteboardWidgetConfig, widget.id)
|
||||
assert cfg.url == ""
|
||||
assert cfg.user_id is None
|
||||
|
||||
|
||||
def test_whiteboard_source_rejects_non_http_url(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
_add_whiteboard_widget(db_session, frame)
|
||||
resp = client.post("/api/frames/1/whiteboard-source", json={"url": "javascript:alert(1)"},
|
||||
headers=csrf_headers(client))
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_whiteboard_source_unlinked_user_cannot_touch_it(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
_add_whiteboard_widget(db_session, frame)
|
||||
make_user(db_session, "mallory") # exists, but never linked to frame 1
|
||||
client.cookies.clear()
|
||||
login(client, "mallory")
|
||||
@@ -87,10 +126,22 @@ def test_whiteboard_source_unlinked_user_cannot_touch_it(client, db_session):
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_whiteboard_source_404s_when_frame_has_no_whiteboard_widget(client, db_session):
|
||||
"""Distinct from the unlinked-user 404 above -- this is a linked,
|
||||
fully-permitted owner hitting the endpoint on a frame that simply
|
||||
doesn't have a whiteboard widget yet (frame #1's auto-migrated
|
||||
widget is a photos widget)."""
|
||||
_setup_two_linked_users(client, db_session)
|
||||
resp = client.post("/api/frames/1/whiteboard-source", json={"url": "https://x.example.com/b.whiteboard"},
|
||||
headers=csrf_headers(client))
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# --- tasks-source ---
|
||||
|
||||
def test_tasks_source_set_always_targets_the_caller(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
widget = _add_calendar_widget(db_session, frame)
|
||||
client.cookies.clear()
|
||||
login(client, "bob")
|
||||
|
||||
@@ -99,13 +150,14 @@ def test_tasks_source_set_always_targets_the_caller(client, db_session):
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
bob_row = db_session.query(User).filter_by(username="bob").one()
|
||||
frame = db_session.get(Frame, 1)
|
||||
assert frame.calendar_tasks_user_id == bob_row.id
|
||||
assert frame.calendar_tasks_calendar_key == "caldav:/some/tasks/"
|
||||
cfg = db_session.get(CalendarWidgetConfig, widget.id)
|
||||
assert cfg.tasks_user_id == bob_row.id
|
||||
assert cfg.tasks_calendar_key == "caldav:/some/tasks/"
|
||||
|
||||
|
||||
def test_tasks_source_anyone_linked_can_clear(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
frame = _setup_two_linked_users(client, db_session)
|
||||
widget = _add_calendar_widget(db_session, frame)
|
||||
client.post("/api/frames/1/tasks-source", json={"calendar_key": "caldav:/alice/tasks/"},
|
||||
headers=csrf_headers(client))
|
||||
|
||||
@@ -114,9 +166,16 @@ def test_tasks_source_anyone_linked_can_clear(client, db_session):
|
||||
resp = client.post("/api/frames/1/tasks-source", json={"calendar_key": None}, headers=csrf_headers(client))
|
||||
assert resp.status_code == 200, resp.text
|
||||
|
||||
frame = db_session.get(Frame, 1)
|
||||
assert frame.calendar_tasks_user_id is None
|
||||
assert frame.calendar_tasks_calendar_key is None
|
||||
cfg = db_session.get(CalendarWidgetConfig, widget.id)
|
||||
assert cfg.tasks_user_id is None
|
||||
assert cfg.tasks_calendar_key is None
|
||||
|
||||
|
||||
def test_tasks_source_404s_when_frame_has_no_calendar_widget(client, db_session):
|
||||
_setup_two_linked_users(client, db_session)
|
||||
resp = client.post("/api/frames/1/tasks-source", json={"calendar_key": "caldav:/some/tasks/"},
|
||||
headers=csrf_headers(client))
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
# --- calendar-select ---
|
||||
|
||||
Reference in New Issue
Block a user