printers: add format=text to install-list + pc-default; vendor via model
The Inno printer installers hand-parsed JSON in Pascal (brittle brace-counting). Add ?format=text to install-list (one printer per line, pipe-delimited: printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy) and to pc-default (printerid|windowsname), so the installer side is a split() with no JSON parser. The web map keeps the default JSON. Also resolve install-list's vendorname via the model (as the batch already does), since the import sets the model, not the printer's direct vendorid - otherwise the installers' HP/Xerox/Brother filter drops every prod printer.
This commit is contained in:
@@ -343,7 +343,7 @@ def printer_install_list():
|
|||||||
'sharename': printer.sharename,
|
'sharename': printer.sharename,
|
||||||
'hostname': printer.hostname,
|
'hostname': printer.hostname,
|
||||||
'ipaddress': ipaddress,
|
'ipaddress': ipaddress,
|
||||||
'vendorname': data.get('vendorname'),
|
'vendorname': _printer_vendor(printer),
|
||||||
'modelnumber': data.get('modelname'),
|
'modelnumber': data.get('modelname'),
|
||||||
'installpath': printer.installpath,
|
'installpath': printer.installpath,
|
||||||
'iscsf': printer.iscsf,
|
'iscsf': printer.iscsf,
|
||||||
@@ -352,6 +352,15 @@ def printer_install_list():
|
|||||||
'mapy': asset.mapy,
|
'mapy': asset.mapy,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# A pipe-delimited text variant for the Inno installers: one printer per
|
||||||
|
# line, fixed field order, so the Pascal side is a split() instead of a
|
||||||
|
# hand-rolled JSON parser. The web map uses the default JSON.
|
||||||
|
if request.args.get('format') == 'text':
|
||||||
|
fields = ('printerid', 'windowsname', 'vendorname', 'modelnumber',
|
||||||
|
'hostname', 'ipaddress', 'mapx', 'mapy')
|
||||||
|
lines = [_text_line(row, fields) for row in rows]
|
||||||
|
return Response('\n'.join(lines), mimetype='text/plain')
|
||||||
|
|
||||||
return success_response(rows)
|
return success_response(rows)
|
||||||
|
|
||||||
|
|
||||||
@@ -378,6 +387,17 @@ def _batch_base_url():
|
|||||||
return 'https://%s%s' % (host, root)
|
return 'https://%s%s' % (host, root)
|
||||||
|
|
||||||
|
|
||||||
|
def _text_line(row, fields):
|
||||||
|
"""One pipe-delimited line for the installer text format. None -> empty;
|
||||||
|
any pipe/newline in a value is neutralized so the field count stays fixed."""
|
||||||
|
parts = []
|
||||||
|
for key in fields:
|
||||||
|
value = row.get(key)
|
||||||
|
text = '' if value is None else str(value)
|
||||||
|
parts.append(text.replace('|', ' ').replace('\r', ' ').replace('\n', ' '))
|
||||||
|
return '|'.join(parts)
|
||||||
|
|
||||||
|
|
||||||
def _printer_vendor(printer):
|
def _printer_vendor(printer):
|
||||||
"""Vendor name for install grouping. The legacy import sets the printer's
|
"""Vendor name for install grouping. The legacy import sets the printer's
|
||||||
model but not its direct vendorid, so resolve via the model's vendor (as the
|
model but not its direct vendorid, so resolve via the model's vendor (as the
|
||||||
@@ -560,14 +580,21 @@ def pc_default_printer():
|
|||||||
Returns {printerid, windowsname}, or {} when the machine is unknown or has
|
Returns {printerid, windowsname}, or {} when the machine is unknown or has
|
||||||
no active default printer set.
|
no active default printer set.
|
||||||
"""
|
"""
|
||||||
|
as_text = request.args.get('format') == 'text'
|
||||||
|
|
||||||
|
def _empty():
|
||||||
|
# Text variant returns an empty body (no default) so the installer's
|
||||||
|
# split yields nothing; JSON keeps the {} contract.
|
||||||
|
return Response('', mimetype='text/plain') if as_text else success_response({})
|
||||||
|
|
||||||
machine = (request.args.get('machine') or '').strip()
|
machine = (request.args.get('machine') or '').strip()
|
||||||
if not machine:
|
if not machine:
|
||||||
return success_response({})
|
return _empty()
|
||||||
|
|
||||||
pc = Asset.query.filter_by(assetnumber=machine, isactive=True).first()
|
pc = Asset.query.filter_by(assetnumber=machine, isactive=True).first()
|
||||||
dp_type = RelationshipType.query.filter_by(relationshiptype='defaultprinter').first()
|
dp_type = RelationshipType.query.filter_by(relationshiptype='defaultprinter').first()
|
||||||
if not pc or not dp_type:
|
if not pc or not dp_type:
|
||||||
return success_response({})
|
return _empty()
|
||||||
|
|
||||||
rel = AssetRelationship.query.filter_by(
|
rel = AssetRelationship.query.filter_by(
|
||||||
sourceassetid=pc.assetid,
|
sourceassetid=pc.assetid,
|
||||||
@@ -575,14 +602,21 @@ def pc_default_printer():
|
|||||||
isactive=True,
|
isactive=True,
|
||||||
).first()
|
).first()
|
||||||
if not rel:
|
if not rel:
|
||||||
return success_response({})
|
return _empty()
|
||||||
|
|
||||||
printer = db.session.query(Printer).join(Asset).filter(
|
printer = db.session.query(Printer).join(Asset).filter(
|
||||||
Printer.assetid == rel.targetassetid,
|
Printer.assetid == rel.targetassetid,
|
||||||
Asset.isactive == True,
|
Asset.isactive == True,
|
||||||
).first()
|
).first()
|
||||||
if not printer:
|
if not printer:
|
||||||
return success_response({})
|
return _empty()
|
||||||
|
|
||||||
|
if as_text:
|
||||||
|
return Response(
|
||||||
|
_text_line({'printerid': printer.printerid,
|
||||||
|
'windowsname': printer.windowsname},
|
||||||
|
('printerid', 'windowsname')),
|
||||||
|
mimetype='text/plain')
|
||||||
|
|
||||||
return success_response({
|
return success_response({
|
||||||
'printerid': printer.printerid,
|
'printerid': printer.printerid,
|
||||||
|
|||||||
@@ -38,6 +38,54 @@ def test_install_list_returns_network_printer_with_map(client, db, auth_headers,
|
|||||||
assert row['mapx'] == 120 and row['mapy'] == 240
|
assert row['mapx'] == 120 and row['mapy'] == 240
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_list_vendorname_resolves_via_model(client, db, auth_headers,
|
||||||
|
printer_assettype):
|
||||||
|
"""vendorname falls back to the model's vendor when the printer has no
|
||||||
|
direct vendorid (the import sets the model, not the printer's vendor). The
|
||||||
|
Inno installer filters on this, so it must not be null on real data."""
|
||||||
|
from shopdb.core.models import Vendor, Model
|
||||||
|
|
||||||
|
hp = Vendor(vendor='HP')
|
||||||
|
db.session.add(hp)
|
||||||
|
db.session.commit()
|
||||||
|
model = Model(modelnumber='LaserJet M607', vendorid=hp.vendorid)
|
||||||
|
db.session.add(model)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
client.post('/api/printers', json={
|
||||||
|
'assetnumber': 'CSF09-2022-HP', 'hostname': 'wjprn09',
|
||||||
|
'modelnumberid': model.modelnumberid,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
|
||||||
|
resp = client.get('/api/printers/install-list', headers=auth_headers)
|
||||||
|
row = next((r for r in resp.get_json()['data']
|
||||||
|
if r['machinenumber'] == 'CSF09-2022-HP'), None)
|
||||||
|
assert row is not None
|
||||||
|
assert row['vendorname'] == 'HP'
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
client.post('/api/printers', json={
|
||||||
|
'assetnumber': 'CSF01-HP', 'windowsname': 'CSF01-HP', 'hostname': 'wjprn01',
|
||||||
|
'mapx': 120, 'mapy': 240,
|
||||||
|
}, headers=auth_headers)
|
||||||
|
|
||||||
|
resp = client.get('/api/printers/install-list?format=text', headers=auth_headers)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.mimetype == 'text/plain'
|
||||||
|
line = next((l for l in resp.get_data(as_text=True).splitlines()
|
||||||
|
if 'CSF01-HP' in l), None)
|
||||||
|
assert line is not None
|
||||||
|
cols = line.split('|')
|
||||||
|
# printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy
|
||||||
|
assert len(cols) == 8
|
||||||
|
assert cols[1] == 'CSF01-HP' # windowsname (falls back to assetnumber name)
|
||||||
|
assert cols[4] == 'wjprn01' # hostname
|
||||||
|
assert cols[6] == '120' and cols[7] == '240'
|
||||||
|
|
||||||
|
|
||||||
def test_install_list_excludes_usb_only_printer(client, db, auth_headers,
|
def test_install_list_excludes_usb_only_printer(client, db, auth_headers,
|
||||||
printer_assettype):
|
printer_assettype):
|
||||||
"""A printer with no hostname and no network IP is excluded."""
|
"""A printer with no hostname and no network IP is excluded."""
|
||||||
|
|||||||
Reference in New Issue
Block a user