WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,557 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The FULL VIEW of the configuration: everything, and where each thing is set.
|
||||
|
||||
The Project Config button is a window of four boxes of controls
|
||||
(`vibe.configform`): the settings a person actually changes between one press
|
||||
and the next, and nothing else. This is the other half -- the page behind its
|
||||
"Full view" button. Every setting the job layer may carry, every layer file,
|
||||
every read-only value with the sentence saying why it is read-only, and the
|
||||
provenance chain of anything more than one file had an opinion about.
|
||||
|
||||
The two are deliberately not two answers to the same question. The window says
|
||||
WHAT IS SET AND LETS YOU CHANGE IT; this says WHAT EXISTS AND WHERE IT IS
|
||||
CHANGED, and the `change it in` column is the join: it names the box that
|
||||
carries each setting, or says the file. That column comes from
|
||||
`configform.placement()`, so the mapping is written once.
|
||||
|
||||
WHAT THIS FIXED, AND WHY IT IS STILL A PAGE
|
||||
--------------------------------------------
|
||||
The button used to be a `CommandSwitchWindow` menu, and it had three defects
|
||||
in one press:
|
||||
|
||||
1. THE SENTENCE THAT EXPLAINS THE SCREEN WAS INSIDE THE SCREEN'S CHROME.
|
||||
`CommandSwitchWindow`'s `message` is a WPF `Label` docked left in a
|
||||
36-pixel row it shares with the search box, in a window fixed at 600 wide
|
||||
with `ResizeMode="NoResize"`. A Label does not wrap, so the explanation
|
||||
was cut off. Prose belongs where there is room for it: on this page, or in
|
||||
the real window's wrapping header.
|
||||
|
||||
2. THE PAGE WAS PRINTED IN THE MIDDLE OF A DIALOG LOOP, so a modal popup came
|
||||
straight back over it and dismissing that popup ended the run. Nothing is
|
||||
printed now until every window is shut.
|
||||
|
||||
3. THE OPTIONS WERE NAMED BY THEIR DOTTED KEYS. Every row in
|
||||
`configmodel.SETTINGS` already carries a `label` ("Report folder") and a
|
||||
`section` ("Output"); the screen showed `handling.report_dir` instead.
|
||||
Everything user-facing here is label first. The dotted key still appears
|
||||
-- it is what somebody grepping the YAML needs -- but as reference small
|
||||
print in its own column, never as the name of the thing.
|
||||
|
||||
THE SHAPE, WHICH IS THE TAB'S SHAPE
|
||||
------------------------------------
|
||||
`vibe.foldreport`: the answer is the headline and is never folded; everything
|
||||
else starts closed; a section carrying a problem opens itself and says why in
|
||||
a sentence. Here the answer is *every setting the job layer may carry, what it
|
||||
reads now, who decided it, and where it is changed*, so that is the headline.
|
||||
|
||||
The three things that open a section by themselves are the three that mean
|
||||
somebody is about to be surprised:
|
||||
|
||||
* a project config file is live (`!`), so the layers on screen are not what
|
||||
the model reads;
|
||||
* a setting no layer sets at all (`?`), which is not a value of nothing --
|
||||
it means nothing here could judge it;
|
||||
* a layer file that is not present, which is how a plant rule silently
|
||||
stops applying.
|
||||
|
||||
NO REVIT AND NO `doc`. Everything Revit-shaped -- the layer list, the
|
||||
locations, the run options -- is read by the button and passed in as data, so
|
||||
the whole page can be built and asserted on host CPython
|
||||
(`WillTools.extension/tests/test_configpage.py`).
|
||||
|
||||
IronPython 2.7: no f-strings, no pathlib, no dataclasses.
|
||||
"""
|
||||
# pylint: disable=invalid-name
|
||||
from vibe import configmodel
|
||||
from vibe import foldreport as fold
|
||||
|
||||
|
||||
NOT_SET = '(not set in any layer)'
|
||||
|
||||
# Comma-separated rather than dot-separated on purpose: `foldreport.md_text`
|
||||
# restores `—`, ` `, `†` and `&` and nothing else, so a
|
||||
# `·` here would reach the plain-text copy of the page as its own
|
||||
# source. Entities the text renderer does not know are not decoration that
|
||||
# degrades, they are visible breakage.
|
||||
LEGEND = ('> `D` defaults, `C` catalogue, `P` plant, `J` job, '
|
||||
'`!` project config, `?` no layer sets it, `+` several layers each '
|
||||
'set part of it (the layers MERGE that key, entry by entry). A `?` '
|
||||
'is **not** a value of nothing: it means nothing here could judge '
|
||||
'the setting.')
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ the rows
|
||||
|
||||
def value_text(key, res):
|
||||
"""What one setting reads as, and never as a blank.
|
||||
|
||||
An unknown value gets a sentence rather than an empty cell, for the reason
|
||||
`configmodel.resolve` gives: "judged and found nothing" and "could not
|
||||
judge" are different answers and an empty cell says the first one.
|
||||
"""
|
||||
if not res or not res.get('known'):
|
||||
return NOT_SET
|
||||
return configmodel.display_value(key, res['value'])
|
||||
|
||||
|
||||
def editable_rows(layers, settings=None):
|
||||
"""Every setting the job layer may carry, in SETTINGS order.
|
||||
|
||||
`n` is a stable position, not an interface: the editing is done in the
|
||||
four-module window (`vibe.configform`), and what this page adds is the
|
||||
column saying WHICH BOX each setting is in -- or that it is in none of
|
||||
them and is changed in the file.
|
||||
|
||||
`settings` overrides the source rows for tests. It defaults to
|
||||
`configmodel.editable()`, which is whatever the loaded build of
|
||||
`configmodel` knows about -- so a build that has not yet learned a setting
|
||||
simply does not number one, rather than numbering a row it cannot edit.
|
||||
"""
|
||||
rows = configmodel.editable() if settings is None else list(settings)
|
||||
out = []
|
||||
for index, row in enumerate(rows):
|
||||
res = configmodel.resolve(layers, row['key'])
|
||||
chip = configmodel.chip(res)
|
||||
value = value_text(row['key'], res)
|
||||
if configmodel.merges(row['key']):
|
||||
# A key the layers MERGE has no single winning layer, and
|
||||
# `resolve` -- which answers for a key that replaces -- would name
|
||||
# the last one that set an entry as though it owned all of them.
|
||||
# `J` on a map the plant also writes is the same lie as a silent
|
||||
# override, one level down.
|
||||
merged = configmodel.merged_map(layers, row['key'])
|
||||
chip = configmodel.map_chip(merged)
|
||||
value = configmodel.format_merged_map(merged) if merged \
|
||||
else NOT_SET
|
||||
res = dict(res)
|
||||
res['layer'] = ' + '.join(configmodel.map_layers(merged)) or None
|
||||
out.append({'n': index + 1,
|
||||
'key': row['key'],
|
||||
'label': row['label'],
|
||||
'section': row['section'],
|
||||
'row': row,
|
||||
'res': res,
|
||||
'chip': chip,
|
||||
'value': value})
|
||||
return out
|
||||
|
||||
|
||||
def group_rows(rows):
|
||||
"""[(section, [row])] in the order the sections first appear."""
|
||||
order = []
|
||||
holder = {}
|
||||
for row in rows or []:
|
||||
if row['section'] not in holder:
|
||||
holder[row['section']] = []
|
||||
order.append(row['section'])
|
||||
holder[row['section']].append(row)
|
||||
return [(name, holder[name]) for name in order]
|
||||
|
||||
|
||||
def unknown_rows(rows):
|
||||
"""The settings nothing in any layer decides."""
|
||||
return [row for row in rows or [] if not row['res']['known']]
|
||||
|
||||
|
||||
def config_rows(rows):
|
||||
"""The settings a live project config file is deciding, over the layers."""
|
||||
return [row for row in rows or [] if row['chip'] == '!']
|
||||
|
||||
|
||||
def contested_rows(rows):
|
||||
"""The settings more than one layer had an opinion about.
|
||||
|
||||
The full chain of a setting exactly one file mentions is one line long and
|
||||
it is already on the page. These are the ones where reading the chain
|
||||
tells the reader something the headline could not.
|
||||
"""
|
||||
return [row for row in rows or []
|
||||
if len(row['res']['chain']) > 1 or row['res']['overrides']]
|
||||
|
||||
|
||||
# -------------------------------------------------------------- the whole page
|
||||
|
||||
def render(title=None, layers=None, rows=None, problems=None,
|
||||
job_path=None, config_path=None, config_missing=False,
|
||||
env=None, run_options=None, run_options_rows=None,
|
||||
run_options_path=None, locations=None, report_dir=None,
|
||||
subtrees=None, muted=None, muted_note=None, muted_rows=None,
|
||||
notes=None, full_path=None, placement=None):
|
||||
"""The page as `foldreport.Section`s: headline first, the rest folded.
|
||||
|
||||
Everything is data. `layers` is `configdump.layer_values(doc,
|
||||
include_config=True)`; `subtrees` is [(name, note, [(key, value, layer)])];
|
||||
`locations` is `configdump.output_locations()`; `notes` are extra sentences
|
||||
for the headline (a build that could not show something says so there).
|
||||
|
||||
`placement` is {key: where a person changes it} -- the four-module
|
||||
window's boxes. A key it does not name is changed in the file, and the
|
||||
page says so rather than leaving the column blank: a blank there would
|
||||
read as "nowhere", and everything in the job layer is changeable
|
||||
somewhere.
|
||||
"""
|
||||
layers = list(layers or [])
|
||||
rows = editable_rows(layers) if rows is None else rows
|
||||
sections = [_headline(title, rows, problems, job_path, config_path,
|
||||
config_missing, notes, full_path, placement)]
|
||||
|
||||
missing_layers = [label for label, _p, exists, _d in layers if not exists]
|
||||
|
||||
unknown = unknown_rows(rows)
|
||||
if unknown:
|
||||
sections.append(_unknown(unknown))
|
||||
|
||||
if config_path:
|
||||
sections.append(_config(config_path, config_missing,
|
||||
config_rows(rows)))
|
||||
|
||||
sections.append(_read_only(layers))
|
||||
|
||||
if muted is not None or muted_note or muted_rows:
|
||||
sections.append(_muted(muted, muted_note, muted_rows))
|
||||
|
||||
sections.append(_run_options(run_options, run_options_rows,
|
||||
run_options_path))
|
||||
sections.extend(_subtrees(subtrees))
|
||||
sections.append(_layers(layers, missing_layers, env, job_path))
|
||||
sections.append(_output(locations, report_dir))
|
||||
sections.append(_chains(contested_rows(rows)))
|
||||
sections.append(_job_file())
|
||||
return sections
|
||||
|
||||
|
||||
def _headline(title, rows, problems, job_path, config_path, config_missing,
|
||||
notes, full_path, placement=None):
|
||||
section = fold.Section('now', 'What this job is set to', headline=True)
|
||||
section.md('# Configuration of {0}'.format(title or 'this model'))
|
||||
section.md('Every setting the job layer may carry, what it reads now, '
|
||||
'and which layer decided it. The **change it in** column says '
|
||||
'where: one of the four boxes in the Project Config window, or '
|
||||
'the file itself. Everything is written to the job layer and '
|
||||
'only the job layer: `{0}`'
|
||||
.format(job_path or '(not created yet)'))
|
||||
section.md('Everything else — what is read-only and why, where each '
|
||||
'file lives, the whole provenance chain, the run-time options '
|
||||
'— is folded below. **If nothing below is open, nothing '
|
||||
'below needs you.**')
|
||||
for line in notes or []:
|
||||
section.md('> ' + line)
|
||||
for line in problems or []:
|
||||
section.md('**{0}**'.format(line))
|
||||
if config_path:
|
||||
section.md(
|
||||
'> ## This model runs on a project config file\n>\n> `{0}`{1}\n>\n'
|
||||
'> It is the last word. **An edit made here does not reach this '
|
||||
'model** until that file is refreshed from the layers or cleared.'
|
||||
.format(config_path, ' **(MISSING)**' if config_missing else ''))
|
||||
for name, group in group_rows(rows):
|
||||
section.md('### ' + name)
|
||||
section.table(['setting', 'in force', 'from', 'change it in',
|
||||
'key'],
|
||||
[[_cell(row['label']), _cell(row['value']),
|
||||
'[{0}] {1}'.format(row['chip'],
|
||||
row['res']['layer'] or 'nothing'),
|
||||
_where(placement, row['key']),
|
||||
'`{0}`'.format(row['key'])]
|
||||
for row in group])
|
||||
section.md(LEGEND)
|
||||
if full_path:
|
||||
section.md('The whole page, nothing folded, is also written to `{0}` '
|
||||
'on every press.'.format(full_path))
|
||||
return section
|
||||
|
||||
|
||||
def _unknown(unknown):
|
||||
section = fold.Section(
|
||||
'unknown', 'Settings no layer sets',
|
||||
summary='{0} setting(s)'.format(len(unknown)),
|
||||
problem='{0} setting(s) that nothing in any layer decides. That is '
|
||||
'not a value of nothing -- whatever reads them falls back to '
|
||||
'its own idea, and nothing here can tell you what that is.'
|
||||
.format(len(unknown)))
|
||||
section.table(['setting', 'key'],
|
||||
[[_cell(row['label']), '`{0}`'.format(row['key'])]
|
||||
for row in unknown])
|
||||
section.md('Each one is still editable: in its box in the window, or in '
|
||||
'the job file.')
|
||||
return section
|
||||
|
||||
|
||||
def _config(config_path, config_missing, overridden):
|
||||
problem = ('A project config file is live, so the layers below are NOT '
|
||||
'what this model reads.')
|
||||
if config_missing:
|
||||
problem = ('The project config file this model points at is MISSING. '
|
||||
'Nothing is falling back quietly -- put it back, refresh '
|
||||
'it, or go back to the layered files.')
|
||||
section = fold.Section(
|
||||
'config', 'The project config file, which is the last word',
|
||||
summary='{0} setting(s) decided by it'.format(len(overridden)),
|
||||
problem=problem)
|
||||
section.md('`{0}`'.format(config_path))
|
||||
if overridden:
|
||||
section.table(['setting', 'in force', 'key'],
|
||||
[[_cell(row['label']), _cell(row['value']),
|
||||
'`{0}`'.format(row['key'])] for row in overridden])
|
||||
else:
|
||||
section.md('It sets none of the settings on this page, so every value '
|
||||
'above still comes from the layer named beside it.')
|
||||
section.md('Until it is refreshed from the layers or cleared, a '
|
||||
'job-layer edit does not reach this model.')
|
||||
return section
|
||||
|
||||
|
||||
def _read_only(layers):
|
||||
rows = []
|
||||
whys = []
|
||||
for row in configmodel.read_only():
|
||||
res = configmodel.resolve(layers, row['key'])
|
||||
rows.append([_cell(row['label']),
|
||||
_cell(value_text(row['key'], res)),
|
||||
'[{0}] {1}'.format(configmodel.chip(res),
|
||||
res['layer'] or 'nothing'),
|
||||
'`{0}`'.format(row['key'])])
|
||||
whys.append('**{0}** — {1}'.format(row['label'],
|
||||
row['why'] or ''))
|
||||
section = fold.Section('readonly', 'Shown here, not editable here',
|
||||
summary='{0} setting(s)'.format(len(rows)))
|
||||
section.md('This button writes the job layer and nothing else. What a '
|
||||
'plant\'s hardware is, and which part number is a face lifter, '
|
||||
'is not a job fact: one plant edit reaches every job, so '
|
||||
'editing that file stays a deliberate act somewhere else. '
|
||||
'Every one of these is still shown, with the sentence saying '
|
||||
'why it is not yours to change here.')
|
||||
section.table(['setting', 'in force', 'from', 'key'], rows)
|
||||
for line in whys:
|
||||
section.md('> ' + line)
|
||||
return section
|
||||
|
||||
|
||||
WHERE_MUTES_ARE_SET = (
|
||||
'Muting is configured in the **Ticket QC** box of the Project Config '
|
||||
'window: it lists every row the checklist asks, with a speaker beside '
|
||||
'each. Clicking one asks for the reason -- required, dated for you, '
|
||||
'written to the job layer.')
|
||||
|
||||
WHAT_A_MUTE_DOES = (
|
||||
'**Muting does not stop a check.** A muted row runs on every ticket '
|
||||
'exactly as before and keeps its place on the table; what changes is how '
|
||||
'it reports, quietly instead of as a red row, when the result is not a '
|
||||
'failure. **A failure is never quietened** — it stays on the table, '
|
||||
'in the tally, and in the machine-readable JSON, whatever this list says.')
|
||||
|
||||
|
||||
def _muted(muted, muted_note, muted_rows=None):
|
||||
"""What is muted, whose mute it is, and WHERE THE CONTROL IS.
|
||||
|
||||
That last sentence is on the page because its absence was the defect: the
|
||||
owner went looking for the mute control twice and did not find it, and
|
||||
this page -- the one surface that lists what is muted -- said nothing at
|
||||
all about where muting is done. A page that shows a state and not the
|
||||
thing that sets it sends the reader hunting.
|
||||
|
||||
`muted_rows` is `configform.mute_table(...)`: the same rows the window
|
||||
draws, so the layer column here and the buttons there cannot disagree. It
|
||||
is passed in rather than computed because it needs the ticket checklist
|
||||
table, which this module has no other business loading. Without it the
|
||||
section falls back to the flat map, which is what every older caller
|
||||
hands over.
|
||||
"""
|
||||
muted = muted if isinstance(muted, dict) else {}
|
||||
listed = [row for row in (muted_rows or []) if row.get('muted')]
|
||||
theirs = [row for row in listed if not row.get('editable')]
|
||||
stale = [row for row in listed if not row.get('known')]
|
||||
problem = None
|
||||
if muted:
|
||||
problem = ('{0} checklist item(s) are muted on this job. Each one '
|
||||
'still RUNS on every ticket and keeps its row -- muting '
|
||||
'only makes a result that is not a failure report quietly '
|
||||
'rather than red. It never quietens a FAIL, and its '
|
||||
'findings still reach the JSON.'.format(len(muted)))
|
||||
if stale:
|
||||
problem = ('{0} muted number(s) are not checklist items at all, so '
|
||||
'they mute NOTHING. Somebody believes a row is off while '
|
||||
'it is on.'.format(len(stale)))
|
||||
section = fold.Section('muted', 'Muted checklist items',
|
||||
summary='{0} muted'.format(len(muted)),
|
||||
problem=problem)
|
||||
if listed:
|
||||
section.table(['item', 'checklist row', 'why this job never has one',
|
||||
'set by'],
|
||||
[[_cell(row['number']), _cell(row['name']),
|
||||
_cell(row['reason'] or '(no reason given)'),
|
||||
_cell(row['layer'] or 'nothing')] for row in listed])
|
||||
elif muted:
|
||||
section.table(['item', 'why this job never has one'],
|
||||
[[_cell(key), _cell(muted[key])]
|
||||
for key in sorted(muted)])
|
||||
else:
|
||||
section.md('Nothing is muted on this job. Every checklist row reports '
|
||||
'at full volume.')
|
||||
section.md('> ' + WHAT_A_MUTE_DOES)
|
||||
section.md('> ' + WHERE_MUTES_ARE_SET)
|
||||
if theirs:
|
||||
# Not editable there, and saying so HERE as well: this page is where
|
||||
# somebody lands after failing to change one in the window, and a
|
||||
# second silence would send them to the wrong file.
|
||||
section.md('> **{0} of them are not the job layer\'s.** The knowledge '
|
||||
'layers MERGE this map, so a job entry cannot take a mute '
|
||||
'set higher up away -- {1}. Edit that layer\'s file to '
|
||||
'unmute.'.format(
|
||||
len(theirs),
|
||||
', '.join('{0} ({1})'.format(row['number'], row['layer'])
|
||||
for row in theirs)))
|
||||
if muted_note:
|
||||
section.md('> ' + muted_note)
|
||||
return section
|
||||
|
||||
|
||||
def _run_options(options, option_rows, path):
|
||||
options = options or {}
|
||||
option_rows = list(option_rows if option_rows is not None
|
||||
else configmodel.RUN_OPTIONS)
|
||||
section = fold.Section(
|
||||
'runopts', 'Run-time options — not knowledge',
|
||||
summary='{0} option(s)'.format(len(option_rows)))
|
||||
section.md('> No layers, no provenance, nothing overrides them. They are '
|
||||
'choices about what a run is willing to spend, which is a '
|
||||
'property of the run and not of the plant. `{0}`'
|
||||
.format(path or '(beside the model)'))
|
||||
section.table(
|
||||
['what it is', 'now', 'default', 'key'],
|
||||
[[_cell(row['label']),
|
||||
_cell('{0}'.format(options.get(row['key'], row['default']))),
|
||||
_cell('{0}'.format(row['default'])), '`{0}`'.format(row['key'])]
|
||||
for row in option_rows])
|
||||
return section
|
||||
|
||||
|
||||
def _subtrees(subtrees):
|
||||
"""One section for the parameter map, one for everything else shown raw."""
|
||||
out = []
|
||||
rest = []
|
||||
for name, note, leaves in subtrees or []:
|
||||
if name == 'parameters':
|
||||
section = fold.Section(
|
||||
'params', 'Revit parameter names this tool reads',
|
||||
summary='{0} role(s)'.format(len(leaves)),
|
||||
problem=None if leaves else
|
||||
'No parameter names are mapped, so the checks that read them '
|
||||
'do not run and say nothing about it.')
|
||||
section.md('> ' + note)
|
||||
section.table(['role', 'parameter name read', 'set by'],
|
||||
[[_cell(k), _cell(v), _cell(w)]
|
||||
for k, v, w in leaves])
|
||||
out.append(section)
|
||||
else:
|
||||
rest.append((name, note, leaves))
|
||||
if rest:
|
||||
section = fold.Section(
|
||||
'published', 'Published data and plant knowledge, read-only',
|
||||
summary='{0} group(s)'.format(len(rest)))
|
||||
for name, note, leaves in rest:
|
||||
section.md('### ' + name)
|
||||
section.md('> ' + note)
|
||||
section.table(['key', 'value read', 'set by'],
|
||||
[[_cell(k), _cell(v), _cell(w)]
|
||||
for k, v, w in leaves])
|
||||
out.append(section)
|
||||
return out
|
||||
|
||||
|
||||
def _layers(layers, missing, env, job_path):
|
||||
problem = None
|
||||
if missing:
|
||||
problem = ('{0} layer file(s) are not present: {1}. Whatever they say '
|
||||
'is not being applied, and nothing else reports that.'
|
||||
.format(len(missing), ', '.join(missing)))
|
||||
section = fold.Section(
|
||||
'layers', 'Where every layer lives',
|
||||
summary='{0} layer(s)'.format(len(layers)), problem=problem)
|
||||
section.md('Each one overrides the one above it.')
|
||||
section.table(['layer', 'file', 'present'],
|
||||
[[_cell(label), _cell(path or '(built into the tool)'),
|
||||
'yes' if exists else 'MISSING']
|
||||
for label, path, exists, _data in layers])
|
||||
section.md('The job layer, which is the one this button writes: `{0}`'
|
||||
.format(job_path or '(not created yet)'))
|
||||
section.table(['environment variable', 'value'],
|
||||
[[_cell(name), _cell(value)] for name, value in env or []])
|
||||
return section
|
||||
|
||||
|
||||
def _output(locations, report_dir):
|
||||
locations = locations or {}
|
||||
section = fold.Section('output', 'Where output goes',
|
||||
summary='{0} place(s)'.format(len(locations)))
|
||||
section.table(['what', 'where'],
|
||||
[[_cell(key.replace('_', ' ')), _cell(locations[key])]
|
||||
for key in sorted(locations)])
|
||||
section.md('`{0}` is the report folder in force, and it is the one path '
|
||||
'here that is a setting.'.format(report_dir))
|
||||
return section
|
||||
|
||||
|
||||
def _chains(rows):
|
||||
section = fold.Section(
|
||||
'chains', 'The full chain, where more than one layer had an opinion',
|
||||
summary='{0} setting(s)'.format(len(rows)))
|
||||
section.md('A setting exactly one file mentions is already answered on '
|
||||
'the page above. These are the ones where a layer was beaten, '
|
||||
'which is the case worth reading.')
|
||||
for row in rows:
|
||||
# No key line of its own: `chain_lines` leads with the dotted key and
|
||||
# the chip, so printing one here would say it twice.
|
||||
section.md('### ' + row['label'])
|
||||
for line in configmodel.chain_lines(row['res']):
|
||||
section.md(' ' + line)
|
||||
if not rows:
|
||||
section.md('Every setting comes from exactly one place.')
|
||||
return section
|
||||
|
||||
|
||||
def _job_file():
|
||||
section = fold.Section('jobfile', 'What panellift is handed',
|
||||
summary='8 field(s)')
|
||||
section.md('> The job file (`*.job.json`) written beside every report is '
|
||||
'not a second place to configure anything: it is generated '
|
||||
'from the settings above on every run. These are the fields it '
|
||||
'carries and where each one comes from, so the two halves of '
|
||||
'"the job layer" read as one thing.')
|
||||
section.table(['job file field', 'comes from'],
|
||||
[['rigging.mode', 'handling.rig_mode'],
|
||||
['rigging.sling_angle_deg', 'handling.sling_angle_deg'],
|
||||
['rigging.group_tolerance_ft',
|
||||
'handling.group_tolerance_ft'],
|
||||
['stages[].rig_mode', 'handling.stages[].rig_mode'],
|
||||
['stages[].multiplier / pci_case / strength',
|
||||
'handling.stages[]'],
|
||||
['stages[].lifters',
|
||||
'clicked on the piece in Panel Lift Check'],
|
||||
['rigging.tension_fos / shear_fos',
|
||||
'catalogue.safety_factors.*'],
|
||||
['panel.*', 'measured from the model']])
|
||||
return section
|
||||
|
||||
|
||||
def _where(placement, key):
|
||||
"""Which of the window's boxes changes this setting, or the file.
|
||||
|
||||
Never blank. A blank cell in a column headed "change it in" reads as
|
||||
"nowhere", and every setting on this page is changeable somewhere.
|
||||
"""
|
||||
if placement is None:
|
||||
return ''
|
||||
return placement.get(key) or 'the file'
|
||||
|
||||
|
||||
def _cell(value):
|
||||
"""One table cell. A pipe in a value would otherwise split the row.
|
||||
|
||||
`output.print_table` builds a markdown pipe table and hands it to the
|
||||
markdown renderer, so a muting reason containing a `|` -- a sentence
|
||||
somebody typed -- would silently become two columns and shift every cell
|
||||
after it.
|
||||
"""
|
||||
text = '' if value is None else '{0}'.format(value)
|
||||
return text.replace('|', '|')
|
||||
Reference in New Issue
Block a user