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:
|
||||
return cls.get_default()
|
||||
|
||||
def language_code_is_valid(self):
|
||||
return self.language_code in get_content_languages()
|
||||
|
||||
def get_display_name(self):
|
||||
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
|
||||
|
||||
site_permission_policy = ModelPermissionPolicy(Site)
|
||||
collection_permission_policy = ModelPermissionPolicy(Collection)
|
||||
task_permission_policy = ModelPermissionPolicy(Task)
|
||||
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.images',
|
||||
'wagtail.sites',
|
||||
'wagtail.locales',
|
||||
'wagtail.users',
|
||||
'wagtail.snippets',
|
||||
'wagtail.documents',
|
||||
|
|
Ładowanie…
Reference in New Issue