wagtail-generic-chooser
provides base classes for building chooser popups and form widgets for the Wagtail admin, matching the look and feel of Wagtail's built-in choosers for pages, documents, snippets and images.
It differs from existing model chooser add-ons (Naeka/wagtailmodelchooser, neon-jungle/wagtailmodelchooser, springload/wagtailmodelchoosers) in that it is designed to be fully configurable through subclassing - in particular, it can be used on data sources other than Django models, such as REST API endpoints.
It is intended that wagtail-generic-chooser
will be expanded to cover all the functionality of Wagtail's built-in choosers, such as inline object creation forms, and will then be incorporated into Wagtail as the new base implementation of those built-in choosers - this will reduce code duplication and greatly simplify the process of building new admin apps.
Wagtail 2.4 or higher
Run: pip install wagtail-generic-chooser
Then add generic_chooser
to your project's INSTALLED_APPS
.
wagtail-generic-chooser
's functionality is split into two distinct components: chooser views (the URL endpoints that implement the modal interface for choosing an item) and chooser widgets (form elements that display the currently selected item, with a button that opens up the modal interface to choose a new one). Chooser views can be used independently of chooser widgets; they are used by rich text editors, for example.
The generic_chooser.views
module provides a viewset class ModelChooserViewSet
, which can be used to build a modal interface for choosing a Django model instance. Viewsets are Wagtail's way of grouping several related views into a single unit along with their URL configuration; this makes it possible to configure the overall behaviour of a workflow within Wagtail without having to know how that workflow breaks down into individual views.
At minimum, a chooser can be implemented by subclassing ModelChooserViewSet
and setting a model
attribute. Other attributes can be specified to customise the look and feel of the chooser, such as the heading icon and number of items per page. For example, to implement a chooser for bakerydemo's People
model:
# myapp/views.py
from django.utils.translation import ugettext_lazy as _
from generic_chooser.views import ModelChooserViewSet
from bakerydemo.base.models import People
class PersonChooserViewSet(ModelChooserViewSet):
icon = 'user'
model = People
page_title = _("Choose a person")
per_page = 10
order_by = 'first_name'
fields = ['first_name', 'last_name', 'job_title']
The viewset can then be registered through Wagtail's register_admin_viewset
hook:
# myapp/wagtail_hooks.py
from wagtail.core import hooks
from myapp.views import PersonChooserViewSet
@hooks.register('register_admin_viewset')
def register_person_chooser_viewset():
return PersonChooserViewSet('person_chooser', url_prefix='person-chooser')
The generic_chooser.views
module also provides a viewset class DRFChooserViewSet
for building choosers based on Django REST Framework API endpoints. Subclasses need to specify an api_base_url
attribute. For example, an API-based chooser for Wagtail's Page model can be implemented as follows:
from django.utils.translation import ugettext_lazy as _
from generic_chooser.views import DRFChooserViewSet
class APIPageChooserViewSet(DRFChooserViewSet):
icon = 'page'
page_title = _("Choose a page")
api_base_url = 'http://localhost:8000/api/v2/pages/'
edit_item_url_name = 'wagtailadmin_pages:edit'
is_searchable = True
per_page = 5
title_field_name = 'title'
This viewset can be registered through Wagtail's register_admin_viewset
hook as above.
Setting a form_class
attribute on the viewset will add a 'Create' tab containing that form, allowing users to create new objects within the chooser.
For a model-based chooser, this form class should be a ModelForm
, and the form will be shown for all users with 'create' permission on the corresponding model. As a shortcut, a fields
list can be specified in place of form_class
.
class PersonChooserViewSet(ModelChooserViewSet):
# ...
fields = ['first_name', 'last_name', 'job_title']
For a Django REST Framework-based chooser, form_class
must be defined explicitly (i.e. the fields
shortcut is not available) and the object will be created by sending a POST request to the API endpoint consisting of the form's cleaned_data
in JSON format. An API-based equivalent of PersonChooserViewSet
would be:
from django import forms
from django.contrib.admin.utils import quote
from django import forms
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from generic_chooser.views import DRFChooserMixin, DRFChooserViewSet
class PersonChooserMixin(DRFChooserMixin):
def get_edit_item_url(self, item):
return reverse('wagtailsnippets:edit', args=('base', 'people', quote(item['id'])))
def get_object_string(self, item):
return "%s %s" % (item['first_name'], item['last_name'])
class PersonForm(forms.Form):
first_name = forms.CharField(required=True)
last_name = forms.CharField(required=True)
job_title = forms.CharField(required=True)
class PersonChooserViewSet(DRFChooserViewSet):
icon = 'user'
api_base_url = 'http://localhost:8000/people-api/'
page_title = _("Choose a person")
per_page = 10
form_class = PersonForm
chooser_mixin_class = PersonChooserMixin
prefix = 'person-chooser'
This example requires the API to be configured with write access enabled, which can be done with a setting such as the following:
REST_FRAMEWORK = {
# Allow unauthenticated write access to the API. You probably don't want to this in production!
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny'
],
'DEFAULT_PAGINATION_CLASS': 'wagtail.api.v2.pagination.WagtailPagination',
'PAGE_SIZE': 100,
}
If the configuration options on ModelChooserViewSet
and DRFChooserViewSet
are not sufficient, it's possible to fully customise the chooser behaviour by overriding methods. To do this you'll need to work with the individual class-based views and mixins that make up the viewsets - this is best done by referring to the base implementations in generic_chooser/views.py
. The classes are:
ChooserMixin
- an abstract class providing helper methods shared by all views. These deal with data retrieval, and providing string and ID representations and URLs corresponding to the objects being chosen. To implement a chooser for a different data source besides Django models and Django REST Framework, you'll need to subclass this.ModelChooserMixin
- implementation ofChooserMixin
using a Django model as the data source.DRFChooserMixin
- implementation ofChooserMixin
using a Django REST Framework endpoint as the data source.ChooserListingTabMixin
- handles the behaviour and rendering of the results listing tab, including pagination and searching.ChooserCreateTabMixin
- handles the behaviour and rendering of the 'create' form tabModelChooserCreateTabMixin
- version ofChooserCreateTabMixin
for model formsDRFChooserCreateTabMixin
- version ofChooserCreateTabMixin
for Django REST FrameworkBaseChooseView
- abstract class-based view handling the main chooser UI. Subclasses should extend this and include the mixinsChooserMixin
,ChooserListingTabMixin
andChooserCreateTabMixin
(or suitable subclasses of them).ModelChooseView
,DRFChooseView
- model-based and DRF-based subclasses ofBaseChooseView
BaseChosenView
- class-based view that returns the chosen object as a JSON responseModelChosenView
,DRFChosenView
- model-based and DRF-based subclasses ofBaseChosenView
ChooserViewSet
- common base implementation ofModelChooserViewSet
andDRFChooserViewSet
For example, we may want to extend the PersonChooserViewSet above to return an 'edit this person' URL as part of its JSON response, pointing to the 'wagtailsnippets:edit'
view. Including an 'edit' URL in the response would normally be achieved by setting the edit_item_url_name
attribute on the viewset to a suitable URL route name, but 'wagtailsnippets:edit'
won't work here; this is because edit_item_url_name
expects it to take a single URL parameter, the ID, whereas the snippet edit view also needs to be passed the model's app name and model name. Instead, we can do this by overriding the get_edit_item_url
method on ModelChooserMixin
:
from django.contrib.admin.utils import quote
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from generic_chooser.views import ModelChooserMixin, ModelChooserViewSet
from bakerydemo.base.models import People
class PersonChooserMixin(ModelChooserMixin):
def get_edit_item_url(self, item):
return reverse('wagtailsnippets:edit', args=('base', 'people', quote(item.pk)))
class PersonChooserViewSet(ModelChooserViewSet):
icon = 'user'
model = People
page_title = _("Choose a person")
per_page = 10
order_by = 'first_name'
chooser_mixin_class = PersonChooserMixin
The generic_chooser.widgets
module provides an AdminChooser
widget to be subclassed. For example, a widget for the People
model, using the chooser views defined above, can be implemented as follows:
from django.contrib.admin.utils import quote
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from generic_chooser.widgets import AdminChooser
from bakerydemo.base.models import People
class PersonChooser(AdminChooser):
choose_one_text = _('Choose a person')
choose_another_text = _('Choose another person')
link_to_chosen_text = _('Edit this person')
model = People
choose_modal_url_name = 'person_chooser:choose'
def get_edit_item_url(self, item):
return reverse('wagtailsnippets:edit', args=('base', 'people', quote(item.pk)))
This widget can now be used in a form:
from myapp.widgets import PersonChooser
class BlogPage(Page):
author = models.ForeignKey(
'base.People', related_name='blog_posts',
null=True, blank=True, on_delete=models.SET_NULL
)
content_panels = [
FieldPanel('author', widget=PersonChooser),
]
generic_chooser.widgets
also provides a DRFChooser
base class for chooser widgets backed by Django Rest Framework API endpoints:
from generic_chooser.widgets import DRFChooser
class PageAPIChooser(DRFChooser):
choose_one_text = _('Choose a page')
choose_another_text = _('Choose another page')
link_to_chosen_text = _('Edit this page')
choose_modal_url_name = 'page_chooser:choose'
edit_item_url_name = 'wagtailadmin_pages:edit'
api_base_url = 'http://localhost:8000/api/v2/pages/'
def get_title(self, instance):
return instance['title']
See the base class implementations in generic_chooser/widgets.py
.