Stop three ways the collector and the forms wrote things nobody asked for
A review of last week's device-identity work found these; two were writing bad data and one was reproduced against a live server before being fixed. AN UPDATE COULD BLANK AN ASSET NUMBER, on all six asset update paths. Create validates it and the column is NOT NULL, but the conflict check only runs when the value DIFFERS, and '' collides with nothing - so an empty assetnumber went straight through to a required column. This is the likely source of the assets found with no number: a form that loaded blank and was then saved. MACHINEFORM COULD LOAD BLANK AND LET YOU SAVE IT. One try/catch wrapped eight reference loads AND the machine fetch, so a single transient failure among them - one page of listAll() timing out during a collector cycle is enough - rejected the whole block and rendered a fully editable EDIT form with every field empty, the error banner far below next to Save. Typing an asset number and saving then wrote the blanks over a real machine. The record now loads in its own try, and a failure shows the reason INSTEAD of the form: an empty edit form is indistinguishable from a record whose fields are genuinely empty. NAMING A DEVICE THAT DID NOT RESOLVE STILL MINTED A TWIN. Both device paths warned "not linked" and then fell through to mint <HOST>-PARTMARKER or <HOST>-CMM - the hostname-derived twin the resolution order exists to prevent. The warning was true about the typo'd number and false about the twin. Naming a device is a commitment: if the name does not resolve, or resolves to the wrong kind of thing, link nothing and say so. Silence still means "work it out", so a bay with no file keeps the reuse-then-mint behaviour it always had. TWO PCS COULD BOTH HOLD ONE DEVICE, ACTIVELY, WITH NO WARNING. Verified against a live server: report as one host, then as another naming the same marker, and both controls rows stayed active. Neither device path had ever looked at who else held the target - only at links whose source was THIS PC - so a replaced PC kept its link forever and an asset-id.txt copied to a second bay claimed the device silently. It now reuses the machine link's rule rather than inventing a second one: an incumbent that has gone quiet past the claim window or been moved off In Use has yielded and is archived, never deleted; a live incumbent keeps the device and the challenger is recorded dormant. The swap test asserted the old behaviour and now asserts the new one, split in two: a live incumbent keeps it, and handover completes once the incumbent yields. Two other tests were passing while their names lied - the unknown-device one checked only that the typo'd asset was not created, not that nothing was linked, and it passed while a twin was minted beside it.
This commit is contained in:
@@ -790,7 +790,7 @@ class ComputersPlugin(BasePlugin):
|
||||
return bool(row) and row[0] == spec['typename']
|
||||
|
||||
def _ensure_device_rows(self, deviceasset, spec, pcasset, controls, label,
|
||||
warnings):
|
||||
warnings, active=True):
|
||||
"""Get-or-create the extension row and the PC -> device control link.
|
||||
|
||||
Shared by the named-device and minted paths: an adopted asset may have
|
||||
@@ -819,7 +819,7 @@ class ComputersPlugin(BasePlugin):
|
||||
targetassetid=deviceasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid).first()
|
||||
if controlrow is not None:
|
||||
controlrow.isactive = True
|
||||
controlrow.isactive = active
|
||||
if not controlrow.label:
|
||||
controlrow.label = label
|
||||
else:
|
||||
@@ -828,7 +828,58 @@ class ComputersPlugin(BasePlugin):
|
||||
targetassetid=deviceasset.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid,
|
||||
label=label,
|
||||
isactive=True))
|
||||
isactive=active))
|
||||
|
||||
def _device_incumbents_yield(self, deviceasset, pcasset, label, warnings):
|
||||
"""Settle a device already controlled by a DIFFERENT PC.
|
||||
|
||||
A physical instrument or marker hangs off one PC. Before this, both
|
||||
paths only ever looked at links whose source was THIS PC, so a bay
|
||||
naming a device another PC still holds produced two active `controls`
|
||||
rows with no warning - the replaced PC kept its link forever, and a
|
||||
file copied to a second bay claimed the device silently.
|
||||
|
||||
The machine link settled this years ago and this reuses its rule rather
|
||||
than inventing a second one: an incumbent that has gone quiet past the
|
||||
claim window, or been moved off In Use, has yielded, and its link is
|
||||
ARCHIVED (never deleted, so "which PC drove this in June" stays
|
||||
answerable). An incumbent still alive keeps the device, and the
|
||||
challenger is recorded dormant instead of contesting a link that can
|
||||
only have one holder.
|
||||
|
||||
Returns True when this PC may hold the device actively.
|
||||
"""
|
||||
from shopdb.api import AssetRelationship, RelationshipType, Asset
|
||||
|
||||
controls = RelationshipType.query.filter_by(
|
||||
relationshiptype='controls').first()
|
||||
if not controls or not deviceasset or not pcasset:
|
||||
return True
|
||||
|
||||
others = AssetRelationship.query.filter(
|
||||
AssetRelationship.targetassetid == deviceasset.assetid,
|
||||
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
|
||||
AssetRelationship.label == label,
|
||||
AssetRelationship.isactive.is_(True),
|
||||
AssetRelationship.sourceassetid != pcasset.assetid,
|
||||
).all()
|
||||
if not others:
|
||||
return True
|
||||
|
||||
holding = []
|
||||
for rel in others:
|
||||
if self._incumbent_has_yielded(rel.sourceassetid):
|
||||
rel.isactive = False
|
||||
else:
|
||||
holder = db.session.get(Asset, rel.sourceassetid)
|
||||
holding.append(holder.assetnumber if holder else rel.sourceassetid)
|
||||
|
||||
if holding:
|
||||
warnings.append(
|
||||
'{} is still controlled by {}; recorded but not activated'
|
||||
.format(deviceasset.assetnumber, ', '.join(str(h) for h in holding)))
|
||||
return False
|
||||
return True
|
||||
|
||||
def _sync_partmarker(self, comp, pctype, machinenumber, warnings,
|
||||
deviceid=None):
|
||||
@@ -892,6 +943,16 @@ class ComputersPlugin(BasePlugin):
|
||||
label = spec['label']
|
||||
|
||||
# --- 1. an explicitly named device wins over everything -------------
|
||||
#
|
||||
# NAMING A DEVICE IS A COMMITMENT. If the bay says which device it
|
||||
# drives and that name does not resolve, the answer is to link nothing
|
||||
# and say so - NOT to fall through and mint <HOST>-PARTMARKER, which is
|
||||
# the hostname-derived twin this whole path exists to prevent. The
|
||||
# warning used to be true about the typo'd number and false about the
|
||||
# twin: it warned, then minted anyway.
|
||||
#
|
||||
# Silence still means "work it out", so a bay with no file keeps the
|
||||
# reuse-then-mint behaviour it has always had.
|
||||
named = (deviceid or '').strip()
|
||||
namedasset = None
|
||||
if named:
|
||||
@@ -901,15 +962,16 @@ class ComputersPlugin(BasePlugin):
|
||||
# device that nobody can account for.
|
||||
warnings.append(
|
||||
'no asset for device {!r}; not linked'.format(named))
|
||||
elif not self._is_device_of_type(candidate, spec):
|
||||
return []
|
||||
if not self._is_device_of_type(candidate, spec):
|
||||
# The name resolved, but not to this pc-type's device. Refuse
|
||||
# and say which asset it hit, rather than filing an unrelated
|
||||
# asset under a device label the collector also owns.
|
||||
warnings.append(
|
||||
'asset {!r} is not a {}; not linked'.format(
|
||||
candidate.assetnumber, spec['typename']))
|
||||
else:
|
||||
namedasset = candidate
|
||||
return []
|
||||
namedasset = candidate
|
||||
|
||||
existing = AssetRelationship.query.filter(
|
||||
AssetRelationship.sourceassetid == pcasset.assetid,
|
||||
@@ -920,6 +982,11 @@ class ComputersPlugin(BasePlugin):
|
||||
or (existing[0] if existing else None)
|
||||
|
||||
if namedasset is not None:
|
||||
# The file is authoritative FOR THIS PC, but the device may still be
|
||||
# held by another one. Settle that first: a yielded incumbent is
|
||||
# archived, a live one keeps it and this link is recorded dormant.
|
||||
mayhold = self._device_incumbents_yield(
|
||||
namedasset, pcasset, label, warnings)
|
||||
# The file is authoritative. A PC that previously minted its own
|
||||
# marker and is now told the real one keeps only the named link;
|
||||
# the stale link is archived by the one-marker-per-PC sweep below.
|
||||
@@ -929,7 +996,7 @@ class ComputersPlugin(BasePlugin):
|
||||
if reuse is not None:
|
||||
reuse.isactive = True
|
||||
self._ensure_device_rows(markerasset, spec, pcasset, controls,
|
||||
label, warnings)
|
||||
label, warnings, active=mayhold)
|
||||
elif reuse:
|
||||
reuse.isactive = True
|
||||
markerasset = db.session.get(Asset, reuse.targetassetid)
|
||||
@@ -1280,10 +1347,14 @@ class ComputersPlugin(BasePlugin):
|
||||
candidate = self._asset_by_number(named)
|
||||
if candidate is None:
|
||||
# Warn rather than invent: a typo in the file must not mint a
|
||||
# phantom instrument that nobody can account for.
|
||||
# phantom instrument that nobody can account for. RETURN, do
|
||||
# not fall through - minting <HOST>-CMM here would create the
|
||||
# hostname-derived twin this resolution order exists to avoid,
|
||||
# while the warning claimed nothing was linked.
|
||||
warnings.append(
|
||||
'no asset for measuring tool {!r}; not linked'.format(named))
|
||||
elif candidate.assetid not in self._measuringtool_assetids():
|
||||
return []
|
||||
if candidate.assetid not in self._measuringtool_assetids():
|
||||
# The name resolved, but not to an instrument. measuringtool-id
|
||||
# .txt holding a machine number would otherwise link the PC to
|
||||
# that MACHINE under a measuring-tool label - a link that reads
|
||||
@@ -1292,8 +1363,8 @@ class ComputersPlugin(BasePlugin):
|
||||
warnings.append(
|
||||
'asset {!r} is not a measuring tool; not linked'.format(
|
||||
candidate.assetnumber))
|
||||
else:
|
||||
adopted = candidate
|
||||
return []
|
||||
adopted = candidate
|
||||
|
||||
# --- 2. a prior collector link (reactivate + retype) -----------------
|
||||
reuse = next((rel for rel in existing if rel.isactive), None) \
|
||||
@@ -1307,6 +1378,11 @@ class ComputersPlugin(BasePlugin):
|
||||
pcasset, controls, machinenumber)
|
||||
|
||||
if adopted is not None:
|
||||
# Settle any OTHER PC still holding this instrument first: a yielded
|
||||
# incumbent is archived, a live one keeps it and this link is
|
||||
# recorded dormant rather than becoming a second active holder.
|
||||
mayhold = self._device_incumbents_yield(
|
||||
adopted, pcasset, MEASURINGTOOL_LINK_ORIGIN, warnings)
|
||||
# CLAIM the existing link rather than adding a second one. A row for
|
||||
# (pc, tool, controls) usually already exists - that is how the tool
|
||||
# was found - and assetrelationships is unique on exactly that
|
||||
@@ -1330,13 +1406,14 @@ class ComputersPlugin(BasePlugin):
|
||||
# is ours from here, so a later cycle reuses it instead of
|
||||
# minting.
|
||||
claimed.label = MEASURINGTOOL_LINK_ORIGIN
|
||||
claimed.isactive = True
|
||||
claimed.isactive = mayhold
|
||||
else:
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=pcasset.assetid,
|
||||
targetassetid=adopted.assetid,
|
||||
relationshiptypeid=controls.relationshiptypeid,
|
||||
label=MEASURINGTOOL_LINK_ORIGIN))
|
||||
label=MEASURINGTOOL_LINK_ORIGIN,
|
||||
isactive=mayhold))
|
||||
if adopted.measuringtool and tooltype:
|
||||
adopted.measuringtool.measuringtooltypeid = \
|
||||
tooltype.measuringtooltypeid
|
||||
|
||||
Reference in New Issue
Block a user