Task: Codegen: design and implement Qt parent-scoping for has_readonly_paginated_list
Table of Contents
This page documents a task in the Calendar entity follow-ups: date picker, list-pagination fix, QuantLib materialization story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Add a :has_parent_scoped_list: knob (paired with :parent_key_field:=/
=:parent_key_param:) to ores.codegen's ores.cpp.qt facet so a
:has_readonly_paginated_list: list window's get-request can be scoped
to an owning parent key (e.g. calendar_dates scoped by
calendar_code) and opened for a specific parent, with that key sent
on every paginated fetch. Landed generically in
client_model=/=controller=/=mdi_window templates – not wired to
calendar_dates itself, that's the follow-on task
(Generate calendar_dates Qt facet and wire Browse Holidays action).
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Calendar entity follow-ups: date picker, list-pagination fix, QuantLib materialization |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-28 |
Acceptance
[X]A new:has_parent_scoped_list:knob exists (meaningful only alongside:has_readonly_paginated_list:), paired with:parent_key_field:(the protocol request field, e.g.calendar_code) and:parent_key_param:(the C++ member/parameter name).[X]Client<Entity>Modelaccepts and stores the parent key, and sets it on the paginated get-request (fetch_<collection>=/ =load_page) on every call, alongside the existing offset/limit plumbing.[X]<Entity>MdiWindowaccepts the parent key in its constructor and threads it through to theClientModelit creates.[X]<Entity>Controllergets a public entry point (analogous toopenAddWithParent) that takes the parent key and opens the list window scoped to it;showListWindow()still satisfiesEntityController's pure-virtual override.[X]Documented inentity_meta_model_cpp_qt.orgalongside:has_readonly_paginated_list:.[X]Verified via--diffagainst existing entities with the knob unset (its default) showing zero drift attributable to this change – generation forcalendar_datesitself is out of scope (follow-on task, needs the messaging layer's protocol types wired in too).
Plan
(Implementation strategy. Written when work starts; key decisions
are distilled into the parent story's * Decisions at close, but the
plan itself stays — it is the historical record of what we did.)
Investigation findings
:has_readonly_paginated_list:(landed in Add codegen Qt facet for paginated read-only junction-scoped lists) already strips add/edit/delete/history affordances from the controller and MDI window. Pagination itself (:has_pagination:, offset/limit plumbing incpp_qt_client_model.cpp.mustache'sfetch_<collection>) is already fully generic. What's missing is purely the parent key: nothing today lets a generated get-request carry a filter key set once when the window opens and resent on every paginated fetch.- There is an existing, unrelated
:parent_entity_singular:=/ =:parent_entity_pascal:=/:has_parent_relationship:= mechanism (core.py:2390-2398), but it's for a different problem entirely: pre-filling a UUID foreign key on create (openAddWithParent,showAddWindow(parentId)) for an entity that's still full CRUD, just listed under a parent window instead of top-level. It doesn't touch the get-request at all, and reusing its naming for a read-only list's filter key would conflate two unrelated concepts. New, distinctly-named knob confirmed as the right call. Client<Entity>Model's constructor (cpp_qt_client_model.cpp.mustache:36-42) takes only(ClientManager*, QObject* parent)today. The paginated get-request is built once per fetch infetch_<collection>(cpp_qt_client_model.cpp.mustache:243-247):{{get_request_class}} request;then conditionallyrequest.offset=/=limitunder:has_pagination:. The parent key just needs a third conditional line here,request.<parent_key_field> = <parent_key_param>_;, plus a stored member set from a new constructor parameter – same shape as the existing conditional ctor-param pattern already used forbadgeCache_=/=imageCache_elsewhere in these templates.<Entity>MdiWindowconstructs theClientModel(cpp_qt_mdi_window.cpp.mustache:212, currentlynew Client<Entity>Model(clientManager_, this)) and needs the same parent-key parameter threaded from its own constructor.<Entity>Controller::showListWindow()is a pure-virtual override ofEntityController(EntityController.hpp:110, zero-arg,= 0) – its signature can't change. Mirrors howhas_parent_relationshipalready solved the analogous problem forshowAddWindow: an optional/defaulted private overload plus a distinctly-named public entry point (openAddWithParent) that forwards into it. Same shape here: keepshowListWindow() overrideas the required zero-arg entry (opens with an empty/unset parent key, for callers that don't care), add a privateshowListWindow(const QString& <parent_key_param>)overload doing the actual construction and storing the key as a member, and a publicopen<ParentPascal>(const QString& <parent_key_param>)that calls it – the real entry point every caller of a parent-scoped list actually uses (e.g. a future "Browse Holidays" button passing the selected calendar'scalendar_code).
Direction
- New boolean knob
:has_parent_scoped_list:(default false), meaningful only when:has_readonly_paginated_list:is also true (not cross-validated at codegen time beyond that – same light-touch convention as other knob pairs in this facet). - Companion string knobs
:parent_key_field:(protocol request field name) and:parent_key_param:(C++ member/parameter name, camelCase) – both required when the knob is set, no derived default, since there's no reliable way to infer either from the entity's own columns (the parent key belongs to a different entity). - Client model: add
const QString& <parent_key_param>as a conditional constructor parameter (before the trailingQObject* parent, matching the existing optional-param convention), store as<parent_key_param>_member, setrequest.<parent_key_field> = <parent_key_param>_.toStdString();in the paginated fetch alongside offset/limit. - MDI window: add the same conditional constructor parameter, store
as a member, forward it into the
ClientModelit constructs. - Controller: add the private overload + public
open<ParentPascal>(...)entry point described above; the zero-argshowListWindow()override delegates to the private overload with an empty key. - Document under a new "Parent-scoped lists" bullet in
entity_meta_model_cpp_qt.org, distinct from the existing "Parent scoping" bullet (parent_entity_singular=/=has_parent_relationship) to avoid conflating the two mechanisms.
Verification plan
Same approach as the prior :has_readonly_paginated_list: task:
retangle, then regenerate a couple of structurally different existing
entities via
compass codegen entity generate <entity> --address ores.cpp.qt --diff
with the new knobs left unset (their default) and confirm zero drift
attributable to this change. Generating a working parent-scoped list
for calendar_dates itself is explicitly out of scope here – that's
the follow-on task, which also needs calendar_date's protocol
request struct to actually carry a calendar_code filter field (it
does, from the just-closed messaging-layer task).
Notes
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
PRs
| PR | Title |
|---|---|
| #1719 | [codegen] Add has_parent_scoped_list Qt knob for parent-scoped lists |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | listWindow_/listMdiSubWindow_ destroyed-cleanup lambda unconditionally nulls both even when a different, still-open parent-scoped window is the one currently tracked (raised independently by 3 of 4 review passes) | cpp_qt_controller.cpp.mustache / ores.cpp.qt.controller_impl.org | Accepted | Fixed: guard the cleanup lambda to only null the pointers when they still reference the specific subwindow being destroyed. Full multi-window reload/staleness tracking is a separate, larger EntityController base-class change (single listWindow() contract) – documented as a known limitation in a code comment on reloadListWindow(), not fixed here. |
| 2 | No generation-time validation that parent_key_field/parent_key_param are set when has_parent_scoped_list is true; would silently render broken C++ (raised by 2 of 4 review passes; a 3rd explicitly said not to flag it as matching existing convention) | core.py | Accepted | Added validate_parent_scoped_list(), following the existing validate_explorer_interface/validate_cached_by raise-ValueError pattern for required companion fields. |
| 3 | parent_key_param_ member left default-constructed in the controller's init list, inconsistent with the class's initialize-everything style | cpp_qt_controller.cpp.mustache / ores.cpp.qt.controller_impl.org | Accepted | Added explicit `_()` initialization in the correct declaration order. |
Result
Added :has_parent_scoped_list: (paired with :parent_key_field:=/
=:parent_key_param:) to ores.codegen's ores.cpp.qt facet, landed
across client_model_header/impl, mdi_window_header/impl, and
controller_header/impl.org:
Client<Entity>Modelgains a conditional constructor parameter, stores it as a member, and sends it on the paginated get-request every fetch, alongside the existing offset/limit plumbing.<Entity>MdiWindowgains the same conditional constructor parameter and threads it into theClientModelit constructs.<Entity>ControllergainsopenForParent(const QString&)– mirroring the existingopenAddWithParentpattern for the unrelatedhas_parent_relationshipmechanism, but scoping a read-only list's get-request rather than pre-filling a create form's foreign key. The requiredEntityController::showListWindow()zero-arg override delegates to a new privateshowListWindow(const QString&)overload that does the real construction (also used byopenForParent); the window key now incorporates the parent key so distinct parents get distinct/ reusable windows.
Documented under a new "Parent-scoped lists" bullet in
entity_meta_model_cpp_qt.org, explicitly distinguished from the
pre-existing "Parent scoping" bullet
(parent_entity_singular=/=has_parent_relationship) to avoid
conflating the two mechanisms.
Verification: retangled, then regenerated book and currency
via --address ores.cpp.qt --diff with the new knobs left unset
(their default) – currency came back byte-identical; book's diff
was pure pre-existing drift unrelated to this change (same class of
drift task 35F743BF already saw and attributed to other, earlier
work). Additionally smoke-tested the new templates by temporarily
setting :has_parent_scoped_list:=/:parent_key_field:=/
:parent_key_param: on currency.org (uncommitted, reverted via
git checkout after inspection) and regenerating with --diff: the
output was syntactically valid, correctly-gated C++ across all six
touched files, matching the design exactly. Full local build (all
targets, including ores.qt.exe) is green.
Generating a working parent-scoped list for calendar_dates itself
is out of scope here – that's the follow-on task,
Generate calendar_dates Qt facet and wire Browse Holidays action.