Printer assignment end to end, and a container that keeps a site's files
Assign printers to a MACHINE and let whichever PC controls it install them, with the observed half kept strictly apart from the assigned half: a collector report can never write an assignment row. Adoption is explicit. Adds a singular relationship type so defaultprinter can mean exactly one, client scripts that converge a bay's queues and correct a drifted one in place, and a wave gate so a fleet-wide driver rollout cannot stampede its own share. On the container side: the compose stack never persisted the instance directory, so a rebuild discarded plugins.json and every upload. It is a volume now, the image ships instance/ owned by the app user so the mountpoint is writable, and the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended since 8.0 went end of life. The image also could not build at all: the frontend stage never copied the plugin staging script it runs. Adds a topology migration guide covering IIS to Docker and back.
This commit is contained in:
@@ -63,7 +63,7 @@ __contract_version__ = '0.20.0'
|
||||
# plugin-contract version above are distinct series with independent
|
||||
# bump rules. Not part of the shopdb.api contract surface, so it is
|
||||
# not re-exported there.
|
||||
__version__ = '0.11.2'
|
||||
__version__ = '0.11.3'
|
||||
|
||||
|
||||
def create_app(config_name: str = None) -> Flask:
|
||||
@@ -89,6 +89,13 @@ def create_app(config_name: str = None) -> Flask:
|
||||
|
||||
app.config.from_object(config_class)
|
||||
|
||||
# The product and contract versions, in config so a PLUGIN can read them
|
||||
# through current_app instead of importing core (ADR-002 forbids that, and
|
||||
# the contract test enforces it). Core code may still use the module
|
||||
# constants directly.
|
||||
app.config['VERSION'] = __version__
|
||||
app.config['CONTRACT_VERSION'] = __contract_version__
|
||||
|
||||
# Load instance config if exists
|
||||
app.config.from_pyfile('config.py', silent=True)
|
||||
|
||||
|
||||
@@ -516,12 +516,43 @@ def seed_reference_data():
|
||||
for at in adr_types:
|
||||
if not _lookup_binary(at['relationshiptype']):
|
||||
db.session.add(RelationshipType(**at))
|
||||
|
||||
# Printer assignment edges. usesprinter = "this printer is installed here",
|
||||
# defaultprinter = which of them is the default. Assignment lives on the
|
||||
# MACHINE asset and reaches whichever PC controls it through the controls
|
||||
# rail seeded below, so a reimaged PC gets its printers back with no
|
||||
# restore step. Created before the rails: _seed_propagation no-ops in
|
||||
# silence when either type is missing.
|
||||
printer_types = [
|
||||
{'relationshiptype': 'usesprinter',
|
||||
'description': 'Asset to a printer installed on it (ADR-001)',
|
||||
'isdirectional': True},
|
||||
{'relationshiptype': 'defaultprinter',
|
||||
'description': 'PC to its default printer (installer preselect, ADR-001)',
|
||||
'isdirectional': True,
|
||||
# One default per asset. The unique constraint is (source, target,
|
||||
# type), so two DIFFERENT defaults are two valid rows and the resolver
|
||||
# then takes the oldest - the new default silently loses. Core's
|
||||
# create path replaces instead, driven by this flag.
|
||||
'issingular': True},
|
||||
]
|
||||
for pt in printer_types:
|
||||
existing = _lookup_binary(pt['relationshiptype'])
|
||||
if not existing:
|
||||
db.session.add(RelationshipType(**pt))
|
||||
elif pt.get('issingular') and not existing.issingular:
|
||||
# A site seeded before the flag existed keeps its row and gains the
|
||||
# rule; without this the upgrade leaves the old silent behaviour.
|
||||
existing.issingular = True
|
||||
db.session.flush()
|
||||
|
||||
# Seed `controls` propagation rails as M:N rows. controls -> partof
|
||||
# (declared; directional rail, not consumed yet) and controls -> Dualpath
|
||||
# (consumed; a dual-bay pair shares one controller so both bays carry
|
||||
# controls). Idempotent, resolved by name, skipped if a type is missing.
|
||||
# Seed propagation rails as M:N rows. controls -> partof (declared;
|
||||
# directional rail, not consumed yet) and controls -> Dualpath (consumed;
|
||||
# a dual-bay pair shares one controller so both bays carry controls).
|
||||
# The two printer rails are READ-TIME only: the create-time fan-out skips
|
||||
# directional through-types, and controls is directional, so a PC's own
|
||||
# rows keep beating what it inherits from the machine it controls.
|
||||
# Idempotent, resolved by name, skipped if a type is missing.
|
||||
from shopdb.core.models.relationship import RelationshipTypePropagation
|
||||
|
||||
def _seed_propagation(sourcename, throughname):
|
||||
@@ -541,15 +572,8 @@ def seed_reference_data():
|
||||
|
||||
_seed_propagation('controls', 'partof')
|
||||
_seed_propagation('controls', 'Dualpath')
|
||||
|
||||
# Default-printer link: a PC asset -> its default printer asset. Read by the
|
||||
# printer-installer endpoint (parity with classic apipcdefaultprinter.asp).
|
||||
# Attribute-style edge, not a position rail, so no propagation.
|
||||
if not _lookup_binary('defaultprinter'):
|
||||
db.session.add(RelationshipType(
|
||||
relationshiptype='defaultprinter',
|
||||
description='PC to its default printer (installer preselect, ADR-001)'
|
||||
))
|
||||
_seed_propagation('usesprinter', 'controls')
|
||||
_seed_propagation('defaultprinter', 'controls')
|
||||
|
||||
db.session.commit()
|
||||
click.echo(click.style("Reference data seeded.", fg='green'))
|
||||
|
||||
@@ -792,6 +792,27 @@ def create_asset_relationship():
|
||||
http_code=409
|
||||
)
|
||||
|
||||
# A SINGULAR type allows one active row per source, and setting a new one
|
||||
# REPLACES rather than refusing: "make this the default printer" means
|
||||
# exactly that, and a card that answered 409 would leave the user to find
|
||||
# and delete the old row first.
|
||||
#
|
||||
# Without this the schema is happy to hold two defaults - the unique
|
||||
# constraint is (source, target, type), so two different targets are two
|
||||
# valid rows - and the resolver takes the OLDEST, so the new default
|
||||
# silently loses.
|
||||
reltype = db.session.get(RelationshipType, type_id)
|
||||
replaced = 0
|
||||
if reltype is not None and getattr(reltype, 'issingular', False):
|
||||
others = AssetRelationship.query.filter(
|
||||
AssetRelationship.sourceassetid == source_id,
|
||||
AssetRelationship.relationshiptypeid == type_id,
|
||||
AssetRelationship.isactive == True,
|
||||
AssetRelationship.targetassetid != target_id).all()
|
||||
for row in others:
|
||||
row.isactive = False
|
||||
replaced += 1
|
||||
|
||||
# And it cannot relate BOTH WAYS on a directional type. Only one direction
|
||||
# can be true - a PC drives a machine, never the reverse - but the check
|
||||
# above is keyed on (source, target, type), so the inverse used to insert
|
||||
|
||||
@@ -24,6 +24,11 @@ class RelationshipType(BaseModel):
|
||||
# True: edge has a source->target meaning (controls, partof, Backup For).
|
||||
# False: symmetric link (Dualpath, connectedto, USB...) shown on the card
|
||||
# once per peer with no direction, both stored direction rows collapsed.
|
||||
# At most one ACTIVE relationship of this type per source asset. A PC has
|
||||
# one default printer; the (source, target, type) unique constraint cannot
|
||||
# express that, because two different targets are two different rows.
|
||||
issingular = db.Column(db.Boolean, nullable=False, default=False)
|
||||
|
||||
isdirectional = db.Column(
|
||||
db.Boolean,
|
||||
default=True,
|
||||
|
||||
@@ -66,7 +66,7 @@ PLUGIN_TABLE_OWNERS: dict[str, Iterable[str]] = {
|
||||
'printedparts': ('printeditems', 'printeditemtransactions',
|
||||
'printeditemfiles'),
|
||||
'printers': ('printertypes', 'printers', 'modelsupplies', 'printerdrivers',
|
||||
'printersupplyalerts'),
|
||||
'printersupplyalerts', 'printerobservedqueues'),
|
||||
'slides': ('tvslides',),
|
||||
'usb': ('usbdevicetypes', 'usbdevices', 'usbcheckouts'),
|
||||
'warranty': ('warranties', 'warrantyassets'),
|
||||
|
||||
Reference in New Issue
Block a user