#6398 Locale components

Open 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/convert_alias.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/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/compare.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/menus.py contrib/modeladmin/mixins.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 locales/views.py locales/tests.py locales/wagtail_hooks.py locales/forms.py locales/utils.py locales/components.py locales/apps.py locales/__init__.py utils/sendfile.py utils/setup.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 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.


@@ -0,0 +1,13 @@
Loading
1 +
LOCALE_COMPONENTS = []
2 +
3 +
4 +
def get_locale_components():
5 +
    return LOCALE_COMPONENTS
6 +
7 +
8 +
def register_locale_component(model):
9 +
    if model not in LOCALE_COMPONENTS:
10 +
        LOCALE_COMPONENTS.append(model)
11 +
        LOCALE_COMPONENTS.sort(key=lambda x: x._meta.verbose_name)
12 +
13 +
    return model

@@ -47,6 +47,7 @@
Loading
47 47
    def test_create(self):
48 48
        response = self.post({
49 49
            'language_code': "fr",
50 +
            'component_tests_FlagLocaleComponent-flag': 'tricolore',
50 51
        })
51 52
52 53
        # Should redirect back to index
@@ -55,9 +56,13 @@
Loading
55 56
        # Check that the locale was created
56 57
        self.assertTrue(Locale.objects.filter(language_code='fr').exists())
57 58
59 +
        # Check the flag was created
60 +
        self.assertEqual(Locale.objects.get(language_code='fr').flag.flag, 'tricolore')
61 +
58 62
    def test_duplicate_not_allowed(self):
59 63
        response = self.post({
60 64
            'language_code': "en",
65 +
            'component_tests_FlagLocaleComponent-flag': 'union-jack',
61 66
        })
62 67
63 68
        # Should return the form with errors
@@ -67,6 +72,7 @@
Loading
67 72
    def test_language_code_must_be_in_settings(self):
68 73
        response = self.post({
69 74
            'language_code': "ja",
75 +
            'component_tests_FlagLocaleComponent-flag': '',
70 76
        })
71 77
72 78
        # Should return the form with errors
@@ -115,6 +121,7 @@
Loading
115 121
    def test_edit(self):
116 122
        response = self.post({
117 123
            'language_code': 'fr',
124 +
            'component_tests_FlagLocaleComponent-flag': 'tricolore',
118 125
        })
119 126
120 127
        # Should redirect back to index
@@ -124,11 +131,15 @@
Loading
124 131
        self.english.refresh_from_db()
125 132
        self.assertEqual(self.english.language_code, 'fr')
126 133
134 +
        # Check the flag was added
135 +
        self.assertEqual(self.english.flag.flag, 'tricolore')
136 +
127 137
    def test_edit_duplicate_not_allowed(self):
128 138
        french = Locale.objects.create(language_code='fr')
129 139
130 140
        response = self.post({
131 141
            'language_code': "en",
142 +
            'component_tests_FlagLocaleComponent-flag': 'union-jack',
132 143
        }, locale=french)
133 144
134 145
        # Should return the form with errors
@@ -138,6 +149,7 @@
Loading
138 149
    def test_edit_language_code_must_be_in_settings(self):
139 150
        response = self.post({
140 151
            'language_code': "ja",
152 +
            'component_tests_FlagLocaleComponent-flag': '',
141 153
        })
142 154
143 155
        # Should return the form with errors

@@ -1,15 +1,78 @@
Loading
1 +
import functools
2 +
3 +
from django.db import transaction
1 4
from django.utils.translation import gettext_lazy
2 5
3 6
from wagtail.admin import messages
7 +
from wagtail.admin.edit_handlers import ObjectList, extract_panel_definitions_from_model_class
4 8
from wagtail.admin.views import generic
5 9
from wagtail.admin.viewsets.model import ModelViewSet
6 10
from wagtail.core.models import Locale
7 11
from wagtail.core.permissions import locale_permission_policy
8 12
13 +
from .components import get_locale_components
9 14
from .forms import LocaleForm
10 15
from .utils import get_locale_usage
11 16
12 17
18 +
@functools.lru_cache()
19 +
def get_locale_component_edit_handler(model):
20 +
    if hasattr(model, "edit_handler"):
21 +
        # use the edit handler specified on the class
22 +
        return model.edit_handler
23 +
    else:
24 +
        panels = extract_panel_definitions_from_model_class(model, exclude=["locale"])
25 +
        return ObjectList(panels)
26 +
27 +
28 +
class ComponentManager:
29 +
    def __init__(self, components):
30 +
        self.components = components
31 +
32 +
    @classmethod
33 +
    def from_request(cls, request, instance=None):
34 +
        components = []
35 +
36 +
        for component_model in get_locale_components():
37 +
            component_instance = component_model.objects.filter(locale=instance).first()
38 +
            edit_handler = get_locale_component_edit_handler(component_model).bind_to(
39 +
                model=component_model, instance=component_instance, request=request
40 +
            )
41 +
            form_class = edit_handler.get_form_class()
42 +
            prefix = "component_{}_{}".format(
43 +
                component_model._meta.app_label, component_model.__name__
44 +
            )
45 +
46 +
            if request.method == "POST":
47 +
                form = form_class(
48 +
                    request.POST,
49 +
                    request.FILES,
50 +
                    instance=component_instance,
51 +
                    prefix=prefix,
52 +
                )
53 +
            else:
54 +
                form = form_class(instance=component_instance, prefix=prefix)
55 +
56 +
            components.append((component_model, component_instance, form))
57 +
58 +
        return cls(components)
59 +
60 +
    def is_valid(self):
61 +
        return all(
62 +
            component_form.is_valid()
63 +
            for component_model, component_instance, component_form in self.components
64 +
        )
65 +
66 +
    def save(self, locale):
67 +
        for component_model, component_instance, component_form in self.components:
68 +
            component_instance = component_form.save(commit=False)
69 +
            component_instance.locale = locale
70 +
            component_instance.save()
71 +
72 +
    def __iter__(self):
73 +
        return iter(self.components)
74 +
75 +
13 76
class IndexView(generic.IndexView):
14 77
    template_name = 'wagtaillocales/index.html'
15 78
    page_title = gettext_lazy("Locales")
@@ -31,6 +94,30 @@
Loading
31 94
    success_message = gettext_lazy("Locale '{0}' created.")
32 95
    template_name = 'wagtaillocales/create.html'
33 96
97 +
    def get_components(self):
98 +
        return ComponentManager.from_request(self.request)
99 +
100 +
    def post(self, request, *args, **kwargs):
101 +
        self.object = None
102 +
        form = self.get_form()
103 +
        self.components = ComponentManager.from_request(self.request)
104 +
105 +
        if form.is_valid() and self.get_components().is_valid():
106 +
            return self.form_valid(form)
107 +
        else:
108 +
            return self.form_invalid(form)
109 +
110 +
    @transaction.atomic
111 +
    def form_valid(self, form):
112 +
        response = super().form_valid(form)
113 +
        self.get_components().save(self.object)
114 +
        return response
115 +
116 +
    def get_context_data(self, **kwargs):
117 +
        context = super().get_context_data()
118 +
        context["components"] = self.get_components()
119 +
        return context
120 +
34 121
35 122
class EditView(generic.EditView):
36 123
    success_message = gettext_lazy("Locale '{0}' updated.")
@@ -40,6 +127,29 @@
Loading
40 127
    template_name = 'wagtaillocales/edit.html'
41 128
    queryset = Locale.all_objects.all()
42 129
130 +
    def get_components(self):
131 +
        return ComponentManager.from_request(self.request, instance=self.object)
132 +
133 +
    def post(self, request, *args, **kwargs):
134 +
        self.object = self.get_object()
135 +
        form = self.get_form()
136 +
137 +
        if form.is_valid() and self.get_components().is_valid():
138 +
            return self.form_valid(form)
139 +
        else:
140 +
            return self.form_invalid(form)
141 +
142 +
    @transaction.atomic
143 +
    def form_valid(self, form):
144 +
        response = super().form_valid(form)
145 +
        self.get_components().save(self.object)
146 +
        return response
147 +
148 +
    def get_context_data(self, **kwargs):
149 +
        context = super().get_context_data()
150 +
        context["components"] = self.get_components()
151 +
        return context
152 +
43 153
44 154
class DeleteView(generic.DeleteView):
45 155
    success_message = gettext_lazy("Locale '{0}' deleted.")

Learn more Showing 12 files with coverage changes found.

Changes in wagtail/admin/wagtail_hooks.py
+4
+8
Loading file...
Changes in wagtail/users/views/users.py
-1
+1
Loading file...
Changes in wagtail/users/forms.py
-1
+1
Loading file...
Changes in wagtail/core/models.py
+80
+7
+4
Loading file...
Changes in wagtail/admin/urls/pages.py
New
Loading file...
New file wagtail/locales/components.py
New
Loading file...
New file wagtail/admin/views/pages/convert_alias.py
New
Loading file...
Changes in wagtail/search/backends/elasticsearch2.py
+2
Loading file...
Changes in wagtail/admin/forms/pages.py
+1
Loading file...
Changes in wagtail/admin/views/pages/edit.py
+6
Loading file...
Changes in wagtail/admin/views/pages/copy.py
+2
Loading file...
Changes in wagtail/documents/models.py
+6
Loading file...
Files Coverage
wagtail -0.05% 90.56%
Project Totals (395 files) 90.56%
Loading