Заполнение виджета CheckboxSelectMultiple с помощью моей собственной модели в админке Wagtail

Context

Я создал модель, соответствующую модель поля и намерен использовать встроенный виджет CheckboxSelectMultiple для использования внутри админки Wagtail. Концепция представляет собой поле разрешения с множественным выбором, которое сохраняется как битовое поле:

# Model class
class Perm(IntFlag):
    Empty  = 0
    Read   = 1
    Write  = 2

Я использовал документацию Django model field's для создания модели поля, которая может транслировать мой Perm тип в и из моей базы данных (сохраненный как целочисленное поле, которое побитовое ИЛИ соответствующих битов разрешения):

# Model field class
class PermField(models.Field):
    description = "Permission field"
    def __init__(self, value=Perm.Empty.value, *args, **kwargs):
        self.value = value
        kwargs["default"] = Perm.Empty.value
        super().__init__(*args, **kwargs)
    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        args += [self.value]
        return name, path, args, kwargs
    def db_type(self, connection):
        return "bigint" # PostgresSQL
    def from_db_value(self, value, expression, connection):
        if value is None:
            return Perm.Empty
        return Perm(value)
    def to_python(self, value):
        if isinstance(value, Perm):
            return value
        if isinstance(value, str):
            return self.parse(value)
        if value is None:
            return value
        return Perm(value)
    def parse(self, value):
        v = Perm.Empty
        if not isinstance(ast.literal_eval(value), list):
            raise ValueError("%s cannot be converted to %s", value, type(Perm))
        for n in ast.literal_eval(value):
            v = v | Perm(int(n))
        return v

Затем я также создал сниппет Wagtail для использования этого нового поля и типа:

perm_choices = [
    (Perm.Read.value, Perm.Read.name),
    (Perm.Write.value, Perm.Write.name)
]

@register_snippet
class Permission(models.Model):
    name = models.CharField(max_length=32, default="None")
    perm = PermField()
    panels = [FieldPanel("perm", widget=forms.CheckboxSelectMultiple(choices=perm_choices))]


Проблема

Создание новых сниппетов работает нормально, но редактирование существующего просто показывает пустой CheckboxSelectMultiple виджет:

Empty Wagtail CheckboxSelectMultiple upon editing an existing snippet


Попытки решения

Мне явно нужно заполнять форму при ее инициализации. В идеале, используя встроенный виджет CheckboxSelectMultiple. Для этого я попробовал определить следующую форму:


@register_snippet
class Permission(models.Model):
    # ...

# Custom form subclass for snippets per documentation
# https://docs.wagtail.org/en/v2.15/advanced_topics/customisation/page_editing_interface.html
class Permission(WagtailAdminModelForm):
    p = forms.IntegerField(
        widget=forms.CheckboxSelectMultiple(
            choices=perm_choices,
        ),
        label="Permission field",
    )
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['p'].initial = {
            k.name for k in [Perm.Read, Perm.Write] if k.value in Perm(int(p))
        }

    def clean_selected_permissions(self):
        selected_p = self.cleaned_data["p"]
        value = Perm.Empty
        for k in selected_p:
            value |= Perm.__members__[k]
        return value

    class Meta:
        model=Permission
        fields=["perm"]

# Models not defined yet error here!
Permission.base_form_class = PermissionForm

Однако я не могу заставить эту форму работать. Существует цикл, в котором PermissionForm требует, чтобы Permission был определен, или наоборот. Использование глобального назначения формы модели, как показано здесь у gasman, не помогло. Мне также интересно, есть ли более простой подход к решению проблемы, с которой я столкнулся, но я его просто не вижу.


Похожие вопросы, которые не касались моей проблемы

Хорошо, я разобрался. В общем, вам придется создать следующее:

  1. Тип поля: Создайте свой класс поля (например, как мой тип PermissionField в вопросе).
  2. Model: Создайте свою обычную модель Django/Wagtail (которая использует ваше пользовательское поле и класс формы)
  3. .
  4. Form: Создайте форму вашей модели, предпочтительно подклассифицировав что-то из Wagtail или Django, например CheckboxSelectMultiple
    • Переопределите метод get_context формы, чтобы добавить состояние «checked».
  5. Шаблон: Используйте пользовательский шаблон, чтобы иметь возможность устанавливать состояние флажка.

Тип поля

Это практически не отличается от вопроса

class PermField(models.Field):
    description = "Permission field"
    def __init__(self, value=Perm.Empty.value, *args, **kwargs):
        self.value = value
        kwargs["default"] = Perm.Empty.value
        super().__init__(*args, **kwargs)
    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        args += [self.value]
        return name, path, args, kwargs
    def db_type(self, connection):
        return "bigint" # PostgresSQL
    def from_db_value(self, value, expression, connection):
        if value is None:
            return Perm.Empty
        return Perm(value)
    def to_python(self, value):
        if isinstance(value, Perm):
            return value
        if isinstance(value, str):
            return self.parse(value)
        if value is None:
            return value
        return Perm(value)
    def parse(self, value):
        v = Perm.Empty
        if isinstance(ast.literal_eval(value), int):
            return Perm(ast.literal_eval(value))
        if not isinstance(ast.literal_eval(value), list):
            raise ValueError("%s cannot be converted to %s", value, type(Perm))

        # Note: The form will use a list: "['1','2',...]" that you need to
        #.      convert back into an actual value of your field type.
        for n in ast.literal_eval(value):
            v = v | Perm(int(n))
        return v

Обратите внимание на метод parse, поскольку при сохранении формы метод to_python вашего типа поля будет вызван со списком в строковой кодировке, содержащим выбранные опции. Поэтому вам нужно (1) обнаружить это и (2) преобразовать обратно в значение вашего типа поля.


Модель

В итоге модель оказывается довольно простой. Просто укажите вашу пользовательскую форму (см. следующий раздел)

@register_snippet
class Permission(models.Model):
    perm = PermField()
    panels = [
        FieldPanel("perm", widget=PermissionForm(choices=perm_choices))
    ]

Модель формы

Я просто подклассифицировал CheckboxSelectMultiple, поскольку тогда вы получаете встроенный аргумент ключевого слова choices и т.д.

class PermissionForm(forms.CheckboxSelectMultiple):
    template_name = "permission_form.html"
    def get_context(self, name, value, attrs):
        context = super().get_context(name, value, attrs)
        # Set the "selected" flag here however you need for your type. 
        for option in context["widget"]["optgroups"]:
            option[1][0]["selected"] = Perm(option[1][0]["value"]) in Perm(value)
        return context

Переопределение контекста

Вы можете переопределить get_context (описано здесь в документации , хотя и не очень хорошо) и предоставить шаблону свой собственный контекст. Я использовал pdb для самостоятельной проверки контекста. В результате структура словаря выглядит следующим образом:

 "widget": {
        "name": "perm",
        "is_hidden": False,
        "required": True,
        "value": ["0"],
        "attrs": {"id": "id_perm"},
        "template_name": "permission_form.html",
        "optgroups": [
            (
                None,
                [
                    {
                        "name": "perm",
                        "value": 1,
                        "label": "Read",
                        "selected": False,
                        "index": "0",
                        "attrs": {"id": "id_perm_0"},
                        "type": "checkbox",
                        "template_name": "django/forms/widgets/checkbox_option.html",
                        "wrap_label": True,
                    }
                ],
                0,
            ),
            (
                None,
                [
                    {
                        "name": "perm",
                        "value": 2,
                        "label": "Write",
                        "selected": False,
                        "index": "1",
                        "attrs": {"id": "id_perm_1"},
                        "type": "checkbox",
                        "template_name": "django/forms/widgets/checkbox_option.html",
                        "wrap_label": True,
                    }
                ],
                1,
            ),
        ],
    }

Вы видите, что я перезаписываю поле selected boolean каждого кортежа опций в списке, используя аргумент value в get_context (это должно быть значение вашего поля в момент создания формы).

Я надеялся, что этого будет достаточно, чтобы флажки уже были установлены при создании формы с помощью шаблона по умолчанию, но это не сработало. В итоге я решил использовать свой собственный шаблон (см. далее)


Шаблон

Я скопировал созданный HTML для виджета CheckboxMultipleSelect и немного отредактировал его, чтобы использовать контекст, который я предоставляю:

<!--mysite/templates/permission_form.html-->

<div>
{% for option in widget.optgroups %}
<label for="{{widget.attrs.id}}_{{option.2}}">
  <input type="checkbox"
     name="{{widget.name}}"
     value="{{option.1.0.value}}"
     id="{{widget.attrs.id}}_{{option.2}}"
     {% if option.1.0.selected %}
     checked
     {% endif %}
  >
{{option.1.0.label}}
</label>
{% endfor %}
</div>

Для того чтобы использовать свой собственный шаблон виджета без возникновения ошибки, необходимо сначала внести некоторые изменения в настройки (за решение этой проблемы спасибо https://stackoverflow.com/a/46208414/1883304):

# In wagtail, it should be something like mysite/settings.py
INSTALLED_APPS = [
  ...
  "django.forms",
  ...
]

FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
Вернуться на верх