The flash report: the one preliminary page a customer leaves the test
with (report feature flash_report).
The full report takes days, because the data are reviewed before it
is written; the program cannot wait that long to know where it stands.
With the feature on, the report is drawn as one page built from what
the project already holds, in the order its reader asks:
- PRELIMINARY across the top, with the sentence that the results are
subject to data review and that the test report supersedes the page;
- the identification (
report.identity), its status Preliminary on
the page and in every printed margin, with the people authorizing it
and a blank line for each signature;
- the headline: each requirement read against its run so far
(
report.requirement.read), one line per run, with how many of its
criteria were met against which document and the criteria not met
and not judged, by name. With the feature run_sections on, each
line is labeled by its run's axis, run and level (report.runs) and
the lines follow the order the runs were run. The lines are results
as read on the day, never a statement of conformity, and the page
says so;
- as run against the plan: the deviations of the findings register
(
report.findings) by number, or "No deviations." where the author
set them to None;
- what is open: the anomalies and nonconformances by number, each with
its status, its fact and its disposition (who owns it), then the
condition of the article on release;
- the opinions given at the console, each marked as an opinion and
attributed;
- what happens next, with the date the test report is due.
With a run log brought into the project (Project.attach_run_log),
the page gains a short part of its own, From the Run Log, before what
happens next, kept to what the page does not already say: for each
extract, which log, test and entry it holds (with its badge when the
log has newer entries), the runs so far with how each ended, the holds
open, the anomalies open that the findings register does not hold,
with their owners, the opinions given at the console that the page
does not record in the same words, and the checklists with lines still
owed. A long list stops at LISTED and says how many more the log
holds. Each is read from the extract as it was taken (only the badge
looks at the live file), and a kind the log holds none of is said to be
held none of by the log at its entry, never to have been none. The
page's own parts stay the author's: the opinions the page records, the
condition on release and what happens next are still typed, and still
owed until they are. So a day with a run log still prints on one
sheet, the part is set small and the page a little tighter while it is
on it (LOG_CSS); a page without a run log keeps its own style.
Nothing on the page is invented. The condition on release, the
opinions, what happens next and the report's date are typed by the
author (enter) and kept with the report's identity, so a report
template, which carries only the laboratory's fields, never passes one
test's page to the next. Anything the project does not hold is drawn as
owed (NOT YET WRITTEN, with what belongs there) and listed by check,
which the export asks about and which stands in the way of issuing the
page. The decision to send it stays the author's.
The feature needs Requirement and results, Identity and issue and the
Findings register; the register lives in the skeleton's deviations
section, so the skeleton is on too, but its sections are not drawn: the
flash report is the page.
Functions:
| Name |
Description |
held |
What the author has typed for the page: each of TYPED that is
|
enter |
Type the parts of the flash report only the author can write.
|
headline |
The headline's lines, plain text: one per requirement of the
|
check |
What the flash report still owes before it is issued, one line
|
log_lines |
What a run log extract adds to report's flash page, kept to what
|
log_html |
The run log's part of report's flash page, drawn as HTML: for
|
page_blocks |
The flash report as the page's blocks, each a text block already
|
Functions:
held
held(report: Any) -> dict[str, Any]
What the author has typed for the page: each of TYPED that is
entered, 'opinions' (a list of {'by', 'text'}) and 'opinions_none'
(True when the author said none were given).
Source code in src/visualdynamics/report/flash.py
| def held(report: Any) -> dict[str, Any]:
"""What the author has typed for the page: each of `TYPED` that is
entered, 'opinions' (a list of {'by', 'text'}) and 'opinions_none'
(True when the author said none were given)."""
return dict(ri._identity(report).get(HELD) or {})
|
enter
enter(report: Any, *, condition: str | None = None, next_steps: str | None = None, report_date: str | None = None, opinions: Sequence[Any] | None = None, opinions_none: bool | None = None) -> None
Type the parts of the flash report only the author can write.
condition is the article's condition on release, next_steps what
happens next, and report_date the date the test report is due,
each a str ('' clears it). opinions replaces the opinions given at
the console, each (by, text) or a mapping of by and text; an
opinion is recorded with who gave it. opinions_none True says no
opinion was given, False takes that back. A part left as None is
not changed.
Refused before anything changes: the feature off; a value of the
wrong type; an opinion without its words or without who gave it;
and opinions entered together with None said of them.
Raises:
| Type |
Description |
ValueError
|
For a refusal; the message names the part and the reason.
|
TypeError
|
When a value is of the wrong type.
|
Source code in src/visualdynamics/report/flash.py
| def enter(report: Any, *, condition: str | None = None,
next_steps: str | None = None, report_date: str | None = None,
opinions: Sequence[Any] | None = None,
opinions_none: bool | None = None) -> None:
"""Type the parts of the flash report only the author can write.
`condition` is the article's condition on release, `next_steps` what
happens next, and `report_date` the date the test report is due,
each a str ('' clears it). `opinions` replaces the opinions given at
the console, each (by, text) or a mapping of `by` and `text`; an
opinion is recorded with who gave it. `opinions_none` True says no
opinion was given, False takes that back. A part left as None is
not changed.
Refused before anything changes: the feature off; a value of the
wrong type; an opinion without its words or without who gave it;
and opinions entered together with None said of them.
Raises
------
ValueError
For a refusal; the message names the part and the reason.
TypeError
When a value is of the wrong type.
"""
_require(report)
page = held(report)
texts = {'condition': condition, 'next_steps': next_steps,
'report_date': report_date}
for key, given in texts.items():
if given is not None and not isinstance(given, str):
raise TypeError(f'{LABELS[key]} is a str, not '
f'{type(given).__name__}')
if opinions_none is not None and not isinstance(opinions_none, bool):
raise TypeError(f'opinions_none is True or False, not '
f'{type(opinions_none).__name__}')
if opinions is not None:
if isinstance(opinions, (str, Mapping)):
raise TypeError('opinions is a list of opinions, each (by, text)')
cleaned = [_opinion(given, number)
for number, given in enumerate(opinions, start=1)]
else:
cleaned = page.get('opinions', [])
none = (opinions_none if opinions_none is not None
else bool(page.get('opinions_none')))
if none and cleaned:
raise ValueError('opinions are entered and also said to be none; '
'clear the opinions or take back the None')
for key, given in texts.items():
if given is None:
continue
if given.strip():
page[key] = given.strip()
else:
page.pop(key, None)
if cleaned:
page['opinions'] = cleaned
else:
page.pop('opinions', None)
if none:
page['opinions_none'] = True
else:
page.pop('opinions_none', None)
identity = ri._identity(report)
if page:
identity[HELD] = page
else:
identity.pop(HELD, None)
report.identity = identity
|
headline
headline(report: Any, objects: Mapping[str, Any], object_groups: Sequence[Mapping[str, Any]] | None = None) -> list[str]
The headline's lines, plain text: one per requirement of the
report read against its run, in the order the runs were run with
the feature run_sections on (each labeled by its run identity),
else in the report's order (each labeled by the record it reads).
A requirement whose specification or run is not in the project is
left out, as the page leaves out a block that does not resolve; one
that cannot be read says nothing was judged, and why. Empty when no
requirement is read.
Source code in src/visualdynamics/report/flash.py
| def headline(report: Any, objects: Mapping[str, Any],
object_groups: Sequence[Mapping[str, Any]] | None = None
) -> list[str]:
"""The headline's lines, plain text: one per requirement of the
report read against its run, in the order the runs were run with
the feature `run_sections` on (each labeled by its run identity),
else in the report's order (each labeled by the record it reads).
A requirement whose specification or run is not in the project is
left out, as the page leaves out a block that does not resolve; one
that cannot be read says nothing was judged, and why. Empty when no
requirement is read."""
from .features import enabled
from .requirement import read, roles_on
runs_on = 'run_sections' in enabled(report)
entries: list[tuple[str | None, str, str]] = []
for block in report.blocks:
if 'requirement' not in block:
continue
name = _measured(block, objects, object_groups)
if name is None:
continue
try:
found = read(block, objects, object_groups,
roles=roles_on(report))
except (TypeError, ValueError) as refusal:
said = f'nothing judged; {refusal}'
else:
if found is None:
continue
said = _result(found)
root = _root(objects, name) if runs_on else None
entries.append((root, name, said))
if not runs_on:
return [f'{name}: {said}.' for _root_name, name, said in entries]
return _by_run(entries, objects, object_groups)
|
check
check(report: Any, objects: Mapping[str, Any], object_groups: Sequence[Mapping[str, Any]] | None = None) -> list[str]
What the flash report still owes before it is issued, one line
each, in the page's order: the identification's fields not entered,
the headline when no requirement is read, each kind of finding
neither entered nor set to None, each finding's fact and its
approval or disposition not written, the typed parts not written,
and the opinions neither recorded nor set to None. Empty when the
page owes nothing.
Raises ValueError when the feature is off: there is then no flash
report to check, and an empty list would read as one complete.
Source code in src/visualdynamics/report/flash.py
| def check(report: Any, objects: Mapping[str, Any],
object_groups: Sequence[Mapping[str, Any]] | None = None
) -> list[str]:
"""What the flash report still owes before it is issued, one line
each, in the page's order: the identification's fields not entered,
the headline when no requirement is read, each kind of finding
neither entered nor set to None, each finding's fact and its
approval or disposition not written, the typed parts not written,
and the opinions neither recorded nor set to None. Empty when the
page owes nothing.
Raises `ValueError` when the feature is off: there is then no flash
report to check, and an empty list would read as one complete.
"""
_require(report)
owed = []
gaps = ri.missing(report)
if gaps:
owed.append(f'Identification: {", ".join(gaps)} {ri.NOT_ENTERED}')
if not headline(report, objects, object_groups):
owed.append('Headline: no requirement is read against a run')
entries = fr.register(report)
said = cf.declared(skeleton.words(report, fr.SECTION))
for kind in cf.KINDS:
mine = [entry for entry in entries if entry['kind'] == kind]
if not mine and kind not in said:
owed.append(f'{cf.PLURAL[kind]}: neither entered nor set '
'to None')
for entry in mine:
missing = []
if not entry['fact']:
missing.append('fact')
if not entry['approval']:
missing.append(cf.FOURTH[entry['kind']].lower())
if missing:
owed.append(f'{entry["number"]}: {" and ".join(missing)} not '
'yet written')
page = held(report)
for key, label, _what in TYPED:
if not page.get(key):
owed.append(f'{label}: not yet written')
if not page.get('opinions') and not page.get('opinions_none'):
owed.append('Opinions: neither recorded nor set to None')
return owed
|
log_lines
log_lines(report: Any, extract: Any) -> tuple[list[tuple[str, list[str]]], list[str]]
What a run log extract adds to report's flash page, kept to what
the page does not say already: ([(title, lines)], [kinds held none
of]).
The parts are the runs so far with how each ended, the holds open
(by what each asks), the anomalies open that the findings register
does not hold under their number, with their owners, the opinions
given at the console that the page does not record in the same
words, and the checklists with lines still owed, each with how many.
A part with more than LISTED lines lists that many and says how
many more the log holds. A part the log holds none of is not drawn;
its kind is named in the second list, so the page says the log
holds none by its last entry, never that there was none.
Source code in src/visualdynamics/report/flash.py
| def log_lines(report: Any, extract: Any) -> tuple[list[tuple[str, list[str]]],
list[str]]:
"""What a run log extract adds to `report`'s flash page, kept to what
the page does not say already: ([(title, lines)], [kinds held none
of]).
The parts are the runs so far with how each ended, the holds open
(by what each asks), the anomalies open that the findings register
does not hold under their number, with their owners, the opinions
given at the console that the page does not record in the same
words, and the checklists with lines still owed, each with how many.
A part with more than `LISTED` lines lists that many and says how
many more the log holds. A part the log holds none of is not drawn;
its kind is named in the second list, so the page says the log
holds none by its last entry, never that there was none.
"""
from ..core.run_log_model import HOLD_RULES
from .runs import _logged_outcome
upto = extract.upto
runs = [f'run {run.number}' + (f' ({run.axis})' if run.axis else '')
+ f' {_logged_outcome(run, upto)}'
for run in sorted(extract.runs(), key=lambda run: run.entries[0].id)]
holds = [f'{HOLD_RULES.get(hold.rule, hold.message)} (#{hold.entry.id})'
for hold in extract.holds()]
entered = {entry['number'] for entry in fr.register(report)}
anomalies = [f'{found.number}, owner {found.owner or "not logged"}: '
f'{found.fact or "fact not logged"}'
for found in extract.open_anomalies()
if found.number not in entered]
typed = {opinion['text'] for opinion in held(report).get('opinions', [])}
opinions = []
for entry in extract.entries:
if entry.kind != 'opinion_given':
continue
words = entry.fields.get('opinion') or entry.text or 'not logged'
if words in typed:
continue
to = entry.fields.get('to')
opinions.append(f'by {entry.author}' + (f' to {to}' if to else '')
+ f': {words}')
owed = [f'{found.title.lower()}, {len(found.owed)} of {len(found.lines)}'
for found in extract.owed()]
parts = (('Runs so far', runs, 'run'), ('Holds open', holds, 'open hold'),
('Anomalies open', anomalies, 'open anomaly'),
('Opinions logged', opinions, 'opinion'),
('Still owed', owed, 'line still owed'))
shown = [(title, _capped(lines)) for title, lines, _none in parts if lines]
return shown, [none for _title, lines, none in parts if not lines]
|
log_html
log_html(report: Any, objects: Mapping[str, Any]) -> str
The run log's part of report's flash page, drawn as HTML: for
each run log extract in objects, a line citing its log, test and
entry (with the extract's badge when the log has newer entries),
then a line for each part of log_lines and one for what the log
holds none of. '' when objects holds no extract, so the page is
as it was.
Source code in src/visualdynamics/report/flash.py
| def log_html(report: Any, objects: Mapping[str, Any]) -> str:
"""The run log's part of `report`'s flash page, drawn as HTML: for
each run log extract in `objects`, a line citing its log, test and
entry (with the extract's badge when the log has newer entries),
then a line for each part of `log_lines` and one for what the log
holds none of. '' when `objects` holds no extract, so the page is
as it was."""
from ..core.run_log_extract import held as extracts_in
extracts = extracts_in(objects)
if not extracts:
return ''
out = [_head(FROM_LOG)]
for _name, extract in extracts:
shown, none = log_lines(report, extract)
lines = [f'<strong>{html.escape(title)}:</strong> '
f'{html.escape("; ".join(items))}' for title, items in shown]
if none:
lines.append(html.escape(_nothing(none, extract.upto)))
out.append(f'<p class="flash-log-source">'
f'{html.escape(f"As recorded in {extract.cited()}.")}</p>'
+ ''.join(f'<p class="flash-log">{line}</p>'
for line in lines))
return ''.join(out)
|
page_blocks
page_blocks(report: Any, objects: Mapping[str, Any], object_groups: Sequence[Mapping[str, Any]] | None = None) -> list[dict[str, Any]]
The flash report as the page's blocks, each a text block already
drawn as HTML (joined with no white space between tags, since the
page keeps a text block's white space): the PRELIMINARY line across
the top, the identification, the headline, the deviations, what is
open, the opinions, what the run log holds (log_html, only with a
run log extract in the project), and what happens next closed by the
end mark.
Source code in src/visualdynamics/report/flash.py
| def page_blocks(report: Any, objects: Mapping[str, Any],
object_groups: Sequence[Mapping[str, Any]] | None = None
) -> list[dict[str, Any]]:
"""The flash report as the page's blocks, each a text block already
drawn as HTML (joined with no white space between tags, since the
page keeps a text block's white space): the PRELIMINARY line across
the top, the identification, the headline, the deviations, what is
open, the opinions, what the run log holds (`log_html`, only with a
run log extract in the project), and what happens next closed by the
end mark."""
entries = fr.register(report)
said = cf.declared(skeleton.words(report, fr.SECTION))
page = held(report)
lines = headline(report, objects, object_groups)
if lines:
head = (''.join(f'<p class="flash-line">{html.escape(line)}</p>'
for line in lines)
+ f'<p class="flash-note">{html.escape(NOT_CONFORMITY)}</p>')
else:
head = _owed(f'Headline: {NO_HEADLINE}')
opinions = page.get('opinions') or []
if opinions:
said_at = ''.join(
f'<p class="flash-opinion"><em>Opinion, by '
f'{html.escape(opinion["by"])}:</em> '
f'{fr._escape(opinion["text"])}</p>' for opinion in opinions)
elif page.get('opinions_none'):
said_at = f'<p class="flash-opinion">{html.escape(NO_OPINIONS)}</p>'
else:
said_at = _owed(f'Opinions: {OPINIONS_OWED}')
parts = [
(f'<p class="flash-preliminary"><strong>{PRELIMINARY}</strong> '
f'{html.escape(SUPERSEDED)}</p>'),
ri.block_html(report),
_head('Headline') + head,
_head('As Run Against the Plan') + _kinds(('deviation',), entries,
said),
(_head('What Is Open')
+ _kinds(('anomaly', 'nonconformance'), entries, said)
+ _typed(page, 'condition')),
_head('Opinions Recorded as Opinions') + said_at,
# the end mark closes the last block rather than a block of its
# own, which would add a block's margin to a page kept to one
(_head('What Happens Next') + _typed(page, 'next_steps')
+ _typed(page, 'report_date')
+ f'<p class="endmark">{html.escape(ri.end_mark(report))}</p>'),
]
logged = log_html(report, objects)
if logged:
parts.insert(-1, logged)
return [{'kind': 'text', 'html': part} for part in parts]
|