Implemented Locales UI

pull/6395/head
Karl Hobley 2020-07-22 15:46:55 +01:00 zatwierdzone przez Matt Westcott
rodzic 7380037269
commit 7c86c4e14f
15 zmienionych plików z 451 dodań i 1 usunięć

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -0,0 +1 @@
default_app_config = 'wagtail.locales.apps.WagtailLocalesAppConfig'

Wyświetl plik

@ -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")

Wyświetl plik

@ -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']

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -0,0 +1 @@
{% extends "wagtailadmin/generic/create.html" %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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 %}

Wyświetl plik

@ -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())

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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'])

Wyświetl plik

@ -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',