-
Notifications
You must be signed in to change notification settings - Fork 27
Story: Serve Wagtail and Legacy News at same endpoint #2649
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
c5bb723
bbc3ae4
e7ca6ec
c85a207
f1a011f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,8 @@ | |
|
|
||
| logger = structlog.get_logger(__name__) | ||
|
|
||
| NEWS_ENTRY_PREFIX = "/news/entry/" | ||
| LEGACY_NEWS_ENTRY_PREFIX = "/news/entry/" | ||
| NEWS_ENTRY_PREFIX = "/news/" | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def fetch_post_views() -> dict[str, int]: | ||
|
|
@@ -51,9 +52,14 @@ def fetch_post_views() -> dict[str, int]: | |
| slug_views: dict[str, int] = {} | ||
| for result in data["results"]: | ||
| path = result["dimensions"][0] | ||
| if not path.startswith(NEWS_ENTRY_PREFIX): | ||
| prefix = ( | ||
| LEGACY_NEWS_ENTRY_PREFIX | ||
| if path.startswith(LEGACY_NEWS_ENTRY_PREFIX) | ||
| else NEWS_ENTRY_PREFIX | ||
| ) | ||
| if not path.startswith(prefix): | ||
| continue | ||
| slug = path[len(NEWS_ENTRY_PREFIX) :].rstrip("/") | ||
| slug = path[len(prefix) :].rstrip("/") | ||
|
Comment on lines
+55
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Aggregate counts for duplicate normalized slugs. When Plausible returns both 🤖 Prompt for AI Agents |
||
| if slug: | ||
| slug_views[slug] = int(result["metrics"][0]) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,14 +3,14 @@ | |
| AllTypesCreateView, | ||
| BlogPostCreateView, | ||
| BlogPostListView, | ||
| EntryListView, | ||
| EntryApproveView, | ||
| EntryDeleteView, | ||
| EntryDetailView, | ||
| EntryListView, | ||
| EntryModerationDetailView, | ||
| EntryModerationListView, | ||
| EntryModerationMagicApproveView, | ||
| EntryUpdateView, | ||
| EntryDetailView, | ||
| LinkCreateView, | ||
| LinkListView, | ||
| NewsCreateView, | ||
|
|
@@ -22,7 +22,7 @@ | |
| ) | ||
|
|
||
| urlpatterns = [ | ||
| path("", EntryListView.as_view(), name="news"), | ||
| path("entry/", EntryListView.as_view(), name="news"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- news/urls.py ---'
cat -n news/urls.py
printf '%s\n' '--- named-route references ---'
rg -n -C 2 "reverse|url ['\"]news['\"]|slugurl ['\"]news['\"]|name=['\"]news['\"]|news/" templates news --glob '*.html' --glob '*.py'
printf '%s\n' '--- route and Wagtail index definitions ---'
rg -n -C 3 "PostIndexPage|EntryListView|include.*news|path\\(|slugurl" . --glob '*.py' --glob '*.html' --glob '!node_modules/**' --glob '!build/**' --glob '!dist/**'Repository: boostorg/website-v2 Length of output: 50375 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- Wagtail index definitions and references ---'
rg -n -C 5 "class PostIndexPage|PostIndexPage|post.?index|slug.?news|slug=['\"]news['\"]" --glob '*.py' --glob '*.html' --glob '!news/migrations/**'
printf '%s\n' '--- URL configuration boundaries ---'
sed -n '1,125p' config/urls.py
sed -n '285,430p' config/urls.py
printf '%s\n' '--- v3 view/template selection ---'
rg -n -C 6 "class V3Mixin|V3Mixin|v3_template_name|v3_enabled|V3" news core config --glob '*.py'
printf '%s\n' '--- exact public navigation templates ---'
cat -n templates/includes/_header.html | sed -n '1,35p'
cat -n templates/homepage.html | sed -n '250,270p'
cat -n templates/v3/homepage.html | sed -n '1,25p'Repository: boostorg/website-v2 Length of output: 206 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- candidate Wagtail files and symbols ---'
git ls-files | rg -i 'wagtail|page|post|news|url|template' | head -200
printf '%s\n' '--- PostIndexPage and news-page references ---'
rg -n -i -C 4 "PostIndexPage|post.?index|news.*page|page.*news|slug.?news|wagtail" . \
--glob '*.py' --glob '*.html' --glob '!news/migrations/**' --glob '!*.min.*' || true
printf '%s\n' '--- URL configuration boundaries ---'
sed -n '1,125p' config/urls.py
sed -n '285,430p' config/urls.py
printf '%s\n' '--- v3 view/template selection ---'
rg -n -C 6 "class V3Mixin|V3Mixin|v3_template_name|v3_enabled|V3" news core config --glob '*.py' || true
printf '%s\n' '--- exact public navigation templates ---'
cat -n templates/includes/_header.html | sed -n '1,35p'
cat -n templates/homepage.html | sed -n '250,270p'
cat -n templates/v3/homepage.html | sed -n '1,25p'Repository: boostorg/website-v2 Length of output: 50376 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- PostIndexPage implementation ---'
cat -n pages/models.py | sed -n '1,180p'
printf '%s\n' '--- BasePage and V3Mixin ---'
cat -n pages/mixins.py | sed -n '1,120p'
rg -n -C 12 "class V3Mixin" core
printf '%s\n' '--- final URL patterns ---'
cat -n config/urls.py | sed -n '385,485p'
printf '%s\n' '--- templates using the site header ---'
rg -n -C 4 "_header|extends ['\"]base|main_content_wrapper|posts_url" templates/base.html templates/_base.html templates/v3/homepage.html templates/homepage.html templates/includes/_header.html
printf '%s\n' '--- exact affected links ---'
cat -n templates/includes/_header.html | sed -n '1,30p'
cat -n templates/homepage.html | sed -n '255,266p'Repository: boostorg/website-v2 Length of output: 21113 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- v3 header ---'
cat -n templates/v3/includes/_header_v3.html | sed -n '1,180p'
printf '%s\n' '--- HomepageView selection ---'
rg -n -C 12 "class HomepageView|homepage.*template|v3/homepage|homepage.html|flag_is_active" ak core --glob '*.py'
printf '%s\n' '--- all template references to the news route name ---'
rg -n -C 1 "\\{\\%[[:space:]]+url[[:space:]]+['\"]news['\"]" templates --glob '*.html' || true
printf '%s\n' '--- route/link verifier ---'
python3 - <<'PY'
import ast
from pathlib import Path
from collections import defaultdict
tree = ast.parse(Path("news/urls.py").read_text())
routes = {}
for node in tree.body:
if isinstance(node, ast.Assign):
targets = [t.id for t in node.targets if isinstance(t, ast.Name)]
if "urlpatterns" in targets and isinstance(node.value, (ast.List, ast.Tuple)):
for item in node.value.elts:
if isinstance(item, ast.Call) and item.args and item.keywords:
route = ast.literal_eval(item.args[0])
kw = {k.arg: ast.literal_eval(k.value) for k in item.keywords if k.arg == "name"}
if "name" in kw:
routes[kw["name"]] = route
print("news route:", routes.get("news"))
print("news route is legacy entry route:", routes.get("news") == "entry/")
refs = defaultdict(list)
for path in Path("templates").rglob("*.html"):
for lineno, line in enumerate(path.read_text().splitlines(), 1):
if "{% url 'news' %}" in line:
refs["url-news"].append(f"{path}:{lineno}")
print("template url-news refs:", refs["url-news"])
PYRepository: boostorg/website-v2 Length of output: 20436 Use the Wagtail index URL for public news links.
🤖 Prompt for AI Agents |
||
| path("blogpost/", BlogPostListView.as_view(), name="news-blogpost-list"), | ||
| path("link/", LinkListView.as_view(), name="news-link-list"), | ||
| path("news/", NewsListView.as_view(), name="news-news-list"), | ||
|
|
@@ -49,7 +49,6 @@ | |
| EntryModerationMagicApproveView.as_view(), | ||
| name="news-magic-approve", | ||
| ), | ||
| path("entry/<slug:slug>/", EntryDetailView.as_view(), name="news-detail"), | ||
| path( | ||
| "entry/<slug:slug>/approve/", | ||
| EntryApproveView.as_view(), | ||
|
|
@@ -65,4 +64,9 @@ | |
| EntryUpdateView.as_view(), | ||
| name="news-update", | ||
| ), | ||
| path( | ||
| "entry/<slug:slug>/", | ||
| EntryDetailView.as_view(), | ||
| name="news-detail", | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ | |
| from django.utils.functional import cached_property | ||
| from django.utils.text import slugify | ||
|
|
||
| from waffle import flag_is_active | ||
|
|
||
|
|
||
| from pages.blocks import POST_BLOCKS | ||
| from pages.mixins import BasePage | ||
|
|
@@ -107,6 +109,45 @@ class PostIndexPage(BasePage): | |
| template = "v3/posts_list.html" | ||
| max_count = 1 | ||
|
|
||
| def route(self, request, path_components): | ||
| """ | ||
| Overwrite routing to allow our PostIndexPage to act as a | ||
| umbrella handler for Legacy Entry serving, as well as | ||
| Wagtail serving | ||
| """ | ||
| from news.models import Entry | ||
|
|
||
| path = request.path.rstrip("/").lstrip("/") | ||
| split_path = path.split("/") | ||
| base, *rest = split_path | ||
|
|
||
| # We need to handle the case in which an Entry exists, but no | ||
| # matching Post Page exists, since this now handles both. We do | ||
| # this by serving this page if an Entry is found, and our serve | ||
| # method then calls the legacy view. | ||
| if match_child := self.get_children().filter(slug=base).first(): | ||
| matched_route = match_child.specific.route(request, rest) | ||
| return matched_route | ||
| if len(rest) > 0 and not flag_is_active(request, "v3"): | ||
| if e := Entry.objects.filter(slug=rest[0]).first(): | ||
| return self, [], {"pk": e.pk} | ||
| return super().route(request, path_components) | ||
|
|
||
| def serve(self, request, *args, **kwargs): | ||
| if not flag_is_active(request, "v3"): | ||
| # Rather than return a 404 on non v3 views, we allow Legacy | ||
| # and wagtail to live at the same endpoint by serving the Legacy view | ||
| from news.views import EntryListView | ||
|
|
||
| if pk := kwargs.get("pk"): | ||
| from news.views import EntryDetailView | ||
|
|
||
| return EntryDetailView.as_view()(request, pk=pk) | ||
|
|
||
| return EntryListView.as_view()(request) | ||
|
|
||
| return super().serve(request, *args, **kwargs) | ||
|
Comment on lines
+112
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^|/)(pages/models\.py|.*models\.py|.*views\.py|.*waffle.*|.*flag.*)$' | head -120
printf '%s\n' '--- pages/models.py outline ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline pages/models.py
else
wc -l pages/models.py
fi
printf '%s\n' '--- relevant source ---'
sed -n '1,180p' pages/models.py
printf '%s\n' '--- flag_is_active references ---'
rg -n -C 3 'flag_is_active|def route\(|path_components' --glob '*.py' .Repository: boostorg/website-v2 Length of output: 23076 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- routing and news URL tests ---'
rg -n -C 5 'PostIndexPage|RoutableHomePage|/news|news/|EntryDetailView|EntryListView|path_components|route\(' --glob '*.py' --glob '*.html' --glob '*.md' . | head -300
printf '%s\n' '--- dependency declarations ---'
rg -n -C 3 'wagtail|django-waffle|waffle' requirements* pyproject.toml setup.cfg setup.py Pipfile* poetry.lock 2>/dev/null || true
printf '%s\n' '--- route-related docs ---'
rg -n -C 5 'path_components|Page\.route|route.*request|RoutablePageMixin|RouteResult' . --glob '*.py' --glob '*.md' | head -240
printf '%s\n' '--- relevant news model/view definitions ---'
if command -v ast-grep >/dev/null 2>&1; then
ast-grep outline news/models.py
ast-grep outline news/views.py
fi
rg -n -C 8 'class Entry|class Entry(List|Detail)View|slug|urlpatterns' news/models.py news/views.py
printf '%s\n' '--- standalone expression probe ---'
python3 - <<'PY'
class Entry:
pk = 37
def __repr__(self):
return "Entry(pk=37)"
def evaluate(entry, flag):
def first():
return entry
def flag_is_active(name):
return flag
try:
value = first() and not flag_is_active("v3")
return value, getattr(value, "pk")
except Exception as exc:
return type(exc).__name__, str(exc)
for entry in (None, Entry()):
for flag in (False, True):
print(f"entry={entry!r}, flag={flag!r} -> {evaluate(entry, flag)!r}")
for path_components in ([], ["news"], ["legacy-slug"], ["news", "legacy-slug"]):
path = "/".join(path_components)
split_path = path.rstrip("/").lstrip("/").split("/")
base, *rest = split_path
try:
result = rest[0]
except Exception as exc:
result = f"{type(exc).__name__}: {exc}"
print(f"path_components={path_components!r}, derived_base={base!r}, rest={rest!r}, rest0={result!r}")
PYRepository: boostorg/website-v2 Length of output: 50375 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- news URL patterns ---'
sed -n '1,180p' news/urls.py
printf '%s\n' '--- page and URL configuration ---'
rg -n -C 8 'WAGTAIL|wagtail|ROOT_URLCONF|Page\.serve|serve\(|RoutableHomePage|PostIndexPage|wagtail_urls|urlpatterns' config pages news --glob '*.py' --glob '*.html' | head -260
printf '%s\n' '--- focused route tests ---'
find . -path '*/tests/*' -type f -name '*.py' -print0 | xargs -0 rg -n -C 5 'PostIndexPage|RoutableHomePage|/news/|news-detail|EntryListView|EntryDetailView|route\(' | head -260
printf '%s\n' '--- standalone Python semantics probe ---'
python3 - <<'PY'
class Entry:
pk = 37
def __repr__(self):
return "Entry(pk=37)"
def current_expression(entry, active):
def first():
return entry
def flag_is_active(*args):
if len(args) != 2:
raise TypeError("flag_is_active() missing required positional argument")
return active
try:
assigned = first() and not flag_is_active("v3")
return ("assigned", assigned, "pk", assigned.pk)
except Exception as exc:
return (type(exc).__name__, str(exc))
def corrected_expression(entry, active):
if not active:
if entry:
return ("fallback", entry.pk)
return ("super",)
for entry in (None, Entry()):
for active in (False, True):
print(f"current entry={entry!r}, active={active}: {current_expression(entry, active)!r}")
print(f"corrected entry={entry!r}, active={active}: {corrected_expression(entry, active)!r}")
for components in ([], ["legacy-slug"], ["legacy-slug", "extra"]):
slug, *rest = components if components else ("<none>",)
print(f"path_components={components!r}: slug={slug!r}, rest={rest!r}, one_component={bool(components) and not rest}")
PYRepository: boostorg/website-v2 Length of output: 25961 🌐 Web query:
💡 Result: In django-waffle 5.0.0, the signature for the Citations:
🌐 Web query:
💡 Result: In Wagtail, the route method is the core mechanism responsible for resolving an HTTP request to a specific Page instance within the Wagtail tree [1][2]. The path_components argument is a list of strings representing the remaining parts of the URL path that have not yet been resolved [1]. How Page.route works: When a request enters Wagtail, the system iterates through the URL by splitting the path into components [1]. The route method on the current page receives these components and proceeds as follows: 1. If path_components is not empty, the method treats the first element as a child slug, attempts to find that child page, and then delegates further routing by calling the child page's route method with the remaining components [1][2]. 2. If path_components is empty, the current page is the target [1][2]. If the page is live, it returns a RouteResult object, which encapsulates the page and instructions for the serve method to handle the request [1][2]. If the page is not live, it raises an Http404 exception [1][2]. Overriding Page.route: You can override the route method in your own Page models to customize how URLs are resolved [3][4]. A common use case for this is providing support for custom URL structures that do not map directly to the page tree, often implemented via the RoutablePageMixin [5][6]. The RoutablePageMixin specifically overrides the default Page.route method to allow matching sub-URL patterns against view methods defined on the page model [5][6]. When it successfully matches a sub-URL, it returns a RouteResult containing the view function and any arguments extracted from the URL, which are then passed to the page's serve method [5][6]. Key Technical Details: - RouteResult: An object that contains the page (and optionally a view function plus args/kwargs) that Wagtail will use to serve the final response [1][2]. - path_components: A list derived from request.path, specifically [component for component in path.split('/') if component] [3]. - Delegation: The standard Page implementation handles tree-based routing by recursively calling.route on children until the terminal page is found [1][7]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- URL ordering around news and Wagtail ---'
sed -n '250,335p' config/urls.py
printf '%s\n' '--- site/page setup references ---'
rg -n -C 8 'RoutableHomePage|PostIndexPage|Site\(|root_page|WAGTAILADMIN|wagtail_urls|serve' --glob '*.py' --glob '*.json' --glob '*.yaml' --glob '*.md' . | head -260
printf '%s\n' '--- focused tests for Wagtail page serving ---'
find pages news core config -path '*/tests/*' -type f -name '*.py' -print0 | xargs -0 rg -n -C 8 'Client|wagtail|page\.url|/news/|news-detail|PostPage|route' | head -260Repository: boostorg/website-v2 Length of output: 40390 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- config URL imports ---'
sed -n '1,130p' config/urls.py
printf '%s\n' '--- config URL tail ---'
tail -140 config/urls.py
printf '%s\n' '--- all Wagtail URL references ---'
rg -n -C 4 'wagtail_urls|include\(.*wagtail|wagtail\.urls|serve' config --glob '*.py'
printf '%s\n' '--- page creation and site root references ---'
rg -n -C 6 'Site\.objects|root_page|RoutableHomePage\(|PostIndexPage\(' --glob '*.py' --glob '*.json' --glob '*.yaml' . | head -240Repository: boostorg/website-v2 Length of output: 19958 Use For For Use the supplied 🤖 Prompt for AI Agents |
||
|
|
||
| def get_children_by_content_type( | ||
| self, content_type: str | list[str] | ||
| ) -> models.QuerySet["PostPage"]: | ||
|
|
@@ -216,6 +257,16 @@ class PostPage(BasePage): | |
| blank=True, default="", help_text="AI generated summary. Delete to regenerate." | ||
| ) | ||
|
|
||
| def serve(self, request, *args, **kwargs): | ||
| if not flag_is_active(request, "v3"): | ||
| # Rather than return a 404 on non v3 views, we allow Legacy | ||
| # and wagtail to live at the same endpoint by serving the Legacy view | ||
| from news.views import EntryDetailView | ||
|
|
||
| return EntryDetailView.as_view()(request, slug=self.slug) | ||
|
|
||
| return super().serve(request, *args, **kwargs) | ||
|
|
||
| def get_content(self): | ||
| if self.post_content_type in ["News", "Blogpost"]: | ||
| return self.content | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: boostorg/website-v2
Length of output: 20601
🏁 Script executed:
Repository: boostorg/website-v2
Length of output: 18121
🏁 Script executed:
Repository: boostorg/website-v2
Length of output: 14230
🏁 Script executed:
Repository: boostorg/website-v2
Length of output: 8756
🏁 Script executed:
Repository: boostorg/website-v2
Length of output: 11490
Preserve Wagtail routing for
/pages/.The catch-all
re_pathmatches/pages/example/beforewagtail_urls, soStaticContentTemplateViewhandles the request instead of Wagtail. Addpages/to the negative lookahead or restorepath("pages/", include(wagtail_urls))before the fallback.🧰 Tools
🪛 Ruff (0.16.2)
[warning] 111-479: Consider iterable unpacking instead of concatenation
(RUF005)
🤖 Prompt for AI Agents