Vendor derived from catalog model; model type shown separately
This commit is contained in:
@@ -95,12 +95,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<span class="info-label">Vendor</span>
|
||||||
<span class="info-value">{{ computer.computer?.vendorname || '-' }}</span>
|
<span class="info-value">
|
||||||
|
{{ computer.computer?.vendorname || '-' }}
|
||||||
|
<!-- Inherited from the catalog model, not stored on this record. -->
|
||||||
|
<small v-if="computer.computer?.vendorfrommodel" class="text-muted">(from model)</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Model</span>
|
<span class="info-label">Model</span>
|
||||||
<span class="info-value">{{ computer.computer?.modelname || '-' }}</span>
|
<span class="info-value">{{ computer.computer?.modelname || '-' }}</span>
|
||||||
</div>
|
</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">
|
||||||
|
<span class="info-label">Model type</span>
|
||||||
|
<span class="info-value">{{ computer.computer.modeltypename }}</span>
|
||||||
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Operating System</span>
|
<span class="info-label">Operating System</span>
|
||||||
<span class="info-value">{{ computer.computer?.osname || '-' }}</span>
|
<span class="info-value">{{ computer.computer?.osname || '-' }}</span>
|
||||||
|
|||||||
@@ -138,6 +138,18 @@ class Computer(BaseModel):
|
|||||||
result['modelname'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
if self.model.imageurl:
|
if self.model.imageurl:
|
||||||
result['imageurl'] = 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)
|
# Names of enabled remote-access protocols (for list badges)
|
||||||
result['accessprotocolnames'] = [
|
result['accessprotocolnames'] = [
|
||||||
|
|||||||
@@ -114,12 +114,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<span class="info-label">Vendor</span>
|
||||||
<span class="info-value">{{ machine.machine?.vendorname || '-' }}</span>
|
<span class="info-value">
|
||||||
|
{{ machine.machine?.vendorname || '-' }}
|
||||||
|
<!-- Inherited from the catalog model, not stored on this record. -->
|
||||||
|
<small v-if="machine.machine?.vendorfrommodel" class="text-muted">(from model)</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Model</span>
|
<span class="info-label">Model</span>
|
||||||
<span class="info-value">{{ machine.machine?.modelname || '-' }}</span>
|
<span class="info-value">{{ machine.machine?.modelname || '-' }}</span>
|
||||||
</div>
|
</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">
|
||||||
|
<span class="info-label">Model type</span>
|
||||||
|
<span class="info-value">{{ machine.machine.modeltypename }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ class Machine(BaseModel):
|
|||||||
result['modelname'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
if self.model.imageurl:
|
if self.model.imageurl:
|
||||||
result['imageurl'] = 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
|
# Add controller info
|
||||||
if self.controllervendor:
|
if self.controllervendor:
|
||||||
|
|||||||
@@ -88,7 +88,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Vendor</span>
|
<span class="info-label">Vendor</span>
|
||||||
<span class="info-value">{{ device.networkdevice?.vendorname || '-' }}</span>
|
<span class="info-value">
|
||||||
|
{{ device.networkdevice?.vendorname || '-' }}
|
||||||
|
<!-- Inherited from the catalog model, not stored on this record. -->
|
||||||
|
<small v-if="device.networkdevice?.vendorfrommodel" class="text-muted">(from model)</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row" v-if="device.networkdevice?.modelname">
|
||||||
|
<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">
|
||||||
|
<span class="info-label">Model type</span>
|
||||||
|
<span class="info-value">{{ device.networkdevice.modeltypename }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Device Type</span>
|
<span class="info-label">Device Type</span>
|
||||||
|
|||||||
@@ -130,5 +130,17 @@ class NetworkDevice(BaseModel):
|
|||||||
result['modelname'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
if self.model.imageurl:
|
if self.model.imageurl:
|
||||||
result['imageurl'] = 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
|
return result
|
||||||
|
|||||||
@@ -39,12 +39,22 @@
|
|||||||
<div class="hero-details">
|
<div class="hero-details">
|
||||||
<div class="hero-detail" v-if="printer.printer?.vendorname">
|
<div class="hero-detail" v-if="printer.printer?.vendorname">
|
||||||
<span class="hero-detail-label">Vendor</span>
|
<span class="hero-detail-label">Vendor</span>
|
||||||
<span class="hero-detail-value">{{ printer.printer.vendorname }}</span>
|
<span class="hero-detail-value">
|
||||||
|
{{ printer.printer.vendorname }}
|
||||||
|
<!-- Inherited from the catalog model, not stored on this record. -->
|
||||||
|
<small v-if="printer.printer?.vendorfrommodel" class="text-muted">(from model)</small>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-detail" v-if="printer.printer?.modelname">
|
<div class="hero-detail" v-if="printer.printer?.modelname">
|
||||||
<span class="hero-detail-label">Model</span>
|
<span class="hero-detail-label">Model</span>
|
||||||
<span class="hero-detail-value">{{ printer.printer.modelname }}</span>
|
<span class="hero-detail-value">{{ printer.printer.modelname }}</span>
|
||||||
</div>
|
</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">
|
||||||
|
<span class="hero-detail-label">Model type</span>
|
||||||
|
<span class="hero-detail-value">{{ printer.printer.modeltypename }}</span>
|
||||||
|
</div>
|
||||||
<div class="hero-detail" v-if="printer.serialnumber">
|
<div class="hero-detail" v-if="printer.serialnumber">
|
||||||
<span class="hero-detail-label">Serial Number</span>
|
<span class="hero-detail-label">Serial Number</span>
|
||||||
<span class="hero-detail-value mono">{{ printer.serialnumber }}</span>
|
<span class="hero-detail-value mono">{{ printer.serialnumber }}</span>
|
||||||
|
|||||||
@@ -119,5 +119,17 @@ class Printer(BaseModel):
|
|||||||
result['modelname'] = self.model.modelnumber
|
result['modelname'] = self.model.modelnumber
|
||||||
if self.model.imageurl:
|
if self.model.imageurl:
|
||||||
result['imageurl'] = 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
|
return result
|
||||||
|
|||||||
121
scripts/backfill_vendor_from_model.py
Normal file
121
scripts/backfill_vendor_from_model.py
Normal file
@@ -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())
|
||||||
Reference in New Issue
Block a user