add Entra device identity to computers and Azure/Thor import sources
PC records now carry Entra/Azure device identity so imported machines keep a durable link back to their directory object instead of matching on hostname alone. Adds azuredeviceid, azureobjectid, and azuredevicecategory to the Computer model with matching migrations, surfaces them read-only in PCDetail as an Entra Device Info card, and lets PCForm edit the device id directly. The computers plugin registers a machineassetnumber import hook that reconciles PC-to-machine links during bulk import and warns on unresolved or conflicting relationships. Adds importsources adapters that normalize Azure device-group and Thor PC inventory exports into canonical ShopDB CSVs via hostname alignment, exposes register_import_hook and RowWarning on the shopdb.api contract surface (contract 0.21.0), and refreshes the Import Site Assets page with clearer source instructions. Includes reconcile_locations maintenance script and coverage for the new import paths.
This commit is contained in:
@@ -173,14 +173,17 @@ def test_generated_templates_only_contain_real_columns(db):
|
||||
|
||||
A hand-written template set had invented columns on 7 of 11 tables and named
|
||||
a table that does not exist. Generating from the schema is what makes that
|
||||
impossible - so assert the generator actually does it.
|
||||
impossible - so assert the generator actually does it. Plugin-registered
|
||||
virtual columns (machineassetnumber) are legitimate, not invented, so they
|
||||
are allowed alongside the table's real columns.
|
||||
"""
|
||||
from shopdb.core.services.csvimport import virtual_columns_for
|
||||
for table in IMPORTABLE:
|
||||
text = generate_template(table)
|
||||
header = [line for line in text.splitlines() if line and not line.startswith('#')][0]
|
||||
real = {c.name for c in importable_columns(table)}
|
||||
allowed = {c.name for c in importable_columns(table)} | virtual_columns_for(table)
|
||||
for column in header.split(','):
|
||||
assert column in real, '%s.csv offers a column that does not exist: %s' % (table, column)
|
||||
assert column in allowed, '%s.csv offers a column that does not exist: %s' % (table, column)
|
||||
|
||||
|
||||
def test_every_template_includes_its_required_columns(db):
|
||||
@@ -234,3 +237,46 @@ def test_a_dry_run_over_several_files_resolves_across_them(db):
|
||||
assert result.ok, [str(p) for p in result.problems]
|
||||
_db.session.rollback()
|
||||
assert count('assets') == 0
|
||||
|
||||
|
||||
def _example_files():
|
||||
"""Reconstruct each template's example rows as an importable CSV: the header
|
||||
plus the live (uncommented) example data rows that follow it."""
|
||||
files = {}
|
||||
for table in IMPORTABLE:
|
||||
header = None
|
||||
rows = []
|
||||
for line in generate_template(table).splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
if header is None:
|
||||
header = line
|
||||
else:
|
||||
rows.append(line)
|
||||
files[table] = '\n'.join([header] + rows) + '\n'
|
||||
return files
|
||||
|
||||
|
||||
def test_template_examples_are_present_for_every_table(db):
|
||||
"""Each template ships at least one worked example row, so a downloaded
|
||||
set is a usable starter workbook rather than a bare header."""
|
||||
files = _example_files()
|
||||
for table in IMPORTABLE:
|
||||
rows = files[table].splitlines()[1:]
|
||||
assert rows, '%s.csv has no example row' % table
|
||||
|
||||
|
||||
def test_template_example_set_imports_cleanly(db):
|
||||
"""The whole curated example set must import as-is: every name a later file
|
||||
references is defined by an earlier one, with no self-references that the
|
||||
validation pass cannot resolve. This is the promise the workbook makes."""
|
||||
resolver = Resolver()
|
||||
files = _example_files()
|
||||
for table in IMPORTABLE:
|
||||
result = import_csv(table, files[table], resolver=resolver)
|
||||
assert result.ok, '%s.csv example failed: %s' % (
|
||||
table, [str(p) for p in result.problems])
|
||||
_db.session.rollback()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user