mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
The Storage Location dropdown listed entries like "H2D-1 - AMS A1" next to
real locations, and they could not be got rid of.
They were never locations. Bambuddy used to record which slot a spool was
loaded into by writing that string into Spoolman's location field, and the
writer went away when Storage Location became something the user picks --
but the strings stayed on people's Spoolman spools, and the location sync
imports every distinct one it finds, so they have been coming back in
through the front door ever since. A printer slot is where a spool is
loaded, not where it is put away, and slot assignments already track the
first.
Deleting one by hand did not work either, which is what made this a dead
end rather than an annoyance: the delete route refuses a location that has
spools, and in Spoolman mode it counts them by matching that same string,
so every marker still sitting on a loaded spool answered 409 -- and the two
that were empty were back on the next sync a minute later.
The import now skips them and a one-shot migration clears the ones already
in the catalogue. The shape is defined once and used by both: an optional
printer-name prefix followed by AMS A1, AMS-HT A1 or External Spool, which
is exactly what convert_ams_slot_to_location produced. It stays narrow on
purpose -- "AMS Drybox" and "Spare AMS trays" are somebody's shelf, and
anything the filter swallowed would be a place they could no longer file a
spool under -- so both directions are pinned by tests.
A row is only removed when no spool in this database points at it, by id or
by legacy free-text name, so an internal-mode user who has deliberately
filed spools under such a name keeps it. Spools in Spoolman are neither
consulted nor touched: their location strings are the user's data on the
user's server, and one that still reads "H2D-1 - AMS A1" in the inventory
list is telling the truth about what Spoolman holds. It simply stops being
offered as a destination.
Verified on a live Postgres instance carrying the reported symptom: 13
locations down to 3, all ten markers removed, the two real shelves and one
hand-typed Spoolman name left alone.
149 lines
4.7 KiB
Python
149 lines
4.7 KiB
Python
"""Cleanup of AMS slot markers imported into the storage-location catalogue.
|
|
|
|
Bambuddy used to write the slot a spool was loaded into -- "<printer> - AMS A1"
|
|
-- into Spoolman's ``location`` field, and the location sync then imported every
|
|
distinct one as a storage location. ``_migrate_drop_ams_slot_locations`` clears
|
|
the rows that already landed; the import side is covered in
|
|
``test_location_service.py``.
|
|
"""
|
|
|
|
import pytest
|
|
from sqlalchemy import text
|
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
|
|
import backend.app.models # noqa: F401 - populate Base.metadata
|
|
from backend.app.core.database import Base, _migrate_drop_ams_slot_locations
|
|
from backend.app.models.location import Location
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.services.location_service import assign_location_name
|
|
|
|
FLAG = "_cleanup_ams_slot_locations_done"
|
|
|
|
|
|
@pytest.fixture
|
|
async def engine(tmp_path):
|
|
eng = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/t.db")
|
|
async with eng.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
try:
|
|
yield eng
|
|
finally:
|
|
await eng.dispose()
|
|
|
|
|
|
def _location(name: str) -> Location:
|
|
loc = Location()
|
|
assign_location_name(loc, name)
|
|
return loc
|
|
|
|
|
|
async def _names(db) -> set[str]:
|
|
return {r[0] for r in (await db.execute(text("SELECT name FROM locations"))).fetchall()}
|
|
|
|
|
|
async def _run(engine):
|
|
async with engine.begin() as conn:
|
|
await _migrate_drop_ams_slot_locations(conn)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_removes_the_slot_markers_and_keeps_real_locations(engine):
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with sm() as db:
|
|
db.add_all(
|
|
[
|
|
_location("H2D-1 - AMS A1"),
|
|
_location("H2D-1 - AMS C3"),
|
|
_location("X1C-2 - AMS-HT A1"),
|
|
_location("P1S - External Spool"),
|
|
_location("Drybox 1"),
|
|
_location("Shelf A"),
|
|
]
|
|
)
|
|
await db.commit()
|
|
|
|
await _run(engine)
|
|
|
|
async with sm() as db:
|
|
assert await _names(db) == {"Drybox 1", "Shelf A"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keeps_a_marker_a_spool_is_actually_filed_under(engine):
|
|
"""Deleting it would strand the spool's location, and someone who has
|
|
deliberately filed spools under that name meant it."""
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with sm() as db:
|
|
loc = _location("H2D-1 - AMS A1")
|
|
db.add(loc)
|
|
await db.flush()
|
|
db.add(
|
|
Spool(
|
|
material="PLA",
|
|
label_weight=1000,
|
|
location_id=loc.id,
|
|
storage_location="H2D-1 - AMS A1",
|
|
)
|
|
)
|
|
await db.commit()
|
|
|
|
await _run(engine)
|
|
|
|
async with sm() as db:
|
|
assert await _names(db) == {"H2D-1 - AMS A1"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_keeps_a_marker_a_legacy_free_text_spool_still_names(engine):
|
|
"""Rows predating the location catalogue carry the name without the FK, and
|
|
the rename cascade still matches them on it."""
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with sm() as db:
|
|
db.add(_location("H2D-1 - AMS A1"))
|
|
await db.flush()
|
|
# Whitespace and case around the name are the legacy shape the rename
|
|
# cascade already has to cope with, so the guard has to match it too.
|
|
db.add(Spool(material="PLA", label_weight=1000, storage_location=" h2d-1 - ams a1 "))
|
|
await db.commit()
|
|
|
|
await _run(engine)
|
|
|
|
async with sm() as db:
|
|
assert await _names(db) == {"H2D-1 - AMS A1"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_runs_exactly_once(engine):
|
|
"""A location the user creates afterwards is theirs, whatever it is named."""
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with sm() as db:
|
|
db.add(_location("H2D-1 - AMS A1"))
|
|
await db.commit()
|
|
|
|
await _run(engine)
|
|
|
|
async with sm() as db:
|
|
db.add(_location("H2D-1 - AMS B2"))
|
|
await db.commit()
|
|
|
|
await _run(engine)
|
|
|
|
async with sm() as db:
|
|
assert await _names(db) == {"H2D-1 - AMS B2"}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_marks_itself_done_on_an_install_with_nothing_to_remove(engine):
|
|
"""Otherwise the whole catalogue is rescanned on every boot for ever."""
|
|
sm = async_sessionmaker(engine, expire_on_commit=False)
|
|
async with sm() as db:
|
|
db.add(_location("Drybox 1"))
|
|
await db.commit()
|
|
|
|
await _run(engine)
|
|
|
|
async with sm() as db:
|
|
done = (await db.execute(text('SELECT value FROM settings WHERE "key" = :k'), {"k": FLAG})).scalar_one_or_none()
|
|
assert done == "true"
|
|
assert await _names(db) == {"Drybox 1"}
|