Identity and issue: which report, which revision, where it ends, and
no silent reissue (report feature report_identity).
A page that cannot say which report and which revision it belongs to,
or where the report ends, cannot be told apart from a page of another
report, and a reader cannot tell a complete copy from a partial one.
With the feature on, every printed page carries the laboratory, the
report number and revision, the title and page n of N, and the first
page carries the identification block: the laboratory's address, where
the work was done, the customer and contact, the dates of receipt, test
and issue, the people authorizing the report with a blank line for each
signature, and the statement that the report is not reproduced except
in full. The report closes with "End of report." (or "End of
amendment.").
Every field is entered by the author (identify). The program never
writes a report number, a revision, a date or a name of its own: a
field not yet entered shows as not entered, and the report cannot be
issued until each one the identification needs is there (missing).
A report is a draft until it is issued. Issuing it (Project.issue_report)
records the revision issued, with the SHA-256 of the issued document
and of its content. From then on the report is not changed under that
identity: an export whose content differs from what was issued is
refused (guard), whatever changed, a block, a number or the data
under a figure. A change after issue is an amendment (amend): its
own revision, the report it amends by number, revision and date, and a
table of what changed (where, what it said, what it now says, and
why), which is then issued in turn. The original's record is kept.
The paginated issued form is a PDF printed from the same page
(report.pdf); the page numbers are the print engine's own page
counters, so page n of N is what the PDF holds.
Classes:
| Name |
Description |
Field |
One identification field the author enters.
|
Functions:
| Name |
Description |
value |
What the author entered for field key ('' or [] when nothing),
|
record |
The issue record: one entry per revision issued, oldest first,
|
identify |
Enter identification fields on report.
|
name |
'Report R-0037, Revision 1': the identity every page carries,
|
missing |
The labels of the fields the identification needs that are not
|
amendment |
Which amendment the report is (1 for the first), or 0 when it is
|
end_mark |
The report's last line: 'End of amendment.' for an amendment,
|
content_hash |
The SHA-256 (hex) of a rendered page: what is compared to tell
|
guard |
Refuse to write page (the report rendered for export) when the
|
issue_ready |
Check that report can be issued now, and return the status it
|
mark_issued |
Set the status issue_ready returned, before the issued page is
|
unmark |
Put back the identity mark_issued returned.
|
record_issue |
Add the revision just issued to the issue record: its number,
|
amend |
Make the issued report an amendment, to be issued in its turn.
|
status_words |
The status as the page says it: 'Draft', 'Draft amendment',
|
block_html |
The identification block for the first page, as HTML.
|
page_css |
The printed page's margins for report: the laboratory and the
|
Classes
Field
dataclass
Field(key: str, label: str, required: bool = True)
One identification field the author enters.
required fields must be entered before the report is issued.
Functions:
value
value(report: Any, key: str) -> Any
What the author entered for field key ('' or [] when nothing),
or the report's status ('draft' until issued) or full_only
(True until turned off).
Source code in src/visualdynamics/report/identity.py
| def value(report: Any, key: str) -> Any:
"""What the author entered for field `key` ('' or [] when nothing),
or the report's `status` ('draft' until issued) or `full_only`
(True until turned off)."""
held = _identity(report)
if key == 'status':
return held.get('status', 'draft')
if key == 'full_only':
return bool(held.get('full_only', True))
if key == 'authorized':
return list(held.get('authorized', []))
return str(held.get(key, ''))
|
record
record(report: Any) -> list[dict[str, Any]]
The issue record: one entry per revision issued, oldest first,
each with its number, revision, status, date of issue, the SHA-256
of the issued PDF and of its content, the file written, and for an
amendment the changes it made.
Source code in src/visualdynamics/report/identity.py
| def record(report: Any) -> list[dict[str, Any]]:
"""The issue record: one entry per revision issued, oldest first,
each with its number, revision, status, date of issue, the SHA-256
of the issued PDF and of its content, the file written, and for an
amendment the changes it made."""
return [dict(entry) for entry in _identity(report).get('record', [])]
|
identify
identify(report: Any, **fields: Any) -> None
Enter identification fields on report.
Each keyword is a key of FIELDS, a string ('' clears it), except
authorized, the people authorizing the report, one per entry or
one per line ("name, role"), and full_only, whether the first
page says the report is not reproduced except in full (True by
default).
Refused before anything changes: an unknown field; the status, the
amendment and the issue record, which change only by issuing and
amending; a value of the wrong type; and the number or revision of
a report that has been issued, which change only by an amendment.
Raises:
| Type |
Description |
ValueError
|
For a refusal; the message names the field and the reason.
|
TypeError
|
When a value is of the wrong type.
|
Source code in src/visualdynamics/report/identity.py
| def identify(report: Any, **fields: Any) -> None:
"""Enter identification fields on `report`.
Each keyword is a key of `FIELDS`, a string ('' clears it), except
`authorized`, the people authorizing the report, one per entry or
one per line ("name, role"), and `full_only`, whether the first
page says the report is not reproduced except in full (True by
default).
Refused before anything changes: an unknown field; the status, the
amendment and the issue record, which change only by issuing and
amending; a value of the wrong type; and the number or revision of
a report that has been issued, which change only by an amendment.
Raises
------
ValueError
For a refusal; the message names the field and the reason.
TypeError
When a value is of the wrong type.
"""
held = _identity(report)
issued = bool(held.get('record'))
changes: dict[str, Any] = {}
for key, given in fields.items():
if key in _CONTROLLED:
raise ValueError(f'{key} is not entered: a report is issued with '
'Project.issue_report and amended with '
'Project.amend_report')
if key == 'full_only':
if not isinstance(given, bool):
raise TypeError(f'full_only is True or False, not '
f'{type(given).__name__}')
changes[key] = given
continue
if key not in BY_KEY:
raise ValueError(f'unknown identification field {key!r}; the '
f'fields are: {", ".join([*BY_KEY, "full_only"])}')
if key == 'authorized':
changes[key] = _people(given)
continue
if not isinstance(given, str):
raise TypeError(f'{BY_KEY[key].label} is a str, not '
f'{type(given).__name__}')
if issued and key in ('number', 'revision') \
and given.strip() != str(held.get(key, '')):
raise ValueError(
f'{BY_KEY[key].label} cannot change: the report was issued '
f'as {_name(held)}, and a change after issue is an '
'amendment (Project.amend_report)')
changes[key] = given.strip()
for key, given in changes.items():
# a blank field, and the statement left on (its default), are
# not stored, so a report with nothing entered stores nothing
if given is True or (given in ('', []) and key != 'full_only'):
held.pop(key, None)
else:
held[key] = given
report.identity = held
|
name
'Report R-0037, Revision 1': the identity every page carries,
with '(number not entered)' in place of what is missing.
Source code in src/visualdynamics/report/identity.py
| def name(report: Any) -> str:
"""'Report R-0037, Revision 1': the identity every page carries,
with '(number not entered)' in place of what is missing."""
return _name(_identity(report))
|
missing
missing(report: Any) -> list[str]
The labels of the fields the identification needs that are not
entered, in order; empty when it is complete.
Source code in src/visualdynamics/report/identity.py
| def missing(report: Any) -> list[str]:
"""The labels of the fields the identification needs that are not
entered, in order; empty when it is complete."""
held = _identity(report)
return [field.label for field in FIELDS
if field.required and not held.get(field.key)]
|
amendment
amendment(report: Any) -> int
Which amendment the report is (1 for the first), or 0 when it is
not an amendment. Counted from the issue record, never entered.
Source code in src/visualdynamics/report/identity.py
| def amendment(report: Any) -> int:
"""Which amendment the report is (1 for the first), or 0 when it is
not an amendment. Counted from the issue record, never entered."""
held = _identity(report)
if not held.get('amends'):
return 0
issued = sum(1 for entry in held.get('record', [])
if entry.get('status') == 'amended')
return issued + (0 if held.get('status') == 'amended' else 1)
|
end_mark
end_mark(report: Any) -> str
The report's last line: 'End of amendment.' for an amendment,
otherwise the skeleton's 'End of report.'.
Source code in src/visualdynamics/report/identity.py
| def end_mark(report: Any) -> str:
"""The report's last line: 'End of amendment.' for an amendment,
otherwise the skeleton's 'End of report.'."""
from .skeleton import END
return END_AMENDMENT if _identity(report).get('amends') else END
|
content_hash
content_hash(page: str) -> str
The SHA-256 (hex) of a rendered page: what is compared to tell
the issued content from a changed one.
Source code in src/visualdynamics/report/identity.py
| def content_hash(page: str) -> str:
"""The SHA-256 (hex) of a rendered page: what is compared to tell
the issued content from a changed one."""
return hashlib.sha256(page.encode('utf-8')).hexdigest()
|
guard
guard(report: Any, page: str) -> None
Refuse to write page (the report rendered for export) when the
report has been issued and the page differs from what was issued.
A report that is a draft, or the feature switched off, passes: a
draft says so on every page. An issued report whose content is
unchanged passes too, since that is a copy of the issued document.
Raises:
| Type |
Description |
ValueError
|
Naming the issued revision and how to make the change.
|
Source code in src/visualdynamics/report/identity.py
| def guard(report: Any, page: str) -> None:
"""Refuse to write `page` (the report rendered for export) when the
report has been issued and the page differs from what was issued.
A report that is a draft, or the feature switched off, passes: a
draft says so on every page. An issued report whose content is
unchanged passes too, since that is a copy of the issued document.
Raises
------
ValueError
Naming the issued revision and how to make the change.
"""
from .features import enabled
if FEATURE not in enabled(report):
return
held = _identity(report)
entry = _issued_entry(held)
if entry is None:
return
if entry.get('content') != content_hash(page):
raise ValueError(
f'{_name(held)} was issued on {entry.get("issued") or "its date"} '
'and has changed since; an issued report is not written again '
'under its identity. A change after issue is an amendment: '
'Project.amend_report, then Project.issue_report.')
|
issue_ready
issue_ready(report: Any, objects: Mapping[str, Any] | None = None, object_groups: Sequence[Mapping[str, Any]] | None = None) -> str
Check that report can be issued now, and return the status it
will carry once issued: 'issued', or 'amended' for an amendment.
Refused: the feature not enabled; a report already issued (a change
is an amendment); a field the identification needs not entered;
and, with skeleton sections on and objects given, a section still
owed (section 9 read off the findings register while that feature
is on, as Project.report_check reads it). With the flash report
on, what that page still owes (report.flash.check) is read in
place of the skeleton's sections, which it does not draw.
Raises:
| Type |
Description |
ValueError
|
For a refusal; the message names what stands in the way.
|
Source code in src/visualdynamics/report/identity.py
| def issue_ready(report: Any, objects: Mapping[str, Any] | None = None,
object_groups: Sequence[Mapping[str, Any]] | None = None
) -> str:
"""Check that `report` can be issued now, and return the status it
will carry once issued: 'issued', or 'amended' for an amendment.
Refused: the feature not enabled; a report already issued (a change
is an amendment); a field the identification needs not entered;
and, with skeleton sections on and `objects` given, a section still
owed (section 9 read off the findings register while that feature
is on, as `Project.report_check` reads it). With the flash report
on, what that page still owes (`report.flash.check`) is read in
place of the skeleton's sections, which it does not draw.
Raises
------
ValueError
For a refusal; the message names what stands in the way.
"""
from .features import enabled
title = getattr(report, 'title', '')
if FEATURE not in enabled(report):
raise ValueError(f'report {title!r} does not have Identity and issue '
'enabled, so it has no identity to issue under')
held = _identity(report)
if held.get('status', 'draft') != 'draft':
raise ValueError(f'{_name(held)} is already issued; a change after '
'issue is an amendment (Project.amend_report)')
gaps = missing(report)
if gaps:
raise ValueError(f'{_name(held)} cannot be issued: '
f'{", ".join(gaps)} {"is" if len(gaps) == 1 else "are"}'
f' {NOT_ENTERED}')
from .flash import FEATURE as FLASH
from .skeleton import FEATURE as SKELETON
if objects is not None and FLASH in enabled(report):
# a flash report is its one page: what it owes is the page's,
# not the skeleton's sections, which it does not draw
from .flash import check as flash_check
owed = flash_check(report, objects, object_groups)
if owed:
raise ValueError(f'{_name(held)} cannot be issued while the flash '
f'report owes {len(owed)} '
f'part{"s" * (len(owed) != 1)}: '
+ '; '.join(owed))
elif objects is not None and SKELETON in enabled(report):
from .findings import check as register_check
from .skeleton import check
owed = register_check(report, check(report, objects, object_groups))
if owed:
raise ValueError(f'{_name(held)} cannot be issued while it owes '
f'{len(owed)} section{"s" * (len(owed) != 1)}: '
+ '; '.join(map(str, owed)))
return 'amended' if held.get('amends') else 'issued'
|
mark_issued
mark_issued(report: Any, status: str) -> dict[str, Any]
Set the status issue_ready returned, before the issued page is
rendered (the page says it is issued). Returns the identity as it
was, for unmark if writing the document fails.
Source code in src/visualdynamics/report/identity.py
| def mark_issued(report: Any, status: str) -> dict[str, Any]:
"""Set the status `issue_ready` returned, before the issued page is
rendered (the page says it is issued). Returns the identity as it
was, for `unmark` if writing the document fails."""
if status not in ('issued', 'amended'):
raise ValueError(f'a report is issued as issued or amended, not '
f'{status!r}')
before = _identity(report)
held = dict(before)
held['status'] = status
report.identity = held
return before
|
unmark
unmark(report: Any, before: Mapping[str, Any]) -> None
Put back the identity mark_issued returned.
Source code in src/visualdynamics/report/identity.py
| def unmark(report: Any, before: Mapping[str, Any]) -> None:
"""Put back the identity `mark_issued` returned."""
report.identity = dict(before)
|
record_issue
record_issue(report: Any, page: str, pdf: bytes, file: str = '') -> dict
Add the revision just issued to the issue record: its number,
revision, status and date of issue as entered, the SHA-256 of the
PDF and of the page it was printed from, and the file written.
Returns the entry.
Source code in src/visualdynamics/report/identity.py
| def record_issue(report: Any, page: str, pdf: bytes, file: str = '') -> dict:
"""Add the revision just issued to the issue record: its number,
revision, status and date of issue as entered, the SHA-256 of the
PDF and of the page it was printed from, and the file written.
Returns the entry."""
held = _identity(report)
entry = {'number': held.get('number', ''),
'revision': held.get('revision', ''),
'status': held.get('status', 'draft'),
'issued': held.get('issued', ''),
'sha256': hashlib.sha256(pdf).hexdigest(),
'content': content_hash(page),
'file': str(file)}
if held.get('amends'):
entry['amends'] = dict(held['amends'])
entry['changes'] = [dict(change) for change in held.get('changes', [])]
held['record'] = [*held.get('record', []), entry]
report.identity = held
return dict(entry)
|
amend
amend(report: Any, changes: Any, *, revision: str, number: str | None = None) -> None
Make the issued report an amendment, to be issued in its turn.
changes lists what the amendment changes, each a mapping of
where (the page or section), was (what it said), now (what it
says) and, where appropriate, reason. revision is the
amendment's own revision, entered by the author; number, when
given, its own report number (otherwise it keeps the original's).
The amendment refers to the revision it amends by number, revision
and date of issue, taken from the issue record. Its date of issue
is cleared, for the author to enter when it is issued.
Refused before anything changes: a report not issued (a draft is
changed in place); a change missing its where, was or now; no
changes; and a number and revision already issued.
Raises:
| Type |
Description |
ValueError
|
For a refusal; the message names the reason.
|
TypeError
|
When a value is of the wrong type.
|
Source code in src/visualdynamics/report/identity.py
| def amend(report: Any, changes: Any, *, revision: str,
number: str | None = None) -> None:
"""Make the issued `report` an amendment, to be issued in its turn.
`changes` lists what the amendment changes, each a mapping of
`where` (the page or section), `was` (what it said), `now` (what it
says) and, where appropriate, `reason`. `revision` is the
amendment's own revision, entered by the author; `number`, when
given, its own report number (otherwise it keeps the original's).
The amendment refers to the revision it amends by number, revision
and date of issue, taken from the issue record. Its date of issue
is cleared, for the author to enter when it is issued.
Refused before anything changes: a report not issued (a draft is
changed in place); a change missing its where, was or now; no
changes; and a number and revision already issued.
Raises
------
ValueError
For a refusal; the message names the reason.
TypeError
When a value is of the wrong type.
"""
from .features import enabled
held = _identity(report)
title = getattr(report, 'title', '')
if FEATURE not in enabled(report):
raise ValueError(f'report {title!r} does not have Identity and issue '
'enabled, so it has nothing issued to amend')
entry = _issued_entry(held)
if entry is None:
raise ValueError(f'{_name(held)} has not been issued; a draft is '
'changed in place and needs no amendment')
clean = _changes(changes)
if not isinstance(revision, str):
raise TypeError(f'revision is a str, not {type(revision).__name__}')
if number is not None and not isinstance(number, str):
raise TypeError(f'number is a str, not {type(number).__name__}')
revision = revision.strip()
new_number = held.get('number', '') if number is None else number.strip()
if not revision:
raise ValueError(f'the amendment needs its own revision; the '
f'revision is {NOT_ENTERED}')
if not new_number:
raise ValueError(f'the amendment needs a report number; the number '
f'is {NOT_ENTERED}')
taken = {(old.get('number'), old.get('revision'))
for old in held.get('record', [])}
if (new_number, revision) in taken:
raise ValueError(f'Report {new_number}, Revision {revision} has '
'already been issued; an amendment carries an '
'identity of its own')
held.update(number=new_number, revision=revision, status='draft',
changes=clean,
amends={'number': entry.get('number', ''),
'revision': entry.get('revision', ''),
'issued': entry.get('issued', '')})
held.pop('issued', None)
report.identity = held
|
status_words
status_words(report: Any) -> str
The status as the page says it: 'Draft', 'Draft amendment',
'Issued' or 'Amendment 1'. A flash report (the report feature
flash_report) is preliminary whatever its status, and says so
first: 'Preliminary (draft)', 'Preliminary (issued)'.
Source code in src/visualdynamics/report/identity.py
| def status_words(report: Any) -> str:
"""The status as the page says it: 'Draft', 'Draft amendment',
'Issued' or 'Amendment 1'. A flash report (the report feature
`flash_report`) is preliminary whatever its status, and says so
first: 'Preliminary (draft)', 'Preliminary (issued)'."""
from .features import enabled
held = _identity(report)
status = held.get('status', 'draft')
if status == 'issued':
words = 'Issued'
elif status == 'amended':
words = f'Amendment {amendment(report)}'
else:
words = 'Draft amendment' if held.get('amends') else 'Draft'
if 'flash_report' in enabled(report):
return f'Preliminary ({words.lower()})'
return words
|
block_html
block_html(report: Any) -> str
The identification block for the first page, as HTML.
The identity rows, then for an amendment the revision it amends and
its table of changes, then each person authorizing the report with
a blank line for the signature and the date, and the statement that
the report is not reproduced except in full. Joined with no white
space between tags, since the page keeps a text block's white
space.
Source code in src/visualdynamics/report/identity.py
| def block_html(report: Any) -> str:
"""The identification block for the first page, as HTML.
The identity rows, then for an amendment the revision it amends and
its table of changes, then each person authorizing the report with
a blank line for the signature and the date, and the statement that
the report is not reproduced except in full. Joined with no white
space between tags, since the page keeps a text block's white
space.
"""
held = _identity(report)
rows = [('Status', html.escape(status_words(report)))]
for field in FIELDS:
if field.key == 'authorized':
continue
given = held.get(field.key, '')
if not field.required and not given:
continue
rows.append((field.label, _text(given)))
parts = ['<div class="identity">',
'<table class="identity"><tbody>']
parts.extend(f'<tr><th>{html.escape(label)}</th><td>{cell}</td></tr>'
for label, cell in rows)
parts.append('</tbody></table>')
amends = held.get('amends')
if amends:
parts.append(
f'<p class="amends">Amendment {amendment(report)} to Report '
f'{_text(amends.get("number"))}, Revision '
f'{_text(amends.get("revision"))}, issued '
f'{_text(amends.get("issued"))}. The report it amends is not '
'altered and is kept with this amendment.</p>')
parts.append('<table class="changes"><thead><tr><th>Where</th>'
'<th>Was</th><th>Now</th><th>Reason</th></tr></thead>'
'<tbody>')
for change in held.get('changes', []):
parts.append('<tr>' + ''.join(
f'<td>{html.escape(change.get(part, ""))}</td>'
for part in CHANGE_PARTS) + '</tr>')
parts.append('</tbody></table>')
people = held.get('authorized', [])
parts.append('<div class="authorized"><p class="label">Authorized by</p>')
if people:
for person in people:
parts.append(f'<div class="signature"><span class="who">'
f'{html.escape(person)}</span><span class="line">'
'Signature</span><span class="line date">Date'
'</span></div>')
else:
parts.append(f'<p>{_text("")}</p>')
parts.append('</div>')
if value(report, 'full_only'):
parts.append(f'<p class="fullonly">{html.escape(FULL_ONLY)}</p>')
parts.append('</div>')
return ''.join(parts)
|
page_css
page_css(report: Any) -> str
The printed page's margins for report: the laboratory and the
report's number and revision at the top of every page, its title and
page n of N at the foot. The numbers are the print engine's own page
counters, so they are true of whatever it prints. A draft says so
beside its number. The classification marking, when there is one,
moves from the page's fixed strips into the margins, top and foot
center, where it is on every page and over nothing.
Source code in src/visualdynamics/report/identity.py
| def page_css(report: Any) -> str:
"""The printed page's margins for `report`: the laboratory and the
report's number and revision at the top of every page, its title and
page n of N at the foot. The numbers are the print engine's own page
counters, so they are true of whatever it prints. A draft says so
beside its number. The classification marking, when there is one,
moves from the page's fixed strips into the margins, top and foot
center, where it is on every page and over nothing."""
held = _identity(report)
laboratory = (str(held.get('laboratory', ''))
or f'(laboratory {NOT_ENTERED})')
identity = _name(held)
status = status_words(report)
if status != 'Issued':
identity += f', {status}'
title = str(getattr(report, 'title', ''))
small = 'font-size: 8pt; color: #1f2328;'
rules = [f' @top-left {{ content: {_css_string(laboratory)}; {small} }}',
f' @top-right {{ content: {_css_string(identity)}; {small} }}',
f' @bottom-left {{ content: {_css_string(title)}; {small} }}',
(' @bottom-right { content: "Page " counter(page) " of " '
f'counter(pages); {small} }}')]
marking = str(getattr(report, 'marking', '')).strip()
if marking:
color = ('#d02b2b' if getattr(report, 'marking_color', 'ink') == 'red'
else '#1f2328')
mark = (f'content: {_css_string(marking)}; font-size: 10pt; '
f'font-weight: 700; letter-spacing: .12em; color: {color};')
rules += [f' @top-center {{ {mark} }}',
f' @bottom-center {{ {mark} }}']
top, side = MARGINS
return (f'\n@page {{ margin: {top}in {side}in;\n' + '\n'.join(rules)
+ ' }\n')
|