Add WAGTAIL_CONTENT_LANGUAGES setting

pull/6387/head
Karl Hobley 2020-07-30 16:07:39 +01:00 zatwierdzone przez Matt Westcott
rodzic e78db08d24
commit a88129236c
6 zmienionych plików z 207 dodań i 71 usunięć

Wyświetl plik

@ -1,13 +1,6 @@
import functools
from django.conf import settings
from django.conf.locale import LANG_INFO
from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed
from django.dispatch import receiver
from django.utils.translation import check_for_language
# A setting that can be used in foreign key declarations
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
@ -18,56 +11,3 @@ try:
except ValueError:
raise ImproperlyConfigured("AUTH_USER_MODEL must be of the form"
" 'app_label.model_name'")
@functools.lru_cache()
def get_languages():
"""
Cache of settings.LANGUAGES in a dictionary for easy lookups by key.
"""
# TODO: Add support for WAGTAIL_LANGUAGES
return dict(settings.LANGUAGES)
# Added in Django 2.1
@functools.lru_cache(maxsize=1000)
def get_supported_language_variant(lang_code, strict=False):
"""
Return the language code that's listed in supported languages, possibly
selecting a more generic variant. Raise LookupError if nothing is found.
If `strict` is False (the default), look for a country-specific variant
when neither the language code nor its generic variant is found.
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
as the provided language codes are taken from the HTTP request. See also
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
"""
if lang_code:
# If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
possible_lang_codes = [lang_code]
try:
possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
except KeyError:
pass
generic_lang_code = lang_code.split("-")[0]
possible_lang_codes.append(generic_lang_code)
supported_lang_codes = get_languages()
for code in possible_lang_codes:
if code in supported_lang_codes and check_for_language(code):
return code
if not strict:
# if fr-fr is not supported, try fr-ca.
for supported_code in supported_lang_codes:
if supported_code.startswith(generic_lang_code + "-"):
return supported_code
raise LookupError(lang_code)
@receiver(setting_changed)
def reset_cache(**kwargs):
"""
Clear cache when global LANGUAGES/LANGUAGE_CODE settings are changed
"""
if kwargs["setting"] in ("LANGUAGES", "LANGUAGE_CODE"):
get_languages.cache_clear()
get_supported_language_variant.cache_clear()

Wyświetl plik

@ -3,14 +3,14 @@
from django.conf import settings
from django.db import migrations
from wagtail.core.compat import get_supported_language_variant
from wagtail.core.utils import get_supported_content_language_variant
def initial_locale(apps, schema_editor):
Locale = apps.get_model("wagtailcore.Locale")
Locale.objects.create(
language_code=get_supported_language_variant(settings.LANGUAGE_CODE),
language_code=get_supported_content_language_variant(settings.LANGUAGE_CODE),
)

Wyświetl plik

@ -49,8 +49,8 @@ from wagtail.core.url_routing import RouteResult
from wagtail.core.utils import WAGTAIL_APPEND_SLASH, camelcase_to_underscore, resolve_model_string
from wagtail.search import index
from .compat import get_languages, get_supported_language_variant
from .utils import find_available_slug
from .utils import (
find_available_slug, get_content_languages, get_supported_content_language_variant)
logger = logging.getLogger('wagtail.core')
@ -296,13 +296,13 @@ def pk(obj):
class LocaleManager(models.Manager):
def get_queryset(self):
# Exclude any locales that have an invalid language code
return super().get_queryset().filter(language_code__in=get_languages().keys())
return super().get_queryset().filter(language_code__in=get_content_languages().keys())
def get_for_language(self, language_code):
"""
Gets a Locale from a language code.
"""
return self.get(language_code=get_supported_language_variant(language_code))
return self.get(language_code=get_supported_content_language_variant(language_code))
class Locale(models.Model):
@ -336,7 +336,7 @@ class Locale(models.Model):
return cls.get_default()
def get_display_name(self):
return get_languages().get(self.language_code)
return get_content_languages().get(self.language_code)
def __str__(self):
return self.get_display_name() or self.language_code
@ -448,7 +448,7 @@ def bootstrap_translatable_model(model, locale):
class BootstrapTranslatableModel(migrations.RunPython):
def __init__(self, model_string, language_code=None):
if language_code is None:
language_code = get_supported_language_variant(settings.LANGUAGE_CODE)
language_code = get_supported_content_language_variant(settings.LANGUAGE_CODE)
def forwards(apps, schema_editor):
model = apps.get_model(model_string)

Wyświetl plik

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase, override_settings
from django.utils.text import slugify
from wagtail.core.models import Page
from wagtail.core.utils import (
accepts_kwarg, camelcase_to_underscore, cautious_slugify, find_available_slug, safe_snake_case,
string_to_ascii)
accepts_kwarg, camelcase_to_underscore, cautious_slugify, find_available_slug,
get_content_languages, get_supported_content_language_variant, safe_snake_case, string_to_ascii)
class TestCamelCaseToUnderscore(TestCase):
@ -138,3 +139,115 @@ class TestFindAvailableSlug(TestCase):
slug = find_available_slug(self.root_page, "home")
self.assertEqual(slug, "home-2")
@override_settings(
USE_I18N=True,
WAGTAIL_I18N_ENABLED=True,
LANGUAGES=[
('en', 'English'),
('de', 'German'),
('de-at', 'Austrian German'),
('pt-br', 'Portuguese (Brazil)'),
],
WAGTAIL_CONTENT_LANGUAGES=[
('en', 'English'),
('de', 'German'),
('de-at', 'Austrian German'),
('pt-br', 'Portuguese (Brazil)'),
],
)
class TestGetContentLanguages(TestCase):
def test_get_content_languages(self):
self.assertEqual(get_content_languages(), {
'de': 'German',
'de-at': 'Austrian German',
'en': 'English',
'pt-br': 'Portuguese (Brazil)'
})
@override_settings(
WAGTAIL_CONTENT_LANGUAGES=[
('en', 'English'),
('de', 'German'),
],
)
def test_can_be_different_to_django_languages(self):
self.assertEqual(get_content_languages(), {
'de': 'German',
'en': 'English',
})
@override_settings(
WAGTAIL_CONTENT_LANGUAGES=[
('en', 'English'),
('de', 'German'),
('zh', 'Chinese'),
],
)
def test_must_be_subset_of_django_languages(self):
with self.assertRaises(ImproperlyConfigured) as e:
get_content_languages()
self.assertEqual(e.exception.args, ("The language zh is specified in WAGTAIL_CONTENT_LANGUAGES but not LANGUAGES. WAGTAIL_CONTENT_LANGUAGES must be a subset of LANGUAGES.", ))
@override_settings(
USE_I18N=True,
WAGTAIL_I18N_ENABLED=True,
LANGUAGES=[
('en', 'English'),
('de', 'German'),
('de-at', 'Austrian German'),
('pt-br', 'Portuguese (Brazil)'),
],
WAGTAIL_CONTENT_LANGUAGES=[
('en', 'English'),
('de', 'German'),
('de-at', 'Austrian German'),
('pt-br', 'Portuguese (Brazil)'),
],
)
class TestGetSupportedContentLanguageVariant(TestCase):
# From: https://github.com/django/django/blob/9e57b1efb5205bd94462e9de35254ec5ea6eb04e/tests/i18n/tests.py#L1481
def test_get_supported_content_language_variant(self):
g = get_supported_content_language_variant
self.assertEqual(g('en'), 'en')
self.assertEqual(g('en-gb'), 'en')
self.assertEqual(g('de'), 'de')
self.assertEqual(g('de-at'), 'de-at')
self.assertEqual(g('de-ch'), 'de')
self.assertEqual(g('pt-br'), 'pt-br')
self.assertEqual(g('pt'), 'pt-br')
self.assertEqual(g('pt-pt'), 'pt-br')
with self.assertRaises(LookupError):
g('pt', strict=True)
with self.assertRaises(LookupError):
g('pt-pt', strict=True)
with self.assertRaises(LookupError):
g('xyz')
with self.assertRaises(LookupError):
g('xy-zz')
@override_settings(WAGTAIL_CONTENT_LANGUAGES=[
('en', 'English'),
('de', 'German'),
])
def test_uses_wagtail_content_languages(self):
# be sure it's not using Django's LANGUAGES
g = get_supported_content_language_variant
self.assertEqual(g('en'), 'en')
self.assertEqual(g('en-gb'), 'en')
self.assertEqual(g('de'), 'de')
self.assertEqual(g('de-at'), 'de')
self.assertEqual(g('de-ch'), 'de')
with self.assertRaises(LookupError):
g('pt-br')
with self.assertRaises(LookupError):
g('pt')
with self.assertRaises(LookupError):
g('pt-pt')
with self.assertRaises(LookupError):
g('xyz')
with self.assertRaises(LookupError):
g('xy-zz')

Wyświetl plik

@ -1,3 +1,4 @@
import functools
import inspect
import re
import unicodedata
@ -5,9 +6,14 @@ from anyascii import anyascii
from django.apps import apps
from django.conf import settings
from django.conf.locale import LANG_INFO
from django.core.exceptions import ImproperlyConfigured
from django.core.signals import setting_changed
from django.db.models import Model
from django.dispatch import receiver
from django.utils.encoding import force_str
from django.utils.text import slugify
from django.utils.translation import check_for_language
WAGTAIL_APPEND_SLASH = getattr(settings, 'WAGTAIL_APPEND_SLASH', True)
@ -195,3 +201,74 @@ def find_available_slug(parent, requested_slug):
number += 1
return slug
@functools.lru_cache()
def get_content_languages():
"""
Cache of settings.WAGTAIL_CONTENT_LANGUAGES in a dictionary for easy lookups by key.
"""
content_languages = getattr(settings, 'WAGTAIL_CONTENT_LANGUAGES', None)
languages = dict(settings.LANGUAGES)
if content_languages is None:
# Default to a single language based on LANGUAGE_CODE
content_languages = [
(settings.LANGUAGE_CODE, languages[settings.LANGUAGE_CODE]),
]
# Check that each content language is in LANGUAGES
for language_code, name in content_languages:
if language_code not in languages:
raise ImproperlyConfigured(
"The language {} is specified in WAGTAIL_CONTENT_LANGUAGES but not LANGUAGES. "
"WAGTAIL_CONTENT_LANGUAGES must be a subset of LANGUAGES.".format(language_code)
)
return dict(content_languages)
@functools.lru_cache(maxsize=1000)
def get_supported_content_language_variant(lang_code, strict=False):
"""
Return the language code that's listed in supported languages, possibly
selecting a more generic variant. Raise LookupError if nothing is found.
If `strict` is False (the default), look for a country-specific variant
when neither the language code nor its generic variant is found.
lru_cache should have a maxsize to prevent from memory exhaustion attacks,
as the provided language codes are taken from the HTTP request. See also
<https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
This is equvilant to Django's `django.utils.translation.get_supported_content_language_variant`
but reads the `WAGTAIL_CONTENT_LANGUAGES` setting instead.
"""
if lang_code:
# If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
possible_lang_codes = [lang_code]
try:
possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
except KeyError:
pass
generic_lang_code = lang_code.split("-")[0]
possible_lang_codes.append(generic_lang_code)
supported_lang_codes = get_content_languages()
for code in possible_lang_codes:
if code in supported_lang_codes and check_for_language(code):
return code
if not strict:
# if fr-fr is not supported, try fr-ca.
for supported_code in supported_lang_codes:
if supported_code.startswith(generic_lang_code + "-"):
return supported_code
raise LookupError(lang_code)
@receiver(setting_changed)
def reset_cache(**kwargs):
"""
Clear cache when global WAGTAIL_CONTENT_LANGUAGES/LANGUAGES/LANGUAGE_CODE settings are changed
"""
if kwargs["setting"] in ("WAGTAIL_CONTENT_LANGUAGES", "LANGUAGES", "LANGUAGE_CODE"):
get_content_languages.cache_clear()
get_supported_content_language_variant.cache_clear()

Wyświetl plik

@ -228,6 +228,12 @@ WAGTAILADMIN_RICH_TEXT_EDITORS = {
}
WAGTAIL_CONTENT_LANGUAGES = [
("en", "English"),
("fr", "French"),
]
# Set a non-standard DEFAULT_AUTHENTICATION_CLASSES value, to verify that the
# admin API still works with session-based auth regardless of this setting
# (see https://github.com/wagtail/wagtail/issues/5585)