Vendor derived from catalog model; model type shown separately
This commit is contained in:
@@ -95,12 +95,22 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<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 class="info-row">
|
||||
<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">
|
||||
<span class="info-label">Model type</span>
|
||||
<span class="info-value">{{ computer.computer.modeltypename }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Operating System</span>
|
||||
<span class="info-value">{{ computer.computer?.osname || '-' }}</span>
|
||||
|
||||
@@ -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'] = [
|
||||
|
||||
@@ -114,12 +114,22 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<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 class="info-row">
|
||||
<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">
|
||||
<span class="info-label">Model type</span>
|
||||
<span class="info-value">{{ machine.machine.modeltypename }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -88,7 +88,21 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<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 class="info-row">
|
||||
<span class="info-label">Device Type</span>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,12 +39,22 @@
|
||||
<div class="hero-details">
|
||||
<div class="hero-detail" v-if="printer.printer?.vendorname">
|
||||
<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 class="hero-detail" v-if="printer.printer?.modelname">
|
||||
<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">
|
||||
<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">
|
||||
<span class="hero-detail-label">Serial Number</span>
|
||||
<span class="hero-detail-value mono">{{ printer.serialnumber }}</span>
|
||||
|
||||
@@ -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
|
||||
|
||||
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