Заполнение виджета 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
виджет:
Попытки решения
Мне явно нужно заполнять форму при ее инициализации. В идеале, используя встроенный виджет 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, не помогло. Мне также интересно, есть ли более простой подход к решению проблемы, с которой я столкнулся, но я его просто не вижу.
Похожие вопросы, которые не касались моей проблемы
- Вопрос: Заполнение CheckboxSelectMultiple существующими данными из формы django model
- Comment: В OP реализована пользовательская
ModelForm
, которая связываетCheckBoxSelectMultiple
прямо сmodels.ManyToManyField
. Это работает, поскольку типManyToManyField
автоматически совместим с виджетом. В моем случае мне приходится настраивать его самостоятельно.
- Comment: В OP реализована пользовательская
- Вопрос: Инициальные значения для CheckboxSelectMultiple.
- Комментарий: ОП использует
MultiSubscriptionForm
, который сам содержит аргумент ключевого слова для заполнения существующих полей. В моей ситуации этого нет.
- Комментарий: ОП использует
- Вопрос: Переопределение шаблона администратора Django: Отображение виджета checkboxselectmultiple.
- Комментарий: ОП описывает структуру таблицы и спрашивает, возможно ли это. Ни один ответ не решает проблему (возможно, плохо сформулированный вопрос) .
- Вопрос: Django - рендеринг виджета CheckboxSelectMultiple() по отдельности в шаблоне (вручную)
- Комментарий: ОП хочет настроить шаблон
CheckboxSelectMultiple
, чтобы показать особое расположение. Ответы содержат шаблонный HTML для этого, но в остальном полагаются на тип поля/отношенияManyToMany
, который автоматически связывает/заполняет флажки.
- Комментарий: ОП хочет настроить шаблон
Хорошо, я разобрался. В общем, вам придется создать следующее:
- Тип поля: Создайте свой класс поля (например, как мой тип
PermissionField
в вопросе). - Model: Создайте свою обычную модель Django/Wagtail (которая использует ваше пользовательское поле и класс формы) .
- Form: Создайте форму вашей модели, предпочтительно подклассифицировав что-то из Wagtail или Django, например
CheckboxSelectMultiple
- Переопределите метод
get_context
формы, чтобы добавить состояние «checked».
- Переопределите метод
- Шаблон: Используйте пользовательский шаблон, чтобы иметь возможность устанавливать состояние флажка.
Тип поля
Это практически не отличается от вопроса
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'