feat: composition rendering — parent notes render child notes inline #63
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-api!63
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/62-composition-rendering"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
positionChanges
src/pal_e_docs/routes/frontend.py: Extracted_render_content()helper for the sanitize-autolink-wrap pipeline. Updatedbrowse_note()to eager-loadchildrenandparentrelationships, apply the rendering pipeline to each public child independently, and passrendered_children+parent_infoto the template.src/pal_e_docs/templates/note.html: Added parent breadcrumb section (shown when viewing a child note directly). Added child note rendering loop after parent content — each child gets an<h2>title and its own.note-contentdiv. Links section remains at the bottom after all content.src/pal_e_docs/templates/base.html: Added CSS for.child-note(left border accent#0366d6, spacing) and.parent-breadcrumbstyling.tests/test_composition_rendering.py: 9 new test scenarios.Test Plan
.child-notedivsReview Checklist
Related Notes
plan-2026-03-01-note-decomposition— Phase 4 (Composition Rendering)Review Pass 1 -- CLEAN
Reviewed the full diff against issue #62 acceptance criteria. No blockers, no nits.
Checklist
_render_content()helper correctly extracts the sanitize-autolink-wrap pipelinechildrenandparentavoids N+1 queriesis_publiccheck in route)positionorder via the model'sorder_byrelationshipnote.parentexists.child-notewithborder-left: 3px solid #0366d6known_slugscached once for parent + all children (no redundant queries)child.sluginidattribute safelyNotes
_render_contentreturn type-> stris technically correct despitewrap_tableshaving astr | Nonesignature, because thehtml or ""fallback at the top guarantees a non-None path through the entire pipeline.id="child-{{ child.slug }}"provides anchor links for future deep-linking to specific child sections -- nice touch not in the spec but harmless and useful.PR #63 Review
BLOCKERS
1.
joinedloadon self-referentialchildrenmay cause row duplication when combined withjoinedload(Note.tags)In the modified
browse_note()query:Using multiple
joinedloadon collection relationships (tags+children) against the same root entity produces a Cartesian product in the generated SQL. SQLAlchemy will de-duplicate the results in-memory, but the.first()call operates on raw SQL rows before de-duplication. When the SQL result has multiple rows (one per tag x child combination),.first()returns only the first row and SQLAlchemy's identity map may not fully populate thechildrenortagscollections.Fix: Switch
children(and/ortags) fromjoinedloadtosubqueryloadto avoid the Cartesian product:This is a correctness issue -- notes with >1 tag AND >1 child would silently return incomplete children or tags. The tests pass because each test note typically has 0 tags, which avoids the Cartesian product.
Severity: BLOCKER -- data may be silently incomplete in production where notes have both tags and children.
2. Child title rendered via
{{ child.title }}is not HTML-escaped in the<h2>context -- but it IS safeActually, on closer inspection, Jinja2 auto-escapes
{{ child.title }}by default (no| safefilter), so this is safe. Withdrawing this concern.NITS
1.
_render_contentcould also DRY upbrowse_project_notes(lines 156-163 of current main)The
browse_project_notesfunction has the same sanitize-autolink-wrap pipeline for project page notes. The new_render_contenthelper could be reused there too. Not in scope for this PR, but worth a follow-up.2.
_render_contenttype annotationThe
known_slugsparameter has no type annotation. Minor, but for consistency with the codebase (which usesAbstractSet[str]inautolink.py), consider:3. Test cleanup pattern
Tests use
_get_test_db()and manually close sessions infinallyblocks. This works, but a context-manager wrapper or a shared fixture would reduce boilerplate. Not blocking.4. Child
<h2>font-size in CSSThe
.child-note h2setsfont-size: 1.25remwhich matches the existing.note-content h2rule at line 126 ofbase.html. Fine, but the duplication means a future change to.note-content h2would not propagate to.child-note h2. Consider whether.child-note h2should inherit instead of re-declare. Purely cosmetic.SOP COMPLIANCE
feat/62-composition-renderingreferences Forgejo issue #62template-pr-body-- Summary, Changes, Test Plan, Review Checklist, Related Notes all presentplan-2026-03-01-note-decompositionreferenced in Related Notestests/test_composition_rendering.py, PR body claims 252 total passVERDICT: NOT APPROVED
One blocker: the
joinedloadCartesian product issue with.first()on a query that joins bothtagsandchildrencollections. Switch one or both tosubqueryload. Once fixed, this is a clean, well-scoped, well-tested PR.PR #63 Review (Round 2)
BLOCKERS
None. The Cartesian product blocker from round 1 has been correctly resolved by switching
Note.tagsandNote.childrentosubqueryloadwhile keeping scalar relationships (Note.project,Note.parent) onjoinedload. The code is correct, secure, and well-tested.NITS
_render_contenttype annotation (frontend.py:191): Theknown_slugsparameter has no type hint. Considerknown_slugs: frozenset[str]to matchget_known_slugsreturn type. Non-blocking -- it is a private helper.DRY opportunity in
browse_project_notes(frontend.py:157-163): The existingbrowse_project_notesfunction manually runs the samesanitize -> autolink -> wrap_tablespipeline that_render_contentnow encapsulates. A follow-up PR could replace lines 159-162 withpage_note_content = _render_content(raw, get_known_slugs(db)). Not in scope for this PR -- the PR correctly limits itself to thebrowse_noteroute.Test cleanup pattern: Tests use
_get_test_db()with try/finally for manual session management rather than a pytest fixture. This is consistent with the existing test patterns in conftest.py (create_test_notealso usesTestingSessionLocaldirectly), so no change needed, but a@pytest.fixturewrapping this pattern could reduce boilerplate in future test files.SOP COMPLIANCE
feat/62-composition-renderingreferences Forgejo issue #62.template-pr-body: All required sections present -- Summary, Changes, Test Plan, Review Checklist, Related Notes.plan-2026-03-01-note-decompositionreferenced in Related Notes section, with Forgejo issue #62 link.test_composition_rendering.pycovering all specified scenarios (inline rendering, monolithic fallback, position ordering, private/public filtering, breadcrumb, rendering pipeline, links placement). Total claimed: 252 (plausible given parametrized expansion of 216def test_functions).testpass123) used with bcrypt hashing -- standard practice.sanitize.py,autolink.py,wrap_tables.py,models.py,schemas.py, or API routes -- matching the issue boundary.CODE QUALITY NOTES
subqueryloadfor collections (tags,children),joinedloadfor scalar (project,parent). This emits separate SELECT queries for collections instead of producing an M*N row explosion._render_contenthelper: Clean extraction of the 3-step rendering pipeline. HandlesNonecontent withhtml or "".get_known_slugs(db)call cached and reused for parent + all children -- avoids N+1 queries.note.childrenin Python withis_publiccheck per child, rather than filtering at the SQL level. Since children are already eager-loaded viasubqueryload, this avoids an extra query and is the right approach.<h1>title, which is the correct UX placement..child-noteuses left-border accent for visual hierarchy..parent-breadcrumbis subtle and non-intrusive. Both use the existing#0366d6link color for consistency.sanitize_html -> autolink_slugs -> wrap_tablespipeline as parent content. The| safefilter in the template is appropriate because content is pre-sanitized server-side.VERDICT: APPROVED