diff --git a/plugins/printers/api/asset_routes.py b/plugins/printers/api/asset_routes.py index 6ad1c21..122abe7 100644 --- a/plugins/printers/api/asset_routes.py +++ b/plugins/printers/api/asset_routes.py @@ -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, diff --git a/tests/test_plugins/test_printer_install_list.py b/tests/test_plugins/test_printer_install_list.py index 1a61a6e..d6d315f 100644 --- a/tests/test_plugins/test_printer_install_list.py +++ b/tests/test_plugins/test_printer_install_list.py @@ -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."""