kopia lustrzana https://github.com/wagtail/wagtail
Implemented Locales UI
rodzic
7380037269
commit
7c86c4e14f
|
@ -170,3 +170,10 @@ svg.icon-spinner { // TODO: leave only class when iconfont styles are removed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.locale-error {
|
||||||
|
vertical-align: text-top;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
color: $color-red;
|
||||||
|
}
|
||||||
|
|
|
@ -347,6 +347,9 @@ class Locale(models.Model):
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
return cls.get_default()
|
return cls.get_default()
|
||||||
|
|
||||||
|
def language_code_is_valid(self):
|
||||||
|
return self.language_code in get_content_languages()
|
||||||
|
|
||||||
def get_display_name(self):
|
def get_display_name(self):
|
||||||
return get_content_languages().get(self.language_code)
|
return get_content_languages().get(self.language_code)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from wagtail.core.models import Collection, Site, Task, Workflow
|
from wagtail.core.models import Collection, Locale, Site, Task, Workflow
|
||||||
from wagtail.core.permission_policies import ModelPermissionPolicy
|
from wagtail.core.permission_policies import ModelPermissionPolicy
|
||||||
|
|
||||||
site_permission_policy = ModelPermissionPolicy(Site)
|
site_permission_policy = ModelPermissionPolicy(Site)
|
||||||
collection_permission_policy = ModelPermissionPolicy(Collection)
|
collection_permission_policy = ModelPermissionPolicy(Collection)
|
||||||
task_permission_policy = ModelPermissionPolicy(Task)
|
task_permission_policy = ModelPermissionPolicy(Task)
|
||||||
workflow_permission_policy = ModelPermissionPolicy(Workflow)
|
workflow_permission_policy = ModelPermissionPolicy(Workflow)
|
||||||
|
locale_permission_policy = ModelPermissionPolicy(Locale)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'wagtail.locales.apps.WagtailLocalesAppConfig'
|
|
@ -0,0 +1,8 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class WagtailLocalesAppConfig(AppConfig):
|
||||||
|
name = 'wagtail.locales'
|
||||||
|
label = 'wagtaillocales'
|
||||||
|
verbose_name = _("Wagtail locales")
|
|
@ -0,0 +1,31 @@
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from wagtail.core.models import Locale
|
||||||
|
from wagtail.core.utils import get_content_languages
|
||||||
|
|
||||||
|
|
||||||
|
class LocaleForm(forms.ModelForm):
|
||||||
|
required_css_class = "required"
|
||||||
|
language_code = forms.ChoiceField(label=_("Language"), choices=get_content_languages().items())
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
instance = kwargs.get('instance')
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Get language codes that are already used
|
||||||
|
used_language_codes = Locale.objects.values_list('language_code', flat=True)
|
||||||
|
|
||||||
|
self.fields['language_code'].choices = [
|
||||||
|
(language_code, display_name)
|
||||||
|
for language_code, display_name in get_content_languages().items()
|
||||||
|
if language_code not in used_language_codes or (instance and instance.language_code == language_code)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If the existing language code is invalid, add an empty value so Django doesn't automatically select a random language
|
||||||
|
if instance and not instance.language_code_is_valid():
|
||||||
|
self.fields['language_code'].choices.insert(0, (None, _("Select a new language")))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Locale
|
||||||
|
fields = ['language_code']
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "wagtailadmin/generic/confirm_delete.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% include "wagtailadmin/shared/header.html" with title=view.page_title subtitle=view.get_page_subtitle icon=view.header_icon %}
|
||||||
|
|
||||||
|
<div class="nice-padding">
|
||||||
|
{% if can_delete %}
|
||||||
|
<p>{{ view.confirmation_message }}</p>
|
||||||
|
|
||||||
|
<form action="{{ view.get_delete_url }}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit" value="{% trans 'Yes, delete' %}" class="button serious" />
|
||||||
|
<a href="{% url 'wagtaillocales:edit' locale.id %}" class="button button-secondary">{% trans "Back" %}</a>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ view.cannot_delete_message }}</p>
|
||||||
|
|
||||||
|
<a href="{% url 'wagtaillocales:edit' locale.id %}" class="button button-secondary">{% trans "Back" %}</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% extends "wagtailadmin/generic/create.html" %}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "wagtailadmin/generic/edit.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block before_form %}
|
||||||
|
{% if not locale.language_code_is_valid %}
|
||||||
|
<p class="help-block help-warning">
|
||||||
|
{% trans "This locale's current language code is not supported. Please choose a new language or delete this locale." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,47 @@
|
||||||
|
{% extends "wagtailadmin/generic/index.html" %}
|
||||||
|
{% load wagtailadmin_tags i18n %}
|
||||||
|
|
||||||
|
{% block listing %}
|
||||||
|
<div class="nice-padding">
|
||||||
|
<div id="locales-list">
|
||||||
|
<table class="listing">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="hostname">
|
||||||
|
{% if ordering == "name" %}
|
||||||
|
<a href="{% url 'wagtaillocales:index' %}" class="icon icon-arrow-down-after teal">
|
||||||
|
{% trans "Language" %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'wagtaillocales:index' %}?ordering=name" class="icon icon-arrow-down-after">
|
||||||
|
{% trans "Language" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
<th>{% trans "Usage" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for locale in locales %}
|
||||||
|
<tr>
|
||||||
|
<td class="hostname title">
|
||||||
|
<div class="title-wrapper">
|
||||||
|
<a href="{% url 'wagtaillocales:edit' locale.id %}">{{ locale }}</a>
|
||||||
|
{% if not locale.language_code_is_valid %}
|
||||||
|
{% trans "This locale's language code is not supported" as error %}
|
||||||
|
{% icon name="warning" class_name="locale-error" title=error %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{# TODO Make this translatable #}
|
||||||
|
{{ locale.num_pages }} pages{% if locale.num_others %} + {{ locale.num_others }} others{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,188 @@
|
||||||
|
from django.contrib.messages import get_messages
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from wagtail.core.models import Locale
|
||||||
|
from wagtail.tests.utils import WagtailTestUtils
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocaleIndexView(TestCase, WagtailTestUtils):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
def get(self, params={}):
|
||||||
|
return self.client.get(reverse('wagtaillocales:index'), params)
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.get()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'wagtaillocales/index.html')
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocaleCreateView(TestCase, WagtailTestUtils):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
self.english = Locale.objects.get()
|
||||||
|
|
||||||
|
def get(self, params={}):
|
||||||
|
return self.client.get(reverse('wagtaillocales:add'), params)
|
||||||
|
|
||||||
|
def post(self, post_data={}):
|
||||||
|
return self.client.post(reverse('wagtaillocales:add'), post_data)
|
||||||
|
|
||||||
|
def test_default_language(self):
|
||||||
|
# we should have loaded with a single locale
|
||||||
|
self.assertEqual(self.english.language_code, 'en')
|
||||||
|
self.assertEqual(self.english.get_display_name(), "English")
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.get()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'wagtaillocales/create.html')
|
||||||
|
|
||||||
|
self.assertEqual(response.context['form'].fields['language_code'].choices, [
|
||||||
|
('fr', 'French')
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
response = self.post({
|
||||||
|
'language_code': "fr",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Should redirect back to index
|
||||||
|
self.assertRedirects(response, reverse('wagtaillocales:index'))
|
||||||
|
|
||||||
|
# Check that the locale was created
|
||||||
|
self.assertTrue(Locale.objects.filter(language_code='fr').exists())
|
||||||
|
|
||||||
|
def test_duplicate_not_allowed(self):
|
||||||
|
response = self.post({
|
||||||
|
'language_code': "en",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Should return the form with errors
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFormError(response, 'form', 'language_code', ['Select a valid choice. en is not one of the available choices.'])
|
||||||
|
|
||||||
|
def test_language_code_must_be_in_settings(self):
|
||||||
|
response = self.post({
|
||||||
|
'language_code': "ja",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Should return the form with errors
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFormError(response, 'form', 'language_code', ['Select a valid choice. ja is not one of the available choices.'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocaleEditView(TestCase, WagtailTestUtils):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
self.english = Locale.objects.get()
|
||||||
|
|
||||||
|
def get(self, params=None, locale=None):
|
||||||
|
locale = locale or self.english
|
||||||
|
return self.client.get(reverse('wagtaillocales:edit', args=[locale.id]), params or {})
|
||||||
|
|
||||||
|
def post(self, post_data=None, locale=None):
|
||||||
|
post_data = post_data or {}
|
||||||
|
locale = locale or self.english
|
||||||
|
post_data.setdefault('language_code', locale.language_code)
|
||||||
|
return self.client.post(reverse('wagtaillocales:edit', args=[locale.id]), post_data)
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.get()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'wagtaillocales/edit.html')
|
||||||
|
|
||||||
|
self.assertEqual(response.context['form'].fields['language_code'].choices, [
|
||||||
|
('en', 'English'), # Note: Current value is displayed even though it's in use
|
||||||
|
('fr', 'French')
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_invalid_language(self):
|
||||||
|
invalid = Locale.objects.create(language_code='foo')
|
||||||
|
|
||||||
|
response = self.get(locale=invalid)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'wagtaillocales/edit.html')
|
||||||
|
|
||||||
|
self.assertEqual(response.context['form'].fields['language_code'].choices, [
|
||||||
|
(None, 'Select a new language'), # This is shown instead of the current value if invalid
|
||||||
|
('fr', 'French')
|
||||||
|
])
|
||||||
|
|
||||||
|
def test_edit(self):
|
||||||
|
response = self.post({
|
||||||
|
'language_code': 'fr',
|
||||||
|
})
|
||||||
|
|
||||||
|
# Should redirect back to index
|
||||||
|
self.assertRedirects(response, reverse('wagtaillocales:index'))
|
||||||
|
|
||||||
|
# Check that the locale was edited
|
||||||
|
self.english.refresh_from_db()
|
||||||
|
self.assertEqual(self.english.language_code, 'fr')
|
||||||
|
|
||||||
|
def test_edit_duplicate_not_allowed(self):
|
||||||
|
french = Locale.objects.create(language_code='fr')
|
||||||
|
|
||||||
|
response = self.post({
|
||||||
|
'language_code': "en",
|
||||||
|
}, locale=french)
|
||||||
|
|
||||||
|
# Should return the form with errors
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFormError(response, 'form', 'language_code', ['Select a valid choice. en is not one of the available choices.'])
|
||||||
|
|
||||||
|
def test_edit_language_code_must_be_in_settings(self):
|
||||||
|
response = self.post({
|
||||||
|
'language_code': "ja",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Should return the form with errors
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFormError(response, 'form', 'language_code', ['Select a valid choice. ja is not one of the available choices.'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestLocaleDeleteView(TestCase, WagtailTestUtils):
|
||||||
|
def setUp(self):
|
||||||
|
self.login()
|
||||||
|
self.english = Locale.objects.get()
|
||||||
|
|
||||||
|
def get(self, params={}, locale=None):
|
||||||
|
locale = locale or self.english
|
||||||
|
return self.client.get(reverse('wagtaillocales:delete', args=[locale.id]), params)
|
||||||
|
|
||||||
|
def post(self, post_data={}, locale=None):
|
||||||
|
locale = locale or self.english
|
||||||
|
return self.client.post(reverse('wagtaillocales:delete', args=[locale.id]), post_data)
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
response = self.get()
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTemplateUsed(response, 'wagtailadmin/generic/confirm_delete.html')
|
||||||
|
|
||||||
|
def test_delete_locale(self):
|
||||||
|
french = Locale.objects.create(language_code='fr')
|
||||||
|
|
||||||
|
response = self.post(locale=french)
|
||||||
|
|
||||||
|
# Should redirect back to index
|
||||||
|
self.assertRedirects(response, reverse('wagtaillocales:index'))
|
||||||
|
|
||||||
|
# Check that the locale was deleted
|
||||||
|
self.assertFalse(Locale.objects.filter(language_code='fr').exists())
|
||||||
|
|
||||||
|
def test_cannot_delete_locales_with_pages(self):
|
||||||
|
response = self.post()
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Check error message
|
||||||
|
messages = list(get_messages(response.wsgi_request))
|
||||||
|
self.assertEqual(messages[0].level_tag, 'error')
|
||||||
|
self.assertEqual(messages[0].message, "This locale cannot be deleted because there are pages and/or other objects using it.\n\n\n\n\n")
|
||||||
|
|
||||||
|
# Check that the locale was not deleted
|
||||||
|
self.assertTrue(Locale.objects.filter(language_code='en').exists())
|
|
@ -0,0 +1,18 @@
|
||||||
|
from wagtail.core.models import Page, get_translatable_models
|
||||||
|
|
||||||
|
|
||||||
|
def get_locale_usage(locale):
|
||||||
|
"""
|
||||||
|
Returns the number of pages and other objects that use a locale
|
||||||
|
"""
|
||||||
|
num_pages = Page.objects.filter(locale=locale).exclude(depth=1).count()
|
||||||
|
|
||||||
|
num_others = 0
|
||||||
|
|
||||||
|
for model in get_translatable_models():
|
||||||
|
if model is Page:
|
||||||
|
continue
|
||||||
|
|
||||||
|
num_others += model.objects.filter(locale=locale).count()
|
||||||
|
|
||||||
|
return num_pages, num_others
|
|
@ -0,0 +1,79 @@
|
||||||
|
from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
|
from wagtail.admin import messages
|
||||||
|
from wagtail.admin.views import generic
|
||||||
|
from wagtail.admin.viewsets.model import ModelViewSet
|
||||||
|
from wagtail.core.models import Locale
|
||||||
|
from wagtail.core.permissions import locale_permission_policy
|
||||||
|
|
||||||
|
from .forms import LocaleForm
|
||||||
|
from .utils import get_locale_usage
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(generic.IndexView):
|
||||||
|
template_name = 'wagtaillocales/index.html'
|
||||||
|
page_title = gettext_lazy("Locales")
|
||||||
|
add_item_label = gettext_lazy("Add a locale")
|
||||||
|
context_object_name = 'locales'
|
||||||
|
queryset = Locale.all_objects.all()
|
||||||
|
|
||||||
|
def get_context_data(self):
|
||||||
|
context = super().get_context_data()
|
||||||
|
|
||||||
|
for locale in context['locales']:
|
||||||
|
locale.num_pages, locale.num_others = get_locale_usage(locale)
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class CreateView(generic.CreateView):
|
||||||
|
page_title = gettext_lazy("Add locale")
|
||||||
|
success_message = gettext_lazy("Locale '{0}' created.")
|
||||||
|
template_name = 'wagtaillocales/create.html'
|
||||||
|
|
||||||
|
|
||||||
|
class EditView(generic.EditView):
|
||||||
|
success_message = gettext_lazy("Locale '{0}' updated.")
|
||||||
|
error_message = gettext_lazy("The locale could not be saved due to errors.")
|
||||||
|
delete_item_label = gettext_lazy("Delete locale")
|
||||||
|
context_object_name = 'locale'
|
||||||
|
template_name = 'wagtaillocales/edit.html'
|
||||||
|
queryset = Locale.all_objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteView(generic.DeleteView):
|
||||||
|
success_message = gettext_lazy("Locale '{0}' deleted.")
|
||||||
|
cannot_delete_message = gettext_lazy("This locale cannot be deleted because there are pages and/or other objects using it.")
|
||||||
|
page_title = gettext_lazy("Delete locale")
|
||||||
|
confirmation_message = gettext_lazy("Are you sure you want to delete this locale?")
|
||||||
|
template_name = 'wagtaillocales/confirm_delete.html'
|
||||||
|
queryset = Locale.all_objects.all()
|
||||||
|
|
||||||
|
def can_delete(self, locale):
|
||||||
|
return get_locale_usage(locale) == (0, 0)
|
||||||
|
|
||||||
|
def get_context_data(self, object=None):
|
||||||
|
context = context = super().get_context_data()
|
||||||
|
context['can_delete'] = self.can_delete(object)
|
||||||
|
return context
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
if self.can_delete(self.get_object()):
|
||||||
|
return super().delete(request, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
messages.error(request, self.cannot_delete_message)
|
||||||
|
return super().get(request)
|
||||||
|
|
||||||
|
|
||||||
|
class LocaleViewSet(ModelViewSet):
|
||||||
|
icon = 'site'
|
||||||
|
model = Locale
|
||||||
|
permission_policy = locale_permission_policy
|
||||||
|
|
||||||
|
index_view_class = IndexView
|
||||||
|
add_view_class = CreateView
|
||||||
|
edit_view_class = EditView
|
||||||
|
delete_view_class = DeleteView
|
||||||
|
|
||||||
|
def get_form_class(self, for_update=False):
|
||||||
|
return LocaleForm
|
|
@ -0,0 +1,33 @@
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from wagtail.admin.menu import MenuItem
|
||||||
|
from wagtail.core import hooks
|
||||||
|
from wagtail.core.permissions import site_permission_policy
|
||||||
|
|
||||||
|
from .views import LocaleViewSet
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('register_admin_viewset')
|
||||||
|
def register_viewset():
|
||||||
|
return LocaleViewSet('wagtaillocales', url_prefix='locales')
|
||||||
|
|
||||||
|
|
||||||
|
class LocalesMenuItem(MenuItem):
|
||||||
|
def is_shown(self, request):
|
||||||
|
return site_permission_policy.user_has_any_permission(
|
||||||
|
request.user, ['add', 'change', 'delete']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('register_settings_menu_item')
|
||||||
|
def register_locales_menu_item():
|
||||||
|
return LocalesMenuItem(_('Locales'), reverse('wagtaillocales:index'),
|
||||||
|
icon_name='site', order=603)
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('register_permissions')
|
||||||
|
def register_permissions():
|
||||||
|
return Permission.objects.filter(content_type__app_label='wagtailcore',
|
||||||
|
codename__in=['add_locale', 'change_locale', 'delete_locale'])
|
|
@ -124,6 +124,7 @@ INSTALLED_APPS = [
|
||||||
'wagtail.embeds',
|
'wagtail.embeds',
|
||||||
'wagtail.images',
|
'wagtail.images',
|
||||||
'wagtail.sites',
|
'wagtail.sites',
|
||||||
|
'wagtail.locales',
|
||||||
'wagtail.users',
|
'wagtail.users',
|
||||||
'wagtail.snippets',
|
'wagtail.snippets',
|
||||||
'wagtail.documents',
|
'wagtail.documents',
|
||||||
|
|
Ładowanie…
Reference in New Issue