#6222 [RFC 54] Internationalisation API changes

Merged kaedroho
Coverage Reach
admin/views/pages/edit.py admin/views/pages/create.py admin/views/pages/revisions.py admin/views/pages/workflow.py admin/views/pages/preview.py admin/views/pages/search.py admin/views/pages/history.py admin/views/pages/moderation.py admin/views/pages/move.py admin/views/pages/copy.py admin/views/pages/listing.py admin/views/pages/lock.py admin/views/pages/unpublish.py admin/views/pages/delete.py admin/views/pages/ordering.py admin/views/pages/usage.py admin/views/pages/utils.py admin/views/workflows.py admin/views/generic.py admin/views/account.py admin/views/mixins.py admin/views/chooser.py admin/views/reports.py admin/views/collections.py admin/views/home.py admin/views/page_privacy.py admin/views/collection_privacy.py admin/views/userbar.py admin/views/tags.py admin/rich_text/converters/html_to_contentstate.py admin/rich_text/converters/editor_html.py admin/rich_text/converters/html_ruleset.py admin/rich_text/converters/contentstate_models.py admin/rich_text/converters/contentstate.py admin/rich_text/editors/hallo.py admin/rich_text/editors/draftail/__init__.py admin/rich_text/editors/draftail/features.py admin/rich_text/__init__.py admin/compare.py admin/edit_handlers.py admin/forms/collections.py admin/forms/workflows.py admin/forms/pages.py admin/forms/models.py admin/forms/choosers.py admin/forms/view_restrictions.py admin/forms/tags.py admin/forms/auth.py admin/forms/search.py admin/forms/__init__.py admin/widgets/chooser.py admin/widgets/datetime.py admin/widgets/button.py admin/widgets/filtered_select.py admin/widgets/tags.py admin/widgets/workflows.py admin/widgets/button_select.py admin/widgets/auto_height_text.py admin/widgets/__init__.py admin/wagtail_hooks.py admin/templatetags/wagtailadmin_tags.py admin/templatetags/wagtailuserbar.py admin/action_menu.py admin/mail.py admin/api/serializers.py admin/api/views.py admin/api/filters.py admin/api/urls.py admin/filters.py admin/auth.py admin/menu.py admin/userbar.py admin/viewsets/model.py admin/viewsets/__init__.py admin/viewsets/base.py admin/checks.py admin/search.py admin/urls/__init__.py admin/urls/pages.py admin/urls/collections.py admin/urls/workflows.py admin/urls/reports.py admin/urls/password_reset.py admin/site_summary.py admin/messages.py admin/log_action_registry.py admin/staticfiles.py admin/navigation.py admin/models.py admin/signal_handlers.py admin/modal_workflow.py admin/localization.py admin/apps.py admin/jinja2tags.py admin/datetimepicker.py admin/blocks.py admin/signals.py admin/__init__.py contrib/modeladmin/views.py contrib/modeladmin/options.py contrib/modeladmin/helpers/button.py contrib/modeladmin/helpers/permission.py contrib/modeladmin/helpers/url.py contrib/modeladmin/helpers/search.py contrib/modeladmin/helpers/__init__.py contrib/modeladmin/templatetags/modeladmin_tags.py contrib/modeladmin/mixins.py contrib/modeladmin/menus.py contrib/modeladmin/forms.py contrib/modeladmin/apps.py contrib/modeladmin/__init__.py contrib/redirects/views.py contrib/redirects/base_formats.py contrib/redirects/management/commands/import_redirects.py contrib/redirects/models.py contrib/redirects/forms.py contrib/redirects/tmp_storages.py contrib/redirects/middleware.py contrib/redirects/utils.py contrib/redirects/wagtail_hooks.py contrib/redirects/apps.py contrib/redirects/urls.py contrib/redirects/permissions.py contrib/redirects/__init__.py contrib/postgres_search/backend.py contrib/postgres_search/query.py contrib/postgres_search/utils.py contrib/postgres_search/models.py contrib/postgres_search/apps.py contrib/postgres_search/__init__.py contrib/forms/views.py contrib/forms/models.py contrib/forms/forms.py contrib/forms/utils.py contrib/forms/edit_handlers.py contrib/forms/wagtail_hooks.py contrib/forms/apps.py contrib/forms/urls.py contrib/forms/__init__.py contrib/frontend_cache/tests.py contrib/frontend_cache/backends.py contrib/frontend_cache/utils.py contrib/frontend_cache/signal_handlers.py contrib/frontend_cache/apps.py contrib/frontend_cache/__init__.py contrib/search_promotions/tests.py contrib/search_promotions/views.py contrib/search_promotions/forms.py contrib/search_promotions/wagtail_hooks.py contrib/search_promotions/models.py contrib/search_promotions/templatetags/wagtailsearchpromotions_tags.py contrib/search_promotions/apps.py contrib/search_promotions/admin_urls.py contrib/search_promotions/__init__.py contrib/routable_page/tests.py contrib/routable_page/models.py contrib/routable_page/templatetags/wagtailroutablepage_tags.py contrib/routable_page/apps.py contrib/routable_page/__init__.py contrib/settings/views.py contrib/settings/models.py contrib/settings/jinja2tags.py contrib/settings/registry.py contrib/settings/context_processors.py contrib/settings/forms.py contrib/settings/templatetags/wagtailsettings_tags.py contrib/settings/wagtail_hooks.py contrib/settings/apps.py contrib/settings/urls.py contrib/settings/permissions.py contrib/settings/__init__.py contrib/table_block/tests.py contrib/table_block/blocks.py contrib/table_block/templatetags/table_block_tags.py contrib/table_block/apps.py contrib/table_block/__init__.py contrib/sitemaps/tests.py contrib/sitemaps/sitemap_generator.py contrib/sitemaps/views.py contrib/sitemaps/apps.py contrib/sitemaps/__init__.py contrib/styleguide/views.py contrib/styleguide/wagtail_hooks.py contrib/styleguide/tests.py contrib/styleguide/apps.py contrib/styleguide/__init__.py core/models.py core/blocks/field_block.py core/blocks/stream_block.py core/blocks/base.py core/blocks/struct_block.py core/blocks/list_block.py core/blocks/static_block.py core/blocks/__init__.py core/blocks/utils.py core/management/commands/fixtree.py core/management/commands/publish_scheduled_pages.py core/management/commands/create_log_entries_from_revisions.py core/management/commands/replace_text.py core/management/commands/purge_revisions.py core/management/commands/move_pages.py core/management/commands/set_url_paths.py core/permission_policies/collections.py core/permission_policies/base.py core/permission_policies/__init__.py core/query.py core/rich_text/rewriters.py core/rich_text/feature_registry.py core/rich_text/__init__.py core/rich_text/pages.py core/utils.py core/templatetags/wagtailcore_tags.py core/fields.py core/whitelist.py core/treebeard.py core/wagtail_hooks.py core/hooks.py core/jinja2tags.py core/views.py core/signal_handlers.py core/forms.py core/workflows.py core/sites.py core/signals.py core/urls.py core/apps.py core/url_routing.py core/compat.py core/permissions.py core/__init__.py core/exceptions.py images/views/images.py images/views/multiple.py images/views/chooser.py images/views/serve.py images/models.py images/image_operations.py images/rect.py images/templatetags/wagtailimages_tags.py images/wagtail_hooks.py images/fields.py images/rich_text/contentstate.py images/rich_text/editor_html.py images/rich_text/__init__.py images/formats.py images/api/v2/views.py images/api/v2/serializers.py images/api/fields.py images/api/admin/views.py images/api/admin/serializers.py images/forms.py images/checks.py images/utils.py images/blocks.py images/widgets.py images/jinja2tags.py images/signal_handlers.py images/edit_handlers.py images/__init__.py images/apps.py images/shortcuts.py images/admin.py images/admin_urls.py images/permissions.py images/urls.py images/exceptions.py search/backends/elasticsearch2.py search/backends/base.py search/backends/db.py search/backends/elasticsearch7.py search/backends/__init__.py search/backends/elasticsearch5.py search/backends/elasticsearch6.py search/index.py search/management/commands/update_index.py search/management/commands/search_garbage_collect.py search/management/commands/wagtail_update_index.py search/utils.py search/models.py search/query.py search/views/queries.py search/signal_handlers.py search/apps.py search/queryset.py search/wagtail_hooks.py search/urls/admin.py search/forms.py search/__init__.py users/tests.py users/forms.py users/views/users.py users/views/groups.py users/wagtail_hooks.py users/templatetags/wagtailusers_tags.py users/models.py users/utils.py users/apps.py users/widgets.py users/urls/users.py users/__init__.py snippets/tests.py snippets/views/snippets.py snippets/views/chooser.py snippets/wagtail_hooks.py snippets/widgets.py snippets/models.py snippets/blocks.py snippets/permissions.py snippets/templatetags/wagtailsnippets_admin_tags.py snippets/edit_handlers.py snippets/apps.py snippets/urls.py snippets/__init__.py api/v2/views.py api/v2/serializers.py api/v2/filters.py api/v2/utils.py api/v2/router.py api/v2/signal_handlers.py api/v2/pagination.py api/v2/apps.py api/v2/__init__.py api/conf.py api/__init__.py documents/views/documents.py documents/views/chooser.py documents/views/serve.py documents/views/multiple.py documents/models.py documents/wagtail_hooks.py documents/rich_text/editor_html.py documents/rich_text/contentstate.py documents/rich_text/__init__.py documents/api/v2/views.py documents/api/v2/serializers.py documents/api/admin/views.py documents/forms.py documents/widgets.py documents/blocks.py documents/__init__.py documents/apps.py documents/signal_handlers.py documents/admin.py documents/edit_handlers.py documents/admin_urls.py documents/permissions.py documents/urls.py embeds/finders/oembed.py embeds/finders/embedly.py embeds/finders/__init__.py embeds/finders/base.py embeds/oembed_providers.py embeds/rich_text/contentstate.py embeds/rich_text/__init__.py embeds/rich_text/editor_html.py embeds/blocks.py embeds/models.py embeds/views/chooser.py embeds/embeds.py embeds/wagtail_hooks.py embeds/templatetags/wagtailembeds_tags.py embeds/format.py embeds/apps.py embeds/exceptions.py embeds/forms.py embeds/urls.py embeds/__init__.py utils/setup.py utils/sendfile.py utils/sendfile_streaming_backend.py utils/deprecation.py utils/decorators.py utils/version.py utils/widgets.py utils/apps.py utils/loading.py utils/urlpatterns.py utils/utils.py sites/tests.py sites/views.py sites/wagtail_hooks.py sites/forms.py sites/apps.py sites/__init__.py locales/tests.py locales/views.py locales/wagtail_hooks.py locales/forms.py locales/utils.py locales/apps.py locales/__init__.py bin/wagtail.py __init__.py

Flags

Flags have been temporarily removed from this view while the flagging feature is refactored for better performance and user experience.

You can still use flags when viewing individual files. Flag-level thresholds will also remain on pull and merge requests in your repository provider.

More information can be found in our documentation.


@@ -1,5 +1,6 @@
Loading
1 1
from collections import OrderedDict
2 2
3 +
from django.conf import settings
3 4
from rest_framework.authentication import SessionAuthentication
4 5
5 6
from wagtail.api.v2.views import PagesAPIViewSet
@@ -26,6 +27,7 @@
Loading
26 27
        'descendants',
27 28
        'parent',
28 29
        'ancestors',
30 +
        'translations',
29 31
    ]
30 32
31 33
    body_fields = PagesAPIViewSet.body_fields + [
@@ -47,6 +49,16 @@
Loading
47 49
        'has_children'
48 50
    ])
49 51
52 +
    @classmethod
53 +
    def get_detail_default_fields(cls, model):
54 +
        detail_default_fields = super().get_detail_default_fields(model)
55 +
56 +
        # When i18n is disabled, remove "translations" from default fields
57 +
        if not getattr(settings, 'WAGTAIL_I18N_ENABLED', False):
58 +
            detail_default_fields.remove('translations')
59 +
60 +
        return detail_default_fields
61 +
50 62
    def get_root_page(self):
51 63
        """
52 64
        Returns the page that is used when the `&child_of=root` filter is used.

@@ -1,5 +1,6 @@
Loading
1 1
from collections import OrderedDict
2 2
3 +
from django.conf import settings
3 4
from django.core.exceptions import FieldDoesNotExist
4 5
from django.http import Http404
5 6
from django.shortcuts import redirect
@@ -13,7 +14,9 @@
Loading
13 14
from wagtail.api import APIField
14 15
from wagtail.core.models import Page, Site
15 16
16 -
from .filters import ChildOfFilter, DescendantOfFilter, FieldsFilter, OrderingFilter, SearchFilter
17 +
from .filters import (
18 +
    ChildOfFilter, DescendantOfFilter, FieldsFilter, LocaleFilter, OrderingFilter, SearchFilter,
19 +
    TranslationOfFilter)
17 20
from .pagination import WagtailPagination
18 21
from .serializers import BaseSerializer, PageSerializer, get_serializer_class
19 22
from .utils import (
@@ -366,12 +369,16 @@
Loading
366 369
        ChildOfFilter,
367 370
        DescendantOfFilter,
368 371
        OrderingFilter,
369 -
        SearchFilter
372 +
        SearchFilter,
373 +
        TranslationOfFilter,
374 +
        LocaleFilter,
370 375
    ]
371 376
    known_query_parameters = BaseAPIViewSet.known_query_parameters.union([
372 377
        'type',
373 378
        'child_of',
374 379
        'descendant_of',
380 +
        'translation_of',
381 +
        'locale',
375 382
    ])
376 383
    body_fields = BaseAPIViewSet.body_fields + [
377 384
        'title',
@@ -384,6 +391,7 @@
Loading
384 391
        'search_description',
385 392
        'first_published_at',
386 393
        'parent',
394 +
        'locale',
387 395
    ]
388 396
    listing_default_fields = BaseAPIViewSet.listing_default_fields + [
389 397
        'title',
@@ -398,6 +406,26 @@
Loading
398 406
    name = 'pages'
399 407
    model = Page
400 408
409 +
    @classmethod
410 +
    def get_detail_default_fields(cls, model):
411 +
        detail_default_fields = super().get_detail_default_fields(model)
412 +
413 +
        # When i18n is disabled, remove "locale" from default fields
414 +
        if not getattr(settings, 'WAGTAIL_I18N_ENABLED', False):
415 +
            detail_default_fields.remove('locale')
416 +
417 +
        return detail_default_fields
418 +
419 +
    @classmethod
420 +
    def get_listing_default_fields(cls, model):
421 +
        listing_default_fields = super().get_listing_default_fields(model)
422 +
423 +
        # When i18n is enabled, add "locale" to default fields
424 +
        if getattr(settings, 'WAGTAIL_I18N_ENABLED', False):
425 +
            listing_default_fields.append('locale')
426 +
427 +
        return listing_default_fields
428 +
401 429
    def get_root_page(self):
402 430
        """
403 431
        Returns the page that is used when the `&child_of=root` filter is used.
@@ -417,7 +445,14 @@
Loading
417 445
        # Filter by site
418 446
        site = Site.find_for_request(self.request)
419 447
        if site:
420 -
            queryset = queryset.descendant_of(site.root_page, inclusive=True)
448 +
            base_queryset = queryset
449 +
            queryset = base_queryset.descendant_of(site.root_page, inclusive=True)
450 +
451 +
            # If internationalisation is enabled, include pages from other language trees
452 +
            if getattr(settings, 'WAGTAIL_I18N_ENABLED', False):
453 +
                for translation in site.root_page.get_translations():
454 +
                    queryset |= base_queryset.descendant_of(translation, inclusive=True)
455 +
421 456
        else:
422 457
            # No sites configured
423 458
            queryset = queryset.none()

@@ -109,9 +109,45 @@
Loading
109 109
        return serializer.to_representation(page.get_ancestors())
110 110
111 111
112 +
class PageTranslationsField(Field):
113 +
    """
114 +
    Serializes the page's translations.
115 +
116 +
    Example:
117 +
    "translations": [
118 +
        {
119 +
            "id": 1,
120 +
            "meta": {
121 +
                "type": "home.HomePage",
122 +
                "detail_url": "/api/v1/pages/1/",
123 +
                "locale": "es"
124 +
            },
125 +
            "title": "Casa"
126 +
        },
127 +
        {
128 +
            "id": 2,
129 +
            "meta": {
130 +
                "type": "home.HomePage",
131 +
                "detail_url": "/api/v1/pages/2/",
132 +
                "locale": "fr"
133 +
            },
134 +
            "title": "Maison"
135 +
        }
136 +
    ]
137 +
    """
138 +
    def get_attribute(self, instance):
139 +
        return instance
140 +
141 +
    def to_representation(self, page):
142 +
        serializer_class = get_serializer_class(Page, ['id', 'type', 'detail_url', 'html_url', 'locale', 'title', 'admin_display_title'], meta_fields=['type', 'detail_url', 'html_url', 'locale'], base=AdminPageSerializer)
143 +
        serializer = serializer_class(context=self.context, many=True)
144 +
        return serializer.to_representation(page.get_translations())
145 +
146 +
112 147
class AdminPageSerializer(PageSerializer):
113 148
    status = PageStatusField(read_only=True)
114 149
    children = PageChildrenField(read_only=True)
115 150
    descendants = PageDescendantsField(read_only=True)
116 151
    ancestors = PageAncestorsField(read_only=True)
152 +
    translations = PageTranslationsField(read_only=True)
117 153
    admin_display_title = ReadOnlyField(source='get_admin_display_title')

@@ -1,9 +1,10 @@
Loading
1 1
from django.conf import settings
2 2
from django.db import models
3 +
from django.shortcuts import get_object_or_404
3 4
from rest_framework.filters import BaseFilterBackend
4 5
from taggit.managers import TaggableManager
5 6
6 -
from wagtail.core.models import Page
7 +
from wagtail.core.models import Locale, Page
7 8
from wagtail.search.backends import get_search_backend
8 9
from wagtail.search.backends.base import FilterFieldError, OrderByFieldError
9 10
@@ -18,6 +19,10 @@
Loading
18 19
        """
19 20
        fields = set(view.get_available_fields(queryset.model, db_fields_only=True))
20 21
22 +
        # Locale is a database field, but we provide a separate filter for it
23 +
        if 'locale' in fields:
24 +
            fields.remove('locale')
25 +
21 26
        for field_name, value in request.GET.items():
22 27
            if field_name in fields:
23 28
                try:
@@ -180,3 +185,52 @@
Loading
180 185
            queryset = queryset.descendant_of(parent_page)
181 186
182 187
        return queryset
188 +
189 +
190 +
class TranslationOfFilter(BaseFilterBackend):
191 +
    """
192 +
    Implements the ?translation_of filter which limits the set of pages to translations
193 +
    of a page.
194 +
    """
195 +
    def filter_queryset(self, request, queryset, view):
196 +
        if 'translation_of' in request.GET:
197 +
            try:
198 +
                page_id = int(request.GET['translation_of'])
199 +
                if page_id < 0:
200 +
                    raise ValueError()
201 +
202 +
                page = view.get_base_queryset().get(id=page_id)
203 +
            except ValueError:
204 +
                if request.GET['translation_of'] == 'root':
205 +
                    page = view.get_root_page()
206 +
                else:
207 +
                    raise BadRequestError("translation_of must be a positive integer")
208 +
            except Page.DoesNotExist:
209 +
                raise BadRequestError("translation_of page doesn't exist")
210 +
211 +
            _filtered_by_child_of = getattr(queryset, '_filtered_by_child_of', None)
212 +
213 +
            queryset = queryset.translation_of(page)
214 +
215 +
            if _filtered_by_child_of:
216 +
                queryset._filtered_by_child_of = _filtered_by_child_of
217 +
218 +
        return queryset
219 +
220 +
221 +
class LocaleFilter(BaseFilterBackend):
222 +
    """
223 +
    Implements the ?locale filter which limits the set of pages to a
224 +
    particular locale.
225 +
    """
226 +
    def filter_queryset(self, request, queryset, view):
227 +
        if 'locale' in request.GET:
228 +
            _filtered_by_child_of = getattr(queryset, '_filtered_by_child_of', None)
229 +
230 +
            locale = get_object_or_404(Locale, language_code=request.GET['locale'])
231 +
            queryset = queryset.filter(locale=locale)
232 +
233 +
            if _filtered_by_child_of:
234 +
                queryset._filtered_by_child_of = _filtered_by_child_of
235 +
236 +
        return queryset

@@ -86,6 +86,17 @@
Loading
86 86
        return name
87 87
88 88
89 +
class PageLocaleField(Field):
90 +
    """
91 +
    Serializes the "locale" field for pages.
92 +
    """
93 +
    def get_attribute(self, instance):
94 +
        return instance
95 +
96 +
    def to_representation(self, page):
97 +
        return page.locale.language_code
98 +
99 +
89 100
class RelatedField(relations.RelatedField):
90 101
    """
91 102
    Serializes related objects (eg, foreign keys).
@@ -310,6 +321,7 @@
Loading
310 321
311 322
class PageSerializer(BaseSerializer):
312 323
    type = PageTypeField(read_only=True)
324 +
    locale = PageLocaleField(read_only=True)
313 325
    html_url = PageHtmlUrlField(read_only=True)
314 326
    parent = PageParentField(read_only=True)
315 327

Learn more Showing 44 files with coverage changes found.

client/src/components/Icon/Icon.js
Loading file...
client/src/components/Explorer/ExplorerPanel.js
Loading file...
client/src/components/Explorer/reducers/nodes.js
Loading file...
client/src/components/LoadingSpinner/LoadingSpinner.js
Loading file...
client/src/includes/initSubmenus.js
Loading file...
client/src/components/Draftail/blocks/ImageBlock.js
Loading file...
client/src/components/Draftail/decorators/TooltipEntity.js
Loading file...
client/src/api/admin.js
Loading file...
client/src/components/Explorer/Explorer.js
Loading file...
client/src/components/Explorer/reducers/explorer.js
Loading file...
client/src/config/wagtailConfig.js
Loading file...
client/src/api/client.js
Loading file...
wagtail/admin/static_src/wagtailadmin/js/page-editor.js
Loading file...
client/src/includes/initSkipLink.js
Loading file...
wagtail/admin/static_src/wagtailadmin/app/wagtailadmin.entry.js
Loading file...
client/src/components/Button/Button.js
Loading file...
client/src/components/Explorer/ExplorerToggle.js
Loading file...
client/src/utils/focus.js
Loading file...
client/src/components/Explorer/PageCount.js
Loading file...
client/src/components/Explorer/ExplorerItem.js
Loading file...
client/src/components/Draftail/decorators/Link.js
Loading file...
client/src/components/Explorer/index.js
Loading file...
client/src/components/Draftail/Tooltip/Tooltip.js
Loading file...
client/src/components/Draftail/DraftUtils.js
Loading file...
client/src/components/PublicationStatus/PublicationStatus.js
Loading file...
client/src/components/Draftail/index.js
Loading file...
client/src/utils/actions.js
Loading file...
client/src/components/Draftail/EditorFallback/EditorFallback.js
Loading file...
client/src/components/Draftail/decorators/Document.js
Loading file...
wagtail/admin/static_src/wagtailadmin/js/vendor/urlify.js
Loading file...
client/src/components/Explorer/ExplorerHeader.js
Loading file...
client/src/components/Draftail/blocks/MediaBlock.js
Loading file...
client/src/components/Portal/Portal.js
Loading file...
wagtail/admin/static_src/wagtailadmin/app/draftail.entry.js
Loading file...
client/src/components/Explorer/actions.js
Loading file...
client/src/components/UpgradeNotification/index.js
Loading file...
client/src/components/Draftail/sources/ModalWorkflowSource.js
Loading file...
client/src/components/Transition/Transition.js
Loading file...
client/src/utils/version.js
Loading file...
wagtail/admin/static_src/wagtailadmin/js/vendor/jquery-3.5.1.min.js
Loading file...
client/src/components/Draftail/blocks/EmbedBlock.js
Loading file...
Changes in wagtail/contrib/frontend_cache/utils.py
-1
-1
+2
Loading file...
Changes in wagtail/core/blocks/stream_block.py
-1
-5
+6
Loading file...
Changes in wagtail/admin/compare.py
-1
+122
+2
Loading file...
Files Coverage
wagtail 0.65% 90.70%
Project Totals (393 files) 90.70%
Loading