• Stars
    star
    116
  • Rank 303,894 (Top 6 %)
  • Language
    Python
  • License
    BSD 3-Clause "New...
  • Created over 5 years ago
  • Updated 12 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A toolkit for custom chooser popups in Wagtail

wagtail-generic-chooser

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.

Requirements

Wagtail 4.1 or higher

Installation

Run: pip install wagtail-generic-chooser

Then add generic_chooser to your project's INSTALLED_APPS.

Usage

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.

Chooser views (model-based)

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 Person model:

# myapp/views.py

from django.utils.translation import gettext_lazy as _

from generic_chooser.views import ModelChooserViewSet

from bakerydemo.base.models import Person


class PersonChooserViewSet(ModelChooserViewSet):
    icon = 'user'
    model = Person
    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 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')

Chooser views (Django REST Framework-based)

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 gettext_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.

Creating objects within the chooser

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 gettext_lazy as _

from generic_chooser.views import DRFChooserMixin, DRFChooserViewSet


class PersonChooserMixin(DRFChooserMixin):
    def get_edit_item_url(self, item):
        # for Wagtail 4.x
        return reverse('wagtailsnippets_base_person:edit', args=(quote(item['id']), ))
        # for Wagtail <= 3.x
        # return reverse('wagtailsnippets:edit', args=('base', 'person', 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/person-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,
}

Customising chooser views

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 of ChooserMixin using a Django model as the data source.
  • DRFChooserMixin - implementation of ChooserMixin 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 tab
  • ModelChooserCreateTabMixin - version of ChooserCreateTabMixin for model forms
  • DRFChooserCreateTabMixin - version of ChooserCreateTabMixin for Django REST Framework
  • BaseChooseView - abstract class-based view handling the main chooser UI. Subclasses should extend this and include the mixins ChooserMixin, ChooserListingTabMixin and ChooserCreateTabMixin (or suitable subclasses of them).
  • ModelChooseView, DRFChooseView - model-based and DRF-based subclasses of BaseChooseView
  • BaseChosenView - class-based view that returns the chosen object as a JSON response
  • ModelChosenView, DRFChosenView - model-based and DRF-based subclasses of BaseChosenView
  • ChooserViewSet - common base implementation of ModelChooserViewSet and DRFChooserViewSet

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 gettext_lazy as _

from generic_chooser.views import ModelChooserMixin, ModelChooserViewSet

from bakerydemo.base.models import Person


class PersonChooserMixin(ModelChooserMixin):
    def get_edit_item_url(self, item):
        # for Wagtail 4.x
        return reverse('wagtailsnippets_base_person:edit', args=(quote(item.pk), ))
        # for Wagtail <= 3.x
        # return reverse('wagtailsnippets:edit', args=('base', 'person', quote(item.pk)))


class PersonChooserViewSet(ModelChooserViewSet):
    icon = 'user'
    model = Person
    page_title = _("Choose a person")
    per_page = 10
    order_by = 'first_name'

    chooser_mixin_class = PersonChooserMixin

Chooser widgets (model-based)

The generic_chooser.widgets module provides an AdminChooser widget to be subclassed. For example, a widget for the Person 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 gettext_lazy as _

from generic_chooser.widgets import AdminChooser

from bakerydemo.base.models import Person


class PersonChooser(AdminChooser):
    choose_one_text = _('Choose a person')
    choose_another_text = _('Choose another person')
    link_to_chosen_text = _('Edit this person')
    model = Person
    choose_modal_url_name = 'person_chooser:choose'
    icon = 'user'

    def get_edit_item_url(self, item):
        # for Wagtail 4.x
        return reverse('wagtailsnippets_base_person:edit', args=(quote(item.pk), ))
        # for Wagtail <= 3.x
        # return reverse('wagtailsnippets:edit', args=('base', 'person', quote(item.pk)))

This widget can now be used in a form:

from myapp.widgets import PersonChooser

class BlogPage(Page):
    author = models.ForeignKey(
        'base.Person', related_name='blog_posts',
        null=True, blank=True, on_delete=models.SET_NULL
    )

    content_panels = [
        FieldPanel('author', widget=PersonChooser),
    ]

Chooser widgets (Django Rest Framework-based)

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']

Chooser widgets (other data sources)

See the base class implementations in generic_chooser/widgets.py.

StreamField blocks

A chooser widget as defined above can be wrapped in Wagtail's ChooserBlock class to be used inside a StreamField. As of Wagtail 2.13, the block definition should be as follows:

from wagtail.blocks import ChooserBlock


class PersonChooserBlock(ChooserBlock):
    @cached_property
    def target_model(self):
        from .models import Person
        return Person

    @cached_property
    def widget(self):
        from .widgets import PersonChooser
        return PersonChooser()

    def get_form_state(self, value):
        return self.widget.get_value_data(value)

Limiting choices via linked fields

wagtail-generic-chooser provides a mechanism for limiting the options displayed in the chooser according to another input field on the calling page. For example, suppose the person model has a country field - we can then set up a page model with a country dropdown and a person chooser, where an editor first selects a country from the dropdown and then opens the person chooser to be presented with a list of people from that country.

First, we customise the chooser view to expose a country URL parameter; to do this, we define a custom chooser_mixin_class for the viewset to use, and override its get_unfiltered_object_list method to filter by the country parameter.

from generic_chooser.views import ModelChooserMixin, ModelChooserViewSet


class PersonChooserMixin(ModelChooserMixin):
    preserve_url_parameters = ['country',]  # preserve this URL parameter on pagination / search

    def get_unfiltered_object_list(self):
        objects = super().get_unfiltered_object_list()
        country = self.request.GET.get('country')
        if country:
            objects = objects.filter(country_id=country)
        return objects


class PersonChooserViewSet(ModelChooserViewSet):
    model = Person
    chooser_mixin_class = PersonChooserMixin

We now set up our chooser widget to inherit from LinkedFieldMixin:

from generic_chooser.widgets import AdminChooser, LinkedFieldMixin

class PersonChooser(LinkedFieldMixin, AdminChooser):
    icon = 'user'
    model = Person
    page_title = _("Choose a person")

This mixin allows us to pass a linked_fields dict when constructing a PersonChooser instance, specifying the URL parameters to pass to the chooser along with a CSS selector to indicate which field each one should be taken from.

class BlogPage(Page):
    country = models.ForeignKey(Country, null=True, blank=True, on_delete=models.SET_NULL)
    author = models.ForeignKey(Person, null=True, blank=True, on_delete=models.SET_NULL)

    content_panels = Page.content_panels + [
        FieldPanel('country'),
        FieldPanel('person', widget=PersonChooser(linked_fields={
            # pass the country selected in the id_country input to the person chooser
            # as a URL parameter `country`
            'country': '#id_country',
        })),
    ]

A number of other lookup mechanisms are available:

PersonChooser(linked_fields={
    'country': {'selector': '#id_country'}  # equivalent to 'country': '#id_country'
})

# Look up by ID
PersonChooser(linked_fields={
    'country': {'id': 'id_country'}
})

# Regexp match, for use in StreamFields and InlinePanels where IDs are dynamic:
# 1) Match the ID of the current widget's (the PersonChooser) against the regexp
#      '^id_blog_person_relationship-\d+-'
# 2) Append 'country' to the matched substring
# 3) Retrieve the input field with that ID
PersonChooser(linked_fields={
    'country': {'match': r'^id_blog_person_relationship-\d+-', 'append': 'country'},
})

More Repositories

1

wagtail

A Django content management system focused on flexibility and user experience
Python
17,837
star
2

bakerydemo

Next generation Wagtail demo, born in Reykjavik
Python
963
star
3

django-modelcluster

Django extension to allow working with 'clusters' of models as a single unit, independently of the database
Python
482
star
4

Willow

A wrapper that combines the functionality of multiple Python image libraries into one API
Python
274
star
5

wagtail-localize

Translation plugin for Wagtail CMS
Python
226
star
6

wagtail-bakery

A set of helpers for baking your Django Wagtail site out as flat files.
Python
156
star
7

telepath

A library for exchanging data between Python and JavaScript
Python
139
star
8

wagtail-ai

Get help with your Wagtail content using AI superpowers.
Python
136
star
9

wagtail-autocomplete

An Autocomplete edit handler for selecting Pages, Snippets, and more.
Python
119
star
10

wagtail-personalisation

Rule-based personalisation for Wagtail CMS
Python
117
star
11

queryish

A library for constructing queries on arbitrary data sources following Django's QuerySet API
Python
106
star
12

wagtailtrans

A Wagtail add-on for supporting multilingual sites
Python
104
star
13

wagtail-factories

Factory boy classes for wagtail
Python
101
star
14

wagtail-transfer

Content transfer for Wagtail
Python
88
star
15

docker-wagtail-develop

Shell
74
star
16

wagtail.org

Wagtail’s official marketing website
Python
66
star
17

rfcs

Wagtail RFCs
45
star
18

wagtail-review

A Wagtail extension for gathering annotations and feedback on pages before publication
Python
43
star
19

wagtail-airtable

Airtable import and export support for Wagtail pages and Django models.
Python
43
star
20

gsoc

Resources, activity, discussions for Wagtail’s participation to Google Summer of Code
Python
39
star
21

wagtail-live

High speed publishing from messaging apps to a Wagtail live blog. A GSoC 2021 project.
Python
37
star
22

nextjs-loves-wagtail

Tutorial: Next.js ❤️ Wagtail
Python
36
star
23

vagrant-wagtail-develop

A script to painlessly set up a Vagrant environment for development of Wagtail
Shell
36
star
24

wagtail-gitpod

Launch a ready-to-code Wagtail development environment with a single click.
30
star
25

guide

A website to teach Wagtail CMS to content editors, moderators and administrators.
Python
30
star
26

sphinx-wagtail-theme

Sphinx theme for Wagtail
SCSS
29
star
27

wagtail-streamfield-migration-toolkit

Python
25
star
28

wagtail-whoosh

Search backend for Wagtail CMS using Whoosh engine.
Python
24
star
29

outreachy

Resources, activity, discussions for Wagtail’s participation to Outreachy
20
star
30

areweheadlessyet

Are we headless yet?
TypeScript
19
star
31

wagtail-vector-index

Store Wagtail pages & Django models as embeddings in vector databases
Python
18
star
32

cookiecutter-wagtail-package

A cookiecutter template for building Wagtail add-on packages
Python
17
star
33

django-permissionedforms

A Django extension for creating forms that vary according to user permissions
Python
12
star
34

roadmap

Wagtail public roadmap
9
star
35

stylelint-config-wagtail

Shareable stylelint config for CSS and SCSS, following Wagtail’s code style.
JavaScript
9
star
36

wagtail-newsletter

Send email newsletters based on Wagtail content
Python
9
star
37

wagtail-multiple-chooser-panel

An InlinePanel variant allowing multiple items to be quickly selected
JavaScript
7
star
38

wagtail-localize-git

Translate Wagtail content using a git repository and PO files
Python
6
star
39

gitpod-wagtail-develop

Dockerfile
6
star
40

eslint-config-wagtail

Shareable ESLint config for Wagtail, based on airbnb/javascript.
JavaScript
6
star
41

wagtail-review-ui

Frontend UI for adding comments on Wagtail pages
TypeScript
4
star
42

wagtail-editable-help

Make help text editable in the Wagtail admin
Python
3
star
43

wagtail-hallo

Wagtail's legacy Hallo.js richtext editor
Python
3
star
44

accessibility

Documentation relating to accessibility efforts in Wagtail
HTML
3
star
45

workshop

2
star
46

how-to-run-a-wagtail-space

Advice for anyone running a community Wagtail event
2
star
47

gsod

Resources, activity, discussions for Wagtail’s participation to Google Season of Docs
2
star
48

telepath-unpack

JavaScript library for unpacking values that have been packed with telepath
JavaScript
2
star
49

your-first-wagtail-site

The solution for Wagtail’s official getting started tutorial
Python
1
star