Add flask seed demo sample data; widen settings.description to TEXT
This commit is contained in:
@@ -511,3 +511,280 @@ def seed_settings():
|
||||
|
||||
db.session.commit()
|
||||
click.echo(click.style(f"{created} default settings created.", fg='green'))
|
||||
|
||||
|
||||
# Demo assets carry this assetnumber prefix so a re-run skips what it made and
|
||||
# an operator can bulk-delete them without touching imported/real rows.
|
||||
DEMO_PREFIX = 'DEMO-'
|
||||
|
||||
|
||||
@seed_cli.command('demo')
|
||||
@click.option('--force', is_flag=True,
|
||||
help='Add demo rows even if DEMO- assets already exist.')
|
||||
@with_appcontext
|
||||
def seed_demo(force):
|
||||
"""Seed a small, broad sample dataset for a dev/eval site.
|
||||
|
||||
Populates a handful of rows across every asset-based plugin (machines,
|
||||
computers, printers, network devices, measuring tools) plus 3D-printed
|
||||
parts, with supporting vendors/business-units/locations and a few
|
||||
relationships, so every screen has something to show. Run AFTER
|
||||
`flask seed reference-data` and after the plugins are installed. Idempotent:
|
||||
all rows are keyed on the DEMO- prefix and skipped if already present.
|
||||
|
||||
Not for production. Remove later with:
|
||||
flask seed demo-clear
|
||||
"""
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import (Asset, AssetType, AssetStatus, Location,
|
||||
BusinessUnit, Vendor)
|
||||
|
||||
existing = Asset.query.filter(
|
||||
Asset.assetnumber.like(f'{DEMO_PREFIX}%')).count()
|
||||
if existing and not force:
|
||||
click.echo(click.style(
|
||||
f"{existing} demo assets already present - nothing to do "
|
||||
f"(use --force to add more, or `flask seed demo-clear` to reset).",
|
||||
fg='yellow'))
|
||||
return
|
||||
|
||||
def status_id(name, fallback=1):
|
||||
# resolve status by name, fall back to whatever id 1 is
|
||||
s = AssetStatus.query.filter_by(status=name).first()
|
||||
return s.statusid if s else fallback
|
||||
|
||||
def get_or_make(model, defaults=None, **lookup):
|
||||
# tiny idempotent upsert keyed on lookup fields
|
||||
row = model.query.filter_by(**lookup).first()
|
||||
if row:
|
||||
return row
|
||||
row = model(**lookup, **(defaults or {}))
|
||||
db.session.add(row)
|
||||
db.session.flush()
|
||||
return row
|
||||
|
||||
# Supporting reference rows (shared across the asset types below).
|
||||
vendors = {v: get_or_make(Vendor, vendor=v) for v in
|
||||
('Haas Automation', 'DMG Mori', 'Dell', 'Zeiss', 'Cisco',
|
||||
'Brother')}
|
||||
units = {u: get_or_make(BusinessUnit, businessunit=u) for u in
|
||||
('Machining', 'Inspection', 'IT')}
|
||||
locations = {loc: get_or_make(Location, locationname=loc) for loc in
|
||||
('Cell A', 'Cell B', 'QA Lab', 'Server Room', 'Front Office')}
|
||||
|
||||
made = {'assets': 0, 'skipped': 0}
|
||||
|
||||
def make_asset(assettype_name, number, name, subtype_model,
|
||||
status='In Use', location=None, unit=None, vendor=None,
|
||||
serialnumber=None, subtype_kwargs=None):
|
||||
# create one Asset + its plugin subtype row, idempotent on assetnumber.
|
||||
# returns the Asset, or None when the plugin type is not installed.
|
||||
atype = AssetType.query.filter_by(assettype=assettype_name).first()
|
||||
if not atype:
|
||||
return None
|
||||
assetnumber = f'{DEMO_PREFIX}{number}'
|
||||
if Asset.query.filter_by(assetnumber=assetnumber).first():
|
||||
made['skipped'] += 1
|
||||
return None
|
||||
asset = Asset(
|
||||
assetnumber=assetnumber,
|
||||
name=name,
|
||||
assettypeid=atype.assettypeid,
|
||||
statusid=status_id(status),
|
||||
serialnumber=serialnumber,
|
||||
locationid=locations[location].locationid if location else None,
|
||||
businessunitid=units[unit].businessunitid if unit else None,
|
||||
)
|
||||
db.session.add(asset)
|
||||
db.session.flush()
|
||||
sub = subtype_model(assetid=asset.assetid, **(subtype_kwargs or {}))
|
||||
db.session.add(sub)
|
||||
made['assets'] += 1
|
||||
return asset
|
||||
|
||||
from plugins.machines.models import Machine
|
||||
from plugins.computers.models import Computer
|
||||
from plugins.printers.models import Printer
|
||||
from plugins.network.models import NetworkDevice
|
||||
from plugins.measuringtools.models import MeasuringTool
|
||||
|
||||
machines = [
|
||||
('MILL-01', 'Haas VF-2 Mill', 'In Use', 'Cell A', 'Machining'),
|
||||
('MILL-02', 'Haas VF-4 Mill', 'In Use', 'Cell A', 'Machining'),
|
||||
('LATHE-01', 'DMG Mori NLX Lathe', 'In Use', 'Cell B', 'Machining'),
|
||||
('LATHE-02', 'DMG Mori CLX Lathe', 'In Repair', 'Cell B', 'Machining'),
|
||||
('EDM-01', 'Wire EDM', 'Inventory', 'Cell B', 'Machining'),
|
||||
('GRIND-01', 'Surface Grinder', 'In Use', 'Cell A', 'Machining'),
|
||||
]
|
||||
for num, name, st, loc, unit in machines:
|
||||
make_asset('machine', num, name, Machine, status=st,
|
||||
location=loc, unit=unit, serialnumber=f'SN-{num}')
|
||||
|
||||
computers = [
|
||||
('PC-01', 'Shopfloor PC - Cell A', 'In Use', 'Cell A'),
|
||||
('PC-02', 'Shopfloor PC - Cell B', 'In Use', 'Cell B'),
|
||||
('PC-03', 'QA Workstation', 'In Use', 'QA Lab'),
|
||||
('PC-04', 'Engineering Laptop', 'In Use', 'Front Office'),
|
||||
('PC-05', 'Spare Desktop', 'Inventory', 'Front Office'),
|
||||
('PC-06', 'Retired Tower', 'Retired', 'Front Office'),
|
||||
]
|
||||
for num, name, st, loc in computers:
|
||||
make_asset('computer', num, name, Computer, status=st,
|
||||
location=loc, unit='IT', serialnumber=f'SN-{num}')
|
||||
|
||||
printers = [
|
||||
('PRN-01', 'Cell A Label Printer', 'In Use', 'Cell A'),
|
||||
('PRN-02', 'QA Report Printer', 'In Use', 'QA Lab'),
|
||||
('PRN-03', 'Office MFP', 'In Use', 'Front Office'),
|
||||
('PRN-04', 'Spare Printer', 'Inventory', 'Front Office'),
|
||||
]
|
||||
for num, name, st, loc in printers:
|
||||
make_asset('printer', num, name, Printer, status=st,
|
||||
location=loc, unit='IT')
|
||||
|
||||
network = [
|
||||
('NET-01', 'Cell A Switch', 'In Use', 'Cell A'),
|
||||
('NET-02', 'Cell B Switch', 'In Use', 'Cell B'),
|
||||
('NET-03', 'Core Switch', 'In Use', 'Server Room'),
|
||||
('NET-04', 'Shop Access Point', 'In Use', 'Cell A'),
|
||||
]
|
||||
for num, name, st, loc in network:
|
||||
make_asset('network_device', num, name, NetworkDevice, status=st,
|
||||
location=loc, unit='IT')
|
||||
|
||||
tools = [
|
||||
('CMM-01', 'Zeiss CMM', 'In Use', 'QA Lab'),
|
||||
('GAGE-01', 'Height Gage', 'In Use', 'QA Lab'),
|
||||
('GAGE-02', 'Bore Gage', 'In Use', 'QA Lab'),
|
||||
('MIC-01', 'Digital Micrometer', 'In Use', 'Cell A'),
|
||||
('CAL-01', 'Digital Caliper', 'Inventory', 'QA Lab'),
|
||||
]
|
||||
for num, name, st, loc in tools:
|
||||
make_asset('measuring_tool', num, name, MeasuringTool, status=st,
|
||||
location=loc, unit='Inspection')
|
||||
|
||||
# 3D-printed parts are not assets - own table. A couple sit below their
|
||||
# low-stock threshold on purpose so the low-stock alert has something to fire.
|
||||
printedparts_made = 0
|
||||
try:
|
||||
from plugins.printedparts.models import PrintedItem
|
||||
parts = [
|
||||
# itemname, itemcode, gagelabtag, qty, threshold, bin
|
||||
('Fixture Bracket', 'PP0001', 'WJRP10021', 12, 4, 'A1'),
|
||||
('Gage Holder', 'PP0002', 'WJRP10022', 3, 5, 'A2'),
|
||||
('Cable Clip', 'PP0003', None, 40, 10, 'B1'),
|
||||
('Sensor Mount', 'PP0004', 'WJRP10023', 2, 6, 'B2'),
|
||||
('Label Guide', 'PP0005', None, 25, 8, 'C1'),
|
||||
('Knob Cover', 'PP0006', None, 0, 3, 'C2'),
|
||||
]
|
||||
for name, code, tag, qty, thr, binloc in parts:
|
||||
if PrintedItem.query.filter_by(itemcode=code).first():
|
||||
continue
|
||||
db.session.add(PrintedItem(
|
||||
itemname=name, itemcode=code, gagelabtag=tag,
|
||||
quantityonhand=qty, lowstockthreshold=thr, binlocation=binloc,
|
||||
itemdescription=f'Sample 3D-printed part: {name}.'))
|
||||
printedparts_made += 1
|
||||
except ImportError:
|
||||
pass # printedparts plugin not installed - skip
|
||||
|
||||
db.session.flush()
|
||||
|
||||
# A few relationships so the map + relationship cards are not empty.
|
||||
rels_made = 0
|
||||
try:
|
||||
from shopdb.core.models.relationship import (RelationshipType,
|
||||
AssetRelationship)
|
||||
|
||||
def asset_by(number):
|
||||
return Asset.query.filter_by(
|
||||
assetnumber=f'{DEMO_PREFIX}{number}').first()
|
||||
|
||||
def link(source_num, target_num, typename):
|
||||
nonlocal rels_made
|
||||
rt = RelationshipType.query.filter_by(
|
||||
relationshiptype=typename).first()
|
||||
s, t = asset_by(source_num), asset_by(target_num)
|
||||
if not (rt and s and t):
|
||||
return
|
||||
exists = AssetRelationship.query.filter_by(
|
||||
sourceassetid=s.assetid, targetassetid=t.assetid,
|
||||
relationshiptypeid=rt.relationshiptypeid).first()
|
||||
if exists:
|
||||
return
|
||||
db.session.add(AssetRelationship(
|
||||
sourceassetid=s.assetid, targetassetid=t.assetid,
|
||||
relationshiptypeid=rt.relationshiptypeid))
|
||||
rels_made += 1
|
||||
|
||||
link('PC-01', 'MILL-01', 'controls') # cell PC drives the mill
|
||||
link('PC-02', 'LATHE-01', 'controls')
|
||||
link('PC-01', 'PRN-01', 'defaultprinter') # PC to its default printer
|
||||
link('MILL-01', 'NET-01', 'connectedto') # machine on the cell switch
|
||||
link('NET-01', 'NET-03', 'connectedto') # cell switch to core
|
||||
except Exception:
|
||||
pass # relationship model surface changed - skip, assets still seeded
|
||||
|
||||
db.session.commit()
|
||||
click.echo(click.style(
|
||||
f"Demo data seeded: {made['assets']} assets, "
|
||||
f"{printedparts_made} printed parts, {rels_made} relationships "
|
||||
f"({made['skipped']} already existed).", fg='green'))
|
||||
click.echo("Remove later with: flask seed demo-clear")
|
||||
|
||||
|
||||
@seed_cli.command('demo-clear')
|
||||
@click.option('--yes', is_flag=True, help='Skip the confirmation prompt.')
|
||||
@with_appcontext
|
||||
def seed_demo_clear(yes):
|
||||
"""Delete everything `flask seed demo` created (DEMO- assets + sample parts).
|
||||
|
||||
Only touches rows the demo seeder made: assets with the DEMO- prefix (their
|
||||
plugin subtype rows cascade) and the PP000x sample printed parts. Leaves
|
||||
reference data, settings, users, and any real/imported rows alone.
|
||||
"""
|
||||
from shopdb.extensions import db
|
||||
from shopdb.core.models import Asset
|
||||
|
||||
demo_ids = [a.assetid for a in Asset.query.filter(
|
||||
Asset.assetnumber.like(f'{DEMO_PREFIX}%')).all()]
|
||||
try:
|
||||
from plugins.printedparts.models import PrintedItem
|
||||
parts_count = PrintedItem.query.filter(
|
||||
PrintedItem.itemcode.like('PP000%')).count()
|
||||
except ImportError:
|
||||
parts_count = 0
|
||||
|
||||
if not demo_ids and not parts_count:
|
||||
click.echo(click.style("No demo data found.", fg='yellow'))
|
||||
return
|
||||
if not yes:
|
||||
click.confirm(
|
||||
f"Delete {len(demo_ids)} demo assets and "
|
||||
f"{parts_count} sample parts?", abort=True)
|
||||
|
||||
if demo_ids:
|
||||
# Drop the demo relationships first - assetrelationships has no cascade
|
||||
# to assets, so a leftover edge would block the asset delete.
|
||||
from shopdb.core.models.relationship import AssetRelationship
|
||||
AssetRelationship.query.filter(
|
||||
db.or_(AssetRelationship.sourceassetid.in_(demo_ids),
|
||||
AssetRelationship.targetassetid.in_(demo_ids))
|
||||
).delete(synchronize_session=False)
|
||||
# Bulk hard-delete via a single DELETE statement so the DB-level
|
||||
# ON DELETE CASCADE removes each plugin subtype row. Per-object
|
||||
# ORM delete would instead try to NULL the child assetid (NOT NULL)
|
||||
# and fail.
|
||||
Asset.query.filter(Asset.assetid.in_(demo_ids)).delete(
|
||||
synchronize_session=False)
|
||||
|
||||
parts_deleted = 0
|
||||
if parts_count:
|
||||
parts_deleted = PrintedItem.query.filter(
|
||||
PrintedItem.itemcode.like('PP000%')).delete(
|
||||
synchronize_session=False)
|
||||
|
||||
db.session.commit()
|
||||
click.echo(click.style(
|
||||
f"Removed {len(demo_ids)} demo assets and "
|
||||
f"{parts_deleted} sample parts.", fg='green'))
|
||||
|
||||
Reference in New Issue
Block a user