Wagtail's Chooser Modal system works a bit differently to a normal Django widget (Class used to render html content of a field), the widget itself mostly renders a button 'Choose Document' and the button then triggers a modal which is a separate view and template.
As you noted, the construct_document_chooser_queryset
hook can limit the results shown in these modals but only with access to the request object of the page being viewed, not of the Widget used to trigger that modal.
There is a way to get some limited desired functionality but it will not work for search results and will not restrict any additional uploads to that file type.
Step 1 - Create a custom DocumentChooserBlock
- This block Class extends the
DocumentChooserBlock
and has a custom __init__
method that pulls out a kwarg accept
and assigns it to the widget attrs.
- Django Widgets all have the ability to accept
attrs
and these are output on the rendered HTML element, our custom Block assigns the value we want to the widget so that other methods have access to it.
- This block can be used in the same way as any other block but will use a kwarg of accept'
doc_block = SpecificDocumentChooserBlock(accept="svg,md") # uses accept kwarg
- You can confirm this is working by viewing the DOM (inspect element in the browser), just after the 'Choose a Document', there will be a hidden attribute with something like
<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
Step 2 - ensure the widget attrs get passed to the modal trigger
- Unfortunately, the data used for the query URL is not located on the above HTML input element but instead on the container div, see the
data-chooser-url
on the document-chooser
block div.
- This data attribute is generated by a system called Telepath.
- The main part to understand is that there is a Class used to tell the browser what to render based on the Widget and this does not, by default, pass down the widget attrs.
- The code below should be added to
wagtail_hooks.py
, as we will need that file anyway and we know it only gets run once at runtime.
- The line
widget.render_html(
is the key part, we are using the **
syntax to unpack any widget.attrs values (one will be the accept
item set by our custom block).
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)
Step 3 - Override the admin template for the document chooser
- Please have a look at the docs for Customising admin templates as you may need to add some more apps to your
INSTALLED_APPS
for this step.
- Create a new file
myapp/templates/wagtaildocs/widgets/document_chooser.html
, the part after templates
is critical here as we want to override and extend this exact template.
- In the template we will extend the original and override the block
chooser_attributes
as this is what adds the data-chooser-url
used by the Chooser Modal trigger.
- Important: Restart your dev server here as you have added a new template override.
- Once complete, in your browser inspect the element containing the 'Choose a Document' button you should be able to see the container element now has a
data-chooser-url
with a query string added to the 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 %}
Step 4 - handle the accept
query string param
- Using the
construct_document_chooser_queryset
now, it will be possible to pull in the GET param accept
and parse it to generate a different set of document results.
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
Caveats
- This solution does not block the user from uploading only specific files within the modal, but you could explore hiding that tab with some CSS (Blocks accept a classname prop).
- When the user searches in the modal, it will not honour the URL set up this way unfortunately.
- The solution could be fragile over various releases, especially the
CustomDocumentChooserAdapter
so be sure to keep an eye on the Wagtail code changes.