Buildings and levels for the floor map, and make every identifier searchable

This commit is contained in:
2026-08-17 13:03:54 -04:00
parent 47fb65fdd2
commit fbd99041b0
54 changed files with 4173 additions and 880 deletions

View File

@@ -0,0 +1,106 @@
"""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

View File

@@ -66,7 +66,14 @@ def test_install_list_vendorname_resolves_via_model(client, db, auth_headers,
def test_install_list_text_format(client, db, auth_headers, printer_assettype):
"""format=text returns one pipe-delimited line per printer (fixed field
order) so the Inno installers split() instead of parsing JSON."""
order) so the Inno installers split() instead of parsing JSON.
FIELD ORDER 0..7 IS FROZEN. The shipped installer reads them by index
(`GetField(Line, 6)` for mapx), so a field inserted anywhere but the end
silently shifts every later one: the installer would read a model number as
a coordinate and still run. levelid is appended as field 8, which installers
built before levels existed simply never read.
"""
client.post('/api/printers', json={
'assetnumber': 'CSF01-HP', 'windowsname': 'CSF01-HP', 'hostname': 'wjprn01',
'mapx': 120, 'mapy': 240,
@@ -80,10 +87,14 @@ def test_install_list_text_format(client, db, auth_headers, printer_assettype):
assert line is not None
cols = line.split('|')
# printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy
assert len(cols) == 8
# |levelid
assert len(cols) == 9
assert cols[1] == 'CSF01-HP' # windowsname (falls back to assetnumber name)
assert cols[4] == 'wjprn01' # hostname
assert cols[6] == '120' and cols[7] == '240'
# levelid last, and empty rather than absent when the printer has no level:
# the field count must not vary between rows or a positional split breaks.
assert cols[8] == ''
def test_install_list_excludes_usb_only_printer(client, db, auth_headers,