Files
shopdb-flask/tests/test_plugins/test_knowledgebase_visibility.py
cproudlock 3324dbd91e
Some checks failed
CI / backend (push) Failing after 9s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 10s
CI / migrations-mysql (push) Failing after 7s
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.
2026-08-17 12:55:51 -04:00

107 lines
4.7 KiB
Python

"""What a knowledge base article's topic decides about seeing the article.
An article's topic is an Application. When that application is retired the
article describes something no longer in service, and showing it beside live
documentation reads as though it were current - so it does not show at all.
`isactive` is the ONLY property of an application that decides this. In
particular `ishidden`, which governs whether an application appears on the tiles
page, says nothing about whether it can be the subject of an article.
"""
import pytest
from shopdb.core.models import Application
from plugins.knowledgebase.models import KnowledgeBase
@pytest.fixture
def library(db):
"""One application of each interesting shape, with an article each."""
applications = {
'live': Application(appname='Live App', isinstallable=True,
ishidden=False, isactive=True),
'notinstallable': Application(appname='Manual App', isinstallable=False,
ishidden=False, isactive=True),
'hidden': Application(appname='Hidden App', isinstallable=False,
ishidden=True, isactive=True),
'retired': Application(appname='Retired App', isinstallable=True,
ishidden=False, isactive=False),
}
db.session.add_all(applications.values())
db.session.flush()
articles = {
'live': KnowledgeBase(shortdescription='Live runbook', linkurl='u',
appid=applications['live'].appid, clicks=3,
isactive=True),
'hidden': KnowledgeBase(shortdescription='Hidden runbook', linkurl='u',
appid=applications['hidden'].appid, clicks=2,
isactive=True),
'retired': KnowledgeBase(shortdescription='Retired runbook', linkurl='u',
appid=applications['retired'].appid, clicks=5,
isactive=True),
'topicless': KnowledgeBase(shortdescription='General note', linkurl='u',
appid=None, clicks=1, isactive=True),
}
db.session.add_all(articles.values())
db.session.commit()
return {'applications': applications, 'articles': articles}
def titles(client, url='/api/knowledgebase'):
return sorted(row['shortdescription']
for row in client.get(url).get_json()['data'])
def test_an_article_about_a_retired_application_does_not_show(client, library):
assert 'Retired runbook' not in titles(client)
def test_an_article_with_no_topic_still_shows(client, library):
"""A null topic is not a retired one. Not every article is about an
application, and those must not be collateral damage."""
assert 'General note' in titles(client)
def test_a_hidden_application_is_still_a_valid_topic(client, library):
"""ishidden keeps an application off the tiles page. It says nothing about
documentation, and isactive is the only filter that applies here."""
assert 'Hidden runbook' in titles(client)
def test_searching_a_retired_application_name_finds_nothing(client, library):
"""The topic search matched on application name without checking isactive,
which surfaced retired articles and printed the retired application as their
subject."""
assert titles(client, '/api/knowledgebase?search=Retired App') == []
def test_searching_a_title_does_not_resurrect_a_retired_article(client, library):
"""Hiding it from the list and finding it by title would be no rule at all."""
found = titles(client, '/api/knowledgebase?search=runbook')
assert 'Retired runbook' not in found
assert 'Live runbook' in found
def test_the_counts_agree_with_the_list(client, library):
"""A total that includes articles nobody can see is a total nobody can
reconcile against the page."""
visible = titles(client)
stats = client.get('/api/knowledgebase/stats').get_json()['data']
total = stats.get('total_articles', stats.get('totalarticles'))
clicks = stats.get('total_clicks', stats.get('totalclicks'))
assert total == len(visible) == 3
# 3 + 2 + 1 from the live, hidden-topic and topicless articles; the retired
# article's 5 clicks are excluded along with the article.
assert clicks == 6
def test_the_topic_list_offers_every_active_application(client, library):
"""What the article form's topic dropdown fetches. Installable or not,
hidden or not - only retired is excluded."""
offered = sorted(row['appname'] for row in client.get(
'/api/applications?perpage=100&showhidden=true').get_json()['data'])
assert offered == ['Hidden App', 'Live App', 'Manual App']
assert 'Retired App' not in offered