Add printer installer endpoint (data + floor-map positions)
Shopfloor 2.0 PCs can't run unsigned .bat printer maps, so the signed printer installer EXE pulls printer data + floor-map positions from the API and renders the picker itself. Adds the endpoint the EXE consumes. GET /api/printers/install-list - flat, unpaginated list of NETWORK printers (USB-only excluded), each with: printerid, name, machinenumber, windowsname, sharename, hostname, ipaddress, vendorname, modelnumber, installpath, iscsf, locationname, mapx, mapy. Replaces the classic apiprinters.asp contract and adds the map position (mapx/mapy) the EXE needs. Tests: network printer appears with map position + installer fields; USB-only printer excluded. 188 tests pass, naming green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -188,6 +188,53 @@ def list_printers():
|
|||||||
return paginated_response(data, page, per_page, total)
|
return paginated_response(data, page, per_page, total)
|
||||||
|
|
||||||
|
|
||||||
|
@printers_asset_bp.route('/install-list', methods=['GET'])
|
||||||
|
@jwt_required(optional=True)
|
||||||
|
def printer_install_list():
|
||||||
|
"""Flat, unpaginated list of network printers for the printer installer.
|
||||||
|
|
||||||
|
Shopfloor 2.0 PCs cannot run unsigned .bat maps, so the signed installer EXE
|
||||||
|
pulls printer data + floor-map positions from here and renders the picker.
|
||||||
|
Replaces the classic apiprinters.asp contract, adding mapx/mapy. Network
|
||||||
|
printers only (USB-only printers are excluded).
|
||||||
|
"""
|
||||||
|
rows = []
|
||||||
|
query = db.session.query(Printer).join(Asset).filter(Asset.isactive == True)
|
||||||
|
for printer in query.all():
|
||||||
|
asset = printer.asset
|
||||||
|
if not asset:
|
||||||
|
continue
|
||||||
|
primary = Communication.query.filter_by(
|
||||||
|
assetid=asset.assetid, isprimary=True).first() \
|
||||||
|
or Communication.query.filter_by(assetid=asset.assetid).first()
|
||||||
|
ipaddress = primary.ipaddress if primary else None
|
||||||
|
|
||||||
|
# Network printers only: must have a hostname or a non-USB IP.
|
||||||
|
is_network = bool(printer.hostname) or (ipaddress and ipaddress != 'USB')
|
||||||
|
if not is_network:
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = printer.to_dict()
|
||||||
|
rows.append({
|
||||||
|
'printerid': printer.printerid,
|
||||||
|
'name': asset.name or asset.assetnumber,
|
||||||
|
'machinenumber': asset.assetnumber,
|
||||||
|
'windowsname': printer.windowsname,
|
||||||
|
'sharename': printer.sharename,
|
||||||
|
'hostname': printer.hostname,
|
||||||
|
'ipaddress': ipaddress,
|
||||||
|
'vendorname': data.get('vendorname'),
|
||||||
|
'modelnumber': data.get('modelname'),
|
||||||
|
'installpath': printer.installpath,
|
||||||
|
'iscsf': printer.iscsf,
|
||||||
|
'locationname': asset.location.locationname if asset.location else None,
|
||||||
|
'mapx': asset.mapx,
|
||||||
|
'mapy': asset.mapy,
|
||||||
|
})
|
||||||
|
|
||||||
|
return success_response(rows)
|
||||||
|
|
||||||
|
|
||||||
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
|
@printers_asset_bp.route('/<int:printer_id>', methods=['GET'])
|
||||||
@jwt_required(optional=True)
|
@jwt_required(optional=True)
|
||||||
def get_printer(printer_id: int):
|
def get_printer(printer_id: int):
|
||||||
|
|||||||
48
tests/test_plugins/test_printer_install_list.py
Normal file
48
tests/test_plugins/test_printer_install_list.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"""Tests for the printer installer endpoint (/api/printers/install-list).
|
||||||
|
|
||||||
|
The signed printer installer EXE pulls network-printer data + floor-map
|
||||||
|
positions from here (shopfloor 2.0 cannot run unsigned .bat maps).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def printer_assettype(db):
|
||||||
|
from shopdb.core.models import AssetType
|
||||||
|
at = AssetType(assettype='printer', pluginname='printers',
|
||||||
|
tablename='printers', description='Printers')
|
||||||
|
db.session.add(at)
|
||||||
|
db.session.commit()
|
||||||
|
return at
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_list_returns_network_printer_with_map(client, db, auth_headers,
|
||||||
|
printer_assettype):
|
||||||
|
"""A network printer appears with flat installer fields incl map position."""
|
||||||
|
created = client.post('/api/printers', json={
|
||||||
|
'assetnumber': 'CSF01-Materials-HP',
|
||||||
|
'hostname': 'wjprn01',
|
||||||
|
'installpath': r'\\srv\drivers\hp',
|
||||||
|
'mapx': 120, 'mapy': 240,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
assert created.status_code == 201, created.get_json()
|
||||||
|
|
||||||
|
resp = client.get('/api/printers/install-list', headers=auth_headers)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
rows = resp.get_json()['data']
|
||||||
|
row = next((r for r in rows if r['machinenumber'] == 'CSF01-Materials-HP'), None)
|
||||||
|
assert row is not None
|
||||||
|
assert row['hostname'] == 'wjprn01'
|
||||||
|
assert row['installpath'] == r'\\srv\drivers\hp'
|
||||||
|
assert row['mapx'] == 120 and row['mapy'] == 240
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_list_excludes_usb_only_printer(client, db, auth_headers,
|
||||||
|
printer_assettype):
|
||||||
|
"""A printer with no hostname and no network IP is excluded."""
|
||||||
|
client.post('/api/printers', json={'assetnumber': 'USB-LABELER'},
|
||||||
|
headers=auth_headers)
|
||||||
|
resp = client.get('/api/printers/install-list', headers=auth_headers)
|
||||||
|
machinenumbers = [r['machinenumber'] for r in resp.get_json()['data']]
|
||||||
|
assert 'USB-LABELER' not in machinenumbers
|
||||||
Reference in New Issue
Block a user