Model type shown only when it differs from the asset type

This commit is contained in:
2026-08-05 10:16:21 -04:00
parent 73940ae992
commit 215d51e7df
5 changed files with 109 additions and 17 deletions

View File

@@ -105,9 +105,13 @@
<span class="info-label">Model</span>
<span class="info-value">{{ computer.computer?.modelname || '-' }}</span>
</div>
<!-- The catalog model's own type. Kept separate from this
asset's type: modeltypes spans every kind of asset. -->
<div class="info-row" v-if="computer.computer?.modeltypename">
<!-- Only when it DIFFERS from this asset's own type. The two are
separate tables - modeltypes spans every kind of asset - but
they carry the same names in practice, so showing both
always put two identical Type rows in one section. Worth
seeing only when they disagree. -->
<div class="info-row"
v-if="computer.computer?.modeltypename && computer.computer.modeltypename !== computer.computer?.computertypename">
<span class="info-label">Model type</span>
<span class="info-value">{{ computer.computer.modeltypename }}</span>
</div>

View File

@@ -124,9 +124,13 @@
<span class="info-label">Model</span>
<span class="info-value">{{ machine.machine?.modelname || '-' }}</span>
</div>
<!-- The catalog model's own type. Kept separate from this
asset's type: modeltypes spans every kind of asset. -->
<div class="info-row" v-if="machine.machine?.modeltypename">
<!-- Only when it DIFFERS from this asset's own type. The two are
separate tables - modeltypes spans every kind of asset - but
they carry the same names in practice, so showing both
always put two identical Type rows in one section. Worth
seeing only when they disagree. -->
<div class="info-row"
v-if="machine.machine?.modeltypename && machine.machine.modeltypename !== machine.machine?.machinetypename">
<span class="info-label">Model type</span>
<span class="info-value">{{ machine.machine.modeltypename }}</span>
</div>

View File

@@ -98,9 +98,10 @@
<span class="info-label">Model</span>
<span class="info-value">{{ device.networkdevice.modelname }}</span>
</div>
<!-- The catalog model's own type, kept separate from the device
type: modeltypes spans every kind of asset. -->
<div class="info-row" v-if="device.networkdevice?.modeltypename">
<!-- Only when it DIFFERS from the device's own type: the two are
separate tables but carry the same names in practice. -->
<div class="info-row"
v-if="device.networkdevice?.modeltypename && device.networkdevice.modeltypename !== device.networkdevice?.networkdevicetypename">
<span class="info-label">Model type</span>
<span class="info-value">{{ device.networkdevice.modeltypename }}</span>
</div>

View File

@@ -49,9 +49,10 @@
<span class="hero-detail-label">Model</span>
<span class="hero-detail-value">{{ printer.printer.modelname }}</span>
</div>
<!-- The catalog model's own type, kept separate from the printer's
type: modeltypes spans every kind of asset. -->
<div class="hero-detail" v-if="printer.printer?.modeltypename">
<!-- Only when it DIFFERS from the printer's own type: the two are
separate tables but carry the same names in practice. -->
<div class="hero-detail"
v-if="printer.printer?.modeltypename && printer.printer.modeltypename !== printer.printer?.printertypename">
<span class="hero-detail-label">Model type</span>
<span class="hero-detail-value">{{ printer.printer.modeltypename }}</span>
</div>

View File

@@ -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.')