Simplify language chooser (#328)

* Simplify language chooser

- Allows usage of i18n_patterns with prefix_default_language=False
- Change templatetag from simple tag to an inclusion tag
- Reduces complexity by relying on Django's behavior in the
  set_language view: it will translate any url passed as 'next'.
  This behavior has been present since Django 1.9.
- Remove individual forms for each language

Fixes #327

Reference: aa5ab114e3

* Add check to ensure LocaleMiddleware

* Remove check in favor of silent warning

* Fix template tag tests
master
Julian Wachholz 2023-12-05 12:18:44 +01:00 committed by GitHub
parent 43dca1ae93
commit b393c11ecb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 71 additions and 123 deletions

View File

@ -186,7 +186,6 @@ flat-theme admin-interface
{{ block.super }} {{ block.super }}
{% get_admin_interface_theme as theme %} {% get_admin_interface_theme as theme %}
{% if theme.language_chooser_active %} {% if theme.language_chooser_active %}
{% get_admin_interface_languages as languages %} {% admin_interface_language_chooser %}
{% include "admin_interface/language_chooser.html" %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,18 +1,15 @@
{% load admin_interface_tags %} {% load admin_interface_tags %}
{% if languages %} {% if set_language_url %}
<div class="language-chooser {% if theme.language_chooser_control == 'minimal-select' %}minimal {% endif %}"> <div class="language-chooser {% if theme.language_chooser_control == 'minimal-select' %}minimal {% endif %}">
{% for language in languages %} <form class="language-chooser-select-form" action="{{ set_language_url }}" method="POST">
<form class="language-chooser-hidden-form" id="language-chooser-hidden-form-{{ language.code }}" action="{{ language.activation_url }}" method="POST">
{% csrf_token %} {% csrf_token %}
<input name="language" type="hidden" value="{{ language.code }}"> <input type="hidden" name="next" value="{{ next }}">
</form> <select name="language" onchange="this.form.submit();">
{% endfor %} {% for code, language in LANGUAGES %}
<form class="language-chooser-select-form"> <option value="{{ code }}" {% if code == LANGUAGE_CODE %}selected{% endif %}>
{% csrf_token %} {% if theme.language_chooser_display == 'code' %}{{ code|upper }}{% elif theme.language_chooser_display == 'name' %}{{ language }}{% endif %}
<select name="language" onchange="document.getElementById(String('language-chooser-hidden-form-' + this.value)).submit();"> </option>
{% for language in languages %}
<option value="{{ language.code }}" {% if language.active %}selected{% endif %}>{% if theme.language_chooser_display == 'code' %}{{ language.code|upper }}{% elif theme.language_chooser_display == 'name' %}{{ language.name }}{% endif %}</option>
{% endfor %} {% endfor %}
</select> </select>
</form> </form>

View File

@ -1,6 +1,7 @@
import datetime import datetime
import hashlib import hashlib
import re import re
import warnings
from django import template from django import template
from django.conf import settings from django.conf import settings
@ -16,45 +17,43 @@ from admin_interface.models import Theme
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.inclusion_tag("admin_interface/language_chooser.html", takes_context=True)
def get_admin_interface_languages(context): def admin_interface_language_chooser(context):
if not settings.USE_I18N: if not settings.USE_I18N:
# i18n disabled # i18n disabled
return None return None
if len(settings.LANGUAGES) < 2: if len(settings.LANGUAGES) < 2:
# less than 2 languages # less than 2 languages
return None return None
if "django.middleware.locale.LocaleMiddleware" not in settings.MIDDLEWARE:
warnings.warn(
"Language chooser requires 'django.middleware.locale.LocaleMiddleware' "
"in your MIDDLEWARE to work.",
stacklevel=1,
)
return None
try: try:
set_language_url = reverse("set_language") context["set_language_url"] = reverse("set_language")
except NoReverseMatch: except NoReverseMatch:
# ImproperlyConfigured - must include i18n urls: warnings.warn(
# urlpatterns += [url(r'^i18n/', include('django.conf.urls.i18n')),] "Language chooser requires Django's `set_language` view: "
"`urlpatterns += [url(r'^i18n/', include('django.conf.urls.i18n'))]`.",
stacklevel=1,
)
return None return None
request = context.get("request", None) request = context.get("request", None)
if not request: if not request:
return None return None
context["LANGUAGES"] = settings.LANGUAGES
full_path = request.get_full_path() full_path = request.get_full_path()
admin_nolang_url = re.sub(r"^\/([\w]{2})([\-\_]{1}[\w]{2,4})?\/", "/", full_path) admin_nolang_url = re.sub(r"^\/([\w]{2})([\-\_]{1}[\w]{2,4})?\/", "/", full_path)
if admin_nolang_url == full_path:
# ImproperlyConfigured - must include admin urls using i18n_patterns:
# from django.conf.urls.i18n import i18n_patterns
# urlpatterns += i18n_patterns(url(r'^admin/', admin.site.urls))
return None
langs_data = []
default_lang_code = settings.LANGUAGE_CODE default_lang_code = settings.LANGUAGE_CODE
current_lang_code = translation.get_language() or default_lang_code current_lang_code = translation.get_language() or default_lang_code
for language in settings.LANGUAGES: context["LANGUAGE_CODE"] = current_lang_code
lang_code = language[0].lower() context["next"] = admin_nolang_url
lang_name = language[1].title() return context
lang_data = {
"code": lang_code,
"name": lang_name,
"default": lang_code == default_lang_code,
"active": lang_code == current_lang_code,
"activation_url": f"{set_language_url}?next=/{lang_code}{admin_nolang_url}",
}
langs_data.append(lang_data)
return langs_data
@register.simple_tag() @register.simple_tag()

View File

@ -26,6 +26,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",
"django.middleware.locale.LocaleMiddleware",
] ]
TEMPLATES = [ TEMPLATES = [

View File

@ -22,118 +22,70 @@ class AdminInterfaceTemplateTagsTestCase(TestCase):
def __render_template(self, string, context=None): def __render_template(self, string, context=None):
return Template(string).render(Context(context or {})) return Template(string).render(Context(context or {}))
def test_get_admin_interface_languages(self): def test_admin_interface_language_chooser(self):
context = Context({"request": self.request_factory.get("/en/admin/")}) context = Context({"request": self.request_factory.get("/en/admin/")})
languages = templatetags.get_admin_interface_languages(context) context = templatetags.admin_interface_language_chooser(context)
languages = context["LANGUAGES"]
expected_languages = [ expected_languages = [
{ ("de", "Deutsch"),
"code": "de", ("en", "English"),
"name": "Deutsch", ("es", "Español"),
"default": False, ("fa", "Farsi"),
"active": False, ("fr", "Français"),
"activation_url": "/i18n/setlang/?next=/de/admin/", ("it", "Italiano"),
}, ("pl", "Polski"),
{ ("pt-BR", "Português"),
"code": "en", ("ru", "Русский"),
"name": "English", ("tr", "Türk"),
"default": True,
"active": True,
"activation_url": "/i18n/setlang/?next=/en/admin/",
},
{
"code": "es",
"name": "Español",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/es/admin/",
},
{
"code": "fa",
"name": "Farsi",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/fa/admin/",
},
{
"code": "fr",
"name": "Français",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/fr/admin/",
},
{
"code": "it",
"name": "Italiano",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/it/admin/",
},
{
"code": "pl",
"name": "Polski",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/pl/admin/",
},
{
"code": "pt-BR",
"name": "Português",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/pt-br/admin/",
},
{
"code": "ru",
"name": "Русский",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/ru/admin/",
},
{
"code": "tr",
"name": "Türk",
"default": False,
"active": False,
"activation_url": "/i18n/setlang/?next=/tr/admin/",
},
] ]
self.assertEqual(len(languages), len(expected_languages)) self.assertEqual(len(languages), len(expected_languages))
self.assertEqual(languages[0], expected_languages[0]) self.assertEqual(languages[0], expected_languages[0])
self.assertEqual(languages[1], expected_languages[1]) self.assertEqual(languages[1], expected_languages[1])
self.assertEqual(context["next"], "/admin/")
@override_settings( @override_settings(
USE_I18N=False, USE_I18N=False,
) )
def test_get_admin_interface_languages_with_i18n_disabled(self): def test_admin_interface_language_chooser_with_i18n_disabled(self):
context = Context({"request": self.request_factory.get("/en/admin/")}) context = Context({"request": self.request_factory.get("/en/admin/")})
languages = templatetags.get_admin_interface_languages(context) tag_context = templatetags.admin_interface_language_chooser(context)
self.assertEqual(languages, None) self.assertEqual(tag_context, None)
@override_settings( @override_settings(
ROOT_URLCONF="tests.urls_without_i18n_patterns", ROOT_URLCONF="tests.urls_without_i18n_patterns",
) )
def test_get_admin_interface_languages_without_i18n_url_patterns(self): def test_admin_interface_language_chooser_without_i18n_url_patterns(self):
context = Context({"request": self.request_factory.get("/en/admin/")}) context = Context({"request": self.request_factory.get("/en/admin/")})
languages = templatetags.get_admin_interface_languages(context) with self.assertWarnsMessage(UserWarning, "django.conf.urls.i18n"):
self.assertEqual(languages, None) tag_context = templatetags.admin_interface_language_chooser(context)
self.assertEqual(tag_context, None)
@override_settings(
MIDDLEWARE=[],
)
def test_admin_interface_language_chooser_without_locale_middleware(self):
context = Context({"request": self.request_factory.get("/en/admin/")})
with self.assertWarnsMessage(UserWarning, "LocaleMiddleware"):
tag_context = templatetags.admin_interface_language_chooser(context)
self.assertEqual(tag_context, None)
@override_settings( @override_settings(
LANGUAGES=(("en", "English"),), LANGUAGES=(("en", "English"),),
) )
def test_get_admin_interface_languages_without_multiple_languages(self): def test_admin_interface_language_chooser_without_multiple_languages(self):
context = Context({"request": self.request_factory.get("/en/admin/")}) context = Context({"request": self.request_factory.get("/en/admin/")})
languages = templatetags.get_admin_interface_languages(context) tag_context = templatetags.admin_interface_language_chooser(context)
self.assertEqual(languages, None) self.assertEqual(tag_context, None)
def test_get_admin_interface_languages_without_request(self): def test_admin_interface_language_chooser_without_request(self):
context = Context({}) context = Context({})
languages = templatetags.get_admin_interface_languages(context) tag_context = templatetags.admin_interface_language_chooser(context)
self.assertEqual(languages, None) self.assertEqual(tag_context, None)
def test_get_admin_interface_languages_without_language_prefix_in_url(self): def test_admin_interface_language_chooser_without_language_prefix_in_url(self):
context = Context({"request": self.request_factory.get("/admin/")}) context = Context({"request": self.request_factory.get("/admin/")})
languages = templatetags.get_admin_interface_languages(context) tag_context = templatetags.admin_interface_language_chooser(context)
self.assertEqual(languages, None) self.assertEqual(tag_context["next"], "/admin/")
def test_get_theme(self): def test_get_theme(self):
Theme.objects.all().delete() Theme.objects.all().delete()