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.
122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
"""routers/manage.py -- the no-login "scan to manage" surface -- against
|
|
the widget-scoped photo state it was just repointed at. No prior test
|
|
coverage existed for this router at all before this file; it's
|
|
exercised here specifically because of how much its photo-queue
|
|
endpoints changed in the widget-system cutover (frame.current_asset_id/
|
|
frame.queue -> the frame's photo widget's own PhotoWidgetConfig)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from app.models import Frame, PhotoWidgetConfig
|
|
|
|
_ASSETS = [{"id": "asset-1"}, {"id": "asset-2"}, {"id": "asset-3"}]
|
|
|
|
|
|
def _mock_immich(monkeypatch):
|
|
"""manage.py imports immich_client_for/list_assets into its own
|
|
module namespace (`from .common import ...`) -- patching those
|
|
names, not app.widgets.photos' separate copies, since manage.py's
|
|
endpoints call its own bound names directly, never through
|
|
app/widgets/."""
|
|
monkeypatch.setattr("app.routers.manage.immich_client_for", lambda frame: object())
|
|
monkeypatch.setattr("app.routers.manage.list_assets", lambda client, album_id: _ASSETS)
|
|
|
|
|
|
def _configure_photo_widget(db_session):
|
|
from app.models import Widget
|
|
|
|
frame = db_session.get(Frame, 1)
|
|
# photo_widget_config_or_404 (see routers/common.py) checks Immich
|
|
# creds directly, not through the mocked immich_client_for below.
|
|
frame.immich_url = "http://immich.example.com"
|
|
frame.immich_api_key = "key"
|
|
widget = db_session.query(Widget).filter_by(frame_id=frame.id, widget_type="photos").one()
|
|
cfg = db_session.get(PhotoWidgetConfig, widget.id)
|
|
cfg.album_id = "album-1"
|
|
db_session.commit()
|
|
return frame, widget
|
|
|
|
|
|
def test_manage_queue_requires_configured_photo_widget(client, db_session):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
frame = db_session.get(Frame, 1)
|
|
resp = client.get(f"/api/m/{frame.manage_token}/queue")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_manage_queue_returns_current_and_upcoming(client, db_session, monkeypatch):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
frame, widget = _configure_photo_widget(db_session)
|
|
_mock_immich(monkeypatch)
|
|
|
|
resp = client.get(f"/api/m/{frame.manage_token}/queue")
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
assert data["current"]["id"] == "asset-1"
|
|
assert [u["id"] for u in data["upcoming"]] == ["asset-2", "asset-3"]
|
|
|
|
|
|
def test_manage_advance_and_back(client, db_session, monkeypatch):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
frame, widget = _configure_photo_widget(db_session)
|
|
_mock_immich(monkeypatch)
|
|
|
|
# establish a current photo first
|
|
client.get(f"/api/m/{frame.manage_token}/queue")
|
|
|
|
resp = client.post(f"/api/m/{frame.manage_token}/advance")
|
|
assert resp.status_code == 200
|
|
cfg = db_session.get(PhotoWidgetConfig, widget.id)
|
|
first_current = cfg.current_asset_id
|
|
assert first_current != ""
|
|
|
|
resp = client.post(f"/api/m/{frame.manage_token}/advance")
|
|
assert resp.status_code == 200
|
|
cfg = db_session.get(PhotoWidgetConfig, widget.id)
|
|
assert cfg.current_asset_id != first_current
|
|
|
|
resp = client.post(f"/api/m/{frame.manage_token}/back")
|
|
assert resp.status_code == 200
|
|
cfg = db_session.get(PhotoWidgetConfig, widget.id)
|
|
assert cfg.current_asset_id == first_current
|
|
|
|
|
|
def test_manage_promote(client, db_session, monkeypatch):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
frame, widget = _configure_photo_widget(db_session)
|
|
_mock_immich(monkeypatch)
|
|
client.get(f"/api/m/{frame.manage_token}/queue") # populate the queue
|
|
|
|
cfg = db_session.get(PhotoWidgetConfig, widget.id)
|
|
assert "asset-3" in cfg.queue
|
|
|
|
resp = client.post(f"/api/m/{frame.manage_token}/promote", json={"asset_id": "asset-3"})
|
|
assert resp.status_code == 200, resp.text
|
|
cfg = db_session.get(PhotoWidgetConfig, widget.id)
|
|
assert cfg.queue[0] == "asset-3"
|
|
|
|
|
|
def test_manage_thumbnail_scoped_to_showing_or_queued(client, db_session, monkeypatch):
|
|
client.post("/setup", data={"username": "alice", "password": "hunter22"})
|
|
frame, widget = _configure_photo_widget(db_session)
|
|
_mock_immich(monkeypatch)
|
|
client.get(f"/api/m/{frame.manage_token}/queue")
|
|
|
|
resp = client.get(f"/api/m/{frame.manage_token}/thumbnail/not-on-this-frame")
|
|
assert resp.status_code == 404
|
|
|
|
monkeypatch.setattr(
|
|
"app.routers.manage.immich_client_for",
|
|
lambda frame: type("C", (), {
|
|
"download_asset_thumbnail": lambda self, asset_id: (b"jpegbytes", "image/jpeg"),
|
|
})(),
|
|
)
|
|
resp = client.get(f"/api/m/{frame.manage_token}/thumbnail/asset-1")
|
|
assert resp.status_code == 200
|
|
assert resp.content == b"jpegbytes"
|
|
|
|
|
|
def test_unknown_manage_token_404s(client, db_session):
|
|
resp = client.get("/api/m/not-a-real-token/queue")
|
|
assert resp.status_code == 404
|