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,
|
||||
'hostname': printer.hostname,
|
||||
'ipaddress': ipaddress,
|
||||
'vendorname': data.get('vendorname'),
|
||||
'vendorname': _printer_vendor(printer),
|
||||
'modelnumber': data.get('modelname'),
|
||||
'installpath': printer.installpath,
|
||||
'iscsf': printer.iscsf,
|
||||
@@ -352,6 +352,15 @@ def printer_install_list():
|
||||
'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)
|
||||
|
||||
|
||||
@@ -378,6 +387,17 @@ def _batch_base_url():
|
||||
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):
|
||||
"""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
|
||||
@@ -560,14 +580,21 @@ def pc_default_printer():
|
||||
Returns {printerid, windowsname}, or {} when the machine is unknown or has
|
||||
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()
|
||||
if not machine:
|
||||
return success_response({})
|
||||
return _empty()
|
||||
|
||||
pc = Asset.query.filter_by(assetnumber=machine, isactive=True).first()
|
||||
dp_type = RelationshipType.query.filter_by(relationshiptype='defaultprinter').first()
|
||||
if not pc or not dp_type:
|
||||
return success_response({})
|
||||
return _empty()
|
||||
|
||||
rel = AssetRelationship.query.filter_by(
|
||||
sourceassetid=pc.assetid,
|
||||
@@ -575,14 +602,21 @@ def pc_default_printer():
|
||||
isactive=True,
|
||||
).first()
|
||||
if not rel:
|
||||
return success_response({})
|
||||
return _empty()
|
||||
|
||||
printer = db.session.query(Printer).join(Asset).filter(
|
||||
Printer.assetid == rel.targetassetid,
|
||||
Asset.isactive == True,
|
||||
).first()
|
||||
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({
|
||||
'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
|
||||
|
||||
|
||||
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,
|
||||
printer_assettype):
|
||||
"""A printer with no hostname and no network IP is excluded."""
|
||||
|
||||
Reference in New Issue
Block a user