DataTalksClub / DataTalksClub/website

Restore path-only podcast guest links after the shared Wiki URL change

Open
#223 19 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug content P0 security testing
Dominant language
Python
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

Product outcome

Restore the accepted #223 podcast guest-link boundary after 912f60fff9aed1dfd306fe9eba9d465674976dcc deliberately broadened the shared Wiki/graph URL helper. Podcast guest links remain safe, root-relative path-only values; Wiki graph, search, and hash-deep-link URLs retain their separately accepted bounded query/fragment behavior.

This is a reopened regression of the contract delivered in 2f64787b5a0c31825ddf9a16b1f7858ac28f3b35, not a redesign of either feature.

Normative authority and exact evidence

  • URL, link, and SEO compatibility: exact podcast/Person detail paths; Wiki search, graph, and hash deep links; unsafe-link and internal-link preservation gates.
  • GitHub content and Person ownership: podcast/Person projection and Podwiki resolution contracts.
  • Verification strategy: podcast, Person, Wiki, URL/link, and browser coverage.
  • Accepted #223 implementation: 2f64787b5a0c31825ddf9a16b1f7858ac28f3b35.
  • Regression source: 912f60fff9aed1dfd306fe9eba9d465674976dcc, which correctly allowed the shared safe_public_graph_url to retain the one bounded Wiki search query and safe fragments, but left the podcast adapter calling that widened contract directly.
  • Exact reproduction anchor: current main face8e4808d65afbf0374d1ced7a88079950d663 renders a synthetic direct guest path /people/safe-guest.html#bio instead of omitting the link.
  • The checked current projection has 205 episodes, 208 guest profiles, 207 non-empty guest paths, and zero guest paths containing ? or #; all 438 checked Person paths also contain zero ? or #. Therefore no data migration, projection rewrite, or current-page copy change is required.

Scope — podcast adapter only

1. Add one path-only podcast boundary

In content/podcast_content.py, add a private podcast-owned validator/wrapper used by _guests for both:

  1. guest_profiles[*].public_path read directly from the podcast record; and
  2. people_by_slug[key].public_path read from the resolved public Person.

The wrapper may reuse safe_public_graph_url for its shared safety checks, but it additionally returns a non-empty value only when URL parsing proves that the accepted root-relative value has an empty query and empty fragment. It must not add a flag/mode to, narrow, duplicate, or otherwise change the public behavior of the shared Wiki/graph helper.

Accepted non-empty podcast guest values:

  • are strings beginning with exactly one /;
  • remain on this site with no scheme, authority, credentials, backslash, control character, malformed percent escape, or decoded traversal segment;
  • have no query component and no fragment component;
  • are returned unchanged rather than normalized, decoded, joined, redirected, fetched, or guessed.

None, an empty string, non-string values, protocol-relative/absolute/credential-bearing URLs, query-bearing values, fragment-bearing values, query-plus-fragment values, raw or percent-decoded traversal/control forms, backslashes, and malformed encodings become the existing empty-path fallback. Do not broaden this correction into a new /people/-prefix rule: #223's accepted safe root-relative path-only contract remains authoritative.

2. Preserve the existing resolution and mismatch matrix

Validation happens independently before the current direct-record/resolved-Person comparison:

Direct record path Resolved Person Result
valid path-only no matching Person direct path is linked
absent/invalid no matching Person guest name remains visible as plain text, with no guest anchor
absent/invalid valid resolved path-only resolved Person path is linked
valid path-only same valid resolved path-only that one path is linked
valid path-only different, absent, or invalid resolved path existing ImproperlyConfigured("Public podcast guest profile path does not match.") failure remains
absent/invalid absent/invalid resolved path guest name remains visible as plain text

Do not silently prefer one of two independently valid different paths. Do not turn an invalid direct value into a mismatch when a valid resolved Person supplies the existing fallback. A fail-closed mismatch exposes no unsafe href, source value, credentials, or Person/profile data in the exception or ordinary logs.

The template already renders Guest.public_path conditionally and does not change. Names, images, media availability, summaries, external profile links, guest order, episode routes, canonical/SEO data, cache behavior, and related episode/navigation behavior remain unchanged.

3. Preserve Wiki behavior exactly

content.public_data.safe_public_graph_url and its current consumers are not implementation targets. The following accepted behavior must remain green:

  • /wiki/search?q=data%20engineering is retained as the sole bounded search-query shape under its existing count, key, length, decoding, and control-character rules;
  • /wiki/topic#section-2 and other already accepted safe page fragments are retained;
  • unsafe protocol-relative, external, credential, traversal, malformed-percent, control, multi/unknown-query, and unsafe-fragment cases remain rejected.

No rollback of 912f60f URL localization, Wiki graph/search/hash links, sponsor/article work, or generated projection is allowed.

Allowed files

  • Runtime: content/podcast_content.py only.
  • Focused regression tests: content/tests/test_podcast_catalog.py only.

Existing shared-helper/Wiki tests in content/tests/test_sponsor_article_charts.py and content/tests/test_wiki_design.py must run unchanged. If a runtime fix requires any other production file or changes the shared helper, stop and return to PM.

Dependencies and ordered combined delivery

No issue supplies a new product model, interface, decision, or infrastructure prerequisite. #223 and #270 remain separate product scopes, but their delivery is serialized because both own distinct changes in content/podcast_content.py and content/tests/test_podcast_catalog.py, and current-main podcast smoke cannot terminate green until #270 restores the three localized resources.

After the shared current-main recovery blockers are repaired and accepted, use one exact repaired base and this protocol:

  1. The #223 engineer rebases/reconstructs and freezes only the two allowed #223 paths/hunks, records its exact patch fingerprint, and runs #223-focused checks. It does not implement, test, or accept #270.
  2. The #270 engineer prepares only the #270 resource-policy/runtime/render delta against that repaired base with the frozen #223 patch treated as ordered input. It must not modify #223's guest wrapper, grammar, fallback/mismatch matrix, or tests, and it does not test or accept #223.
  3. A named integration owner—not either independent tester—constructs one uncommitted candidate from the repaired base by applying the exact #223 patch first and the exact #270 delta second. Any non-mechanical conflict or changed issue fingerprint returns to the owning engineer/PM; the integration owner may not invent cross-scope behavior.
  4. Generate one versioned full plan/envelope for the exact combined source fingerprint. The independent tester validates both patch identities and the ordered final tree, runs the complete shared envelope once, and posts separate tester-final reports to #223 and #270. #223's screenshot disposition is recorded separately (expected not_applicable only if its recomputed issue impact and zero-suffix census still support it); #270's required route screenshots remain under .tmp/screenshots/issue-270/ and cannot be credited as #223 product evidence.
  5. PM posts separate acceptance decisions. Only after both PASS, create focused commits from the accepted patch boundaries in exact order: Closes #223, then Closes #270. Verify that the resulting two-commit tip is byte-identical to the accepted combined tree, then merge/push once and dispatch one exact-SHA on-call observation.

The old requirement that #223 independently reach a green full smoke result and land before #270 starts is superseded. Old face8e4 red/partial reports remain diagnostic only; no engineer report, focused run, or cross-engineer assertion is tester evidence.

Acceptance criteria

  • Both direct podcast-record and resolved-Person public paths pass through one podcast-owned path-only boundary before comparison or rendering.
  • A safe root-relative path-only value is preserved byte-for-byte; query or fragment is rejected even though the shared Wiki helper correctly accepts its bounded Wiki forms.
  • Every invalid type/security shape above produces an empty guest path without builtin parsing failure, unsafe href, value reflection, external request, redirect, or logging.
  • The exact direct/resolved fallback and fail-closed mismatch table above is preserved, including a visible unlinked guest name for empty/invalid final paths.
  • The current 205-episode/208-guest-profile projection renders the same guest names, order, and 207 non-empty destinations; its zero query/fragment census remains zero and no checked projection changes.
  • Shared Wiki query/fragment acceptance and rejection tests pass unchanged; safe_public_graph_url, Wiki routes/graph/search, and 912f60f behavior are unchanged.
  • No template, route, projection, source pin, manifest/hash, SEO/canonical, cache, CSS, JavaScript, copy, image, Person record, or external profile-link change occurs.
  • #223-focused checks pass on its frozen patch, and the graph-selected format/lint/type, Django/content, migration-drift, browser, container, and versioned verification gates terminate green on the exact ordered combined #223-then-#270 candidate. No retry, skip, xfail, quarantine, assertion weakening, timeout increase, partial run, or cross-engineer result is acceptance evidence.
  • Engineer and independent tester record exact base/candidate/diff/plan/run/report digests and terminal counts; the previous 2026-08-24 tester evidence proves the historical implementation only and is not relabelled as current-candidate evidence.

Required regression scenarios

  1. Direct record only: valid path; empty/None; bool/integer/list/mapping; //host; absolute and credential-bearing URL; ?query; #fragment; combined suffix; backslash; raw control; malformed percent; raw and percent-decoded .. segment.
  2. Resolved Person only: the same accepted/rejected matrix, proving query/fragment values cannot bypass the direct-record check.
  3. Direct/resolved combinations: same valid path, different valid paths, valid direct plus empty/invalid resolved, invalid direct plus valid resolved, and both invalid. Assert the exact link/fallback/mismatch result in the table.
  4. Rendered episode response: safe direct and resolved paths appear once as guest anchors; invalid final paths keep escaped visible guest names without an anchor or unsafe value; mismatch fails before a response can contain either candidate path.
  5. Current checked projection: all 205 episodes compose, all 208 guests retain name/order, the current 207 destinations are unchanged path-only values, and no query/fragment link appears.
  6. Shared regression: existing Wiki search-query, safe-fragment, graph/link localization, and unsafe-value tests remain unchanged and green.

Verification and screenshots

Use only uv-backed commands and Make targets. At minimum run:

  • focused content.tests.test_podcast_catalog, content.tests.test_sponsor_article_charts, and content.tests.test_wiki_design;
  • the affected content suite, locked format/lint/type checks, migration drift check, and current content/projection invariants;
  • the versioned verification-plan and verification-run, accepting a safer/full selection on unknown graph or evidence history;
  • the graph-selected Playwright tier, with a focused representative podcast episode/linked Person navigation and Wiki search/hash-deep-link scenario. A partial or timed-out browser run is red.

Expected dispositions:

  • Migrations: no model/migration change; new migration execution is not_applicable, while migration drift remains required.
  • Render behavior: focused rendered Django coverage is required for safe link and plain-text fallback. The checked projection has no affected value, so existing public pages have no expected DOM/copy/visual delta.
  • Screenshots: expected not_applicable because no template/static/current-projection output changes. The engineer records pending_independent_tester; only the independent tester may confirm not_applicable after recomputing the frozen graph and the zero-suffix projection census. If the actual diff or graph shows render impact, the selected podcast route and linked Person/fallback states require inspected desktop/mobile screenshots instead.
  • JavaScript enabled/disabled comparison: not_applicable; no script behavior changes. If actual scope reaches JavaScript, return to PM.

Non-goals

  • No change to safe_public_graph_url, Wiki query/fragment grammar, URL localization, Wiki graph/search/deep links, or any 912f60f product behavior.
  • No template, CSS, JavaScript, copy, visual design, route, redirect, canonical, metadata, sitemap, cache, or Person-page change.
  • No generated projection, source repository/pin, adoption ledger, manifest/tree/hash, migration, or data cleanup; current data already satisfies the boundary.
  • No new Person-path family restriction, URL normalization, redirect following, network validation, source guessing, or mismatch fallback.
  • No external guest profile-link policy change; those remain separately validated external URLs.
  • No broad URL-helper, podcast adapter, projection, test, or security refactor; no unrelated gate recovery.

Lifecycle

The #223 engineer owns only its frozen two-path guest-boundary patch and focused report. After the repaired-base ordered integration described above, one independent tester validates the combined fingerprint/full envelope but posts a distinct #223 report and screenshot disposition; PM accepts #223 separately. After both #223 and #270 pass tester and PM gates, create the exact focused Closes #223 commit first and Closes #270 second, prove the two-commit tip matches the accepted tree, then merge/push once. On-call alone observes the resulting exact-SHA CI/deployment. Neither engineer is the other issue's tester, and neither issue's acceptance is inferred from the other's report.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with content/podcast_content.py, focusing on _guests and the existing safe_public_graph_url usage, then read the accepted #223 implementation and content/tests/test_podcast_catalog.py. Run the focused podcast, sponsor/article, and Wiki test modules. Done means the direct/resolved path matrix passes, path-only values remain unchanged, invalid values produce safe fallbacks, and shared Wiki behavior stays green.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend, security, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.