mirror of
https://github.com/wagtail/wagtail.git
synced 2024-11-29 09:33:54 +01:00
Add contrib simple translation (#6528)
This commit is contained in:
parent
4f8ef843d0
commit
b1b69360a3
@ -21,10 +21,9 @@ This document describes how to configure Wagtail for authoring content in
|
||||
multiple languages.
|
||||
|
||||
.. note::
|
||||
Wagtail provides the infrastructure for creating and serving content in multiple languages,
|
||||
but does not itself provide an admin interface for managing translations of the same content
|
||||
across different languages. For this, the `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_
|
||||
app must be installed separately.
|
||||
Wagtail provides the infrastructure for creating and serving content in multiple languages.
|
||||
There are two options for managing translations across different languages in the admin interface:
|
||||
:ref:`wagtail.contrib.simple_translation<simple_translation>` or the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_ (third-party package).
|
||||
|
||||
This document only covers the internationalisation of content managed by Wagtail.
|
||||
For information on how to translate static content in template files, JavaScript
|
||||
@ -611,9 +610,22 @@ data migration).
|
||||
Translation workflow
|
||||
--------------------
|
||||
|
||||
As mentioned at the beginning, Wagtail does not supply any built-in user interface
|
||||
or external integration that provides a translation workflow. This has been left
|
||||
for third-party packages to solve.
|
||||
As mentioned at the beginning, Wagtail does supply ``wagtail.contrib.simple_translation``.
|
||||
|
||||
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
|
||||
|
||||
- Copies are created in the source language (not translated)
|
||||
- Copies of pages are in draft status
|
||||
|
||||
Content editors need to translate the content and publish the pages.
|
||||
|
||||
To enable add ``"wagtail.contrib.simple_translation"`` to ``INSTALLED_APPS``
|
||||
and run ``python manage.py migrate`` to create the ``submit_translation`` permissions.
|
||||
In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
|
||||
|
||||
.. note::
|
||||
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_.
|
||||
|
||||
|
||||
Wagtail Localize
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
@ -15,6 +15,7 @@ Wagtail ships with a variety of extra optional modules.
|
||||
modeladmin/index
|
||||
postgres_search
|
||||
searchpromotions
|
||||
simple_translation
|
||||
table_block
|
||||
redirects
|
||||
legacy_richtext
|
||||
@ -63,6 +64,12 @@ A module allowing for more customisable representation and management of custom
|
||||
A module for managing "Promoted Search Results"
|
||||
|
||||
|
||||
:doc:`simple_translation`
|
||||
-------------------------
|
||||
|
||||
A module for copying translatables (pages and snippets) to another language.
|
||||
|
||||
|
||||
:doc:`table_block`
|
||||
-----------------------
|
||||
|
||||
|
31
docs/reference/contrib/simple_translation.rst
Normal file
31
docs/reference/contrib/simple_translation.rst
Normal file
@ -0,0 +1,31 @@
|
||||
.. _simple_translation:
|
||||
|
||||
Simple translation
|
||||
==================
|
||||
|
||||
The simple_translation module provides a user interface that allows users to copy pages and translatable snippets into another language.
|
||||
|
||||
- Copies are created in the source language (not translated)
|
||||
- Copies of pages are in draft status
|
||||
|
||||
Content editors need to translate the content and publish the pages.
|
||||
|
||||
.. note::
|
||||
Simple Translation is optional. It can be switched out by third-party packages. Like the more advanced `wagtail-localize <https://github.com/wagtail/wagtail-localize>`_.
|
||||
|
||||
|
||||
Basic configuration
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add ``"wagtail.contrib.simple_translation"`` to INSTALLED_APPS in your settings file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
INSTALLED_APPS = [
|
||||
...
|
||||
"wagtail.contrib.simple_translation",
|
||||
]
|
||||
|
||||
Run ``python manage.py migrate`` to create the necessary permissions.
|
||||
|
||||
In the Wagtail admin, go to settings and give some users or groups the "Can submit translations" permission.
|
3
wagtail/contrib/simple_translation/__init__.py
Normal file
3
wagtail/contrib/simple_translation/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
default_app_config = (
|
||||
"wagtail.contrib.simple_translation.apps.SimpleTranslationAppConfig"
|
||||
)
|
8
wagtail/contrib/simple_translation/apps.py
Normal file
8
wagtail/contrib/simple_translation/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class SimpleTranslationAppConfig(AppConfig):
|
||||
name = "wagtail.contrib.simple_translation"
|
||||
label = "simple_translation"
|
||||
verbose_name = _("Wagtail simple translation")
|
48
wagtail/contrib/simple_translation/forms.py
Normal file
48
wagtail/contrib/simple_translation/forms.py
Normal file
@ -0,0 +1,48 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy, ngettext
|
||||
|
||||
from wagtail.core.models import Locale, Page
|
||||
|
||||
|
||||
class SubmitTranslationForm(forms.Form):
|
||||
# Note: We don't actually use select_all in Python, it is just the
|
||||
# easiest way to add the widget to the form. It's controlled in JS.
|
||||
select_all = forms.BooleanField(label=gettext_lazy("Select all"), required=False)
|
||||
locales = forms.ModelMultipleChoiceField(
|
||||
label=gettext_lazy("Locales"),
|
||||
queryset=Locale.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
include_subtree = forms.BooleanField(
|
||||
required=False, help_text=gettext_lazy("All child pages will be created.")
|
||||
)
|
||||
|
||||
def __init__(self, instance, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
hide_include_subtree = True
|
||||
|
||||
if isinstance(instance, Page):
|
||||
descendant_count = instance.get_descendants().count()
|
||||
|
||||
if descendant_count > 0:
|
||||
hide_include_subtree = False
|
||||
self.fields["include_subtree"].label = ngettext(
|
||||
"Include subtree ({} page)",
|
||||
"Include subtree ({} pages)",
|
||||
descendant_count,
|
||||
).format(descendant_count)
|
||||
|
||||
if hide_include_subtree:
|
||||
self.fields["include_subtree"].widget = forms.HiddenInput()
|
||||
|
||||
self.fields["locales"].queryset = Locale.objects.exclude(
|
||||
id__in=instance.get_translations(inclusive=True).values_list(
|
||||
"locale_id", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
# Using len() instead of count() here as we're going to evaluate this queryset
|
||||
# anyway and it gets cached so it'll only have one query in the end.
|
||||
if len(self.fields["locales"].queryset) < 2:
|
||||
self.fields["select_all"].widget = forms.HiddenInput()
|
@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.2.16 on 2021-04-12 20:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SimpleTranslation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
],
|
||||
options={
|
||||
'permissions': [('submit_translation', 'Can submit translations')],
|
||||
'default_permissions': [],
|
||||
},
|
||||
),
|
||||
]
|
16
wagtail/contrib/simple_translation/models.py
Normal file
16
wagtail/contrib/simple_translation/models.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
class SimpleTranslation(Model):
|
||||
"""
|
||||
SimpleTranslation, dummy model to create the `submit_translation` permission.
|
||||
|
||||
We need this model to be concrete or the following management commands will misbehave:
|
||||
- `remove_stale_contenttypes`, will drop the perm
|
||||
- `dump_data`, will complain about the missing table
|
||||
"""
|
||||
class Meta:
|
||||
default_permissions = []
|
||||
permissions = [
|
||||
('submit_translation', "Can submit translations"),
|
||||
]
|
@ -0,0 +1,48 @@
|
||||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n wagtailadmin_tags %}
|
||||
{% block titletag %}{{ view.get_title }} {{ view.get_subtitle }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=view.get_title subtitle=view.get_subtitle icon="doc-empty-inverse" %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if next_url %}
|
||||
<input type="hidden" name="next" value="{{ next_url }}">
|
||||
{% endif %}
|
||||
|
||||
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
|
||||
|
||||
<ul class="fields">
|
||||
{% block visible_fields %}
|
||||
{% for field in form.visible_fields %}
|
||||
{% include "wagtailadmin/shared/field_as_li.html" %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
<li>
|
||||
<input type="submit" value="{% trans 'Submit' %}" class="button" />
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var selectAll = document.querySelector('[name="select_all"]');
|
||||
var locales = document.querySelectorAll('[name="locales"]');
|
||||
|
||||
selectAll.addEventListener('change', function() {
|
||||
for (var i = 0; i < locales.length; i++) {
|
||||
locales[i].checked = selectAll.checked;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
86
wagtail/contrib/simple_translation/tests/test_forms.py
Normal file
86
wagtail/contrib/simple_translation/tests/test_forms.py
Normal file
@ -0,0 +1,86 @@
|
||||
from django.forms import CheckboxInput, HiddenInput
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
from wagtail.contrib.simple_translation.forms import SubmitTranslationForm
|
||||
from wagtail.core.models import Locale, Page
|
||||
from wagtail.tests.i18n.models import TestPage
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
)
|
||||
class TestSubmitPageTranslation(WagtailTestUtils, TestCase):
|
||||
def setUp(self):
|
||||
self.en_locale = Locale.objects.first()
|
||||
self.fr_locale = Locale.objects.create(language_code="fr")
|
||||
self.de_locale = Locale.objects.create(language_code="de")
|
||||
|
||||
self.en_homepage = Page.objects.get(depth=2)
|
||||
self.fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
|
||||
self.de_homepage = self.en_homepage.copy_for_translation(self.de_locale)
|
||||
|
||||
self.en_blog_index = TestPage(title="Blog", slug="blog")
|
||||
self.en_homepage.add_child(instance=self.en_blog_index)
|
||||
|
||||
self.en_blog_post = TestPage(title="Blog post", slug="blog-post")
|
||||
self.en_blog_index.add_child(instance=self.en_blog_post)
|
||||
|
||||
def test_include_subtree(self):
|
||||
form = SubmitTranslationForm(instance=self.en_blog_post)
|
||||
self.assertIsInstance(form.fields["include_subtree"].widget, HiddenInput)
|
||||
|
||||
form = SubmitTranslationForm(instance=self.en_blog_index)
|
||||
self.assertIsInstance(form.fields["include_subtree"].widget, CheckboxInput)
|
||||
self.assertEqual(
|
||||
form.fields["include_subtree"].label, "Include subtree (1 page)"
|
||||
)
|
||||
|
||||
form = SubmitTranslationForm(instance=self.en_homepage)
|
||||
self.assertEqual(
|
||||
form.fields["include_subtree"].label, "Include subtree (2 pages)"
|
||||
)
|
||||
|
||||
def test_locales_queryset(self):
|
||||
# Homepage is translated to all locales.
|
||||
form = SubmitTranslationForm(instance=self.en_homepage)
|
||||
self.assertEqual(
|
||||
list(
|
||||
form.fields["locales"].queryset.values_list("language_code", flat=True)
|
||||
),
|
||||
[],
|
||||
)
|
||||
# Blog index can be translated to `de` and `fr`.
|
||||
form = SubmitTranslationForm(instance=self.en_blog_index)
|
||||
self.assertEqual(
|
||||
list(
|
||||
form.fields["locales"].queryset.values_list("language_code", flat=True)
|
||||
),
|
||||
["de", "fr"],
|
||||
)
|
||||
# Blog post can be translated to `de` and `fr`.
|
||||
form = SubmitTranslationForm(instance=self.en_blog_post)
|
||||
self.assertEqual(
|
||||
list(
|
||||
form.fields["locales"].queryset.values_list("language_code", flat=True)
|
||||
),
|
||||
["de", "fr"],
|
||||
)
|
||||
|
||||
def test_select_all(self):
|
||||
form = SubmitTranslationForm(instance=self.en_homepage)
|
||||
# Homepage is translated to all locales.
|
||||
self.assertIsInstance(form.fields["select_all"].widget, HiddenInput)
|
||||
|
||||
form = SubmitTranslationForm(instance=self.en_blog_index)
|
||||
# Blog post can be translated to `de` and `fr`.
|
||||
self.assertIsInstance(form.fields["select_all"].widget, CheckboxInput)
|
18
wagtail/contrib/simple_translation/tests/test_migrations.py
Normal file
18
wagtail/contrib/simple_translation/tests/test_migrations.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from wagtail.tests.utils import TestCase
|
||||
|
||||
|
||||
class TestMigrations(TestCase):
|
||||
def test_content_type_exists(self):
|
||||
self.assertTrue(
|
||||
ContentType.objects.filter(
|
||||
app_label="simple_translation", model="simpletranslation"
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_permission_exists(self):
|
||||
self.assertTrue(
|
||||
Permission.objects.filter(codename="submit_translation").exists()
|
||||
)
|
265
wagtail/contrib/simple_translation/tests/test_views.py
Normal file
265
wagtail/contrib/simple_translation/tests/test_views.py
Normal file
@ -0,0 +1,265 @@
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import Http404
|
||||
from django.test import RequestFactory, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.contrib.simple_translation.forms import SubmitTranslationForm
|
||||
from wagtail.contrib.simple_translation.views import (
|
||||
SubmitPageTranslationView, SubmitSnippetTranslationView, SubmitTranslationView)
|
||||
from wagtail.core.models import Locale, Page, ParentNotTranslatedError
|
||||
from wagtail.tests.i18n.models import TestPage
|
||||
from wagtail.tests.snippets.models import TranslatableSnippet
|
||||
from wagtail.tests.utils import TestCase, WagtailTestUtils
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
)
|
||||
class TestSubmitTranslationView(WagtailTestUtils, TestCase):
|
||||
def setUp(self):
|
||||
self.en_locale = Locale.objects.first()
|
||||
self.fr_locale = Locale.objects.create(language_code="fr")
|
||||
self.de_locale = Locale.objects.create(language_code="de")
|
||||
self.en_homepage = Page.objects.get(depth=2)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_template_name(self):
|
||||
self.assertEqual(
|
||||
SubmitTranslationView.template_name,
|
||||
"simple_translation/admin/submit_translation.html",
|
||||
)
|
||||
|
||||
def test_title(self):
|
||||
self.assertEqual(SubmitTranslationView().title, gettext_lazy("Translate"))
|
||||
self.assertEqual(SubmitTranslationView().get_title(), gettext_lazy("Translate"))
|
||||
|
||||
def test_subtitle(self):
|
||||
view = SubmitTranslationView()
|
||||
view.object = self.en_homepage
|
||||
self.assertEqual(view.get_subtitle(), str(self.en_homepage))
|
||||
|
||||
def test_get_form(self):
|
||||
view = SubmitTranslationView()
|
||||
view.request = self.factory.get("/path/does/not/matter/")
|
||||
view.object = self.en_homepage
|
||||
form = view.get_form()
|
||||
self.assertIsInstance(form, SubmitTranslationForm)
|
||||
|
||||
def test_get_success_url(self):
|
||||
with self.assertRaises(NotImplementedError):
|
||||
view = SubmitTranslationView()
|
||||
view.object = self.en_homepage
|
||||
view.get_success_url()
|
||||
|
||||
def test_get_context_data(self, **kwargs):
|
||||
view = SubmitTranslationView()
|
||||
view.request = self.factory.get("/path/does/not/matter/")
|
||||
view.object = self.en_homepage
|
||||
context = view.get_context_data()
|
||||
self.assertTrue("form" in context.keys())
|
||||
self.assertIsInstance(context["form"], SubmitTranslationForm)
|
||||
|
||||
def test_dispatch_as_anon(self):
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_homepage.id,)
|
||||
)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, f"/admin/login/?next={url}")
|
||||
|
||||
def test_dispatch_as_moderator(self):
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_homepage.id,)
|
||||
)
|
||||
user = self.login()
|
||||
group = Group.objects.get(name="Moderators")
|
||||
user.groups.add(group)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_dispatch_as_user_with_perm(self):
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_homepage.id,)
|
||||
)
|
||||
user = self.login()
|
||||
permission = Permission.objects.get(codename="submit_translation")
|
||||
user.user_permissions.add(permission)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
)
|
||||
class TestSubmitPageTranslationView(WagtailTestUtils, TestCase):
|
||||
def setUp(self):
|
||||
self.en_locale = Locale.objects.first()
|
||||
self.fr_locale = Locale.objects.create(language_code="fr")
|
||||
self.de_locale = Locale.objects.create(language_code="de")
|
||||
|
||||
self.en_homepage = Page.objects.get(depth=2)
|
||||
self.fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
|
||||
self.de_homepage = self.en_homepage.copy_for_translation(self.de_locale)
|
||||
|
||||
self.en_blog_index = TestPage(title="Blog", slug="blog")
|
||||
self.en_homepage.add_child(instance=self.en_blog_index)
|
||||
|
||||
self.en_blog_post = TestPage(title="Blog post", slug="blog-post")
|
||||
self.en_blog_index.add_child(instance=self.en_blog_post)
|
||||
|
||||
def test_title(self):
|
||||
self.assertEqual(SubmitPageTranslationView.title, "Translate page")
|
||||
|
||||
def test_get_subtitle(self):
|
||||
view = SubmitPageTranslationView()
|
||||
view.object = self.en_homepage
|
||||
self.assertEqual(view.get_subtitle(), "Welcome to your new Wagtail site!")
|
||||
|
||||
def test_submit_page_translation_view_test_get(self):
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
|
||||
)
|
||||
self.login()
|
||||
response = self.client.get(url)
|
||||
assert isinstance(response.context["form"], SubmitTranslationForm)
|
||||
|
||||
def test_submit_page_translation_view_test_post_invalid(self):
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
|
||||
)
|
||||
self.login()
|
||||
response = self.client.post(url, {})
|
||||
assert response.status_code == 200
|
||||
assert response.context["form"].errors == {
|
||||
"locales": ["This field is required."]
|
||||
}
|
||||
|
||||
def test_submit_page_translation_view_test_post_single_locale(self):
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
|
||||
)
|
||||
de = Locale.objects.get(language_code="de").id
|
||||
data = {"locales": [de], "include_subtree": True}
|
||||
self.login()
|
||||
response = self.client.post(url, data)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
|
||||
|
||||
response = self.client.get(response.url) # follow the redirect
|
||||
assert [msg.message for msg in response.context["messages"]] == [
|
||||
"The page 'Blog' was successfully created in German"
|
||||
]
|
||||
|
||||
def test_submit_page_translation_view_test_post_multiple_locales(self):
|
||||
# Needs an extra page to hit recursive function
|
||||
en_blog_post_sub = Page(title="Blog post sub", slug="blog-post-sub")
|
||||
self.en_blog_post.add_child(instance=en_blog_post_sub)
|
||||
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_blog_post.id,)
|
||||
)
|
||||
de = Locale.objects.get(language_code="de").id
|
||||
fr = Locale.objects.get(language_code="fr").id
|
||||
data = {"locales": [de, fr], "include_subtree": True}
|
||||
self.login()
|
||||
|
||||
with self.assertRaisesMessage(ParentNotTranslatedError, ""):
|
||||
self.client.post(url, data)
|
||||
|
||||
url = reverse(
|
||||
"simple_translation:submit_page_translation", args=(self.en_blog_index.id,)
|
||||
)
|
||||
response = self.client.post(url, data)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == f"/admin/pages/{self.en_blog_index.get_parent().id}/"
|
||||
|
||||
response = self.client.get(response.url) # follow the redirect
|
||||
assert [msg.message for msg in response.context["messages"]] == [
|
||||
"The page 'Blog' was successfully created in 2 locales"
|
||||
]
|
||||
|
||||
|
||||
@override_settings(
|
||||
LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
WAGTAIL_CONTENT_LANGUAGES=[
|
||||
("en", "English"),
|
||||
("fr", "French"),
|
||||
("de", "German"),
|
||||
],
|
||||
)
|
||||
class TestSubmitSnippetTranslationView(WagtailTestUtils, TestCase):
|
||||
def setUp(self):
|
||||
self.en_locale = Locale.objects.first()
|
||||
self.fr_locale = Locale.objects.create(language_code="fr")
|
||||
self.en_snippet = TranslatableSnippet(text="Hello world", locale=self.en_locale)
|
||||
self.en_snippet.save()
|
||||
|
||||
def test_get_title(self):
|
||||
view = SubmitSnippetTranslationView()
|
||||
view.object = self.en_snippet
|
||||
self.assertEqual(view.get_title(), "Translate translatable snippet")
|
||||
|
||||
def test_get_object(self):
|
||||
view = SubmitSnippetTranslationView()
|
||||
view.object = self.en_snippet
|
||||
view.kwargs = {
|
||||
"app_label": "some_app",
|
||||
"model_name": "some_model",
|
||||
"pk": 1,
|
||||
}
|
||||
with self.assertRaises(Http404):
|
||||
view.get_object()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.en_snippet)
|
||||
view.kwargs = {
|
||||
"app_label": content_type.app_label,
|
||||
"model_name": content_type.model,
|
||||
"pk": str(self.en_snippet.pk),
|
||||
}
|
||||
self.assertEqual(view.get_object(), self.en_snippet)
|
||||
|
||||
def test_get_success_url(self):
|
||||
view = SubmitSnippetTranslationView()
|
||||
view.object = self.en_snippet
|
||||
view.kwargs = {
|
||||
"app_label": "some_app",
|
||||
"model_name": "some_model",
|
||||
"pk": 99,
|
||||
}
|
||||
self.assertEqual(
|
||||
view.get_success_url(), "/admin/snippets/some_app/some_model/99/"
|
||||
)
|
||||
|
||||
def test_get_success_message(self):
|
||||
view = SubmitSnippetTranslationView()
|
||||
view.object = self.en_snippet
|
||||
self.assertEqual(
|
||||
view.get_success_message(self.fr_locale),
|
||||
f"Successfully created French for translatable snippet 'TranslatableSnippet object ({self.en_snippet.id})'",
|
||||
)
|
102
wagtail/contrib/simple_translation/tests/test_wagtail_hooks.py
Normal file
102
wagtail/contrib/simple_translation/tests/test_wagtail_hooks.py
Normal file
@ -0,0 +1,102 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from wagtail.admin import widgets as wagtailadmin_widgets
|
||||
from wagtail.contrib.simple_translation.wagtail_hooks import (
|
||||
page_listing_more_buttons, register_submit_translation_permission)
|
||||
from wagtail.core.models import Locale, Page
|
||||
from wagtail.tests.i18n.models import TestPage
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
|
||||
class Utils(WagtailTestUtils, TestCase):
|
||||
def setUp(self):
|
||||
self.en_locale = Locale.objects.first()
|
||||
self.fr_locale = Locale.objects.create(language_code="fr")
|
||||
self.de_locale = Locale.objects.create(language_code="de")
|
||||
|
||||
self.en_homepage = Page.objects.get(depth=2)
|
||||
self.fr_homepage = self.en_homepage.copy_for_translation(self.fr_locale)
|
||||
self.de_homepage = self.en_homepage.copy_for_translation(self.de_locale)
|
||||
|
||||
self.en_blog_index = TestPage(title="Blog", slug="blog")
|
||||
self.en_homepage.add_child(instance=self.en_blog_index)
|
||||
|
||||
self.en_blog_post = TestPage(title="Blog post", slug="blog-post")
|
||||
self.en_blog_index.add_child(instance=self.en_blog_post)
|
||||
|
||||
|
||||
class TestWagtailHooksURLs(TestCase):
|
||||
def test_register_admin_urls_page(self):
|
||||
self.assertEqual(
|
||||
reverse("simple_translation:submit_page_translation", args=(1,)),
|
||||
"/admin/translation/submit/page/1/",
|
||||
)
|
||||
|
||||
def test_register_admin_urls_snippet(self):
|
||||
app_label = "foo"
|
||||
model_name = "bar"
|
||||
pk = 1
|
||||
self.assertEqual(
|
||||
reverse(
|
||||
"simple_translation:submit_snippet_translation",
|
||||
args=(app_label, model_name, pk),
|
||||
),
|
||||
"/admin/translation/submit/snippet/foo/bar/1/",
|
||||
)
|
||||
|
||||
|
||||
class TestWagtailHooksPermission(Utils):
|
||||
def test_register_submit_translation_permission(self):
|
||||
assert list(
|
||||
register_submit_translation_permission().values_list("id", flat=True)
|
||||
) == [
|
||||
Permission.objects.get(
|
||||
content_type__app_label="simple_translation",
|
||||
codename="submit_translation",
|
||||
).id
|
||||
]
|
||||
|
||||
|
||||
class TestWagtailHooksButtons(Utils):
|
||||
class PagePerms:
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
|
||||
def test_page_listing_more_buttons(self):
|
||||
# Root, no button
|
||||
root_page = self.en_blog_index.get_root()
|
||||
|
||||
if get_user_model().USERNAME_FIELD == 'email':
|
||||
user = get_user_model().objects.create_user(email="jos@example.com")
|
||||
else:
|
||||
user = get_user_model().objects.create_user(username="jos")
|
||||
assert list(page_listing_more_buttons(root_page, self.PagePerms(user))) == []
|
||||
|
||||
# No permissions, no button
|
||||
home_page = self.en_homepage
|
||||
assert list(page_listing_more_buttons(root_page, self.PagePerms(user))) == []
|
||||
|
||||
# Homepage is translated to all languages, no button
|
||||
perm = Permission.objects.get(codename="submit_translation")
|
||||
|
||||
if get_user_model().USERNAME_FIELD == 'email':
|
||||
user = get_user_model().objects.create_user(email="henk@example.com")
|
||||
else:
|
||||
user = get_user_model().objects.create_user(username="henk")
|
||||
|
||||
# New user, to prevent permission cache.
|
||||
user.user_permissions.add(perm)
|
||||
group = Group.objects.get(name="Editors")
|
||||
user.groups.add(group)
|
||||
page_perms = self.PagePerms(user)
|
||||
assert list(page_listing_more_buttons(home_page, page_perms)) == []
|
||||
|
||||
# Page does not have translations yet... button!
|
||||
blog_page = self.en_blog_post
|
||||
assert isinstance(
|
||||
list(page_listing_more_buttons(blog_page, page_perms))[0],
|
||||
wagtailadmin_widgets.Button,
|
||||
)
|
145
wagtail/contrib/simple_translation/views.py
Normal file
145
wagtail/contrib/simple_translation/views.py
Normal file
@ -0,0 +1,145 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.admin.utils import unquote
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django.views.generic import TemplateView
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from wagtail.core.models import Page, TranslatableMixin
|
||||
from wagtail.snippets.views.snippets import get_snippet_model_from_url_params
|
||||
|
||||
from .forms import SubmitTranslationForm
|
||||
|
||||
|
||||
class SubmitTranslationView(SingleObjectMixin, TemplateView):
|
||||
template_name = "simple_translation/admin/submit_translation.html"
|
||||
title = gettext_lazy("Translate")
|
||||
|
||||
def get_title(self):
|
||||
return self.title
|
||||
|
||||
def get_subtitle(self):
|
||||
return str(self.object)
|
||||
|
||||
def get_form(self):
|
||||
if self.request.method == "POST":
|
||||
return SubmitTranslationForm(self.object, self.request.POST)
|
||||
return SubmitTranslationForm(self.object)
|
||||
|
||||
def get_success_url(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(
|
||||
{
|
||||
"form": self.get_form(),
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
def post(self, request, **kwargs): # pragma: no mccabe
|
||||
form = self.get_form()
|
||||
|
||||
if form.is_valid():
|
||||
with transaction.atomic():
|
||||
for locale in form.cleaned_data["locales"]:
|
||||
if isinstance(self.object, Page):
|
||||
self.object.copy_for_translation(locale)
|
||||
if form.cleaned_data["include_subtree"]:
|
||||
|
||||
def _walk(current_page):
|
||||
for child_page in current_page.get_children():
|
||||
child_page.copy_for_translation(locale)
|
||||
|
||||
if child_page.numchild:
|
||||
_walk(child_page)
|
||||
|
||||
_walk(self.object)
|
||||
else:
|
||||
self.object.copy_for_translation(
|
||||
locale
|
||||
).save() # pragma: no cover
|
||||
|
||||
if len(form.cleaned_data["locales"]) == 1:
|
||||
locales = form.cleaned_data["locales"][0].get_display_name()
|
||||
else:
|
||||
# Note: always plural
|
||||
locales = _("{} locales").format(len(form.cleaned_data["locales"]))
|
||||
|
||||
messages.success(self.request, self.get_success_message(locales))
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
context = self.get_context_data(**kwargs)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.has_perms(["simple_translation.submit_translation"]):
|
||||
raise PermissionDenied
|
||||
|
||||
self.object = self.get_object()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class SubmitPageTranslationView(SubmitTranslationView):
|
||||
title = gettext_lazy("Translate page")
|
||||
|
||||
def get_subtitle(self):
|
||||
return self.object.get_admin_display_title()
|
||||
|
||||
def get_object(self):
|
||||
page = get_object_or_404(Page, id=self.kwargs["page_id"]).specific
|
||||
|
||||
# Can't translate the root page
|
||||
if page.is_root():
|
||||
raise Http404
|
||||
|
||||
return page
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("wagtailadmin_explore", args=[self.get_object().get_parent().id])
|
||||
|
||||
def get_success_message(self, locales):
|
||||
return _(
|
||||
"The page '{page_title}' was successfully created in {locales}"
|
||||
).format(page_title=self.object.get_admin_display_title(), locales=locales)
|
||||
|
||||
|
||||
class SubmitSnippetTranslationView(SubmitTranslationView):
|
||||
def get_title(self):
|
||||
return _("Translate {model_name}").format(
|
||||
model_name=self.object._meta.verbose_name
|
||||
)
|
||||
|
||||
def get_object(self):
|
||||
model = get_snippet_model_from_url_params(
|
||||
self.kwargs["app_label"], self.kwargs["model_name"]
|
||||
)
|
||||
|
||||
if not issubclass(model, TranslatableMixin):
|
||||
raise Http404
|
||||
|
||||
return get_object_or_404(model, pk=unquote(self.kwargs["pk"]))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse(
|
||||
"wagtailsnippets:edit",
|
||||
args=[
|
||||
self.kwargs["app_label"],
|
||||
self.kwargs["model_name"],
|
||||
self.kwargs["pk"],
|
||||
],
|
||||
)
|
||||
|
||||
def get_success_message(self, locales):
|
||||
return _("Successfully created {locales} for {model_name} '{object}'").format(
|
||||
model_name=self.object._meta.verbose_name,
|
||||
object=str(self.object),
|
||||
locales=locales,
|
||||
)
|
78
wagtail/contrib/simple_translation/wagtail_hooks.py
Normal file
78
wagtail/contrib/simple_translation/wagtail_hooks.py
Normal file
@ -0,0 +1,78 @@
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.urls import include, path, reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin import widgets as wagtailadmin_widgets
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Locale, TranslatableMixin
|
||||
from wagtail.snippets.widgets import SnippetListingButton
|
||||
|
||||
from .views import SubmitPageTranslationView, SubmitSnippetTranslationView
|
||||
|
||||
|
||||
@hooks.register("register_admin_urls")
|
||||
def register_admin_urls():
|
||||
urls = [
|
||||
path(
|
||||
"submit/page/<int:page_id>/",
|
||||
SubmitPageTranslationView.as_view(),
|
||||
name="submit_page_translation",
|
||||
),
|
||||
path(
|
||||
"submit/snippet/<slug:app_label>/<slug:model_name>/<str:pk>/",
|
||||
SubmitSnippetTranslationView.as_view(),
|
||||
name="submit_snippet_translation",
|
||||
),
|
||||
]
|
||||
|
||||
return [
|
||||
path(
|
||||
"translation/",
|
||||
include(
|
||||
(urls, "simple_translation"),
|
||||
namespace="simple_translation",
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@hooks.register("register_permissions")
|
||||
def register_submit_translation_permission():
|
||||
return Permission.objects.filter(content_type__app_label="simple_translation", codename="submit_translation")
|
||||
|
||||
|
||||
@hooks.register("register_page_listing_more_buttons")
|
||||
def page_listing_more_buttons(page, page_perms, is_parent=False, next_url=None):
|
||||
if page_perms.user.has_perm("simple_translation.submit_translation") and not page.is_root():
|
||||
# If there's at least one locale that we haven't translated into yet, show "Translate this page" button
|
||||
has_locale_to_translate_to = Locale.objects.exclude(
|
||||
id__in=page.get_translations(inclusive=True).values_list("locale_id", flat=True)
|
||||
).exists()
|
||||
|
||||
if has_locale_to_translate_to:
|
||||
url = reverse("simple_translation:submit_page_translation", args=[page.id])
|
||||
yield wagtailadmin_widgets.Button(_("Translate"), url, priority=60)
|
||||
|
||||
|
||||
@hooks.register("register_snippet_listing_buttons")
|
||||
def register_snippet_listing_buttons(snippet, user, next_url=None):
|
||||
model = type(snippet)
|
||||
|
||||
if issubclass(model, TranslatableMixin) and user.has_perm("simple_translation.submit_translation"):
|
||||
# If there's at least one locale that we haven't translated into yet, show "Translate" button
|
||||
has_locale_to_translate_to = Locale.objects.exclude(
|
||||
id__in=snippet.get_translations(inclusive=True).values_list("locale_id", flat=True)
|
||||
).exists()
|
||||
|
||||
if has_locale_to_translate_to:
|
||||
url = reverse(
|
||||
"simple_translation:submit_snippet_translation",
|
||||
args=[model._meta.app_label, model._meta.model_name, quote(snippet.pk)],
|
||||
)
|
||||
yield SnippetListingButton(
|
||||
_("Translate"),
|
||||
url,
|
||||
attrs={"aria-label": _("Translate '%(title)s'") % {"title": str(snippet)}},
|
||||
priority=100,
|
||||
)
|
@ -121,6 +121,7 @@ INSTALLED_APPS = [
|
||||
'wagtail.tests.search',
|
||||
'wagtail.tests.modeladmintest',
|
||||
'wagtail.tests.i18n',
|
||||
'wagtail.contrib.simple_translation',
|
||||
'wagtail.contrib.styleguide',
|
||||
'wagtail.contrib.routable_page',
|
||||
'wagtail.contrib.frontend_cache',
|
||||
|
Loading…
Reference in New Issue
Block a user