Files
Donkie 6520ce6d1b Let NFC tags identify filaments as well as spools
A tag can now be linked to a filament type, not just to one spool of it. The
tag table already carried filament_id and target_type for exactly this, so
there is no migration.

API, all additive:

  POST   /filament/{id}/tag        link; 409 if anything else holds the UID
  DELETE /filament/{id}/tag/{uid}  unlink
  GET    /filament?tag=<uid>       exact lookup on the normalized UID
  Filament.tags                    also inside every spool's embedded filament
  TagScan.matched_filament_id      always present, like matched_spool_id
  TagConflictMessage.filament_id   names a filament holder on a 409

One UID still identifies one thing across both kinds: a spool's tag cannot be
linked to a filament or the reverse, and neither ?tag= lookup answers with the
other kind. The scan debounce key now includes the kind, since spool 7 and
filament 7 are different answers.

SpoolTag and SpoolTagParameters are renamed Tag and TagLinkParameters while
they are unreleased, so the schema does not describe filament tags as spool
tags forever.

client_v2: the filament inspector gets the Tags section, the link dialog names
and moves a tag held by either kind, and scanning a filament's tag opens that
filament when auto-navigate is on.

Tag having two delete-orphan parents was checked on SQLAlchemy 2.0.51: new tags
flush, unlinking deletes the row, and deleting either parent cascades.

Proven on sqlite, postgres, mariadb and cockroachdb: 558 integration tests
each. Unit tests, svelte-check and the Playwright tag specs pass.
2026-09-13 10:10:13 +02:00

37 lines
1.1 KiB
Python

"""Various exceptions used."""
class ItemNotFoundError(Exception):
pass
class ItemDeleteError(Exception):
pass
class ItemCreateError(Exception):
pass
class SpoolMeasureError(Exception):
pass
class TagConflictError(Exception):
"""A tag UID is already linked to something else.
Carries the holder's ID rather than only a message, so the API can hand it to the
client and the client can offer "move the tag here instead" without a second lookup.
At most one of `spool_id` and `filament_id` is set, naming whichever holds the tag.
Both are None when the holder is not a row at all (see `database.models.Tag`); the
message says what in that case, and a client with nothing to offer beyond it simply
reports it.
"""
def __init__(self, message: str, spool_id: int | None = None, filament_id: int | None = None) -> None:
"""Initialize with the message and the ID of the spool or filament holding the tag."""
super().__init__(message)
self.spool_id = spool_id
self.filament_id = filament_id