diff --git a/plugins/computers/frontend/views/PCDetail.vue b/plugins/computers/frontend/views/PCDetail.vue
index 64d7e652..6970377f 100644
--- a/plugins/computers/frontend/views/PCDetail.vue
+++ b/plugins/computers/frontend/views/PCDetail.vue
@@ -105,9 +105,13 @@
Model
{{ computer.computer?.modelname || '-' }}
-
-
+
+
Model type
{{ printer.printer.modeltypename }}
diff --git a/scripts/backfill_vendor_from_model.py b/scripts/backfill_vendor_from_model.py
index ec7065ad..d80f923f 100644
--- a/scripts/backfill_vendor_from_model.py
+++ b/scripts/backfill_vendor_from_model.py
@@ -10,11 +10,18 @@ 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.
+It also fills the asset's TYPE from the model's type, but ONLY where the two
+names are identical. 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 the two cannot be
+equated in general. Across the whole catalog only about two thirds of the names
+overlap. Restricted to the models an asset class actually uses the picture is
+different: every one of the 262 machines in the development database maps
+exactly, because the non-machine entries are never used by machines.
+
+So the rule is exact name match or nothing. A model type with no same-named
+entry in the asset's own type table is REPORTED and left alone, never guessed
+at, because the failure mode is a machine labelled "Desktop PC".
Dry run by default; nothing is written without --commit.
@@ -38,6 +45,16 @@ TARGETS = [
('networkdevices', 'networkdeviceid', 'hostname'),
]
+# The asset's own type column, and the table it points at. Each asset class
+# keeps its own type list; the catalog's modeltypes is a different taxonomy, so
+# these are joined by NAME and only when the name matches exactly.
+TYPE_TARGETS = {
+ 'machines': ('machinetypeid', 'machinetypes', 'machinetype'),
+ 'computers': ('computertypeid', 'computertypes', 'computertype'),
+ 'printers': ('printertypeid', 'printertypes', 'printertype'),
+ 'networkdevices': ('networkdevicetypeid', 'networkdevicetypes', 'networkdevicetype'),
+}
+
def resolve(connection, table, idcol, labelcol):
"""Rows that would change, newest table columns tolerated."""
@@ -62,6 +79,46 @@ def resolve(connection, table, idcol, labelcol):
return label, rows
+def resolve_types(connection, table, idcol, labelcol):
+ """Rows whose type could be taken from the model, plus the ones that cannot.
+
+ Returns (fillable, unmatched). unmatched rows have a model type that has no
+ identically named entry in this asset's own type table, and are never
+ touched - they are reported so somebody can decide.
+ """
+ from sqlalchemy import inspect, text
+ spec = TYPE_TARGETS.get(table)
+ if not spec:
+ return [], []
+ typecol, typetable, typenamecol = spec
+ columns = {c['name'] for c in inspect(connection).get_columns(table)}
+ if 'modelnumberid' not in columns or typecol not in columns:
+ return [], []
+ label = labelcol if labelcol in columns else idcol
+
+ fillable = connection.execute(text(f"""
+ SELECT a.{idcol} AS assetid, a.{label} AS label,
+ mt.modeltype AS modeltype, tt.{typenamecol} AS typename
+ FROM {table} a
+ JOIN models mo ON a.modelnumberid = mo.modelnumberid
+ JOIN modeltypes mt ON mo.modeltypeid = mt.modeltypeid
+ JOIN {typetable} tt ON tt.{typenamecol} = mt.modeltype
+ WHERE a.{typecol} IS NULL
+ ORDER BY a.{idcol}
+ """)).fetchall()
+
+ unmatched = connection.execute(text(f"""
+ SELECT mt.modeltype AS modeltype, COUNT(*) AS n
+ FROM {table} a
+ JOIN models mo ON a.modelnumberid = mo.modelnumberid
+ JOIN modeltypes mt ON mo.modeltypeid = mt.modeltypeid
+ LEFT JOIN {typetable} tt ON tt.{typenamecol} = mt.modeltype
+ WHERE a.{typecol} IS NULL AND tt.{typenamecol} IS NULL
+ GROUP BY mt.modeltype ORDER BY n DESC
+ """)).fetchall()
+ return fillable, unmatched
+
+
def main():
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -108,6 +165,31 @@ def main():
AND m.vendorid IS NOT NULL
"""))
+ # Type, by exact name only.
+ typed, unmatched = resolve_types(connection, table, idcol, labelcol)
+ spec = TYPE_TARGETS.get(table)
+ print(f'{table}: {len(typed)} row(s) would get a type')
+ for row in typed[:args.limit]:
+ print(f' {row.assetid:>7} {str(row.label)[:28]:<28} '
+ f'model type {row.modeltype[:22]:<22} -> {row.typename}')
+ if len(typed) > args.limit:
+ print(f' ... and {len(typed) - args.limit} more')
+ for row in unmatched:
+ print(f' LEFT ALONE: {row.n} row(s) with model type '
+ f'"{row.modeltype}" - no matching {spec[1]} entry')
+ total += len(typed)
+
+ if args.commit and typed:
+ typecol, typetable, typenamecol = spec
+ connection.execute(text(f"""
+ UPDATE {table} a
+ JOIN models mo ON a.modelnumberid = mo.modelnumberid
+ JOIN modeltypes mt ON mo.modeltypeid = mt.modeltypeid
+ JOIN {typetable} tt ON tt.{typenamecol} = mt.modeltype
+ SET a.{typecol} = tt.{typecol}
+ WHERE a.{typecol} IS NULL
+ """))
+
if args.commit:
db.session.commit()
print(f'\nCommitted. {total} row(s) updated.')