pythondjangowagtailwagtail-localize

Wagtail - customise FieldPanel to show results for current Locale


I have a site which is i18n enabled and using wagtail-localize. When editing (or creating) the original language of a page, all the snippets show values for every language, if you use the standard FieldPanel. Using the SnipperChooserPanel is not an option because there are a lot of ParentalManytoManyFields in the model, it would be too cluttered for the editors.

Screenshot 2022-07-29 at 15 07 59

This is how the model and snippet is constructed.

@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),
        ])

I'm trying to work out how to subclass FieldPanel so it uses the page's locale to filter the snippet queryset.

I have hacky/temporary solution to this using the limit_choices_to kwarg for ParentalManyToManyField but I can only filter by the user language not the page language.

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

Solution

  • Turns out the locale is lurking in the BoundPanel.instance

    Here's a select panel that will filter according to locale. It'll match the default panel type for the field, or you can override with an appropriate form widget (one of CheckboxSelectMultiple, RadioSelect, Select or SelectMultiple). Set typed_choice_field=True to force Select into a dropdown widget (default is a list).

    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
    

    So in your Activity class, you'd call this with

    LocalizedSelectPanel(
        'level', 
        widget_class=CheckboxSelectMultiple, 
        ),