Wagtail - настройка панели FieldPanel для отображения результатов для текущей локали

У меня есть сайт, который поддерживает i18n и использует wagtail-localize. При редактировании (или создании) исходного языка страницы все сниппеты показывают значения для каждого языка, если использовать стандартный FieldPanel. Использование SnipperChooserPanel не является вариантом, потому что в модели много ParentalManytoManyField, это было бы слишком загромождено для редакторов.

Screenshot 2022-07-29 at 15 07 59

Вот как строится модель и сниппет.

@register_snippet
class Level(TranslatableMixin):
    name = models.CharField(max_length=255)
    def __str__(self):
        return self.name

    class Meta:
        verbose_name = "Educational Level"
        unique_together = ('translation_key', 'locale')

class Activity(Page):
       ...
       level = ParentalManyToManyField(Level, verbose_name='Education level', blank=True)

        MultiFieldPanel([
           ....
            FieldPanel('level', widget=forms.CheckboxSelectMultiple),
        ])

Я пытаюсь понять, как подкласс FieldPanel, чтобы он использовал локаль страницы для фильтрации кверисета сниппетов.

У меня есть халтурное/временное решение для этого, используя limit_choices_to kwarg для ParentalManyToManyField, но я могу фильтровать только по языку пользователя, а не по языку страницы.

def limit_lang_choice():
    limit = models.Q(locale__language_code=get_language())
    return limit

Я долго мучился с этой проблемой, когда создавал систему меню. Мои объекты меню могут быть добавлены как подменю других объектов меню, но та же проблема, что и у вас, это должно быть 4 версии каждого меню (4 языка).

Единственный способ, который я смог найти для определения локали - это URI страницы редактирования. Для новых элементов URI заканчивается на /?locale=xx. Для отредактированных элементов последним параметром является id объекта, по которому можно запросить локаль. Хитрый способ, но он работает.

Проблема в том, что вы не можете получить доступ к URI из определения модели, для этого нужен пользовательский edit_handler.

Для этой системы меню подменю добавляются как упорядоченные к объекту родительского меню.

Первое, что мне понадобилось в models.py - это вызываемый класс, возвращающий все объекты редактируемого класса (в данном случае Menu):

class MenuListQuerySet(object):
    # Call as class()() to act as a function call, passes all menus to SubMenuPanel dropdown
    # Useful to make a function call in class declaration to make dynamic class variables 
    def __call__(self, *args, **kwds):
        return Menu.objects.all()
    

Затем в определении панелей моего подменю orderable:

SubMenuFieldPanel("submenu_id", MenuListQuerySet()()),

Наконец, панель FieldPanel:

Важно, чтобы в классе, вызывающем эту панель, вы не определили choices, иначе он переопределит эту панель и создаст статический список выбора.

Поскольку здесь перечислены объекты меню, которые могут быть добавлены в качестве подменю, я отфильтровываю вызывающий объект (id родителя в этой панели), чтобы меню не могло быть добавлено к самому себе в качестве подменю. Удалите код, связанный с parent_menu_id, если вы используете его для других целей.

Это дает вам выпадающий селектор, в котором перечислены только опции для добавляемой/редактируемой локали.

enter image description here

В качестве edit_handler используется Wagtail 3.x, версию 2.x можно найти внизу edit_handlers.py

Пожалуй, не так много работы, чтобы сделать это общим - переименуйте ссылки на Menu, удалите код parent_menu_id, обновите класс MenuListQuerySet, чтобы передавать соответствующие данные.

Оказывается, локаль скрывается в BoundPanel.instance

Вот панель выбора, которая будет фильтровать в зависимости от локали. Она будет соответствовать типу панели по умолчанию для данного поля, или вы можете переопределить его с помощью соответствующего виджета формы (один из CheckboxSelectMultiple, RadioSelect, Select или SelectMultiple). Установите typed_choice_field=True, чтобы заставить Select использовать выпадающий виджет (по умолчанию это список).

from django.core.exceptions import ImproperlyConfigured
from django.forms.models import ModelChoiceIterator
from django.forms.widgets import (CheckboxSelectMultiple, RadioSelect, Select,
                                  SelectMultiple)
from django.utils.translation import gettext_lazy as _
from wagtail.admin.panels import FieldPanel


class LocalizedSelectPanel(FieldPanel):
    """
    Customised FieldPanel to filter choices based on locale of page/model being created/edited
    Usage: 
    widget_class - optional, override field widget type
                 - should be CheckboxSelectMultiple, RadioSelect, Select or SelectMultiple
    typed_choice_field - set to True with Select widget forces drop down list 
    """

    def __init__(self, field_name, widget_class=None, typed_choice_field=False, *args, **kwargs):
        if not widget_class in [None, CheckboxSelectMultiple, RadioSelect, Select, SelectMultiple]:
            raise ImproperlyConfigured(_(
                "widget_class should be a Django form widget class of type "
                "CheckboxSelectMultiple, RadioSelect, Select or SelectMultiple"
            ))
        self.widget_class = widget_class
        self.typed_choice_field = typed_choice_field
        super().__init__(field_name, *args, **kwargs)

    def clone_kwargs(self):
        return {
            'heading': self.heading,
            'classname': self.classname,
            'help_text': self.help_text,
            'widget_class': self.widget_class,
            'typed_choice_field': self.typed_choice_field,
            'field_name': self.field_name,
        }

    class BoundPanel(FieldPanel.BoundPanel):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)           
            if not self.panel.widget_class:
                self.form.fields[self.field_name].widget.choices=self.choice_list
            else:
                self.form.fields[self.field_name].widget = self.panel.widget_class(choices=self.choice_list)
            if self.panel.typed_choice_field:
                self.form.fields[self.field_name].__class__.__name__ = 'typed_choice_field'
            pass

        @property
        def choice_list(self):
            self.form.fields[self.field_name].queryset = self.form.fields[self.field_name].queryset.filter(locale_id=self.instance.locale_id)
            choices = ModelChoiceIterator(self.form.fields[self.field_name])
            return choices

Так что в классе Activity вы бы вызвали это с помощью

LocalizedSelectPanel(
    'level', 
    widget_class=CheckboxSelectMultiple, 
    ),
Вернуться на верх