1

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
Zemogle
  • 584
  • 4
  • 16

1 Answers1

2

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, 
    ),
  • This is a great answer! It works flawlessly. Its worth noting that I had to upgrade to at least Wagtail 3.0.2 to get the `BoundPanel` functionality. – Zemogle Sep 07 '22 at 08:52
  • 1
    Glad it worked! Yeah, sorry, should have mentioned BoundPanel only came in with Wagtail 3.x. This solution won't work with orderables - a new orderable instance has no locale attribute and no reference back to the parent clusterable object. Solution to that is similar but needs a slightly hacky reading the URL to grab either the locale (for new instance) or parent ID (if it pre-exists). – Rich - enzedonline Sep 12 '22 at 12:17