diff --git a/docs/how-to/modeladmin.md b/docs/how-to/modeladmin.md new file mode 100644 index 00000000..f7f0f48e --- /dev/null +++ b/docs/how-to/modeladmin.md @@ -0,0 +1,34 @@ +# Translatable ModelAdmin + +Wagtail Localize supports translation of custom Wagtail's [ModelAdmin](https://docs.wagtail.org/en/stable/reference/contrib/modeladmin/index.html) registered models. + +## Installation + +Add `wagtail_localize.modeladmin` to your `INSTALLED_APPS`: + +```python +INSTALLED_APPS = [ + # ... + "wagtail_localize.modeladmin", +] +``` + +## How to use + +When registering your custom models you can use the supplied `TranslatableModelAdmin` in place of Wagtail's `ModelAdmin` class. + +```python +from wagtail.contrib.modeladmin.options import modeladmin_register +from wagtail_localize.modeladmin.options import TranslatableModelAdmin + +from .models import MyTranslatableModel + + +class MyTranslatableModelAdmin(TranslatableModelAdmin): + model = MyTranslatableModel + + +modeladmin_register(MyTranslatableModelAdmin) +``` + +That's it! You can translate your custom ModelAdmin models in the admin dashboard the same way you would Wagtail snippets. diff --git a/mkdocs.yml b/mkdocs.yml index d039f578..960a5c54 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,6 +49,7 @@ nav: - How To Guides: - Installation guide: how-to/installation.md - Configuring translatable fields: how-to/field-configuration.md + - Translatable ModelAdmin: how-to/modeladmin.md - Integrations: - Pontoon: how-to/integrations/pontoon.md - Machine Translation: how-to/integrations/machine-translation.md diff --git a/wagtail_localize/modeladmin/__init__.py b/wagtail_localize/modeladmin/__init__.py new file mode 100644 index 00000000..77d021fe --- /dev/null +++ b/wagtail_localize/modeladmin/__init__.py @@ -0,0 +1,10 @@ +from django import VERSION as DJANGO_VERSION + + +if DJANGO_VERSION >= (3, 2): + # The declaration is only needed for older Django versions + pass +else: + default_app_config = ( + "wagtail_localize.modeladmin.apps.WagtailLocalizeModelAdminAppConfig" + ) diff --git a/wagtail_localize/modeladmin/apps.py b/wagtail_localize/modeladmin/apps.py new file mode 100644 index 00000000..cf8e18b9 --- /dev/null +++ b/wagtail_localize/modeladmin/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class WagtailLocalizeModelAdminAppConfig(AppConfig): + name = "wagtail_localize.modeladmin" + label = "wagtail_localize_modeladmin" + verbose_name = "Wagtail localize modeladmin" diff --git a/wagtail_localize/modeladmin/helpers.py b/wagtail_localize/modeladmin/helpers.py new file mode 100644 index 00000000..ad3665ee --- /dev/null +++ b/wagtail_localize/modeladmin/helpers.py @@ -0,0 +1,80 @@ +from urllib.parse import urlencode + +from django.contrib.admin.utils import quote +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from wagtail.contrib.modeladmin.helpers import ButtonHelper, PageButtonHelper +from wagtail.contrib.modeladmin.views import InspectView +from wagtail.core.models import Locale, Page, TranslatableMixin + +from wagtail_localize.models import TranslationSource + + +class TranslatableButtonHelper(ButtonHelper): + def get_buttons_for_obj(self, obj, **kwargs): + btns = super().get_buttons_for_obj(obj, **kwargs) + user = self.request.user + next_url = self.request.get_full_path() + if isinstance(self.view, InspectView): + classname = "button button-secondary" + else: + classname = "button button-secondary button-small" + btns += list(get_translation_buttons(obj, user, next_url, classname)) + return btns + + +class TranslatablePageButtonHelper(TranslatableButtonHelper, PageButtonHelper): + pass + + +def get_translation_buttons(obj, user, next_url=None, classname=""): + """ + Generate listing buttons for translating/syncing objects in modeladmin. + """ + model = type(obj) + + if issubclass(model, TranslatableMixin) and user.has_perm( + "wagtail_localize.submit_translation" + ): + + # If there's at least one locale that we haven't translated into yet, show "Translate" button + if isinstance(obj, Page): + has_locale_to_translate_to = Locale.objects.exclude( + id__in=obj.get_translations(inclusive=True) + .exclude(alias_of__isnull=False) + .values_list("locale_id", flat=True) + ).exists() + else: + has_locale_to_translate_to = Locale.objects.exclude( + id__in=obj.get_translations(inclusive=True).values_list( + "locale_id", flat=True + ) + ).exists() + + if has_locale_to_translate_to: + url = reverse( + "wagtail_localize_modeladmin:submit_translation", + args=[model._meta.app_label, model._meta.model_name, quote(obj.pk)], + ) + yield { + "url": url, + "label": _("Translate"), + "classname": classname, + "title": _("Translate"), + } + + # If the object is the source for translations, show "Sync translated" button + source = TranslationSource.objects.get_for_instance_or_none(obj) + if source is not None and source.translations.filter(enabled=True).exists(): + url = reverse("wagtail_localize:update_translations", args=[source.id]) + if next_url is not None: + url += "?" + urlencode({"next": next_url}) + + yield { + "url": url, + "label": _("Sync translated %(model_name)s") + % {"model_name": obj._meta.verbose_name_plural}, + "classname": classname, + "title": _("Sync translated %(model_name)s") + % {"model_name": obj._meta.verbose_name_plural}, + } diff --git a/wagtail_localize/modeladmin/options.py b/wagtail_localize/modeladmin/options.py new file mode 100644 index 00000000..63fdf540 --- /dev/null +++ b/wagtail_localize/modeladmin/options.py @@ -0,0 +1,57 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from wagtail.contrib.modeladmin.options import ModelAdmin +from wagtail.core.models import TranslatableMixin + +from .helpers import TranslatableButtonHelper, TranslatablePageButtonHelper +from .views import ( + TranslatableChooseParentView, + TranslatableCreateView, + TranslatableDeleteView, + TranslatableEditView, + TranslatableHistoryView, + TranslatableIndexView, + TranslatableInspectView, +) + + +class TranslatableModelAdmin(ModelAdmin): + index_view_class = TranslatableIndexView + create_view_class = TranslatableCreateView + edit_view_class = TranslatableEditView + inspect_view_class = TranslatableInspectView + delete_view_class = TranslatableDeleteView + history_view_class = TranslatableHistoryView + choose_parent_view_class = TranslatableChooseParentView + + def __init__(self, parent=None): + super().__init__(parent) + + if "wagtail_localize.modeladmin" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured( + 'To use the TranslatableModelAdmin class "wagtail_localize.modeladmin" ' + "must be added to your INSTALLED_APPS setting." + ) + + if not issubclass(self.model, TranslatableMixin): + raise ImproperlyConfigured( + f"Model `{self.model}` used in translatable admin `{self.__class__}` " + f"must subclass the `{TranslatableMixin}` class." + ) + + def get_button_helper_class(self): + if self.button_helper_class: + return self.button_helper_class + if self.is_pagemodel: + return TranslatablePageButtonHelper + return TranslatableButtonHelper + + def get_templates(self, action="index"): + app_label = self.opts.app_label.lower() + model_name = self.opts.model_name.lower() + return [ + "wagtail_localize/modeladmin/%s/%s/translatable_%s.html" + % (app_label, model_name, action), + "wagtail_localize/modeladmin/%s/translatable_%s.html" % (app_label, action), + "wagtail_localize/modeladmin/translatable_%s.html" % (action,), + ] diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/includes/button.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/includes/button.html new file mode 100644 index 00000000..f28691f8 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/includes/button.html @@ -0,0 +1 @@ +<a{% if button.url %} href="{{ button.url }}{% if locale %}?locale={{ locale.language_code }}{% endif %}"{% endif %} class="{{ button.classname }}" title="{{ button.title }}"{% if button.target %} target="{{ button.target }}"{% endif %}{% if button.rel %} rel="{{ button.rel }}"{% endif %}>{{ button.label }}</a> diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/includes/header_with_breadcrumb.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/includes/header_with_breadcrumb.html new file mode 100644 index 00000000..5305ed02 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/includes/header_with_breadcrumb.html @@ -0,0 +1,5 @@ +{% extends "wagtailadmin/shared/header_with_locale_selector.html" %} + +{% block breadcrumb %} + {% include "modeladmin/includes/breadcrumb.html" %} +{% endblock %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_choose_parent.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_choose_parent.html new file mode 100644 index 00000000..99b98786 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_choose_parent.html @@ -0,0 +1 @@ +{% extends "modeladmin/choose_parent.html" %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_create.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_create.html new file mode 100644 index 00000000..4d792ffc --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_create.html @@ -0,0 +1,7 @@ +{% extends "modeladmin/create.html" %} + +{% block header %} + {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 %} +{% endblock %} + +{% block form_action %}{{ view.create_url }}?locale={{ locale.language_code }}{% endblock %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_delete.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_delete.html new file mode 100644 index 00000000..477a8080 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_delete.html @@ -0,0 +1 @@ +{% extends "modeladmin/delete.html" %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_edit.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_edit.html new file mode 100644 index 00000000..e6112ce7 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_edit.html @@ -0,0 +1,34 @@ +{% extends "modeladmin/edit.html" %} +{% load i18n wagtailadmin_tags %} + +{% block header %} + {% if wagtail_version >= "2.15" %} + {% include "modeladmin/includes/header_with_history.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 latest_log_entry=latest_log_entry history_url=history_url %} + {% else %} + {% include "wagtailadmin/shared/header_with_locale_selector.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=1 merged=1 %} + {% endif %} +{% endblock %} + +{% block form_actions %} + <div class="dropdown dropup dropdown-button match-width"> + <button type="submit" class="button action-save button-longrunning" data-clicked-text="{% trans 'Saving…' %}"> + {% icon name="spinner" %}<em>{% trans "Save" %}</em> + </button> + + {% if user_can_delete %} + <div class="dropdown-toggle">{% icon name="arrow-up" %}</div> + <ul> + {% if translation %} + <li> + <form method="POST"> + <input type="hidden" name="localize-restart-translation"> + <button class="button">{% trans "Start Synced translation" %}</button> + </form> + </li> + {% endif %} + <li><a href="{{ view.delete_url }}" class="shortcut">{% trans "Delete" %}</a></li> + </ul> + {% endif %} + + </div> +{% endblock %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_history.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_history.html new file mode 100644 index 00000000..4c756f93 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_history.html @@ -0,0 +1 @@ +{% extends "modeladmin/history.html" %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_index.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_index.html new file mode 100644 index 00000000..02e37c86 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_index.html @@ -0,0 +1,24 @@ +{% extends "modeladmin/index.html" %} +{% load i18n modeladmin_tags wagtailadmin_tags %} + +{% block header_extra %} + <div class="right header-right"> + <div class="col"> + {% include 'wagtailadmin/shared/locale_selector.html' with class='c-dropdown--large' %} + </div> + {% if user_can_create %} + <div class="actionbutton col"> + {% include 'wagtail_localize/modeladmin/includes/button.html' with button=view.button_helper.add_button %} + </div> + {% endif %} + {% if view.list_export %} + <div class="dropdown dropdown-button match-width col"> + <a href="?export=xlsx&{{ request.GET.urlencode }}" class="button bicolor button--icon">{% icon name="download" wrapped=1 %}{% trans 'Download XLSX' %}</a> + <div class="dropdown-toggle">{% icon name="arrow-down" %}</div> + <ul> + <li><a class="button bicolor button--icon" href="?export=csv&{{ request.GET.urlencode }}">{% icon name="download" wrapped=1 %}{% trans 'Download CSV' %}</a></li> + </ul> + </div> + {% endif %} + </div> +{% endblock %} diff --git a/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_inspect.html b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_inspect.html new file mode 100644 index 00000000..c53fa824 --- /dev/null +++ b/wagtail_localize/modeladmin/templates/wagtail_localize/modeladmin/translatable_inspect.html @@ -0,0 +1,5 @@ +{% extends "modeladmin/inspect.html" %} + +{% block header %} + {% include "wagtail_localize/modeladmin/includes/header_with_breadcrumb.html" with title=view.get_page_title subtitle=view.get_page_subtitle icon=view.header_icon tabbed=True %} +{% endblock %} diff --git a/wagtail_localize/modeladmin/tests.py b/wagtail_localize/modeladmin/tests.py new file mode 100644 index 00000000..4cc47d47 --- /dev/null +++ b/wagtail_localize/modeladmin/tests.py @@ -0,0 +1,767 @@ +from urllib.parse import urlencode + +from django.contrib.admin.utils import quote +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group, Permission +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ImproperlyConfigured +from django.test import RequestFactory, TestCase, override_settings +from django.urls import reverse +from wagtail.core.models import Locale, Page +from wagtail.tests.utils import WagtailTestUtils + +from wagtail_localize.modeladmin import helpers +from wagtail_localize.modeladmin.options import ModelAdmin, TranslatableModelAdmin +from wagtail_localize.modeladmin.views import ( + TranslatableIndexView, + TranslatableInspectView, +) +from wagtail_localize.models import Translation, TranslationSource +from wagtail_localize.test.models import NonTranslatableModel, TestModel, TestPage +from wagtail_localize.test.wagtail_hooks import TestModelAdmin, TestPageAdmin +from wagtail_localize.tests.utils import assert_permission_denied + + +def strip_user_perms(): + """ + Removes user permissions so they can still access admin and edit pages but can't submit anything for translation. + """ + editors_group = Group.objects.get(name="Editors") + editors_group.permissions.filter(codename="submit_translation").delete() + + for permission in Permission.objects.filter( + content_type=ContentType.objects.get_for_model(TestModel) + ): + editors_group.permissions.add(permission) + + user = get_user_model().objects.get() + user.is_superuser = False + user.groups.add(editors_group) + user.save() + + +class TestModelAdminViews(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + self.en_locale = Locale.objects.get() + self.fr_locale = Locale.objects.create(language_code="fr") + self.de_locale = Locale.objects.create(language_code="de") + + self.en_modeladmin = TestModel.objects.create(title="Test modeladmin") + self.modeladmin_source, created = TranslationSource.get_or_create_from_instance( + self.en_modeladmin + ) + self.modeladmin_translation = Translation.objects.create( + source=self.modeladmin_source, target_locale=self.fr_locale + ) + self.modeladmin_translation.save_target(publish=True) + self.fr_modeladmin = self.en_modeladmin.get_translation(self.fr_locale) + + def test(self): + with self.assertRaises(ImproperlyConfigured): + TranslatableIndexView( + model_admin=type( + "NonTranslatableModelAdmin", + (ModelAdmin,), + {"model": NonTranslatableModel}, + )() + ) + + def test_index_view(self): + response = self.client.get( + reverse("wagtail_localize_test_testmodel_modeladmin_index") + ) + self.assertContains(response, "Translate") + + def test_create_view(self): + create_url = reverse("wagtail_localize_test_testmodel_modeladmin_create") + response = self.client.get(create_url) + + self.assertContains( + response, + '<a href="/admin/wagtail_localize_test/testmodel/create/?locale=de"', + ) + + # Create a new DE object if and check for locale + response = self.client.post(create_url + "?locale=de", {"title": "New model"}) + self.assertRedirects( + response, + reverse("wagtail_localize_test_testmodel_modeladmin_index") + "?locale=de", + ) + obj = TestModel.objects.last() + self.assertEqual(obj.title, "New model") + self.assertEqual(obj.locale, self.de_locale) + + def test_edit_view(self): + response = self.client.get( + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.en_modeladmin.pk], + ) + ) + self.assertContains( + response, + '<a href="/admin/wagtail_localize_test/testmodel/edit/{}/?locale=fr"'.format( + self.fr_modeladmin.pk + ), + ) + + # Check restart translation button is displayed + self.modeladmin_translation.enabled = False + self.modeladmin_translation.save() + response = self.client.get( + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.fr_modeladmin.pk], + ) + ) + self.assertContains(response, "Start Synced translation") + + # Check restart translation triggers correctly + response = self.client.post( + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.fr_modeladmin.pk], + ), + {"localize-restart-translation": True}, + ) + self.assertRedirects( + response, + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.fr_modeladmin.pk], + ), + ) + self.modeladmin_translation.refresh_from_db() + self.assertTrue(self.modeladmin_translation.enabled) + + # Check existant FR translation renders the "edit_translation" view + response = self.client.get( + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.fr_modeladmin.pk], + ) + ) + self.assertContains( + response, + "Translation of TestModel object ({}) into French".format( + self.en_modeladmin.pk + ), + ) + + def test_inspect_view(self): + response = self.client.get( + reverse( + "wagtail_localize_test_testmodel_modeladmin_inspect", + args=[self.en_modeladmin.pk], + ) + ) + self.assertContains( + response, + '<a href="/admin/wagtail_localize_test/testmodel/inspect/{}/?locale=fr"'.format( + self.fr_modeladmin.pk + ), + ) + self.assertContains(response, "Translate") + self.assertContains(response, "Sync translated test models") + + # Create DE translation and check "Translate" button not showing + Translation.objects.create( + source=self.modeladmin_source, target_locale=self.de_locale + ).save_target(publish=True) + + response = self.client.get( + reverse( + "wagtail_localize_test_testmodel_modeladmin_inspect", + args=[self.fr_modeladmin.pk], + ) + ) + self.assertNotContains(response, "Translate") + self.assertNotContains(response, "Sync translated test models") + + +class TestModelAdminAdmin(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + self.model_admin = TestModelAdmin() + self.page_admin = TestPageAdmin() + + def test(self): + with self.assertRaises(ImproperlyConfigured): + type( + "NonTranslatableModelAdmin", + (TranslatableModelAdmin,), + {"model": NonTranslatableModel}, + )() + + def test_button_helper_getter(self): + self.assertEqual( + self.model_admin.get_button_helper_class(), + helpers.TranslatableButtonHelper, + ) + self.model_admin.button_helper_class = helpers.ButtonHelper + self.assertEqual( + self.model_admin.get_button_helper_class(), + helpers.ButtonHelper, + ) + self.assertEqual( + self.page_admin.get_button_helper_class(), + helpers.TranslatablePageButtonHelper, + ) + + def test_get_templates(self): + def result(action): + return [ + "wagtail_localize/modeladmin/wagtail_localize_test/testmodel/translatable_%s.html" + % action, + "wagtail_localize/modeladmin/wagtail_localize_test/translatable_%s.html" + % action, + "wagtail_localize/modeladmin/translatable_%s.html" % action, + ] + + self.assertEqual(self.model_admin.get_templates("index"), result("index")) + self.assertEqual(self.model_admin.get_templates("create"), result("create")) + self.assertEqual(self.model_admin.get_templates("edit"), result("edit")) + self.assertEqual(self.model_admin.get_templates("inspect"), result("inspect")) + self.assertEqual(self.model_admin.get_templates("delete"), result("delete")) + self.assertEqual( + self.model_admin.get_templates("choose_parent"), result("choose_parent") + ) + + +class TestModelAdminHelpers(TestCase, WagtailTestUtils): + def setUp(self): + self.user = self.login() + self.factory = RequestFactory() + + self.model_admin = TestModelAdmin() + self.page_admin = TestPageAdmin() + + self.en_locale = Locale.objects.get() + self.fr_locale = Locale.objects.create(language_code="fr") + self.de_locale = Locale.objects.create(language_code="de") + + self.en_modeladmin = TestModel.objects.create(title="Test modeladmin") + self.modeladmin_source, created = TranslationSource.get_or_create_from_instance( + self.en_modeladmin + ) + self.modeladmin_translation = Translation.objects.create( + source=self.modeladmin_source, target_locale=self.fr_locale + ) + self.modeladmin_translation.save_target(publish=True) + self.fr_modeladmin = self.en_modeladmin.get_translation(self.fr_locale) + + root_page = Page.objects.get(id=1) + root_page.get_children().delete() + root_page.refresh_from_db() + + self.en_modeladmin_page = root_page.add_child(instance=TestPage(title="Test")) + ( + self.modeladmin_page_source, + created, + ) = TranslationSource.get_or_create_from_instance(self.en_modeladmin_page) + self.modeladmin_page_translation = Translation.objects.create( + source=self.modeladmin_page_source, target_locale=self.fr_locale + ) + self.modeladmin_page_translation.save_target(publish=True) + + def test_button_helper(self): + request = self.factory.get( + reverse("wagtail_localize_test_testmodel_modeladmin_index") + ) + request.user = self.user + + view = TranslatableIndexView(self.model_admin) + helper = self.model_admin.get_button_helper_class()(view, request) + buttons = helper.get_buttons_for_obj(self.en_modeladmin) + self.assertEqual( + buttons[-1]["classname"], + "button button-secondary button-small", + ) + + # Check inspect view sets different button classes + view = TranslatableInspectView(self.model_admin, str(self.en_modeladmin.pk)) + helper = self.model_admin.get_button_helper_class()(view, request) + buttons = helper.get_buttons_for_obj(self.en_modeladmin) + self.assertEqual(buttons[-1]["classname"], "button button-secondary") + + def test_get_translation_buttons(self): + btns = helpers.get_translation_buttons( + self.en_modeladmin, self.user, "/next/url/", "button-class" + ) + self.assertEqual( + next(btns), + { + "url": reverse( + "wagtail_localize_modeladmin:submit_translation", + args=[ + self.en_modeladmin._meta.app_label, + self.en_modeladmin._meta.model_name, + quote(self.en_modeladmin.pk), + ], + ), + "label": "Translate", + "classname": "button-class", + "title": "Translate", + }, + ) + self.assertEqual( + next(btns), + { + "url": reverse( + "wagtail_localize:update_translations", + args=[self.modeladmin_source.pk], + ) + + "?" + + urlencode({"next": "/next/url/"}), + "label": "Sync translated test models", + "classname": "button-class", + "title": "Sync translated test models", + }, + ) + + with self.assertRaises(StopIteration): + next(btns) + + def test_get_translation_buttons_for_page(self): + btns = helpers.get_translation_buttons(self.en_modeladmin_page, self.user) + self.assertEqual( + next(btns), + { + "url": reverse( + "wagtail_localize_modeladmin:submit_translation", + args=[ + self.en_modeladmin_page._meta.app_label, + self.en_modeladmin_page._meta.model_name, + quote(self.en_modeladmin_page.pk), + ], + ), + "label": "Translate", + "classname": "", + "title": "Translate", + }, + ) + self.assertEqual( + next(btns), + { + "url": reverse( + "wagtail_localize:update_translations", + args=[self.modeladmin_page_source.pk], + ), + "label": "Sync translated test pages", + "classname": "", + "title": "Sync translated test pages", + }, + ) + + with self.assertRaises(StopIteration): + next(btns) + + def test_get_translation_buttons_no_locale_to_translate_to(self): + Translation.objects.create( + source=self.modeladmin_source, target_locale=self.de_locale + ).save_target(publish=True) + + btns = helpers.get_translation_buttons(self.en_modeladmin, self.user) + self.assertEqual( + next(btns), + { + "url": reverse( + "wagtail_localize:update_translations", + args=[self.modeladmin_source.pk], + ), + "label": "Sync translated test models", + "classname": "", + "title": "Sync translated test models", + }, + ) + + with self.assertRaises(StopIteration): + next(btns) + + def test_get_translation_buttons_no_user_perms(self): + strip_user_perms() + + self.user.refresh_from_db() + btns = helpers.get_translation_buttons(self.en_modeladmin, self.user) + + with self.assertRaises(StopIteration): + next(btns) + + +@override_settings( + LANGUAGES=[ + ("en", "English"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ], + WAGTAIL_CONTENT_LANGUAGES=[ + ("en", "English"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ], +) +class TestTranslateModelAdminListingButton(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + self.en_locale = Locale.objects.get() + self.fr_locale = Locale.objects.create(language_code="fr") + self.de_locale = Locale.objects.create(language_code="de") + + self.en_modeladmin = TestModel.objects.create( + title="Test modeladmin", test_textfield="Test modeladmin" + ) + self.fr_modeladmin = self.en_modeladmin.copy_for_translation(self.fr_locale) + self.fr_modeladmin.save() + + self.not_translatable_modeladmin = NonTranslatableModel.objects.create() + + def test(self): + response = self.client.get( + reverse("wagtail_localize_test_testmodel_modeladmin_index") + ) + self.assertContains( + response, + ( + f'href="/admin/localize/modeladmin/submit/wagtail_localize_test/testmodel/{self.en_modeladmin.id}/" ' + f'class="button button-secondary button-small" title="Translate">Translate</a>' + ), + ) + + def test_hides_if_modeladmin_already_translated(self): + de_modeladmin = self.en_modeladmin.copy_for_translation(self.de_locale) + de_modeladmin.save() + + response = self.client.get( + reverse("wagtail_localize_test_testmodel_modeladmin_index") + ) + + self.assertNotContains(response, "Translate") + + def test_hides_if_modeladmin_isnt_translatable(self): + de_modeladmin = self.en_modeladmin.copy_for_translation(self.de_locale) + de_modeladmin.save() + + response = self.client.get( + reverse("wagtail_localize_test_testmodel_modeladmin_index") + ) + + self.assertNotContains(response, "Translate") + + def test_hides_if_user_doesnt_have_permission(self): + strip_user_perms() + + response = self.client.get( + reverse("wagtail_localize_test_testmodel_modeladmin_index") + ) + + self.assertNotContains(response, "Translate") + + +@override_settings( + LANGUAGES=[ + ("en", "English"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ], + WAGTAIL_CONTENT_LANGUAGES=[ + ("en", "English"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ], +) +class TestSubmitModelAdminTranslation(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + self.en_locale = Locale.objects.get() + self.fr_locale = Locale.objects.create(language_code="fr") + self.de_locale = Locale.objects.create(language_code="de") + + self.en_modeladmin = TestModel.objects.create( + title="Test modeladmin", test_textfield="Test modeladmin" + ) + + self.not_translatable_modeladmin = NonTranslatableModel.objects.create() + + def test_get_submit_modeladmin_translation(self): + response = self.client.get( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ) + ) + + self.assertEqual(response.status_code, 200) + + self.assertListEqual( + list(response.context["form"]["locales"].field.queryset), + [self.de_locale, self.fr_locale], + ) + + # More than one locale so show "Select all" + self.assertFalse(response.context["form"]["select_all"].field.widget.is_hidden) + + # ModelAdmin can't have children so hide include_subtree + self.assertTrue( + response.context["form"]["include_subtree"].field.widget.is_hidden + ) + + def test_get_submit_modeladmin_translation_when_not_modeladmin(self): + response = self.client.get( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtailcore", "page", 1], + ), + # Need to follow as Django will initiall redirect to /en/admin/ + follow=True, + ) + + self.assertEqual(response.status_code, 404) + + def test_get_submit_modeladmin_translation_when_invalid_model(self): + response = self.client.get( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtailcore", "foo", 1], + ), + # Need to follow as Django will initiall redirect to /en/admin/ + follow=True, + ) + + self.assertEqual(response.status_code, 404) + + def test_get_submit_modeladmin_translation_when_not_translatable(self): + response = self.client.get( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=[ + "wagtail_localize_test", + "nontranslatablemodel", + self.not_translatable_modeladmin.id, + ], + ), + # Need to follow as Django will initiall redirect to /en/admin/ + follow=True, + ) + + self.assertEqual(response.status_code, 404) + + def test_get_submit_modeladmin_translation_without_permissions(self): + strip_user_perms() + + response = self.client.get( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ) + ) + + assert_permission_denied(self, response) + + def test_get_submit_modeladmin_translation_when_already_translated(self): + # Locales that have been translated into shouldn't be included + translation = self.en_modeladmin.copy_for_translation(self.de_locale) + translation.save() + + response = self.client.get( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ) + ) + + self.assertEqual(response.status_code, 200) + + self.assertListEqual( + list(response.context["form"]["locales"].field.queryset), [self.fr_locale] + ) + + # Since there is only one locale, the "Select All" checkbox should be hidden + self.assertTrue(response.context["form"]["select_all"].field.widget.is_hidden) + + def test_post_submit_modeladmin_translation(self): + response = self.client.post( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ), + {"locales": [self.fr_locale.id]}, + ) + + translation = Translation.objects.get() + self.assertEqual(translation.source.locale, self.en_locale) + self.assertEqual(translation.target_locale, self.fr_locale) + self.assertTrue(translation.created_at) + + # The translated modeladmin should've been created + translated_modeladmin = self.en_modeladmin.get_translation(self.fr_locale) + self.assertEqual(translated_modeladmin.test_textfield, "Test modeladmin") + + self.assertRedirects( + response, + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[translated_modeladmin.id], + ), + ) + + def test_post_submit_modeladmin_translation_into_multiple_locales(self): + response = self.client.post( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ), + {"locales": [self.fr_locale.id, self.de_locale.id]}, + ) + + self.assertRedirects( + response, + reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.en_modeladmin.id], + ), + ) + + # Check French translation + fr_translation = Translation.objects.get(target_locale=self.fr_locale) + self.assertEqual(fr_translation.source.locale, self.en_locale) + self.assertTrue(fr_translation.created_at) + + # Check German translation + de_translation = Translation.objects.get(target_locale=self.de_locale) + self.assertEqual(de_translation.source.locale, self.en_locale) + self.assertTrue(de_translation.created_at) + + def test_post_submit_modeladmin_translation_with_missing_locale(self): + response = self.client.post( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ), + {"locales": []}, + ) + + self.assertEqual(response.status_code, 200) + self.assertFalse(Translation.objects.exists()) + self.assertFormError(response, "form", "locales", ["This field is required."]) + + def test_post_submit_modeladmin_translation_without_permissions(self): + strip_user_perms() + + response = self.client.post( + reverse( + "wagtail_localize_modeladmin:submit_translation", + args=["wagtail_localize_test", "testmodel", self.en_modeladmin.id], + ), + {"locales": [self.fr_locale.id]}, + ) + + assert_permission_denied(self, response) + + +@override_settings( + LANGUAGES=[ + ("en", "English"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ], + WAGTAIL_CONTENT_LANGUAGES=[ + ("en", "English"), + ("fr", "French"), + ("de", "German"), + ("es", "Spanish"), + ], +) +class TestUpdateModelAdminTranslations(TestCase, WagtailTestUtils): + def setUp(self): + self.login() + + self.en_locale = Locale.objects.get() + self.fr_locale = Locale.objects.create(language_code="fr") + self.de_locale = Locale.objects.create(language_code="de") + + # Create modeladmin object and FR translation + self.en_modeladmin = TestModel.objects.create( + title="Test modeladmin", test_textfield="Test modeladmin" + ) + self.modeladmin_source, created = TranslationSource.get_or_create_from_instance( + self.en_modeladmin + ) + self.modeladmin_translation = Translation.objects.create( + source=self.modeladmin_source, target_locale=self.fr_locale + ) + self.modeladmin_translation.save_target(publish=True) + self.fr_modeladmin = self.en_modeladmin.get_translation(self.fr_locale) + + def test_get_update_modeladmin_translation(self): + response = self.client.get( + reverse( + "wagtail_localize:update_translations", + args=[self.modeladmin_source.id], + ) + ) + + self.assertEqual(response.status_code, 200) + + self.assertEqual( + response.context["translations"], + [ + { + "title": str(self.fr_modeladmin), + "locale": self.fr_locale, + "edit_url": reverse( + "wagtail_localize_test_testmodel_modeladmin_edit", + args=[self.fr_modeladmin.id], + ), + } + ], + ) + + def test_post_update_modeladmin_translation(self): + self.en_modeladmin.test_textfield = "Edited modeladmin" + self.en_modeladmin.save() + + response = self.client.post( + reverse( + "wagtail_localize:update_translations", + args=[self.modeladmin_source.id], + ) + ) + + self.assertRedirects( + response, + reverse("wagtail_localize_test_testmodel_modeladmin_index"), + ) + + # The FR version shouldn't be updated yet + self.fr_modeladmin.refresh_from_db() + self.assertEqual(self.fr_modeladmin.test_textfield, "Test modeladmin") + + def test_post_update_modeladmin_translation_with_publish_translations(self): + self.en_modeladmin.test_textfield = "Edited modeladmin" + self.en_modeladmin.save() + + response = self.client.post( + reverse( + "wagtail_localize:update_translations", + args=[self.modeladmin_source.id], + ), + {"publish_translations": "on"}, + ) + + self.assertRedirects( + response, + reverse("wagtail_localize_test_testmodel_modeladmin_index"), + ) + + # The FR version should be updated + self.fr_modeladmin.refresh_from_db() + self.assertEqual(self.fr_modeladmin.test_textfield, "Edited modeladmin") diff --git a/wagtail_localize/modeladmin/views.py b/wagtail_localize/modeladmin/views.py new file mode 100644 index 00000000..3a4ea201 --- /dev/null +++ b/wagtail_localize/modeladmin/views.py @@ -0,0 +1,215 @@ +from django.apps import apps +from django.conf import settings +from django.contrib.admin.utils import unquote +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ImproperlyConfigured +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.urls import NoReverseMatch, reverse +from django.utils.decorators import method_decorator +from django.utils.translation import gettext as _ +from wagtail import VERSION as WAGTAIL_VERSION +from wagtail.contrib.modeladmin.views import ( + ChooseParentView, + CreateView, + DeleteView, + EditView, + IndexView, + InspectView, +) +from wagtail.core.models import Locale, TranslatableMixin +from wagtail.utils.version import get_main_version + +from wagtail_localize.models import Translation +from wagtail_localize.views import edit_translation +from wagtail_localize.views.submit_translations import SubmitTranslationView + + +if WAGTAIL_VERSION >= (2, 15): + from wagtail.contrib.modeladmin.views import HistoryView +else: + HistoryView = object + + +class TranslatableViewMixin: + def __init__(self, *args, **kwargs): + self.locale = None + super().__init__(*args, **kwargs) + + if "wagtail_localize.modeladmin" not in settings.INSTALLED_APPS: + raise ImproperlyConfigured( + 'To use the TranslatableView class "wagtail_localize.modeladmin" ' + "must be added to your INSTALLED_APPS setting." + ) + + if not issubclass(self.model, TranslatableMixin): + raise ImproperlyConfigured( + f"Model `{self.model}` used in translatable view `{self.__class__}` " + f"must subclass the `{TranslatableMixin}` class." + ) + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + if getattr(self, "instance", None): + self.locale = self.instance.locale + if "locale" in request.GET: + self.locale = get_object_or_404(Locale, language_code=request.GET["locale"]) + if not self.locale: + self.locale = Locale.get_active() + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["locale"] = self.locale + context["translations"] = [] + context["wagtail_version"] = get_main_version() + return context + + +class TranslatableIndexView(TranslatableViewMixin, IndexView): + def get_filters(self, request): + filters = super().get_filters(request) + # Update the 'lookup_params' part of the filters tuple to filter objects + # using the currently active locale + filters[2]["locale"] = self.locale.id + return filters + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["translations"] = [ + { + "locale": locale, + "url": self.index_url + "?locale=" + locale.language_code, + } + for locale in Locale.objects.exclude(id=self.locale.id) + ] + return context + + +class TranslatableCreateView(TranslatableViewMixin, CreateView): + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + kwargs["instance"].locale = self.locale + return kwargs + + def get_success_url(self): + return self.index_url + "?locale=" + self.locale.language_code + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["translations"] = [ + { + "locale": locale, + "url": self.create_url + "?locale=" + locale.language_code, + } + for locale in Locale.objects.exclude(id=self.locale.id) + ] + return context + + +class TranslatableEditView(TranslatableViewMixin, EditView): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Attempt to find the translation for the edited instance or set to None + self.translation = Translation.objects.filter( + source__object_id=self.instance.translation_key, + target_locale_id=self.instance.locale_id, + ).first() + + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + # Check if the user has clicked the "Start Synced translation" menu item + if ( + request.method == "POST" + and "localize-restart-translation" in request.POST + and self.translation + and not self.translation.enabled + ): + return edit_translation.restart_translation( + request, self.translation, self.instance + ) + + # Overrides the edit view if the object is translatable and the target of a translation + if self.translation and self.translation.enabled: + return edit_translation.edit_translation( + request, self.translation, self.instance + ) + + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["translation"] = self.translation + context["translations"] = [ + { + "locale": translation.locale, + "url": self.url_helper.get_action_url("edit", translation.pk) + + "?locale=" + + translation.locale.language_code, + } + for translation in self.instance.get_translations().select_related("locale") + ] + return context + + +class TranslatableInspectView(TranslatableViewMixin, InspectView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["translations"] = [ + { + "locale": translation.locale, + "url": self.url_helper.get_action_url("inspect", translation.pk) + + "?locale=" + + translation.locale.language_code, + } + for translation in self.instance.get_translations().select_related("locale") + ] + return context + + +class TranslatableDeleteView(TranslatableViewMixin, DeleteView): + pass + + +class TranslatableHistoryView(TranslatableViewMixin, HistoryView): + pass + + +class TranslatableChooseParentView(TranslatableViewMixin, ChooseParentView): + pass + + +class SubmitModelAdminTranslationView(SubmitTranslationView): + def get_title(self): + return _("Translate {model_name}").format( + model_name=self.object._meta.verbose_name + ) + + def get_object(self): + try: + model = apps.get_model(self.kwargs["app_label"], self.kwargs["model_name"]) + except LookupError: + raise Http404 + if not issubclass(model, TranslatableMixin): + raise Http404 + return get_object_or_404(model, pk=unquote(self.kwargs["pk"])) + + def get_default_success_url(self, translated_object=None): + pk = translated_object.pk if translated_object else self.kwargs["pk"] + try: + return reverse( + "{app_label}_{model_name}_modeladmin_edit".format(**self.kwargs), + args=[pk], + ) + except NoReverseMatch: + raise Http404 + + def get_success_message(self, locales): + return _( + "The {model_name} '{object}' was successfully submitted for translation into {locales}" + ).format( + model_name=self.object._meta.verbose_name, + object=str(self.object), + locales=locales, + ) diff --git a/wagtail_localize/modeladmin/wagtail_hooks.py b/wagtail_localize/modeladmin/wagtail_hooks.py new file mode 100644 index 00000000..cc1bbe05 --- /dev/null +++ b/wagtail_localize/modeladmin/wagtail_hooks.py @@ -0,0 +1,24 @@ +from django.urls import include, path +from wagtail.core import hooks + +from .views import SubmitModelAdminTranslationView + + +@hooks.register("register_admin_urls") +def register_admin_urls(): + urls = [ + path( + "submit/<slug:app_label>/<slug:model_name>/<str:pk>/", + SubmitModelAdminTranslationView.as_view(), + name="submit_translation", + ), + ] + return [ + path( + "localize/modeladmin/", + include( + (urls, "wagtail_localize_modeladmin"), + namespace="wagtail_localize_modeladmin", + ), + ) + ] diff --git a/wagtail_localize/models.py b/wagtail_localize/models.py index 1481da98..09d6b3f6 100644 --- a/wagtail_localize/models.py +++ b/wagtail_localize/models.py @@ -47,6 +47,7 @@ get_translatable_models, ) from wagtail.core.utils import find_available_slug +from wagtail.snippets.models import get_snippet_models from .compat import DATE_FORMAT from .fields import copy_synchronised_fields @@ -116,7 +117,7 @@ def get_edit_url(instance): if isinstance(instance, Page): return reverse("wagtailadmin_pages:edit", args=[instance.id]) - else: + elif instance._meta.model in get_snippet_models(): return reverse( "wagtailsnippets:edit", args=[ @@ -126,6 +127,15 @@ def get_edit_url(instance): ], ) + elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS: + return reverse( + "{app_label}_{model_name}_modeladmin_edit".format( + app_label=instance._meta.app_label, + model_name=instance._meta.model_name, + ), + args=[quote(instance.pk)], + ) + def get_schema_version(app_label): """ diff --git a/wagtail_localize/test/migrations/0022_nontranslatablemodel.py b/wagtail_localize/test/migrations/0022_nontranslatablemodel.py new file mode 100644 index 00000000..73dd1e97 --- /dev/null +++ b/wagtail_localize/test/migrations/0022_nontranslatablemodel.py @@ -0,0 +1,31 @@ +# Generated by Django 4.0.3 on 2022-03-31 11:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "wagtail_localize_test", + "0021_testuuidmodel_testuuidsnippet", + ), + ] + + operations = [ + migrations.CreateModel( + name="NonTranslatableModel", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(blank=True, max_length=255)), + ], + ), + ] diff --git a/wagtail_localize/test/models.py b/wagtail_localize/test/models.py index 0fb30e78..dc5aa131 100644 --- a/wagtail_localize/test/models.py +++ b/wagtail_localize/test/models.py @@ -336,6 +336,10 @@ class TestModel(TranslatableMixin): ] +class NonTranslatableModel(models.Model): + title = models.CharField(max_length=255, blank=True) + + class InheritedTestModel(TestModel): class Meta: unique_together = None diff --git a/wagtail_localize/test/settings.py b/wagtail_localize/test/settings.py index 909dae30..0fd2e16a 100644 --- a/wagtail_localize/test/settings.py +++ b/wagtail_localize/test/settings.py @@ -38,6 +38,7 @@ INSTALLED_APPS = [ "wagtail_localize", "wagtail_localize.locales", + "wagtail_localize.modeladmin", "wagtail_localize.test", "wagtail.contrib.search_promotions", "wagtail.contrib.forms", diff --git a/wagtail_localize/test/wagtail_hooks.py b/wagtail_localize/test/wagtail_hooks.py new file mode 100644 index 00000000..d00654b1 --- /dev/null +++ b/wagtail_localize/test/wagtail_hooks.py @@ -0,0 +1,28 @@ +from wagtail.contrib.modeladmin.options import ( + ModelAdmin, + ModelAdminGroup, + modeladmin_register, +) + +from wagtail_localize.modeladmin.options import TranslatableModelAdmin + +from .models import NonTranslatableModel, TestModel, TestPage + + +class TestPageAdmin(TranslatableModelAdmin): + model = TestPage + + +class TestModelAdmin(TranslatableModelAdmin): + model = TestModel + inspect_view_enabled = True + + +class NonTranslatableModelAdmin(ModelAdmin): + model = NonTranslatableModel + + +@modeladmin_register +class ModelAdminAdmin(ModelAdminGroup): + items = (TestPageAdmin, TestModelAdmin, NonTranslatableModelAdmin) + menu_label = "Model Admin" diff --git a/wagtail_localize/views/edit_translation.py b/wagtail_localize/views/edit_translation.py index 7b148b17..0dbc90bd 100644 --- a/wagtail_localize/views/edit_translation.py +++ b/wagtail_localize/views/edit_translation.py @@ -5,6 +5,7 @@ import polib +from django.conf import settings from django.contrib.admin.utils import quote from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied, ValidationError @@ -728,6 +729,55 @@ def edit_translation(request, translation, instance): } ) + def get_edit_url(instance): + if isinstance(instance, Page): + return reverse("wagtailadmin_pages:edit", args=[instance.id]) + + elif instance._meta.model in get_snippet_models(): + return reverse( + "wagtailsnippets:edit", + args=[ + instance._meta.app_label, + instance._meta.model_name, + quote(instance.pk), + ], + ) + + elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS: + return reverse( + "{app_label}_{model_name}_modeladmin_edit".format( + app_label=instance._meta.app_label, + model_name=instance._meta.model_name, + ), + args=[quote(instance.pk)], + ) + + def get_submit_translation_url(instance): + if isinstance(instance, Page): + return reverse( + "wagtail_localize:submit_page_translation", args=[instance.id] + ) + + elif instance._meta.model in get_snippet_models(): + return reverse( + "wagtail_localize:submit_snippet_translation", + args=[ + instance._meta.app_label, + instance._meta.model_name, + quote(instance.id), + ], + ) + + elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS: + return reverse( + "wagtail_localize_modeladmin:submit_translation", + args=[ + instance._meta.app_label, + instance._meta.model_name, + quote(instance.id), + ], + ) + def get_source_object_info(segment): instance = segment.get_source_instance() @@ -736,32 +786,15 @@ def get_source_object_info(segment): "title": str(instance), "isLive": instance.live, "liveUrl": instance.full_url, - "editUrl": reverse("wagtailadmin_pages:edit", args=[instance.id]), - "createTranslationRequestUrl": reverse( - "wagtail_localize:submit_page_translation", args=[instance.id] - ), + "editUrl": get_edit_url(instance), + "createTranslationRequestUrl": get_submit_translation_url(instance), } - else: return { "title": str(instance), "isLive": True, - "editUrl": reverse( - "wagtailsnippets:edit", - args=[ - instance._meta.app_label, - instance._meta.model_name, - quote(instance.id), - ], - ), - "createTranslationRequestUrl": reverse( - "wagtail_localize:submit_snippet_translation", - args=[ - instance._meta.app_label, - instance._meta.model_name, - quote(instance.id), - ], - ), + "editUrl": get_edit_url(instance), + "createTranslationRequestUrl": get_submit_translation_url(instance), } def get_dest_object_info(segment): @@ -774,21 +807,13 @@ def get_dest_object_info(segment): "title": str(instance), "isLive": instance.live, "liveUrl": instance.full_url, - "editUrl": reverse("wagtailadmin_pages:edit", args=[instance.id]), + "editUrl": get_edit_url(instance), } - else: return { "title": str(instance), "isLive": True, - "editUrl": reverse( - "wagtailsnippets:edit", - args=[ - instance._meta.app_label, - instance._meta.model_name, - quote(instance.id), - ], - ), + "editUrl": get_edit_url(instance), } def get_translation_progress(segment, locale): @@ -911,18 +936,7 @@ def get_translation_progress(segment, locale): "code": translated_instance.locale.language_code, "displayName": translated_instance.locale.get_display_name(), }, - "editUrl": reverse( - "wagtailadmin_pages:edit", args=[translated_instance.id] - ) - if isinstance(translated_instance, Page) - else reverse( - "wagtailsnippets:edit", - args=[ - translated_instance._meta.app_label, - translated_instance._meta.model_name, - quote(translated_instance.id), - ], - ), + "editUrl": get_edit_url(translated_instance), } for translated_instance in instance.get_translations().select_related( "locale" @@ -1084,13 +1098,21 @@ def restart_translation(request, translation, instance): if isinstance(instance, Page): return redirect("wagtailadmin_pages:edit", instance.id) - else: + elif instance._meta.model in get_snippet_models(): return redirect( "wagtailsnippets:edit", instance._meta.app_label, instance._meta.model_name, quote(instance.pk), ) + elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS: + return redirect( + "{app_label}_{model_name}_modeladmin_edit".format( + app_label=instance._meta.app_label, + model_name=instance._meta.model_name, + ), + instance_pk=quote(instance.pk), + ) @api_view(["PUT", "DELETE"]) diff --git a/wagtail_localize/views/update_translations.py b/wagtail_localize/views/update_translations.py index d21688bd..02128585 100644 --- a/wagtail_localize/views/update_translations.py +++ b/wagtail_localize/views/update_translations.py @@ -1,4 +1,5 @@ from django import forms +from django.conf import settings from django.contrib import messages from django.contrib.admin.utils import quote from django.core.exceptions import PermissionDenied, ValidationError @@ -11,6 +12,7 @@ from django.views.generic.detail import SingleObjectMixin from wagtail.admin.views.pages.utils import get_valid_next_url_from_request from wagtail.core.models import Page +from wagtail.snippets.models import get_snippet_models from wagtail_localize.models import TranslationSource from wagtail_localize.views.submit_translations import TranslationComponentManager @@ -60,7 +62,8 @@ def get_default_success_url(self): if isinstance(instance, Page): return reverse("wagtailadmin_explore", args=[instance.get_parent().id]) - else: + + elif instance._meta.model in get_snippet_models(): return reverse( "wagtailsnippets:edit", args=[ @@ -70,10 +73,19 @@ def get_default_success_url(self): ], ) + elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS: + return reverse( + "{app_label}_{model_name}_modeladmin_index".format( + app_label=instance._meta.app_label, + model_name=instance._meta.model_name, + ) + ) + def get_edit_url(self, instance): if isinstance(instance, Page): return reverse("wagtailadmin_pages:edit", args=[instance.id]) - else: + + elif instance._meta.model in get_snippet_models(): return reverse( "wagtailsnippets:edit", args=[ @@ -83,6 +95,15 @@ def get_edit_url(self, instance): ], ) + elif "wagtail_localize.modeladmin" in settings.INSTALLED_APPS: + return reverse( + "{app_label}_{model_name}_modeladmin_edit".format( + app_label=instance._meta.app_label, + model_name=instance._meta.model_name, + ), + args=[quote(instance.pk)], + ) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update(