Enhance SlugInput widget to support `locale` & `formatters`

- Fixes #11916
pull/13263/head
LB 2024-11-19 17:30:08 +10:00 zatwierdzone przez Matthew Westcott
rodzic b896d1a40f
commit 5cf6fdfbdf
4 zmienionych plików z 314 dodań i 9 usunięć

Wyświetl plik

@ -25,7 +25,7 @@ class CopyForm(forms.Form):
initial=self.page.slug,
label=_("New slug"),
allow_unicode=allow_unicode,
widget=widgets.SlugInput,
widget=widgets.SlugInput(locale=self.page.locale),
)
self.fields["new_parent_page"] = forms.ModelChoiceField(
initial=self.page.get_parent(),

Wyświetl plik

@ -868,3 +868,54 @@ class TestPageCopy(WagtailTestUtils, TestCase):
reverse("wagtailadmin_pages:edit", args=(page_copy.id,)),
)
self.assertEqual("Edit", edit_button.text)
def test_page_copy_form_widgets(self):
"""
For widget rendering, we want to validate that the locale gets passed to the slug
and that the title is NOT syncing with the slug as this is NOT required on copy form.
"""
response = self.client.get(
reverse("wagtailadmin_pages:copy", args=(self.test_page.id,))
)
# Check response
self.assertEqual(response.status_code, 200)
# Check that the form has the correct widgets
self.assertContains(response, "id_new_title")
self.assertContains(response, "id_new_slug")
soup = self.get_soup(response.content)
# Check the attributes on the slug field (locale available & slugify used on blue)
slug_field = soup.find("input", id="id_new_slug")
self.assertEqual(
slug_field.attrs,
{
"type": "text",
"name": "new_slug",
"value": "hello-world",
"data-controller": "w-slug",
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-w-slug-allow-unicode-value": "",
"data-w-slug-compare-as-param": "urlify",
"data-w-slug-trim-value": "true",
"data-w-slug-locale-value": "en",
"required": "",
"id": "id_new_slug",
},
)
# Check the attributes on the title field (no syncing)
title_field = soup.find("input", id="id_new_title")
self.assertEqual(
title_field.attrs,
{
"type": "text",
"name": "new_title",
"value": "Hello world!",
"required": "",
"id": "id_new_title",
},
)

Wyświetl plik

@ -2,6 +2,7 @@ import json
import re
from html import unescape
from bs4 import BeautifulSoup
from django import forms
from django.test import TestCase
from django.test.utils import override_settings
@ -9,7 +10,7 @@ from django.utils.html import escape
from wagtail.admin import widgets
from wagtail.admin.forms.tags import TagField
from wagtail.models import Page
from wagtail.models import Locale, Page
from wagtail.test.testapp.forms import AdminStarDateInput
from wagtail.test.testapp.models import EventPage, RestaurantTag, SimplePage
from wagtail.utils.deprecation import RemovedInWagtail80Warning
@ -698,8 +699,18 @@ class TestFilteredSelect(TestCase):
class TestSlugInput(TestCase):
def getAttrs(self, *args, **kwargs):
return (
BeautifulSoup(
widgets.SlugInput(*args, **kwargs).render("slug", None),
"html.parser",
)
.find("input")
.attrs
)
def test_has_data_attr(self):
widget = widgets.slug.SlugInput()
widget = widgets.SlugInput()
html = widget.render("test", None, attrs={"id": "test-id"})
@ -709,9 +720,203 @@ class TestSlugInput(TestCase):
)
@override_settings(WAGTAIL_ALLOW_UNICODE_SLUGS=False)
def test_render_data_atrrs_from_settings(self):
widget = widgets.slug.SlugInput()
def test_render_data_attrs_from_settings(self):
widget = widgets.SlugInput()
html = widget.render("test", None, attrs={"id": "test-id"})
self.assertNotIn("data-w-slug-allow-unicode-value", html)
def test_with_locale_and_formatters_not_provided(self):
self.assertEqual(
self.getAttrs(),
{
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-controller": "w-slug",
"data-w-slug-allow-unicode-value": "",
"data-w-slug-compare-as-param": "urlify",
# "data-w-slug-formatters-value":... # not included at all
# "data-w-slug-locale-value":... # not included at all
"data-w-slug-trim-value": "true",
"name": "slug",
"type": "text",
},
)
self.assertEqual(
self.getAttrs(formatters=[], locale=None),
{
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-controller": "w-slug",
"data-w-slug-allow-unicode-value": "",
"data-w-slug-compare-as-param": "urlify",
# "data-w-slug-formatters-value":... # not included at all
# "data-w-slug-locale-value":... # not included at all
"data-w-slug-trim-value": "true",
"name": "slug",
"type": "text",
},
)
# Test formatters argument
def test_with_formatters_provided_are_escaped(self):
self.assertEqual(
self.getAttrs(formatters=[(r"\D\s[']+", "'?'")])[
"data-w-slug-formatters-value"
],
'[[["\\\\D\\\\s[\']+","gu"],"\'?\'"]]',
)
def test_with_formatters_as_string(self):
self.assertEqual(
self.getAttrs(formatters=[r"\d"])["data-w-slug-formatters-value"],
'[[["\\\\d","gu"],""]]',
)
# handling of inline flags
self.assertEqual(
self.getAttrs(formatters=[r"(?m)^\d+"])["data-w-slug-formatters-value"],
'[[["^\\\\d+","gmu"],""]]',
)
def test_with_formatters_as_pattern(self):
self.assertEqual(
self.getAttrs(formatters=[re.compile(r"\d")])[
"data-w-slug-formatters-value"
],
'[[["\\\\d","gu"],""]]',
)
# handling of inline flags
self.assertEqual(
self.getAttrs(
formatters=[re.compile(r"(?i)\b(?:and\|or\|the\|in\|of\|to)\b")]
)["data-w-slug-formatters-value"],
'[[["\\\\b(?:and\\\\|or\\\\|the\\\\|in\\\\|of\\\\|to)\\\\b","giu"],""]]',
)
def test_with_formatters_as_list_like_with_string(self):
self.assertEqual(
self.getAttrs(formatters=[["ABC"]])["data-w-slug-formatters-value"],
'[[["ABC","gu"],""]]',
)
self.assertEqual(
self.getAttrs(formatters=[(r"\d",)])["data-w-slug-formatters-value"],
'[[["\\\\d","gu"],""]]',
)
def test_with_formatters_as_list_like_with_pattern(self):
self.assertEqual(
self.getAttrs(formatters=[[re.compile(r"(?i)!")]])[
"data-w-slug-formatters-value"
],
'[[["!","giu"],""]]',
)
self.assertEqual(
self.getAttrs(formatters=[(re.compile("(?m)^A"),)])[
"data-w-slug-formatters-value"
],
'[[["^A","gmu"],""]]',
)
def test_with_formatters_with_replace(self):
self.assertEqual(
self.getAttrs(formatters=[(r"\d", "X")])["data-w-slug-formatters-value"],
'[[["\\\\d","gu"],"X"]]',
)
self.assertEqual(
self.getAttrs(formatters=[[re.compile(r"\d{1,3}"), "X"]])[
"data-w-slug-formatters-value"
],
'[[["\\\\d{1,3}","gu"],"X"]]',
)
def test_with_formatters_with_replace_and_flags(self):
self.assertEqual(
self.getAttrs(
formatters=[
[re.compile(r"^(?!blog[-\s])", flags=re.MULTILINE), "blog-", "u"]
]
)["data-w-slug-formatters-value"],
'[[["^(?!blog[-\\\\s])","mu"],"blog-"]]',
)
self.assertEqual(
self.getAttrs(
formatters=[[re.compile(r"(?i)a*"), "Z", "g"], [r"the", "", "u"]]
)["data-w-slug-formatters-value"],
'[[["a*","gi"],"Z"],[["the","u"],""]]',
)
def test_with_multiple_formatters(self):
self.assertEqual(
self.getAttrs(
formatters=[
r"\d",
[r"(?<!\S)Й", "Y"],
[re.compile(r"(?<!\S)Є"), "Ye", "u"],
(r"(?i)the",),
]
)["data-w-slug-formatters-value"],
'[[["\\\\d","gu"],""],[["(?<!\\\\S)\\u0419","gu"],"Y"],[["(?<!\\\\S)\\u0404","u"],"Ye"],[["the","giu"],""]]',
)
# Test locale argument
def test_with_locale_provided(self):
self.assertEqual(
self.getAttrs(locale="uk-UK"),
{
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-controller": "w-slug",
"data-w-slug-allow-unicode-value": "",
"data-w-slug-compare-as-param": "urlify",
"data-w-slug-locale-value": "uk-UK", # from provided locale
"data-w-slug-trim-value": "true",
"name": "slug",
"type": "text",
},
)
french = Locale.objects.create(language_code="fr")
self.assertEqual(
self.getAttrs(locale=french),
{
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-controller": "w-slug",
"data-w-slug-allow-unicode-value": "",
"data-w-slug-compare-as-param": "urlify",
"data-w-slug-locale-value": "fr", # from provided locale
"data-w-slug-trim-value": "true",
"name": "slug",
"type": "text",
},
)
self.assertEqual(
self.getAttrs(locale=False),
{
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-controller": "w-slug",
"data-w-slug-allow-unicode-value": "",
"data-w-slug-compare-as-param": "urlify",
"data-w-slug-locale-value": "und", # from False (aka 'undetermined')
"data-w-slug-trim-value": "true",
"name": "slug",
"type": "text",
},
)
def test_with_locale_blank_override(self):
self.assertEqual(
self.getAttrs(locale=False)["data-w-slug-locale-value"],
"und",
)
self.assertEqual(
self.getAttrs(locale="")["data-w-slug-locale-value"],
"und",
)

Wyświetl plik

@ -1,16 +1,35 @@
import json
import re
from typing import List, Optional, Tuple, Union
from django.conf import settings
from django.forms import widgets
from wagtail.coreutils import get_js_regex
class SlugInput(widgets.TextInput):
"""
Associates the input field with the Stimulus w-slug (CleanController).
Slugifies content based on `WAGTAIL_ALLOW_UNICODE_SLUGS` and supports
fields syncing their value to this field (see `TitleFieldPanel`) if
also used.
Slugifies content based on ``WAGTAIL_ALLOW_UNICODE_SLUGS`` and supports
fields syncing their value to this field (see `TitleFieldPanel`) if used.
Allows the ability to define the ``locale`` to allow locale (language code)
specific transliteration, or ``formatters`` for more custom handling.
"""
def __init__(self, attrs=None):
def __init__(
self,
attrs: Optional[dict] = None,
formatters: Optional[
List[
Tuple[
Union[re.Pattern, str, bytes],
Optional[str],
],
]
] = [],
locale: Optional[object] = None,
):
default_attrs = {
"data-controller": "w-slug",
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
@ -20,6 +39,36 @@ class SlugInput(widgets.TextInput):
"data-w-slug-compare-as-param": "urlify",
"data-w-slug-trim-value": "true",
}
if formatters:
# If formatters are provided, parse them into a JSON string
# Support flexible input of regex, replace, and flags
parsed_formatters = []
for item in formatters:
if isinstance(item, (list, tuple)):
regex_args = [item[0]]
replace = item[1] if len(item) > 1 else ""
if len(item) == 3:
# If base_js_flags are provided, add them to the regex_args
regex_args.append(item[2])
else:
regex_args = [item]
replace = ""
parsed_formatters.append([get_js_regex(*regex_args), replace])
default_attrs["data-w-slug-formatters-value"] = json.dumps(
parsed_formatters,
separators=(",", ":"),
)
if locale is not None:
# Attempt to resolve a non-empty locale string from a `Locale` instance or the string itself.
# If no locale can be found, use 'und' - as per ISO639-2 standard, 'und' represents 'undetermined'.
# If the attribute is not set, the input will fall back to the ``ACTIVE_LOCALE`` or document lang within the CleanController.
default_attrs["data-w-slug-locale-value"] = (
getattr(locale, "language_code", locale) or "und"
)
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)