cadrumo.application.user_profile.overview module

The profile overview: what the operator’s profile actually contains.

This is the projection behind the profile manager’s landing page. It answers “what do I know about this taxpayer, and what is still blank” by walking the schema and pairing every declared field with its recorded value — so a field the operator has not filled in is a visible empty row rather than an absence they have to infer.

A declaration does not always stand for one row. A repeatable section’s rows and an object field’s instances live under an index the schema never mentions, so the record decides how many there are while the schema decides what each holds; both are expanded here.

That inversion is the point. The surface this replaces enumerated the steps of a setup wizard: a list of questions with a status glyph, which told the operator where they were in a process but never what their profile held. A profile is data, not a process, and the operator should be looking at the data.

Completeness rides along as a count rather than a gate. Every field is editable at any time; missing_required names what filing will eventually need, so the surface can show progress without ever refusing to display a profile that is not finished.

Secret-classed values never leave this module in the clear: a field whose declared SensitivityClass is SECRET carries MASKED_PLACEHOLDER as its value and reports masked, so no caller can render a secret by accident. The class is read from the schema’s own declaration rather than from a list of field names kept here, so a newly-declared secret is masked the moment it is declared.

See also

ProfilePreflightService

Per-modelo requirements; this module reports schema-level completeness, which is the broader “is the profile filled in” question rather than “can I file THIS modelo”.

MASKED_PLACEHOLDER: Final[str]

Rendered in place of a secret-classed value.

A fixed-width glyph run rather than the value’s own length, so the mask does not leak how long the secret is.

MASK_KEYWORDS: Final[frozenset[str]]

Substrings that mark a sensitive fact the schema has NOT classified.

Defence in depth for exactly that case: a fact reaching a surface under a path no schema field declares still masks if it is named like a credential.

They are not consulted for a field the schema HAS classified. Reading them over a declared field let authored prose override an explicit declaration, which is how auth.provider came to mask: its description mentions a password only to say that no password is stored there, and the heuristic could not tell the difference. A heuristic pointed at text this project writes is answerable to an editorial change nobody would review as a security change – in both directions, since deleting a word would equally have unmasked a real credential.

Both English and Spanish stems are carried because an undeclared fact’s path may be authored in either language, and a surface that only knew one of them would leave the other’s facts in the clear.

Bare key deliberately subsumes the compound key names – api_key, apikey, private_key, private key – so they are not listed separately. That subsumption is load-bearing rather than incidental, and is pinned by a test: trimming key from this set would silently unmask every compound key field. It stays a local addition and must NOT be promoted to the shared base, where it would match header_key, producer_key and casilla_key tree-wide.

The set composes cadrumo.core.redaction.rules.ALWAYS_REDACT_KEY_TERMS rather than redeclaring terms beside it. It previously did not, and was missing eight base terms – nif, tax_id, nie, bearer, certificate, cookie, authorization, pkcs12 – so an undeclared fact named like a NIF rendered in the clear on this surface while the logging and live-diagnostic predicates both redacted it. Only the additions above are declared here; a term that must never diverge belongs in the base.

class ProfileFieldChoice(**data)[source]

Bases: BaseModel

One value a field may be answered with: the token stored, the words shown.

The two halves are separate because they are decided by different authorities. The token is the schema’s – it is what gets written and what every reader matches on – while the words are copy, resolved into the output language. A surface offering the token as its own label is stating that the schema’s vocabulary is already readable, which is true of a régimen name and false of true.

Parameters:

data (Any)

value: str
label: str
profile_field_choices(field, *, path=None)[source]

The closed answer set for one field, or empty when it is free text.

THE authority on “may this field be answered by picking rather than by typing”, so the manager’s edit dialog and the add-row form offer the same editor for the same declaration instead of each deciding alone. They did decide alone, and disagreed: the add-row form had always offered a boolean as Yes/No, while the manager gave it a text box – so the same field was a two-item list on one surface and a guess-the-vocabulary prompt on the other, and the manager stored whatever spelling of yes the operator happened to reach for.

A BOOLEAN is a closed set of two, which is why it belongs here rather than in a checkbox: a page of mixed fields reads better with two named options than with a box whose meaning depends on which row the cursor is on, and the same reasoning already governs FormFieldKind. The tokens are the canonical true / false the fact carrier promotes to a real bool, so picking Yes stores a boolean rather than the word.

The schema path selects existing canonical copy for choices whose stored tokens are dispatch keys rather than operator language. Passing the path avoids guessing a field’s meaning from an accidentally matching enum set.

Every other type is free text and returns empty, which is how a caller tells “pick one of these” from “type a value”.

Return type:

tuple[ProfileFieldChoice, ...]

Parameters:
class ProfileFieldView(**data)[source]

Bases: BaseModel

One schema field paired with whatever the profile records for it.

Parameters:

data (Any)

path: str
label: str
value: str | None
masked: bool
required: bool
field_type: ProfileFieldType

What the schema declares this field holds.

Carried so a surface can offer the editor the value deserves and say what shape it accepts. A date, a percentage and a NIF are all “a string you type” to a page that does not know the difference, and the operator finds out which one they got wrong only after the write door refuses.

choices: tuple[ProfileFieldChoice, ...]

The closed answer set, or empty for a field that is typed into.

The manager receives the declaration rather than guessing from the current value, so an unanswered enum is still presented as a choice. Built by profile_field_choices(), which is also what the add-row form reads, so the two surfaces cannot offer different editors for one declaration.

row_index: str | None

Which instance of a repeated fact this row belongs to, if any.

A taxpayer with three socios gets three rows labelled NIF, and nothing in the label tells them apart – the path that does is not a column. So the instance is stated here as data and left to the surface to present, rather than folded into the label: the label is the schema’s, translated, and a projection that edited it would be writing copy.

The value is the stored index verbatim, not a display ordinal, because it is the identity the fact path itself carries – what a write to <section>.<index>.<field> addresses, and what row assembly and index allocation agree on. Renumbering it for display would leave the surface naming a row the store does not have. None marks a row that belongs to no instance – every ordinary field, and the unindexed implicit row the setup wizard writes.

property present: bool

Whether the operator has supplied a value for this field.

Reads the shared presence rule rather than restating it, so the count the operator is shown cannot disagree with the gate that decides whether the same profile may file.

class ProfileSectionView(**data)[source]

Bases: BaseModel

One schema section and its fields, in declaration order.

Parameters:

data (Any)

key: str
title: str
fields: tuple[ProfileFieldView, ...]
property present_count: int

Return the number of fields carrying values in this section.

property total_count: int

Return the number of declared fields in this section.

class ProfileOverview(**data)[source]

Bases: BaseModel

Everything the manager’s landing page renders for one profile.

Parameters:

data (Any)

profile_id: ProfileId
label: str
setup_state: ProfileSetupState
sections: tuple[ProfileSectionView, ...]
missing_required: tuple[str, ...]
notices: tuple[Notice, ...]

Typed envelope advisories that apply to this profile projection.

property present_count: int

Return the total number of fields carrying values across sections.

property total_count: int

Return the total number of declared fields across sections.

property complete: bool

Whether every schema-required field now carries a value.

Deliberately not “every field”: optional detail staying blank is a finished profile, not an unfinished one.

property missing_required_fields: tuple[ProfileFieldView, ...]

Schema-labelled field views for every currently missing requirement.

mask_profile_field(*, path, label, sensitivity)[source]

Decide whether a profile field’s value must be masked before display.

This is the single masking authority for every surface that projects profile facts – the overview behind the manager landing page and the read-only status page alike. It is deliberately public and shared: two surfaces that decide confidentiality independently will diverge, and the direction that divergence takes is a surface exposing a value its sibling protects.

A field the schema classifies is decided by its declaration alone: it masks when classed SECRET and not otherwise. Only a fact the schema does not declare falls through to MASK_KEYWORDS, which is what “defence in depth” means here – a net under the unclassified, not a second opinion on the classified.

That fall-through used to run for every non-SECRET field, which is the opposite of what this docstring promised and let a field’s own description decide its confidentiality. It masked auth.provider – a closed enum naming the authentication mode, no more secret than a username – because the description says a password is not stored there, while no field in the schema declared SECRET at all. Prose documenting the absence of a credential is not evidence of one, and a declaration the schema makes explicitly must not be silently overridden by wording.

Parameters:
  • path (str) – Dotted schema path of the field, e.g. auth.dni_nie.

  • label (str) – Operator-facing description, or the path when none exists. Read only when sensitivity is None.

  • sensitivity (SensitivityClass | None) – Declared SensitivityClass, or None for a fact the schema does not know (masking then rests on the keywords).

Return type:

bool

Returns:

Whether the value must be replaced with MASKED_PLACEHOLDER before it reaches an operator.

resolve_profile_field_label_for_path(schema, path)[source]

Return the operator-facing label the schema declares for path, or None.

Scans every non-repeatable section’s own (unindexed) fields for one whose section.field dotted address matches path. A repeatable section’s row fields and a namespace field’s own indexed leaves are deliberately not resolved here: a cotejo divergence axis names a plain schema path (contact.fiscal_address), never an indexed instance.

Public so both profile-facts surfaces reading a censal cotejo divergence — the manager’s build_profile_overview() and the read-only status page’s fact walk — resolve a divergence axis’s raw schema path to the SAME operator-facing label, rather than each surface answering “which field is this” on its own and risking disagreement.

Return type:

str | None

Parameters:
build_profile_overview(record, *, label='', schema=None)[source]

Project record into the manager’s landing-page view.

The walk is driven by the SCHEMA, not by the record’s facts: every declared field yields a row whether or not the profile has a value for it. A fact-driven walk would render only what is already filled in, which is precisely the information the operator does not need — they need to see the blanks.

What the schema cannot say alone is how MANY rows a declaration stands for. A repeatable section’s rows and an object field’s instances both live under an index the schema never mentions, so the record decides their count and the schema decides their content. Reading only the schema meant every such fact was invisible: a taxpayer’s socios, activities, properties, usage ratios and censal divergences were each represented by one blank row whatever they held, on the page whose whole purpose is saying what the profile holds.

Parameters:
Return type:

ProfileOverview

Returns:

A ProfileOverview covering every declared section and field.

Note

Section titles and field labels are resolved into the output language at build time, so the returned view is bound to the language that was active when it was built. A surface that lets the operator change language must rebuild the overview rather than re-render the existing one, or the table keeps the old language.