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:
@@ -1,63 +1,72 @@
|
||||
"""get_or_refresh_whiteboard's fetch throttle (and force=True bypassing
|
||||
it) plus the whiteboard-browse HTTP endpoint. whiteboard.fetch_and_render
|
||||
is monkeypatched -- it talks to a real WebDAV server and the Node render
|
||||
sidecar (see render-service/), neither of which this suite needs a real
|
||||
copy of to verify the *throttle*/*permission* logic around it."""
|
||||
"""get_or_refresh_whiteboard_for_widget's fetch throttle (and force=True
|
||||
bypassing it) plus the whiteboard-browse HTTP endpoint.
|
||||
whiteboard.fetch_and_render is monkeypatched -- it talks to a real
|
||||
WebDAV server and the Node render sidecar (see render-service/), neither
|
||||
of which this suite needs a real copy of to verify the *throttle*/
|
||||
*permission* logic around it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
from app import whiteboard
|
||||
from app.models import Frame, User
|
||||
from app.routers.common import get_or_refresh_whiteboard
|
||||
from app.models import Frame, User, Widget, WhiteboardWidgetConfig
|
||||
|
||||
from .conftest import csrf_headers, link_user, login, make_user
|
||||
|
||||
|
||||
def _configure_whiteboard(db_session, frame: Frame, user: User) -> None:
|
||||
frame.whiteboard_user_id = user.id
|
||||
frame.whiteboard_url = "http://example.invalid/board.whiteboard"
|
||||
frame.whiteboard_cached_image = b"OLD_CACHED_PNG"
|
||||
frame.whiteboard_checked_at = time.time() # just refreshed -- well within the throttle
|
||||
def _configure_whiteboard(db_session, frame: Frame, user: User) -> Widget:
|
||||
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, user_id=user.id, url="http://example.invalid/board.whiteboard",
|
||||
cached_image=b"OLD_CACHED_PNG", checked_at=time.time(), # just refreshed -- well within the throttle
|
||||
))
|
||||
db_session.commit()
|
||||
return widget
|
||||
|
||||
|
||||
def test_unforced_call_within_throttle_uses_cache(client, db_session, monkeypatch):
|
||||
from app.routers.common import get_or_refresh_whiteboard_for_widget
|
||||
|
||||
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
||||
alice = db_session.query(User).filter_by(username="alice").one()
|
||||
alice.webdav_username = "alice"
|
||||
alice.webdav_password = "secret"
|
||||
frame = db_session.get(Frame, 1)
|
||||
_configure_whiteboard(db_session, frame, alice)
|
||||
widget = _configure_whiteboard(db_session, frame, alice)
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(whiteboard, "fetch_and_render",
|
||||
lambda url, u, p: calls.append(1) or b"NEW_PNG")
|
||||
|
||||
result = get_or_refresh_whiteboard(db_session, frame)
|
||||
result = get_or_refresh_whiteboard_for_widget(db_session, frame, widget)
|
||||
assert result == b"OLD_CACHED_PNG"
|
||||
assert calls == []
|
||||
|
||||
|
||||
def test_force_bypasses_throttle_and_persists(client, db_session, monkeypatch):
|
||||
from app.routers.common import get_or_refresh_whiteboard_for_widget
|
||||
|
||||
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
||||
alice = db_session.query(User).filter_by(username="alice").one()
|
||||
alice.webdav_username = "alice"
|
||||
alice.webdav_password = "secret"
|
||||
frame = db_session.get(Frame, 1)
|
||||
_configure_whiteboard(db_session, frame, alice)
|
||||
widget = _configure_whiteboard(db_session, frame, alice)
|
||||
|
||||
calls = []
|
||||
monkeypatch.setattr(whiteboard, "fetch_and_render",
|
||||
lambda url, u, p: calls.append(1) or b"NEW_PNG")
|
||||
|
||||
result = get_or_refresh_whiteboard(db_session, frame, force=True)
|
||||
result = get_or_refresh_whiteboard_for_widget(db_session, frame, widget, force=True)
|
||||
assert result == b"NEW_PNG"
|
||||
assert len(calls) == 1
|
||||
|
||||
db_session.refresh(frame)
|
||||
assert frame.whiteboard_cached_image == b"NEW_PNG"
|
||||
cfg = db_session.get(WhiteboardWidgetConfig, widget.id)
|
||||
assert cfg.cached_image == b"NEW_PNG"
|
||||
|
||||
|
||||
def test_preview_endpoint_force_query_param_bypasses_throttle(client, db_session, monkeypatch):
|
||||
@@ -67,10 +76,10 @@ def test_preview_endpoint_force_query_param_bypasses_throttle(client, db_session
|
||||
alice.webdav_password = "secret"
|
||||
frame = db_session.get(Frame, 1)
|
||||
|
||||
# The preview endpoint runs whatever get_or_refresh_whiteboard returns
|
||||
# through PIL (Image.open) -- unlike the other tests in this file,
|
||||
# the placeholder "cached" bytes need to be real, valid PNG data, not
|
||||
# just an arbitrary marker string.
|
||||
# The preview endpoint runs whatever get_or_refresh_whiteboard_for_widget
|
||||
# returns through PIL (Image.open) -- unlike the other tests in this
|
||||
# file, the placeholder "cached" bytes need to be real, valid PNG
|
||||
# data, not just an arbitrary marker string.
|
||||
import io
|
||||
|
||||
from PIL import Image
|
||||
@@ -78,10 +87,14 @@ def test_preview_endpoint_force_query_param_bypasses_throttle(client, db_session
|
||||
Image.new("RGB", (1, 1), (255, 255, 255)).save(buf, format="PNG")
|
||||
tiny_png = buf.getvalue()
|
||||
|
||||
frame.whiteboard_user_id = alice.id
|
||||
frame.whiteboard_url = "http://example.invalid/board.whiteboard"
|
||||
frame.whiteboard_cached_image = tiny_png
|
||||
frame.whiteboard_checked_at = time.time()
|
||||
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, user_id=alice.id, url="http://example.invalid/board.whiteboard",
|
||||
cached_image=tiny_png, checked_at=time.time(),
|
||||
))
|
||||
db_session.commit()
|
||||
|
||||
calls = []
|
||||
|
||||
Reference in New Issue
Block a user