Skip to content

visualdynamics.report.flash

flash

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]