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:
116
tests/test_core/test_singular_relationship_types.py
Normal file
116
tests/test_core/test_singular_relationship_types.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""A relationship type that means "at most one of these per asset".
|
||||
|
||||
A PC has ONE default printer. The unique constraint on assetrelationships is
|
||||
(source, target, type), which accepts two DIFFERENT defaults quite happily - and
|
||||
the resolver takes the oldest, so setting a new default through the generic
|
||||
relationships card left the OLD one winning, with nothing to show why.
|
||||
|
||||
The rule lives on the type, not in the printers plugin: core's create path is
|
||||
where every hand-made link passes, and the next type meaning "exactly one" gets
|
||||
it for free.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Asset, AssetType, AssetRelationship, RelationshipType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scene(db):
|
||||
assettype = AssetType.query.filter_by(assettype='printer').first()
|
||||
if not assettype:
|
||||
assettype = AssetType(assettype='printer', pluginname='printers',
|
||||
tablename='printers', description='p')
|
||||
db.session.add(assettype)
|
||||
db.session.flush()
|
||||
|
||||
pc = Asset(assetnumber='PC-SINGULAR', assettypeid=assettype.assettypeid, isactive=True)
|
||||
first = Asset(assetnumber='PRN-A', assettypeid=assettype.assettypeid, isactive=True)
|
||||
second = Asset(assetnumber='PRN-B', assettypeid=assettype.assettypeid, isactive=True)
|
||||
db.session.add_all([pc, first, second])
|
||||
|
||||
singular = RelationshipType.query.filter_by(relationshiptype='defaultprinter').first()
|
||||
if not singular:
|
||||
singular = RelationshipType(relationshiptype='defaultprinter',
|
||||
description='default', isdirectional=True)
|
||||
db.session.add(singular)
|
||||
singular.issingular = True
|
||||
|
||||
plural = RelationshipType.query.filter_by(relationshiptype='usesprinter').first()
|
||||
if not plural:
|
||||
plural = RelationshipType(relationshiptype='usesprinter',
|
||||
description='installed here', isdirectional=True)
|
||||
db.session.add(plural)
|
||||
plural.issingular = False
|
||||
|
||||
db.session.commit()
|
||||
return {'pc': pc, 'first': first, 'second': second,
|
||||
'singular': singular, 'plural': plural}
|
||||
|
||||
|
||||
def _active(pc, reltype):
|
||||
return AssetRelationship.query.filter_by(
|
||||
sourceassetid=pc.assetid, relationshiptypeid=reltype.relationshiptypeid,
|
||||
isactive=True).all()
|
||||
|
||||
|
||||
def _link(client, headers, source, target, reltype):
|
||||
return client.post('/api/assets/relationships', headers=headers, json={
|
||||
'sourceassetid': source.assetid,
|
||||
'targetassetid': target.assetid,
|
||||
'relationshiptypeid': reltype.relationshiptypeid,
|
||||
})
|
||||
|
||||
|
||||
def test_setting_a_second_default_replaces_the_first(client, db, scene, auth_headers):
|
||||
"""The bug this exists for. Both POSTs succeed today and the table then
|
||||
holds two defaults."""
|
||||
assert _link(client, auth_headers, scene['pc'], scene['first'], scene['singular']).status_code == 201
|
||||
assert _link(client, auth_headers, scene['pc'], scene['second'], scene['singular']).status_code == 201
|
||||
|
||||
rows = _active(scene['pc'], scene['singular'])
|
||||
assert len(rows) == 1
|
||||
assert rows[0].targetassetid == scene['second'].assetid
|
||||
|
||||
|
||||
def test_the_replaced_row_is_soft_deleted_not_destroyed(client, db, scene, auth_headers):
|
||||
"""Consistent with every other delete here, and it keeps the history."""
|
||||
_link(client, auth_headers, scene['pc'], scene['first'], scene['singular'])
|
||||
_link(client, auth_headers, scene['pc'], scene['second'], scene['singular'])
|
||||
|
||||
old = AssetRelationship.query.filter_by(
|
||||
sourceassetid=scene['pc'].assetid,
|
||||
targetassetid=scene['first'].assetid,
|
||||
relationshiptypeid=scene['singular'].relationshiptypeid).first()
|
||||
assert old is not None
|
||||
assert old.isactive is False
|
||||
|
||||
|
||||
def test_a_plural_type_is_untouched(client, db, scene, auth_headers):
|
||||
"""usesprinter means "installed here" and a bay has several. If the rule
|
||||
leaked to every type, assigning a second printer would remove the first."""
|
||||
assert _link(client, auth_headers, scene['pc'], scene['first'], scene['plural']).status_code == 201
|
||||
assert _link(client, auth_headers, scene['pc'], scene['second'], scene['plural']).status_code == 201
|
||||
assert len(_active(scene['pc'], scene['plural'])) == 2
|
||||
|
||||
|
||||
def test_setting_the_same_default_twice_is_still_a_conflict(client, db, scene, auth_headers):
|
||||
"""Replacing is for a DIFFERENT target. The same link twice is the existing
|
||||
duplicate case and must keep answering 409, or the card loses its only
|
||||
signal that nothing changed."""
|
||||
assert _link(client, auth_headers, scene['pc'], scene['first'], scene['singular']).status_code == 201
|
||||
assert _link(client, auth_headers, scene['pc'], scene['first'], scene['singular']).status_code == 409
|
||||
assert len(_active(scene['pc'], scene['singular'])) == 1
|
||||
|
||||
|
||||
def test_another_asset_keeps_its_own_default(client, db, scene, auth_headers):
|
||||
"""The rule is per SOURCE. One PC's default must not disturb another's."""
|
||||
other = Asset(assetnumber='PC-OTHER', assettypeid=scene['pc'].assettypeid, isactive=True)
|
||||
db.session.add(other)
|
||||
db.session.commit()
|
||||
|
||||
_link(client, auth_headers, scene['pc'], scene['first'], scene['singular'])
|
||||
_link(client, auth_headers, other, scene['second'], scene['singular'])
|
||||
|
||||
assert len(_active(scene['pc'], scene['singular'])) == 1
|
||||
assert len(_active(other, scene['singular'])) == 1
|
||||
Reference in New Issue
Block a user