Buildings and levels for the floor map, and make every identifier searchable
The map was one picture of one floor. A second floor was added, the blueprint changed size, and machines moved, so a position now records WHICH DRAWING its coordinates belong to. Buildings and levels (ADR-017). Each level owns its blueprint per theme and its own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the site. A position whose level is unknown renders "level unknown" and is never drawn on the default level, because a marker on the wrong floor plan looks entirely correct while pointing at the wrong place. Repositioning in bulk: filter by unplaced, needs-review or level, search, place, confirm. Landmark recalibration solves the transform PER AXIS from landmark pairs and never from image dimensions - the canvas grew taller without rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong everywhere. It defaults to a dry run, reports what would land off the drawing, snapshots before applying, and clears mapverifiedat because a transform is a guess awaiting review. Snapshots restore, including the level and the review state, and a restore snapshots first so an undo is undoable. Search: gaugelabreference was matched only for measuring tools and maintenancereference was matched nowhere at all, for any asset type, while Settings happily offers both identifiers on machines and PCs. A tag an operator is told to record has to be findable or it is a write-only field. USB devices and printed items were unreachable from search entirely - neither is an asset, so the generic asset search could not see them and no searcher existed; they now match on serial, asset tag, label, bin code and gage-lab tag, honouring isactive, with Settings toggles and result labels to match. The retired-application rule was half a rule: GET /api/knowledgebase hid articles whose topic application is retired while global search still returned them and printed the retired application as the subject. A filter is only real if every path that reaches the row applies it. Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location gained levelid, and resolve_asset_position returns the levelid belonging to whichever source supplied the coordinates. The five plugins that write a map position are re-pinned. The install-list text format gained levelid as a NINTH field, appended, because the shipped Pascal installer reads fields 0-7 by index. That installer still compiles in one drawing's dimensions and bundles one blueprint, so its map is accurate for the default level only; /api/maplevels is deliberately unauthenticated so it can read both at runtime once rebuilt. Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps. Migration 7d33 converts an existing single-map site into one building and one default level carrying the old map_* settings, then assigns every placed asset and location to it. Nothing moves on screen. Old settings rows are kept so a rollback still finds them. Verified end to end on MySQL 5.6 from a production-shaped database.
This commit is contained in:
236
tests/test_core/test_map_transform.py
Normal file
236
tests/test_core/test_map_transform.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""The landmark transform, and the wrong answer it must not give.
|
||||
|
||||
The case this exists for: a site's blueprint goes from 3300x2550 to 3308x4000
|
||||
because a second level was added below the first. The existing floor is drawn at
|
||||
the same scale - so the correct transform is identity, or identity plus an
|
||||
offset. A transform derived from the ratio of image dimensions would scale Y by
|
||||
4000/2550 = 1.5686 and be wrong for every marker on the level, while looking
|
||||
like arithmetic somebody had thought about.
|
||||
|
||||
So the first test here is not that the transform works. It is that it does not
|
||||
produce that particular plausible wrong answer.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Asset, AssetType, Building, MapLevel
|
||||
from shopdb.core.api.mappositions import derive_transform, solve_axis
|
||||
|
||||
|
||||
# -- the arithmetic, with no app or database involved ------------------------
|
||||
|
||||
def test_two_landmarks_give_identity_when_the_drawing_did_not_move():
|
||||
"""A canvas that grew taller with the old floor untouched: scale 1, no offset.
|
||||
|
||||
This is the assertion that rules out the dimension-derived answer. Nothing
|
||||
about these landmarks mentions 2550 or 4000, and the result must not either.
|
||||
"""
|
||||
transform, problem = derive_transform([
|
||||
{'fromx': 100, 'fromy': 100, 'tox': 100, 'toy': 100},
|
||||
{'fromx': 3000, 'fromy': 2000, 'tox': 3000, 'toy': 2000},
|
||||
])
|
||||
assert problem is None
|
||||
assert transform['scalex'] == pytest.approx(1.0)
|
||||
assert transform['scaley'] == pytest.approx(1.0)
|
||||
assert transform['offsetx'] == pytest.approx(0.0)
|
||||
assert transform['offsety'] == pytest.approx(0.0)
|
||||
# The wrong answer, stated so a regression names itself.
|
||||
assert transform['scaley'] != pytest.approx(4000 / 2550, abs=0.01)
|
||||
|
||||
|
||||
def test_a_level_added_above_gives_a_pure_y_offset():
|
||||
"""Existing floor pushed down by 1450px: identity scale, offset 1450."""
|
||||
transform, problem = derive_transform([
|
||||
{'fromx': 100, 'fromy': 100, 'tox': 100, 'toy': 1550},
|
||||
{'fromx': 3000, 'fromy': 2000, 'tox': 3000, 'toy': 3450},
|
||||
])
|
||||
assert problem is None
|
||||
assert transform['scaley'] == pytest.approx(1.0)
|
||||
assert transform['offsety'] == pytest.approx(1450.0)
|
||||
assert transform['scalex'] == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_a_genuine_rescale_is_derived_per_axis():
|
||||
"""X and Y can scale differently, which one uniform factor cannot express."""
|
||||
transform, _ = derive_transform([
|
||||
{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 0},
|
||||
{'fromx': 1000, 'fromy': 1000, 'tox': 2000, 'toy': 1500},
|
||||
])
|
||||
assert transform['scalex'] == pytest.approx(2.0)
|
||||
assert transform['scaley'] == pytest.approx(1.5)
|
||||
|
||||
|
||||
def test_a_third_landmark_averages_measurement_error():
|
||||
"""Points picked by eye carry a few pixels of error; more points cancel it
|
||||
rather than accumulating it."""
|
||||
transform, _ = derive_transform([
|
||||
{'fromx': 0, 'fromy': 0, 'tox': 2, 'toy': -2},
|
||||
{'fromx': 1000, 'fromy': 1000, 'tox': 999, 'toy': 1001},
|
||||
{'fromx': 2000, 'fromy': 2000, 'tox': 2001, 'toy': 1999},
|
||||
])
|
||||
assert transform['scalex'] == pytest.approx(1.0, abs=0.01)
|
||||
assert transform['scaley'] == pytest.approx(1.0, abs=0.01)
|
||||
|
||||
|
||||
def test_landmarks_on_one_column_are_refused_not_guessed():
|
||||
"""Two points with the same X cannot determine a horizontal scale.
|
||||
|
||||
Returning 1.0 here would look like success and silently leave X unscaled,
|
||||
which is the failure a user would discover after the write.
|
||||
"""
|
||||
transform, problem = derive_transform([
|
||||
{'fromx': 500, 'fromy': 100, 'tox': 600, 'toy': 100},
|
||||
{'fromx': 500, 'fromy': 2000, 'tox': 600, 'toy': 2000},
|
||||
])
|
||||
assert transform is None
|
||||
assert 'X' in problem
|
||||
|
||||
|
||||
def test_one_landmark_is_not_enough():
|
||||
transform, problem = derive_transform([
|
||||
{'fromx': 1, 'fromy': 1, 'tox': 2, 'toy': 2}])
|
||||
assert transform is None
|
||||
assert 'two landmarks' in problem
|
||||
|
||||
|
||||
def test_solve_axis_reports_an_undetermined_axis():
|
||||
assert solve_axis([(5, 9), (5, 11)]) is None
|
||||
assert solve_axis([(0, 0), (10, 20)]) == (2.0, 0.0)
|
||||
|
||||
|
||||
# -- end to end, against the endpoints --------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def floor(db):
|
||||
"""One building, two levels sized like the real before and after."""
|
||||
assettype = AssetType.query.filter_by(assettype='machine').first()
|
||||
if not assettype:
|
||||
assettype = AssetType(assettype='machine')
|
||||
db.session.add(assettype)
|
||||
db.session.flush()
|
||||
building = Building(buildingname='Main')
|
||||
db.session.add(building)
|
||||
db.session.flush()
|
||||
ground = MapLevel(buildingid=building.buildingid, levelname='Ground floor',
|
||||
sortorder=0, isdefault=True, mapwidth=3300, mapheight=2550)
|
||||
second = MapLevel(buildingid=building.buildingid, levelname='Second floor',
|
||||
sortorder=1, mapwidth=3308, mapheight=4000)
|
||||
db.session.add_all([ground, second])
|
||||
db.session.flush()
|
||||
assets = []
|
||||
for index, (x, y) in enumerate([(100, 100), (1500, 1200), (3200, 2500)]):
|
||||
asset = Asset(assetnumber='M%03d' % index, assettypeid=assettype.assettypeid,
|
||||
mapx=x, mapy=y, levelid=ground.levelid,
|
||||
mapverifiedat=datetime(2026, 1, 1))
|
||||
db.session.add(asset)
|
||||
assets.append(asset)
|
||||
db.session.commit()
|
||||
return {'ground': ground, 'second': second, 'assets': assets}
|
||||
|
||||
|
||||
def _transform(client, headers, **body):
|
||||
return client.post('/api/mappositions/transform', json=body, headers=headers)
|
||||
|
||||
|
||||
def test_a_dry_run_writes_nothing(client, db, floor, auth_headers):
|
||||
before = [(a.mapx, a.mapy, a.mapverifiedat) for a in floor['assets']]
|
||||
resp = _transform(client, auth_headers,
|
||||
levelid=floor['ground'].levelid,
|
||||
landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1450},
|
||||
{'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2450}])
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_json()['data']
|
||||
assert body['dryrun'] is True
|
||||
assert body['assetcount'] == 3
|
||||
db.session.expire_all()
|
||||
after = [(a.mapx, a.mapy, a.mapverifiedat)
|
||||
for a in Asset.query.order_by(Asset.assetid).all()]
|
||||
assert after == before
|
||||
|
||||
|
||||
def test_applying_moves_markers_clears_review_and_snapshots(
|
||||
client, db, floor, auth_headers):
|
||||
resp = _transform(client, auth_headers,
|
||||
levelid=floor['ground'].levelid, dryrun=False,
|
||||
landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1450},
|
||||
{'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2450}])
|
||||
assert resp.status_code == 200
|
||||
body = resp.get_json()['data']
|
||||
assert body['dryrun'] is False
|
||||
assert body['snapshotid']
|
||||
|
||||
db.session.expire_all()
|
||||
moved = Asset.query.filter_by(assetnumber='M000').first()
|
||||
assert (moved.mapx, moved.mapy) == (100, 1550)
|
||||
# A transform is a guess, so the review state it had is gone.
|
||||
assert moved.mapverifiedat is None
|
||||
|
||||
|
||||
def test_markers_pushed_off_the_canvas_are_counted(client, db, floor, auth_headers):
|
||||
"""A marker at y=2500 offset by 1600 lands at 4100, past the 4000px canvas.
|
||||
|
||||
Worth checking because a bad landmark pair is most likely to show up as
|
||||
markers leaving the drawing entirely, and a preview that does not count them
|
||||
lets that through.
|
||||
"""
|
||||
resp = _transform(client, auth_headers,
|
||||
levelid=floor['ground'].levelid,
|
||||
tolevelid=floor['second'].levelid,
|
||||
landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1600},
|
||||
{'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2600}])
|
||||
body = resp.get_json()['data']
|
||||
assert body['outofboundscount'] == 1
|
||||
offender = next(m for m in body['moves'] if m['outofbounds'])
|
||||
assert offender['toy'] > floor['second'].mapheight
|
||||
|
||||
|
||||
def test_restore_puts_positions_and_review_state_back(
|
||||
client, db, floor, auth_headers):
|
||||
original = {a.assetnumber: (a.mapx, a.mapy, a.mapverifiedat)
|
||||
for a in floor['assets']}
|
||||
applied = _transform(client, auth_headers,
|
||||
levelid=floor['ground'].levelid, dryrun=False,
|
||||
landmarks=[{'fromx': 0, 'fromy': 0, 'tox': 0, 'toy': 1450},
|
||||
{'fromx': 1000, 'fromy': 1000, 'tox': 1000, 'toy': 2450}])
|
||||
snapshotid = applied.get_json()['data']['snapshotid']
|
||||
|
||||
resp = client.post('/api/mappositions/snapshots/%d/restore' % snapshotid,
|
||||
headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['data']['restored'] == 3
|
||||
|
||||
db.session.expire_all()
|
||||
for asset in Asset.query.all():
|
||||
assert (asset.mapx, asset.mapy, asset.mapverifiedat) == \
|
||||
original[asset.assetnumber]
|
||||
|
||||
|
||||
def test_a_position_without_a_level_is_refused(client, db, floor, auth_headers):
|
||||
"""The rule the whole model rests on: a position with no level cannot be
|
||||
stored, because it cannot be rendered."""
|
||||
resp = client.post('/api/mappositions/positions', json={
|
||||
'positions': [{'assetid': floor['assets'][0].assetid,
|
||||
'mapx': 10, 'mapy': 20}]}, headers=auth_headers)
|
||||
assert resp.status_code == 400
|
||||
assert 'levelid' in resp.get_json()['data']['error']['message']
|
||||
|
||||
|
||||
def test_placing_by_hand_counts_as_review(client, db, floor, auth_headers):
|
||||
asset = floor['assets'][0]
|
||||
client.post('/api/mappositions/verify',
|
||||
json={'assetids': [asset.assetid], 'unverify': True},
|
||||
headers=auth_headers)
|
||||
db.session.expire_all()
|
||||
assert Asset.query.get(asset.assetid).mapverifiedat is None
|
||||
|
||||
resp = client.post('/api/mappositions/positions', json={
|
||||
'positions': [{'assetid': asset.assetid, 'mapx': 42, 'mapy': 43,
|
||||
'levelid': floor['second'].levelid}]},
|
||||
headers=auth_headers)
|
||||
assert resp.status_code == 200
|
||||
db.session.expire_all()
|
||||
moved = Asset.query.get(asset.assetid)
|
||||
assert (moved.mapx, moved.mapy, moved.levelid) == (42, 43, floor['second'].levelid)
|
||||
assert moved.mapverifiedat is not None
|
||||
201
tests/test_core/test_search_coverage.py
Normal file
201
tests/test_core/test_search_coverage.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""What global search can reach, and what it must not return.
|
||||
|
||||
Two plugins own records that are NOT assets - USB devices and printed items -
|
||||
so the generic asset search cannot see them, and nothing else did either: those
|
||||
records were unreachable from search entirely while their list pages existed and
|
||||
their labels were printed on physical bins and sticks.
|
||||
|
||||
The other half of this file is the retired-application rule. Hiding an article
|
||||
about a decommissioned application from the plugin's own listing while global
|
||||
search still returned it is not a rule at all - the article stayed two
|
||||
keystrokes away, and the result printed the retired application as its subject.
|
||||
A filter is only real if every path that reaches the row applies it.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Application
|
||||
|
||||
|
||||
def search(client, query):
|
||||
"""Global search rows for a query."""
|
||||
resp = client.get(f'/api/search?q={query}')
|
||||
assert resp.status_code == 200, resp.get_json()
|
||||
return resp.get_json()['data']['results']
|
||||
|
||||
|
||||
def titles(client, query, resulttype=None):
|
||||
return [r['title'] for r in search(client, query)
|
||||
if resulttype is None or r['type'] == resulttype]
|
||||
|
||||
|
||||
# --- USB devices (usb plugin, not an asset) ---------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def usbdevices(db):
|
||||
"""One live device and one deactivated one, distinctive enough that only the
|
||||
USB searcher can match them."""
|
||||
from plugins.usb.models import USBDevice
|
||||
live = USBDevice(serialnumber='ZZSERIAL1234', label='Loaner stick 4',
|
||||
assetnumber='USB-0004', productname='Kingston DataTraveler',
|
||||
isactive=True)
|
||||
retired = USBDevice(serialnumber='ZZSERIAL9999', label='Dead stick',
|
||||
assetnumber='USB-0009', productname='Kingston DataTraveler',
|
||||
isactive=False)
|
||||
db.session.add_all([live, retired])
|
||||
db.session.commit()
|
||||
return {'live': live.usbdeviceid, 'retired': retired.usbdeviceid}
|
||||
|
||||
|
||||
def test_a_usb_device_is_found_by_serial(client, usbdevices):
|
||||
"""The serial is what is printed on the stick in someone's hand."""
|
||||
rows = [r for r in search(client, 'ZZSERIAL1234') if r['type'] == 'usb_device']
|
||||
assert len(rows) == 1
|
||||
assert rows[0]['id'] == usbdevices['live']
|
||||
assert rows[0]['url'] == f"/usb/{usbdevices['live']}"
|
||||
# An exact serial is the strongest possible match for this domain.
|
||||
assert rows[0]['relevance'] == 100
|
||||
|
||||
|
||||
def test_a_usb_device_is_found_by_label_and_asset_tag(client, usbdevices):
|
||||
assert 'Loaner stick 4' in titles(client, 'Loaner', 'usb_device')
|
||||
assert 'Loaner stick 4' in titles(client, 'USB-0004', 'usb_device')
|
||||
|
||||
|
||||
def test_a_deactivated_usb_device_is_not_found(client, usbdevices):
|
||||
assert titles(client, 'ZZSERIAL9999', 'usb_device') == []
|
||||
# And it does not ride along on a query that matches both devices.
|
||||
assert 'Dead stick' not in titles(client, 'Kingston', 'usb_device')
|
||||
|
||||
|
||||
def test_a_usb_holder_is_not_searchable_by_name(client, db):
|
||||
"""currentusername records who holds a device. Making search a way to list
|
||||
what a named person checked out is a different feature, deliberately not
|
||||
built into this searcher."""
|
||||
from plugins.usb.models import USBDevice
|
||||
db.session.add(USBDevice(serialnumber='ZZSERIAL5555', label='Held stick',
|
||||
currentusername='Distinctivesurname', isactive=True))
|
||||
db.session.commit()
|
||||
assert titles(client, 'Distinctivesurname', 'usb_device') == []
|
||||
|
||||
|
||||
# --- Printed items (printedparts plugin, not an asset) ----------------------
|
||||
|
||||
@pytest.fixture
|
||||
def printeditems(db):
|
||||
from plugins.printedparts.models import PrintedItem
|
||||
live = PrintedItem(itemcode='3DP-8001', gagelabtag='WJRP8001',
|
||||
itemname='Fixture clamp', itemdescription='Holds a part',
|
||||
binlocation='Bin 12', quantityonhand=4,
|
||||
lowstockthreshold=2, isactive=True)
|
||||
retired = PrintedItem(itemcode='3DP-8009', gagelabtag='WJRP8009',
|
||||
itemname='Obsolete clamp', quantityonhand=0,
|
||||
lowstockthreshold=1, isactive=False)
|
||||
db.session.add_all([live, retired])
|
||||
db.session.commit()
|
||||
return {'live': live.printeditemid, 'retired': retired.printeditemid}
|
||||
|
||||
|
||||
def test_a_printed_item_is_found_by_bin_code(client, printeditems):
|
||||
"""itemcode is the bin label someone reads off the shelf."""
|
||||
rows = [r for r in search(client, '3DP-8001') if r['type'] == 'printed_item']
|
||||
assert len(rows) == 1
|
||||
assert rows[0]['id'] == printeditems['live']
|
||||
assert rows[0]['url'] == f"/printedparts/{printeditems['live']}"
|
||||
assert rows[0]['relevance'] == 100
|
||||
|
||||
|
||||
def test_a_printed_item_is_found_by_gage_lab_tag(client, printeditems):
|
||||
rows = [r for r in search(client, 'WJRP8001') if r['type'] == 'printed_item']
|
||||
assert [r['title'] for r in rows] == ['Fixture clamp']
|
||||
|
||||
|
||||
def test_a_printed_item_is_found_by_name(client, printeditems):
|
||||
assert 'Fixture clamp' in titles(client, 'Fixture clamp', 'printed_item')
|
||||
|
||||
|
||||
def test_a_printed_item_subtitle_locates_it(client, printeditems):
|
||||
"""A hit is only useful if it says where to go and get the part."""
|
||||
rows = [r for r in search(client, '3DP-8001') if r['type'] == 'printed_item']
|
||||
assert 'Bin 12' in rows[0]['subtitle']
|
||||
|
||||
|
||||
def test_a_deactivated_printed_item_is_not_found(client, printeditems):
|
||||
assert titles(client, '3DP-8009', 'printed_item') == []
|
||||
assert 'Obsolete clamp' not in titles(client, 'clamp', 'printed_item')
|
||||
|
||||
|
||||
# --- The retired-application rule reaches global search too ----------------
|
||||
|
||||
@pytest.fixture
|
||||
def kbarticles(db):
|
||||
from plugins.knowledgebase.models import KnowledgeBase
|
||||
live = Application(appname='Livesearchapp', isactive=True)
|
||||
retired = Application(appname='Retiredsearchapp', isactive=False)
|
||||
db.session.add_all([live, retired])
|
||||
db.session.flush()
|
||||
db.session.add_all([
|
||||
KnowledgeBase(shortdescription='Zzrunbook for the live one', linkurl='u',
|
||||
keywords='zzkeyword', appid=live.appid, isactive=True),
|
||||
KnowledgeBase(shortdescription='Zzrunbook for the retired one', linkurl='u',
|
||||
keywords='zzkeyword', appid=retired.appid, isactive=True),
|
||||
KnowledgeBase(shortdescription='Zzrunbook with no topic', linkurl='u',
|
||||
keywords='zzkeyword', appid=None, isactive=True),
|
||||
])
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def test_global_search_hides_an_article_about_a_retired_application(client, kbarticles):
|
||||
found = titles(client, 'zzkeyword', 'knowledgebase')
|
||||
assert 'Zzrunbook for the retired one' not in found
|
||||
assert 'Zzrunbook for the live one' in found
|
||||
|
||||
|
||||
def test_global_search_keeps_an_article_with_no_topic(client, kbarticles):
|
||||
"""A null topic is not a retired one."""
|
||||
assert 'Zzrunbook with no topic' in titles(client, 'zzkeyword', 'knowledgebase')
|
||||
|
||||
|
||||
def test_global_search_hides_a_retired_application_itself(client, kbarticles):
|
||||
"""The application domain has always filtered isactive; pinned here so the
|
||||
two rules stay together and cannot drift apart again."""
|
||||
assert titles(client, 'Retiredsearchapp', 'application') == []
|
||||
assert titles(client, 'Livesearchapp', 'application') == ['Livesearchapp']
|
||||
|
||||
|
||||
# --- The optional asset identifiers (ADR-001) -------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def taggedmachine(client, db, auth_headers):
|
||||
"""A machine carrying both optional identifiers.
|
||||
|
||||
A MACHINE on purpose, not a measuring tool: gaugelabreference was searchable
|
||||
only through the measuring-tools searcher, so the same tag on a machine
|
||||
matched nothing even though Settings offers the identifier for machines.
|
||||
"""
|
||||
from shopdb.core.models import AssetType, Asset
|
||||
if not AssetType.query.filter_by(assettype='machine').first():
|
||||
db.session.add(AssetType(assettype='machine', pluginname='machines',
|
||||
tablename='machines', description='m'))
|
||||
db.session.commit()
|
||||
resp = client.post('/api/machines', json={'assetnumber': 'ZZMACH01'},
|
||||
headers=auth_headers)
|
||||
assert resp.status_code == 201, resp.get_json()
|
||||
asset = Asset.query.filter_by(assetnumber='ZZMACH01').first()
|
||||
asset.gaugelabreference = 'ZZGAUGE777'
|
||||
asset.maintenancereference = 'ZZMAINT888'
|
||||
db.session.commit()
|
||||
return asset.assetid
|
||||
|
||||
|
||||
def test_a_machine_is_found_by_its_gauge_lab_reference(client, taggedmachine):
|
||||
rows = [r for r in search(client, 'ZZGAUGE777') if r['id'] == taggedmachine
|
||||
or r['title'] == 'ZZMACH01']
|
||||
assert rows, 'a gauge-lab tag on a machine matched nothing'
|
||||
|
||||
|
||||
def test_a_machine_is_found_by_its_maintenance_reference(client, taggedmachine):
|
||||
"""maintenancereference was searched by nothing at all, for any asset
|
||||
type - a field the UI collects and search could not find."""
|
||||
rows = [r for r in search(client, 'ZZMAINT888') if r['title'] == 'ZZMACH01']
|
||||
assert rows, 'a maintenance reference matched nothing'
|
||||
Reference in New Issue
Block a user