在 Wagtail Steamfield 中限制特定 DocumentChooserBlock() 块的文件类型

Limiting file types for a specific DocumentChooserBlock() Block in Wagtail Steamfield

我正在尝试限制 wagtail 流字段块内特定 DocumentChooserBlock 的查询结果。

我已经知道您可以使用 hooks 限制页面类型的 DocumentChooser 的文件类型,但我想避免在页面范围内限制可能的文件类型,以防其他 StreamField 块需要它们。

是否有任何可能的方法来实现我在这里想要实现的目标?

Wagtail 的 Chooser Modal 系统与普通的 Django 小部件(Class 用于呈现 html 字段内容)的工作方式略有不同,小部件本身主要呈现一个按钮 'Choose Document'然后该按钮触发一个模式,它是一个单独的视图和模板。

如您所述,construct_document_chooser_queryset hook 可以限制这些模式中显示的结果,但只能访问正在查看的页面的请求对象,而不是用于触发该模式的 Widget。

有一种方法可以获得一些有限的所需功能,但它不适用于搜索结果,并且不会限制对该文件类型的任何其他上传。

第 1 步 - 创建自定义 DocumentChooserBlock

  • 这个块 Class 扩展了 DocumentChooserBlock 并且有一个自定义的 __init__ 方法可以提取一个 kwarg accept 并将其分配给小部件属性。
  • Django Widgets 都具有 accept attrs 的能力,这些都在呈现的 HTML 元素上输出,我们的自定义 Block 将我们想要的值分配给小部件,以便其他方法可以访问它.
  • 这个块可以像任何其他块一样使用,但会使用 accept' doc_block = SpecificDocumentChooserBlock(accept="svg,md") # uses accept kwarg
  • 您可以通过查看 DOM(在浏览器中检查元素)来确认这是有效的,就在 '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 输入元素上,而是在容器 div 上,参见 data-chooser-url document-chooser 区块 div.
  • 此数据属性由名为 Telepath 的系统生成。
  • 要理解的主要部分是有一个 Class 用于告诉浏览器根据 Widget 呈现什么,默认情况下,这不会传递 widget 属性。
  • 应将下面的代码添加到 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 步 - 覆盖文档选择器的管理模板

  • 请查看 Customising admin templates 的文档,因为这一步您可能需要向 INSTALLED_APPS 添加更多应用程序。
  • 创建一个新文件 myapp/templates/wagtaildocs/widgets/document_chooser.htmltemplates 之后的部分在这里很关键,因为我们要覆盖和扩展这个确切的模板。
  • 在模板中,我们将扩展原始内容并覆盖块 chooser_attributes,因为这是添加 Chooser Modal 触发器使用的 data-chooser-url 的内容。
  • 重要提示:在此处重新启动您的开发服务器,因为您添加了新的模板覆盖。
  • 完成后,在浏览器中检查包含 'Choose a Document' 按钮的元素,您应该能够看到容器元素现在有一个 data-chooser-url,并在 [=140= 中添加了一个查询字符串] <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 查询字符串参数

  • 现在使用 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 隐藏该选项卡(块接受类名属性)。
  • 当用户在模态中搜索时,不幸的是,它不会接受 URL 以这种方式设置。
  • 该解决方案在各种版本中可能很脆弱,尤其是 CustomDocumentChooserAdapter,因此请务必关注 Wagtail 代码更改。

使用 wagtail-generic-chooser 提供更多自定义选择器模式工作方式的能力。

第 1 步 - 安装 wagtail-generic-chooser

  • 运行: pip install wagtail-generic-chooser
  • 然后将 generic_chooser 添加到您项目的 INSTALLED_APPS

第 2 步 - 设置选择器视图集

  • 类似于 setting up a Chooser view set
  • 上的文档说明
  • 确保我们可以通过创建扩展 ModelChooserMixin 的自定义 class 来处理 accept 参数,这意味着搜索时仍会传递该参数。
  • 添加 accept URL 参数的处理以有条件地过滤返回值。
  • 设置一个扩展 ModelChooserViewSet 的 class,它将处理 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
  • 类似于 Setting up of a model based Widget 创建一个扩展 AdminChooser 的 class。
  • __init__ 方法中,我们取出 accept kwarg,以便我们可以使用它来生成自定义 URL 参数。
  • 重写 get_edit_item_url 方法,该方法允许单击 选定的 文档进行编辑。
  • 覆盖在这里工作的``get_choose_modal_urlto append the URL query param (note: I could not getreverse`,没有更多的争论。

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

  • 此 class 扩展 ChooserBlock 并包装已创建的 RestrictedDocumentChooser 小部件。
  • __init__ 上,相同的 kwarg accept 被拉出并在创建时传递给 RestrictedDocumentChooser
  • 这个块可以通过调用它类似于任何其他块来使用,但是使用 kwarg acceptdoc_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)