diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue index 173a4dc..64d7e65 100644 --- a/plugins/computers/frontend/views/PCDetail.vue +++ b/plugins/computers/frontend/views/PCDetail.vue @@ -95,12 +95,22 @@
Vendor - {{ computer.computer?.vendorname || '-' }} + + {{ computer.computer?.vendorname || '-' }} + + (from model) +
Model {{ computer.computer?.modelname || '-' }}
+ +
+ Model type + {{ computer.computer.modeltypename }} +
Operating System {{ computer.computer?.osname || '-' }} diff --git a/plugins/computers/models/computer.py b/plugins/computers/models/computer.py index 514fe2e..6a35b4f 100644 --- a/plugins/computers/models/computer.py +++ b/plugins/computers/models/computer.py @@ -138,6 +138,18 @@ class Computer(BaseModel): result['modelname'] = self.model.modelnumber if self.model.imageurl: result['imageurl'] = self.model.imageurl + # The catalog model already knows its maker, so an asset that has a + # model but no vendor of its own is showing a blank the database can + # fill. Flagged rather than merged silently: the edit form still has + # an empty vendor box, and a page implying otherwise would be lying. + if not self.vendor and self.model.vendor: + result['vendorname'] = self.model.vendor.vendor + result['vendorfrommodel'] = True + # Exposed under its OWN name. modeltypes is the catalog-wide list + # covering every kind of asset, so it is not interchangeable with + # this asset's own type and must never be substituted for it. + if self.model.modeltype: + result['modeltypename'] = self.model.modeltype.modeltype # Names of enabled remote-access protocols (for list badges) result['accessprotocolnames'] = [ diff --git a/plugins/machines/frontend/views/MachineDetail.vue b/plugins/machines/frontend/views/MachineDetail.vue index 16e3e10..945834f 100644 --- a/plugins/machines/frontend/views/MachineDetail.vue +++ b/plugins/machines/frontend/views/MachineDetail.vue @@ -114,12 +114,22 @@
Vendor - {{ machine.machine?.vendorname || '-' }} + + {{ machine.machine?.vendorname || '-' }} + + (from model) +
Model {{ machine.machine?.modelname || '-' }}
+ +
+ Model type + {{ machine.machine.modeltypename }} +
diff --git a/plugins/machines/models/machine.py b/plugins/machines/models/machine.py index d73090c..507c374 100644 --- a/plugins/machines/models/machine.py +++ b/plugins/machines/models/machine.py @@ -123,6 +123,18 @@ class Machine(BaseModel): result['modelname'] = self.model.modelnumber if self.model.imageurl: result['imageurl'] = self.model.imageurl + # The catalog model already knows its maker, so an asset that has a + # model but no vendor of its own is showing a blank the database can + # fill. Flagged rather than merged silently: the edit form still has + # an empty vendor box, and a page implying otherwise would be lying. + if not self.vendor and self.model.vendor: + result['vendorname'] = self.model.vendor.vendor + result['vendorfrommodel'] = True + # Exposed under its OWN name. modeltypes is the catalog-wide list + # covering every kind of asset, so it is not interchangeable with + # this asset's own type and must never be substituted for it. + if self.model.modeltype: + result['modeltypename'] = self.model.modeltype.modeltype # Add controller info if self.controllervendor: diff --git a/plugins/network/frontend/views/NetworkDeviceDetail.vue b/plugins/network/frontend/views/NetworkDeviceDetail.vue index 9bc774a..c7a56c7 100644 --- a/plugins/network/frontend/views/NetworkDeviceDetail.vue +++ b/plugins/network/frontend/views/NetworkDeviceDetail.vue @@ -88,7 +88,21 @@
Vendor - {{ device.networkdevice?.vendorname || '-' }} + + {{ device.networkdevice?.vendorname || '-' }} + + (from model) + +
+
+ Model + {{ device.networkdevice.modelname }} +
+ +
+ Model type + {{ device.networkdevice.modeltypename }}
Device Type diff --git a/plugins/network/models/network_device.py b/plugins/network/models/network_device.py index 41d51be..d8ce412 100644 --- a/plugins/network/models/network_device.py +++ b/plugins/network/models/network_device.py @@ -130,5 +130,17 @@ class NetworkDevice(BaseModel): result['modelname'] = self.model.modelnumber if self.model.imageurl: result['imageurl'] = self.model.imageurl + # The catalog model already knows its maker, so an asset that has a + # model but no vendor of its own is showing a blank the database can + # fill. Flagged rather than merged silently: the edit form still has + # an empty vendor box, and a page implying otherwise would be lying. + if not self.vendor and self.model.vendor: + result['vendorname'] = self.model.vendor.vendor + result['vendorfrommodel'] = True + # Exposed under its OWN name. modeltypes is the catalog-wide list + # covering every kind of asset, so it is not interchangeable with + # this asset's own type and must never be substituted for it. + if self.model.modeltype: + result['modeltypename'] = self.model.modeltype.modeltype return result diff --git a/plugins/printers/frontend/views/PrinterDetail.vue b/plugins/printers/frontend/views/PrinterDetail.vue index 877d224..7fbe610 100644 --- a/plugins/printers/frontend/views/PrinterDetail.vue +++ b/plugins/printers/frontend/views/PrinterDetail.vue @@ -39,12 +39,22 @@
Vendor - {{ printer.printer.vendorname }} + + {{ printer.printer.vendorname }} + + (from model) +
Model {{ printer.printer.modelname }}
+ +
+ Model type + {{ printer.printer.modeltypename }} +
Serial Number {{ printer.serialnumber }} diff --git a/plugins/printers/models/printer.py b/plugins/printers/models/printer.py index 428a2f2..0c0094e 100644 --- a/plugins/printers/models/printer.py +++ b/plugins/printers/models/printer.py @@ -119,5 +119,17 @@ class Printer(BaseModel): result['modelname'] = self.model.modelnumber if self.model.imageurl: result['imageurl'] = self.model.imageurl + # The catalog model already knows its maker, so an asset that has a + # model but no vendor of its own is showing a blank the database can + # fill. Flagged rather than merged silently: the edit form still has + # an empty vendor box, and a page implying otherwise would be lying. + if not self.vendor and self.model.vendor: + result['vendorname'] = self.model.vendor.vendor + result['vendorfrommodel'] = True + # Exposed under its OWN name. modeltypes is the catalog-wide list + # covering every kind of asset, so it is not interchangeable with + # this asset's own type and must never be substituted for it. + if self.model.modeltype: + result['modeltypename'] = self.model.modeltype.modeltype return result diff --git a/scripts/backfill_vendor_from_model.py b/scripts/backfill_vendor_from_model.py new file mode 100644 index 0000000..ec7065a --- /dev/null +++ b/scripts/backfill_vendor_from_model.py @@ -0,0 +1,121 @@ +"""Fill in an asset's vendor from the catalog model it already points at. + +An asset that carries a model but no vendor is showing a blank the database can +answer: the model records its own vendor, and both sides reference the same +vendors table. The detail pages fall back to it at display time, but the record +itself stays empty, so the edit form shows an empty vendor box and any report +reading vendorid directly still sees nothing. This writes it down. + +ONLY fills rows where the asset's vendor is NULL and the model names one. It +never overwrites a vendor somebody chose, and it never guesses: if the model has +no vendor either, the row is left alone. + +DELIBERATELY NOT DONE HERE: the asset's TYPE. modeltypes is the catalog-wide +list covering every kind of asset - it holds "Access Point", "Camera" and +"Desktop PC" alongside the machine entries - so it is a different taxonomy from +machinetypes, and mapping one onto the other by name would mistype whatever does +not match. Only about two thirds of the names overlap. + +Dry run by default; nothing is written without --commit. + + python scripts/backfill_vendor_from_model.py + python scripts/backfill_vendor_from_model.py --commit + python scripts/backfill_vendor_from_model.py --only machines --commit +""" + +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Each asset table that carries both a model link and its own vendor, with the +# column that identifies a row in reports and logs. +TARGETS = [ + ('machines', 'machineid', 'machinename'), + ('computers', 'computerid', 'computername'), + ('printers', 'printerid', 'printername'), + ('networkdevices', 'networkdeviceid', 'hostname'), +] + + +def resolve(connection, table, idcol, labelcol): + """Rows that would change, newest table columns tolerated.""" + from sqlalchemy import text + columns = {c['name'] for c in __import__('sqlalchemy').inspect(connection).get_columns(table)} + if 'modelnumberid' not in columns or 'vendorid' not in columns: + return None, [] + label = labelcol if labelcol in columns else idcol + rows = connection.execute(text(f""" + SELECT a.{idcol} AS assetid, + a.{label} AS label, + m.modelnumber AS modelnumber, + m.vendorid AS vendorid, + v.vendor AS vendorname + FROM {table} a + JOIN models m ON a.modelnumberid = m.modelnumberid + JOIN vendors v ON m.vendorid = v.vendorid + WHERE a.vendorid IS NULL + AND m.vendorid IS NOT NULL + ORDER BY a.{idcol} + """)).fetchall() + return label, rows + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--commit', action='store_true', + help='write the changes (default is a dry run)') + parser.add_argument('--only', metavar='TABLE', + help='restrict to one table, e.g. machines') + parser.add_argument('--limit', type=int, default=20, + help='rows to list per table in the preview (default 20)') + args = parser.parse_args() + + # FLASK_ENV is deliberately NOT forced. The app reads it from .env, which on + # a server already says production; overriding it here demanded a SECRET_KEY + # the environment had no reason to provide and the script could not run. + from sqlalchemy import text + from shopdb import create_app + from shopdb.extensions import db + + app = create_app() + with app.app_context(): + connection = db.session.connection() + total = 0 + for table, idcol, labelcol in TARGETS: + if args.only and args.only != table: + continue + label, rows = resolve(connection, table, idcol, labelcol) + if label is None: + print(f'{table}: no model or vendor column here, skipped') + continue + print(f'\n{table}: {len(rows)} row(s) would get a vendor') + for row in rows[:args.limit]: + print(f' {row.assetid:>7} {str(row.label)[:28]:<28} ' + f'model {row.modelnumber[:22]:<22} -> {row.vendorname}') + if len(rows) > args.limit: + print(f' ... and {len(rows) - args.limit} more') + total += len(rows) + + if args.commit and rows: + connection.execute(text(f""" + UPDATE {table} a + JOIN models m ON a.modelnumberid = m.modelnumberid + SET a.vendorid = m.vendorid + WHERE a.vendorid IS NULL + AND m.vendorid IS NOT NULL + """)) + + if args.commit: + db.session.commit() + print(f'\nCommitted. {total} row(s) updated.') + else: + print(f'\nDRY RUN - nothing written. {total} row(s) would change.') + print('Re-run with --commit to apply.') + return 0 + + +if __name__ == '__main__': + sys.exit(main())