Per-type board rows with shared categories, a network device IP field, and two dark-mode dialogs

This commit is contained in:
2026-08-07 10:43:32 -04:00
parent bcf7a009f8
commit 7c5b1bc74e
12 changed files with 1154 additions and 829 deletions

View File

@@ -0,0 +1,117 @@
"""Tests for notificationtypes.boardcategory (shared row on the shopfloor board).
The board grouped cards by display style alone, so every type set to grid landed
in the Recertification row and every carousel type in Recognition's, under a
heading naming someone else's type. Each type now gets a row of its own, and
this column is the opt-in for the other direction: Change, Awareness and
Incident share one row under a category name.
The grouping itself is frontend (ShopfloorDashboard.vue); what is pinned here is
the contract it groups on - the feed must carry boardcategory per card, and the
type API must round-trip it.
"""
from plugins.notifications.models import Notification, NotificationType
def _make_type(db, typename, displaystyle='grid', boardcategory=None):
t = NotificationType(typename=typename, typecolor='#17a2b8', isactive=True,
displaystyle=displaystyle, boardcategory=boardcategory)
db.session.add(t)
db.session.commit()
return t
def _make_note(db, ntype, text):
n = Notification(notificationtypeid=ntype.notificationtypeid,
notification=text, businessunitid=None,
isactive=True, isshopfloor=True)
db.session.add(n)
db.session.commit()
return n
def _current(client):
resp = client.get('/api/notifications/shopfloor')
assert resp.status_code == 200, resp.get_json()
return resp.get_json()['data']['current']
def test_feed_carries_the_category_per_card(client, db):
"""The dashboard groups on this field, so it has to reach the card."""
ntype = _make_type(db, 'Awareness', boardcategory='Alerts')
_make_note(db, ntype, 'watch your step')
card = _current(client)[0]
assert card['boardcategory'] == 'Alerts'
assert card['typename'] == 'Awareness'
assert card['displaystyle'] == 'grid'
def test_uncategorised_type_reports_an_empty_category(client, db):
"""Blank, not null: the dashboard falls back to the type name for a row."""
ntype = _make_type(db, 'Recertification')
_make_note(db, ntype, 'forklift cert due')
assert _current(client)[0]['boardcategory'] == ''
def test_types_sharing_a_category_keep_their_own_names(client, db):
"""Grouping is the dashboard's job; the feed must not flatten the types."""
change = _make_type(db, 'Change', boardcategory='Alerts')
incident = _make_type(db, 'Incident', boardcategory='Alerts')
_make_note(db, change, 'line 3 retooling')
_make_note(db, incident, 'press 12 down')
cards = _current(client)
assert {c['typename'] for c in cards} == {'Change', 'Incident'}
assert {c['boardcategory'] for c in cards} == {'Alerts'}
def test_api_round_trips_the_category(client, db, auth_headers):
create = client.post('/api/notifications/types',
json={'typename': 'Awareness', 'displaystyle': 'grid',
'boardcategory': 'Alerts'},
headers=auth_headers)
assert create.status_code in (200, 201), create.get_json()
typeid = create.get_json()['data']['notificationtypeid']
assert create.get_json()['data']['boardcategory'] == 'Alerts'
listed = client.get('/api/notifications/types', headers=auth_headers)
row = next(t for t in listed.get_json()['data']
if t['notificationtypeid'] == typeid)
assert row['boardcategory'] == 'Alerts'
def test_blank_category_stores_as_null_not_an_empty_group(client, db, auth_headers):
"""Every uncategorised type sharing the empty string would be one big row."""
create = client.post('/api/notifications/types',
json={'typename': 'Solo', 'boardcategory': ' '},
headers=auth_headers)
assert create.status_code in (200, 201), create.get_json()
typeid = create.get_json()['data']['notificationtypeid']
stored = db.session.get(NotificationType, typeid)
assert stored.boardcategory is None
assert create.get_json()['data']['boardcategory'] == ''
def test_api_rejects_an_overlong_category(client, db, auth_headers):
resp = client.post('/api/notifications/types',
json={'typename': 'Bad', 'boardcategory': 'x' * 51},
headers=auth_headers)
assert resp.status_code == 400
assert 'boardcategory' in resp.get_data(as_text=True)
def test_category_change_moves_the_config_version(client, db, auth_headers):
"""Open kiosks reload on a layout change; a re-grouped board is one."""
ntype = _make_type(db, 'Awareness')
before = client.get('/api/notifications/shopfloor').get_json()['data']['configversion']
client.put(f'/api/notifications/types/{ntype.notificationtypeid}',
json={'boardcategory': 'Alerts'}, headers=auth_headers)
after = client.get('/api/notifications/shopfloor').get_json()['data']['configversion']
assert before != after

View File

@@ -57,6 +57,39 @@ def test_update_network_device_ipaddress_upserts(client, db, auth_headers, netwo
assert len(comms) == 1 and comms[0].ipaddress == '10.0.0.9'
def test_clearing_the_ip_takes_it_back_out(client, db, auth_headers, network_setup):
"""Blanking the form field must clear the stored IP.
The upsert helper returned early on a blank value, so an address could be
typed and never removed - invisible until the form grew an IP field.
"""
from shopdb.core.models import Communication
created = client.post('/api/network', json={
'assetnumber': 'SW-09', 'name': 'Switch', 'ipaddress': '10.0.0.5',
}, headers=auth_headers).get_json()['data']
devid = created['networkdevice']['networkdeviceid']
cleared = client.put(f'/api/network/{devid}', json={'ipaddress': ''},
headers=auth_headers)
assert cleared.status_code == 200, cleared.get_json()
assert not cleared.get_json()['data']['ipaddress']
comms = Communication.query.filter_by(assetid=created['assetid']).all()
assert len(comms) == 1 and comms[0].ipaddress is None
def test_omitting_ipaddress_leaves_it_alone(client, db, auth_headers, network_setup):
"""A PUT that never mentions the field must not wipe the stored IP."""
created = client.post('/api/network', json={
'assetnumber': 'SW-10', 'name': 'Switch', 'ipaddress': '10.0.0.7',
}, headers=auth_headers).get_json()['data']
devid = created['networkdevice']['networkdeviceid']
updated = client.put(f'/api/network/{devid}', json={'hostname': 'sw-10'},
headers=auth_headers)
assert updated.get_json()['data']['ipaddress'] == '10.0.0.7'
def test_seed_reference_data_creates_ip_commtype(app, db):
"""The reference-data seed now creates the IP communication type the asset
import depends on."""