Ограничение типов файлов для определенного блока DocumentChooserBlock() в Wagtail Steamfield
Я пытаюсь ограничить результаты запроса для определенного блока DocumentChooserBlock внутри блока поля потока wagtail.
Я уже знаю, что можно ограничить типы файлов для DocumentChooser для типа страницы с помощью hooks, но я хотел бы избежать ограничения возможных типов файлов по всей странице, если они мне понадобятся для других блоков StreamField.
Существуют ли возможные способы реализации того, что я пытаюсь достичь здесь?
Модальная система Chooser от Wagtail работает немного иначе, чем обычный виджет Django (класс, используемый для вывода html-содержимого поля), сам виджет в основном выводит на экран кнопку "Выбрать документ", а кнопка затем вызывает модальное окно, которое является отдельным представлением и шаблоном.
Как вы отметили, хук construct_document_chooser_queryset
может ограничить результаты, показанные в этих модалах, но только с доступом к объекту запроса просматриваемой страницы, а не виджета, используемого для запуска этого модала.
Есть способ получить некоторую ограниченную желаемую функциональность, но она не будет работать для результатов поиска и не будет ограничивать любые дополнительные загрузки для этого типа файлов.
Шаг 1 - Создание пользовательского DocumentChooserBlock
- Этот класс блоков расширяет
DocumentChooserBlock
и имеет пользовательский__init__
метод, который извлекает kwargaccept
и присваивает его виджету attrs. - Виджеты Django все имеют возможность принимать
attrs
и они выводятся на отрисованный HTML элемент, наш пользовательский блок присваивает нужное нам значение виджету, чтобы другие методы имели к нему доступ. - Этот блок может быть использован так же, как и любой другой блок, но будет использовать kwarg accept'
doc_block = SpecificDocumentChooserBlock(accept="svg,md") # uses accept kwarg
.
- Вы можете убедиться, что это работает, просмотрев DOM (элемент inspect в браузере), сразу после 'Choose a Document', там будет скрытый атрибут с чем-то вроде
<input type="hidden" name="body-2-value" accept="svg,md" id="body-2-value" value="">
.
blocks.py
from wagtail.documents.blocks import DocumentChooserBlock
class SpecificDocumentChooserBlock(DocumentChooserBlock):
"""
Existing DocumentChooserBlock with the ability to add widget attrs based on the
accept kwarg, anything on self.widget.attrs will be added to the hidden
input field (so be careful what key is used).
"""
def __init__(self, accept=None, **kwargs):
super().__init__(**kwargs)
self.widget.attrs["accept"] = accept
Шаг 2 - убедитесь, что атрибуты виджета передаются в модальный триггер
- К сожалению, данные, используемые для URL запроса, расположены не в вышеуказанном элементе HTML input, а в контейнере div, см.
data-chooser-url
в блоке div.document-chooser
Этот атрибут данных генерируется системой под названием - Telepath. Главная часть, которую нужно понять, заключается в том, что есть класс, используемый для того, чтобы сообщить браузеру, что нужно отобразить на основе виджета, и он по умолчанию не передает атрибуты виджета.
- Код ниже должен быть добавлен в
- , так как нам все равно понадобится этот файл, и мы знаем, что он запускается только один раз во время выполнения.
wagtail_hooks.py
Строка - является ключевой частью, мы используем синтаксис
widget.render_html(
для распаковки любых значений widget.attrs (одним из них будет элемент**
, установленный нашим пользовательским блоком).accept
hooks.py
from wagtail.core.telepath import register as telepath_register
from wagtail.documents.widgets import AdminDocumentChooser, DocumentChooserAdapter
class CustomDocumentChooserAdapter(DocumentChooserAdapter):
def js_args(self, widget):
return [
widget.render_html(
# this line is changed, allocate any widget.attrs to the attrs passed to render_html
"__NAME__",
None,
attrs={**widget.attrs, "id": "__ID__"},
),
widget.id_for_label("__ID__"),
]
telepath_register(CustomDocumentChooserAdapter(), AdminDocumentChooser)
Шаг 3 - Переопределение шаблона администратора для выбора документа
- Пожалуйста, посмотрите документацию по Настройке шаблонов администратора , так как вам может понадобиться добавить еще несколько приложений к вашему
INSTALLED_APPS
для этого шага. - Создайте новый файл
myapp/templates/wagtaildocs/widgets/document_chooser.html
, часть послеtemplates
здесь очень важна, так как мы хотим переопределить и расширить именно этот шаблон. - В шаблоне мы расширим оригинал и переопределим блок
chooser_attributes
, поскольку именно он добавляетdata-chooser-url
, используемый триггером Chooser Modal. - Важно: Перезапустите ваш сервер разработки здесь, так как вы добавили новое переопределение шаблона.
- После завершения, в браузере осмотрите элемент, содержащий кнопку 'Choose a Document', вы должны увидеть, что элемент контейнера теперь имеет
data-chooser-url
с добавленной строкой запроса URL<div id="body-2-value-chooser" class="chooser document-chooser blank" data-chooser-url="/admin/documents/chooser/?accept=svg,md">
.
myapp/templates/wagtaildocs/widgets/document_chooser.html
{% extends "wagtaildocs/widgets/document_chooser.html" %}
{% comment %}
This template overrides the Wagtail default chooser field, this is not the modal but
the button / selected value shown in the page editor.
chooser_attributes are the attributes that are used by the modal trigger, we will
override the 'data-chooser-url' value with a url param
{% endcomment %}
{% block chooser_attributes %}data-chooser-url="{% url "wagtaildocs:chooser" %}{% if attrs.accept %}?accept={{ attrs.accept }}{% endif %}"{% endblock %}
Шаг 4 - обработка строки запроса accept
param
- Теперь, используя
construct_document_chooser_queryset
, можно получить GET-параметрaccept
и разобрать его, чтобы сгенерировать другой набор результатов документа.
wagtail_hooks.py
@hooks.register("construct_document_chooser_queryset")
def show_accepted_documents_only(documents, request):
accept = request.GET.get("accept")
if accept:
accepted_files = accept.split(",")
queries = [Q(file__iendswith=f".{value}") for value in accepted_files]
query = queries.pop()
for item in queries:
query |= item
documents = documents.filter(query)
return documents
Кавеаты
- Данное решение не блокирует загрузку пользователем только определенных файлов в модале, но вы можете скрыть эту вкладку с помощью CSS (блоки принимают свойство classname).
- Когда пользователь ищет в модале, он не будет использовать URL, установленный таким образом, к сожалению.
- Решение может быть хрупким в различных релизах, особенно в
CustomDocumentChooserAdapter
, поэтому обязательно следите за изменениями кода Wagtail.
Использование wagtail-generic-chooser
предоставляет гораздо больше возможностей для настройки работы модального окна Chooser.
Шаг 1 - установка wagtail-generic-chooser
- Выполнить:
pip install wagtail-generic-chooser
- Затем добавьте
generic_chooser
вINSTALLED_APPS
ваш проект.
Шаг 2 - Настройка набора представлений Chooser
- Подобно инструкциям в документации по установке набора представлений Chooser .
- Убедитесь, что мы можем обрабатывать параметр
accept
путем создания пользовательского класса, который расширяетModelChooserMixin
, это будет означать, что параметр будет передаваться при поиске. - Добавьте обработку параметра URL
accept
для условной фильтрации возвращаемых значений. - Создайте класс, расширяющий
ModelChooserViewSet
, который будет обрабатывать показDocument
листинга внутри модала.
base/views.py
from django.db.models import Q
from generic_chooser.views import ModelChooserMixin, ModelChooserViewSet
from wagtail.documents.models import Document
class RestrictedDocumentChooserMixin(ModelChooserMixin):
# preserve this URL parameter on pagination / search
preserve_url_parameters = [
"accept",
]
def get_unfiltered_object_list(self):
objects = super().get_unfiltered_object_list()
accept = self.request.GET.get("accept")
print("get_unfiltered_object_list", accept)
if accept:
accepted_files = accept.split(",")
queries = [Q(file__iendswith=f".{value}") for value in accepted_files]
query = queries.pop()
for item in queries:
query |= item
objects = objects.filter(query)
return objects
class RestrictedDocumentChooserViewSet(ModelChooserViewSet):
chooser_mixin_class = RestrictedDocumentChooserMixin
icon = "doc"
model = Document
page_title = "Choose a document"
per_page = 10
order_by = "title"
fields = ["title", "file"]
Шаг 3 - Создание виджета выбора
- Этот виджет не является
Block
, но будет использоваться как основа дляBlock
, а также может быть использован дляFieldPanel
. - По аналогии с Установкой виджета на основе модели создайте класс, расширяющий
AdminChooser
. - В методе
__init__
мы извлекаемaccept
kwarg, чтобы использовать его для генерации пользовательского параметра URL. - Определите метод
get_edit_item_url
, который позволит щелкнуть на выбранном документе для его редактирования. - Переопределить метод ``get_choose_modal_url
to append the URL query param (note: I could not get
reverse'', работающий здесь без кучи дополнительных заморочек).
base/models.py
from django.contrib.admin.utils import quote
from django.urls import reverse
from generic_chooser.widgets import AdminChooser
from wagtail.documents.models import Document
class RestrictedDocumentChooser(AdminChooser):
def __init__(self, **kwargs):
self.accept = kwargs.pop("accept")
super().__init__(**kwargs)
choose_one_text = "Choose a Document"
choose_another_text = "Choose another document"
link_to_chosen_text = "Edit this document"
model = Document
choose_modal_url_name = "restricted_document_chooser:choose"
def get_choose_modal_url(self):
url = super().get_choose_modal_url()
return url + "?accept=%s" % self.accept
def get_edit_item_url(self, item):
return reverse("wagtaildocs:edit", args=[item.id])
Шаг 4 - Регистрация набора представлений выбора в Wagtail Hooks
- Здесь не нужно использовать
construct_document_chooser_queryset
, вместо этого используйте крючокregister_admin_viewset
и зарегистрируйтеRestrictedDocumentChooserViewSet
.
base/wagtail_hooks.py
from wagtail.core import hooks
from .views import RestrictedDocumentChooserViewSet
# ... other hooks etc
@hooks.register("register_admin_viewset")
def register_restricted_document_chooser_viewset():
return RestrictedDocumentChooserViewSet(
"restricted_document_chooser", url_prefix="restricted-document-chooser"
)
Шаг 5 - Настройка и использование пользовательского Block
- Этот класс расширяет
ChooserBlock
и оборачивает созданный виджетRestrictedDocumentChooser
. - При создании
__init__
вытаскивается тот же самый каргaccept
и передается вRestrictedDocumentChooser
. - Этот блок можно использовать, вызывая его аналогично любому другому блоку, правда, с kwarg
accept
.doc_block = RestrictedDocumentChooserBlock(accept="svg,md")
base/blocks.py
from django.utils.functional import cached_property
from wagtail.images.blocks import ChooserBlock
# ...
class RestrictedDocumentChooserBlock(ChooserBlock):
def __init__(self, **kwargs):
self.accept = kwargs.pop("accept")
super().__init__(**kwargs)
@cached_property
def target_model(self):
from wagtail.documents.models import Document
return Document
@cached_property
def widget(self):
from .widgets import RestrictedDocumentChooser
return RestrictedDocumentChooser(accept=self.accept)
def get_form_state(self, value):
return self.widget.get_value_data(value)