mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
File Manager cards, search and sort keyed off file_metadata.print_name, which ThreeMFParser lifts from the 3MF's <metadata name="Title">. That title is the in-app project title — generic "Exported 3D Model" for any Bambu Studio "Save As", a marketing title for a MakerWorld download — and almost never the filename the user saved as. A card for Whatever.3mf showed "Exported 3D Model"; correcting it needed a rename round-trip, since the Rename dialog disables Save while the name is unchanged. The slicer-output write path already dropped print_name for this exact reason; the four other paths that store parsed 3MF metadata onto a LibraryFile did not — external-folder scan, managed multipart upload, the multi-file ZIP-upload branch, and MakerWorld import. Add a shared _without_print_name() helper and apply it at all four import paths; switch the slicer path to it so there is one rule. A LibraryFile's display name is its filename — only PrintArchive carries a real print_name, which is untouched. Remove the now-redundant filename->print_name mirroring in the rename route. Add a one-time idempotent data migration (_migrate_drop_library_print_name, SQLite json_remove / PostgreSQL jsonb key-removal branched on is_sqlite()) so libraries imported before the fix correct themselves without the rename workaround. No frontend change: print_name || filename yields the filename once print_name is gone. Tests: 6 new in test_library_print_name.py cover _without_print_name and the migration (incl. idempotency, siblings preserved, null metadata). SQLite migration branch verified by test; PostgreSQL branch verified against a real Postgres instance.
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""Tests for library files displaying the filename, not the embedded 3MF Title (#1489).
|
|
|
|
The 3MF ``<metadata name="Title">`` is the in-app project title — generic
|
|
("Exported 3D Model") for a Bambu Studio "Save As", a marketing title for a
|
|
MakerWorld download — never the filename the user saved as. The FileManager
|
|
keyed its display name / search / sort off ``file_metadata.print_name``, so
|
|
storing the Title made every card show the wrong name. ``_without_print_name``
|
|
strips it on import; ``_migrate_drop_library_print_name`` clears it from rows
|
|
imported before the fix.
|
|
"""
|
|
|
|
from sqlalchemy import select
|
|
|
|
from backend.app.api.routes.library import _without_print_name
|
|
from backend.app.core.database import _migrate_drop_library_print_name
|
|
from backend.app.models.library import LibraryFile
|
|
|
|
# --- _without_print_name ---------------------------------------------------
|
|
|
|
|
|
def test_strips_print_name_keeps_siblings():
|
|
cleaned = _without_print_name({"print_name": "Exported 3D Model", "print_time_seconds": 100})
|
|
assert cleaned == {"print_time_seconds": 100}
|
|
|
|
|
|
def test_none_passes_through():
|
|
assert _without_print_name(None) is None
|
|
|
|
|
|
def test_dict_without_print_name_returned_unchanged():
|
|
meta = {"print_time_seconds": 50}
|
|
# No copy needed when there's nothing to strip — same object back.
|
|
assert _without_print_name(meta) is meta
|
|
|
|
|
|
def test_does_not_mutate_input():
|
|
original = {"print_name": "Whatever", "filament_used_grams": 12}
|
|
cleaned = _without_print_name(original)
|
|
assert original == {"print_name": "Whatever", "filament_used_grams": 12} # untouched
|
|
assert cleaned == {"filament_used_grams": 12}
|
|
|
|
|
|
def test_print_name_only_collapses_to_empty_dict():
|
|
assert _without_print_name({"print_name": "Exported 3D Model"}) == {}
|
|
|
|
|
|
# --- _migrate_drop_library_print_name --------------------------------------
|
|
|
|
|
|
async def test_migration_strips_print_name_from_existing_rows(db_session, monkeypatch):
|
|
"""Rows imported before the fix get print_name cleared; siblings and rows
|
|
that never had it are untouched. Idempotent on a second run.
|
|
|
|
The test DB is SQLite; is_sqlite() reads settings.database_url (not the
|
|
test engine), so pin it to exercise the SQLite branch deterministically.
|
|
The PostgreSQL branch is verified against a real PG instance separately."""
|
|
monkeypatch.setattr("backend.app.core.database.is_sqlite", lambda: True)
|
|
db_session.add_all(
|
|
[
|
|
LibraryFile(
|
|
filename="halloween.3mf",
|
|
file_path="/a",
|
|
file_type="3mf",
|
|
file_size=1,
|
|
file_metadata={"print_name": "Haunted House", "print_time_seconds": 100},
|
|
),
|
|
LibraryFile(
|
|
filename="no_meta.3mf",
|
|
file_path="/b",
|
|
file_type="3mf",
|
|
file_size=1,
|
|
file_metadata={"print_time_seconds": 50},
|
|
),
|
|
LibraryFile(
|
|
filename="null_meta.3mf",
|
|
file_path="/c",
|
|
file_type="3mf",
|
|
file_size=1,
|
|
file_metadata=None,
|
|
),
|
|
]
|
|
)
|
|
await db_session.commit()
|
|
|
|
conn = await db_session.connection()
|
|
await _migrate_drop_library_print_name(conn)
|
|
await _migrate_drop_library_print_name(conn) # idempotent
|
|
|
|
db_session.expire_all()
|
|
rows = (await db_session.execute(select(LibraryFile).order_by(LibraryFile.filename))).scalars().all()
|
|
by_name = {r.filename: r for r in rows}
|
|
|
|
assert by_name["halloween.3mf"].file_metadata == {"print_time_seconds": 100}
|
|
assert by_name["no_meta.3mf"].file_metadata == {"print_time_seconds": 50}
|
|
assert by_name["null_meta.3mf"].file_metadata is None
|