Skip to content
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ apps/guide/settings/local.py
/static/
.coverage
.ruff_cache
.idea/
41 changes: 39 additions & 2 deletions apps/core/blocks.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
from django.utils.translation import gettext as _
from django.core.validators import RegexValidator
from django.utils.translation import gettext_lazy as _
from wagtail import blocks
from wagtail.blocks import RichTextBlock

VERSION_VALIDATOR = RegexValidator(
regex=r"^\d+\.\d+$",
message=_("Enter a version in x.y format, e.g. 7.4 or 8.10."),
)


class TextBlock(RichTextBlock):
class Meta:
template = "core/blocks/text.html"


class AnnotatedTextBlock(blocks.StructBlock):
content = RichTextBlock()
version = blocks.CharBlock(
max_length=20,
required=False,
validators=[VERSION_VALIDATOR],
help_text=_("Wagtail version, in x.y format, e.g. 7.4 or 8.10."),
)
change_type = blocks.ChoiceBlock(
choices=[
("added", _("Added")),
("changed", _("Changed")),
("removed", _("Removed")),
],
required=False,
)

class Meta:
template = "core/blocks/text_annotated.html"
icon = "pilcrow"
label = _("Text (annotated)")
form_layout = blocks.BlockGroup(
children=["content"],
settings=["version", "change_type"],
)


class SectionStructValue(blocks.StructValue):
def icon(self):
return f"core/svg/{self.get('section')}.svg"
Expand Down Expand Up @@ -62,5 +95,9 @@ class Meta:
value_class = AlertStructValue


CONTENT_BLOCKS = [("text", TextBlock()), ("alert", AlertBlock())]
CONTENT_BLOCKS = [
("text", TextBlock()),
("text_annotated", AnnotatedTextBlock()),
("alert", AlertBlock()),
]
HOME_BLOCKS = [("section_grid", SectionGridBlock())]
20 changes: 20 additions & 0 deletions apps/core/migrations/0012_alter_contentpage_body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 6.0.6 on 2026-07-09 05:39

import django.core.validators
import wagtail.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('core', '0011_alter_footercontent_locale_alter_footeritem_locale'),
]

operations = [
migrations.AlterField(
model_name='contentpage',
name='body',
field=wagtail.fields.StreamField([('text', 0), ('text_annotated', 4), ('alert', 7)], block_lookup={0: ('apps.core.blocks.TextBlock', (), {}), 1: ('wagtail.blocks.RichTextBlock', (), {}), 2: ('wagtail.blocks.CharBlock', (), {'help_text': 'Wagtail version, in x.y format, e.g. 7.4 or 8.10.', 'max_length': 20, 'required': False, 'validators': [django.core.validators.RegexValidator(message='Enter a version in x.y format, e.g. 7.4 or 8.10.', regex='^\\d+\\.\\d+$')]}), 3: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('added', 'Added'), ('changed', 'Changed'), ('removed', 'Removed')], 'required': False}), 4: ('wagtail.blocks.StructBlock', [[('content', 1), ('version', 2), ('change_type', 3)]], {}), 5: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('Warning', 'Warning'), ('Note', 'Note')]}), 6: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 7: ('wagtail.blocks.StructBlock', [[('alert_type', 5), ('alert_body', 6)]], {})}),
),
]
14 changes: 14 additions & 0 deletions apps/core/templates/core/blocks/text_annotated.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{% load wagtailcore_tags %}

{% if value.version or value.change_type %}
<span class="version-badge{% if value.change_type %} version-badge--{{ value.change_type }}{% endif %}">
{% if value.change_type and value.version %}
{{ value.change_type|title }} in Wagtail {{ value.version }}
{% elif value.change_type %}
{{ value.change_type|title }}
{% else %}
Wagtail {{ value.version }}
{% endif %}
</span>
{% endif %}
{{ value.content|richtext }}
83 changes: 83 additions & 0 deletions apps/core/tests/test_text_block_annotated.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import json

from django.core.exceptions import ValidationError
from django.test import TestCase

from apps.core.blocks import AnnotatedTextBlock
from apps.core.factories import ContentPageFactory


class TestAnnotatedTextBlock(TestCase):
def _page_with_annotated_block(self, content, version=None, change_type=None):
value = {"content": content}
if version is not None:
value["version"] = version
if change_type is not None:
value["change_type"] = change_type
body = json.dumps([{"type": "text_annotated", "value": value}])
return ContentPageFactory(body=body)

def test_renders_badge_with_change_type_and_version(self):
page = self._page_with_annotated_block(
"<p>Added content</p>", version="7.0", change_type="added"
)
response = self.client.get(page.url)
self.assertContains(response, "Added in Wagtail 7.0", count=1)
self.assertContains(response, "version-badge version-badge--added", count=1)
self.assertContains(response, "<p>Added content</p>", count=1)

def test_renders_badge_with_multidigit_version(self):
page = self._page_with_annotated_block(
"<p>Added content</p>", version="8.10", change_type="added"
)
response = self.client.get(page.url)
self.assertContains(response, "Added in Wagtail 8.10", count=1)

def test_renders_badge_with_change_type_only(self):
page = self._page_with_annotated_block(
"<p>Some text</p>", change_type="changed"
)
response = self.client.get(page.url)
self.assertContains(response, "version-badge version-badge--changed", count=1)
self.assertNotContains(response, "in Wagtail")

def test_renders_badge_with_version_only(self):
page = self._page_with_annotated_block(
"<p>Annotated content</p>", version="6.3"
)
response = self.client.get(page.url)
self.assertContains(response, "Wagtail 6.3", count=1)
self.assertContains(response, "version-badge", count=1)

def test_no_badge_without_version_or_change_type(self):
page = self._page_with_annotated_block("<p>Plain content</p>")
response = self.client.get(page.url)
self.assertNotContains(response, "version-badge")
self.assertContains(response, "<p>Plain content</p>", count=1)

def test_removed_change_type_uses_base_badge_class(self):
page = self._page_with_annotated_block(
"<p>Some text</p>", change_type="removed"
)
response = self.client.get(page.url)
self.assertContains(response, "version-badge version-badge--removed", count=1)

def test_uses_annotated_template(self):
page = self._page_with_annotated_block("<p>Content</p>", change_type="added")
response = self.client.get(page.url)
self.assertTemplateUsed(response, "core/blocks/text_annotated.html", count=1)

def test_version_validation_accepts_x_y_format(self):
version_block = AnnotatedTextBlock().child_blocks["version"]
for valid in ["7.4", "8.10", "10.0", "0.1"]:
version_block.clean(valid)

def test_version_validation_rejects_other_formats(self):
version_block = AnnotatedTextBlock().child_blocks["version"]
for invalid in ["7", "7.4.1", "v7.4", "seven-four", "7.x"]:
with self.assertRaises(ValidationError):
version_block.clean(invalid)

def test_version_validation_optional_allows_empty(self):
version_block = AnnotatedTextBlock().child_blocks["version"]
version_block.clean("")
31 changes: 31 additions & 0 deletions apps/frontend/static_src/scss/components/version-badge.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.version-badge {
display: inline-block;
margin-inline-end: 0.4em;
padding: 1px 7px;
border-radius: $border-radius--m;
font-size: 0.8em;
font-weight: 600;
line-height: 1.4;
vertical-align: baseline;
background-color: light-dark(
rgba($color--grey, 0.12),
rgba($color--light-grey, 0.1)
);
color: light-dark($color--grey, $color--light-grey);

&--added {
background-color: light-dark(
$color--extra-light-teal,
rgba($color--light-teal, 0.12)
);
color: light-dark($color--teal, $color--light-teal);
}

&--changed {
background-color: light-dark(
$color--extra-light-blue,
rgba($color--light-blue, 0.12)
);
color: light-dark($color--blue, $color--light-blue);
}
}
1 change: 1 addition & 0 deletions apps/frontend/static_src/scss/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

// Custom component styles
@import './components/alert';
@import './components/version-badge';
@import './components/app';
@import './components/copy-button';
@import './components/autocomplete';
Expand Down
3 changes: 3 additions & 0 deletions apps/llms_txt/jinja2/llms_txt/page.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Page URL: {{ page.full_url }}
{%- for block in page.body %}
{%- if block.block_type == 'text' %}
{{ block.value|richtext_markdown }}
{%- elif block.block_type == 'text_annotated' %}
{%- if block.value.change_type and block.value.version %}{{ block.value.change_type|capitalize }} in Wagtail {{ block.value.version }}: {% endif -%}
{{ block.value.content|richtext_markdown }}
{%- elif block.block_type == 'alert' %}
{{block.value.alert_type}}: {{ block.value.alert_body|richtext_markdown }}
{%- endif %}
Expand Down
Loading