A UI toolkit that extends Wagtail's admin list views and allows you to build custom filters, buttons, panels and more.
pip install wagtail-admin-list-controls
and add 'admin_list_controls'
to INSTALLED_APPS
in your settings.
The following example provides two text inputs that allow the user to perform different queries against
the name
fields on two different models.
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from admin_list_controls.views import ListControlsIndexView
from admin_list_controls.components import Button, Panel, Columns
from admin_list_controls.actions import SubmitForm
from admin_list_controls.filters import TextFilter
class IndexView(ListControlsIndexView):
def build_list_controls(self):
return [
Panel()(
Columns()(
TextFilter(
name="product_name",
label="Product name",
apply_to_queryset=lambda queryset, value: queryset.filter(name__icontains=value)
),
TextFilter(
name="creators_name",
label="Creator's name",
apply_to_queryset=lambda queryset, value: queryset.filter(created_by__name__icontains=value)
),
),
Button(action=SubmitForm())(
"Apply filters",
),
),
]
@modeladmin_register
class MyModelAdmin(ModelAdmin):
index_view_class = IndexView
# ...
The code above should give you a UI that looks like
To use this lib, you'll want to override the index view on your ModelAdmin
instance. To do this,
it's recommended to subclass admin_list_controls.views.ListControlsIndexView
. If you already have
a custom index view defined, you can use admin_list_controls.views.ListControlsIndexViewMixin
.
To define the controls used for the UI, define the build_list_controls
method on your index view and
return them in a list.
If you want to effect the queryset based on multiple controls, you can use the apply_list_controls_to_queryset
method.
Components are the basic building blocks of the UI. They are invoked with options, and a second call is used to define their children. For example:
# Render multiple components nested within other components
from admin_list_controls.components import Block, Icon, Text, Button
Block(style={'color': 'blue'})(
'This is some blue text before a button',
Button()(
Icon('icon icon-plus'),
'Click me!',
),
Text('This is some pink text after a button', style={'color': 'pink'}),
)
All components share some options:
extra_classes
: a string of classnames to add to the component. For example,'some_class and_another_class'
.style
: a dictionary of inline styles that are applied to the component. For example,{'color': 'red'}
.
Blocks are analogous to HTML divs. They are block elements that are mostly useful for tweaking the UI in small ways.
# Render a block element with a custom classname that uses floats to move content right.
from admin_list_controls.components import Block
Block(extra_classes='custom_class', style={'float': 'right'})(
# Child components ...
)
Spacers are a pre-configured Block component that provides some vertical space. Useful for moving text and buttons further down the UI.
# Render a block element that uses HTML float styling.
from admin_list_controls.components import Spacer
Spacer()(
# Child components ...
)
Column components divide their child components into two equally-spaced columns. You can use the column_count
argument to define how many columns are used.
# Render a block element that uses HTML float styling.
from admin_list_controls.components import Columns
# Two columns of components
Columns()(
# Child components ...
)
# Three columns of components
Columns(column_count=3)(
# Child components ...
)
Divider are analogous to a HTML hr element. They are used to draw horizontal lines that distinguish between areas of the UI.
from admin_list_controls.components import Divider
Divider()
Panels are container components that provide a white background and are best used to house filters or other components that involve a textual component.
They can be collapsed or expanded by default, by defining the collapsed
argument.
You can dynamically control the collapsed/expanded state of a panel by using the
from admin_list_controls.components import Panel
# An expanded panel
Panel()(
# Child components ...
)
# A panel collapsed by default, with the ref `my_panel`
Panel(collapsed=True, ref='foo')(
# Child components ...
)
# A button that will toggle the collapsed/expanded state of the `foo` Panel
from admin_list_controls.components import Button
from admin_list_controls.actions import TogglePanel
Button(action=TogglePanel(ref='foo'))(
'Click me to open and close the `foo` panel'
)
Icon components are used to insert icons, usually from wagtail's built-in icons are from a library such as
wagtailfontawesome
. They are invoked with a classname argument.
from admin_list_controls.components import Icon
Icon('icon icon-plus')
Text components are used to wrap inline text. You can invoke a Text
component to define options on it, or
you can define it as a string within another component.
Text can have a size
argument defined, it defaults to Text.MEDIUM
. There is also a Text.LARGE
constant
to display text in a size fit for a heading.
from admin_list_controls.components import Block, Text
Text('Some text that will display')
Text('Some large heading text', size=Text.LARGE)
# Text components can also be defined by using a normal string as a child of another component
Block()('Some more text will display')
Button components are used for adding dynamic behavior to your UI, such as selecting values, submitting forms, expanding panels, following links, etc.
Buttons accept an action
argument, which can be an Action instance or a list of Actions.
from admin_list_controls.components import Button
from admin_list_controls.actions import SetValue, SubmitForm, Link
Button()('Some basic button with text')
Button(action=[
SetValue(name='name_of_param', value='value_of_param'),
SubmitForm(),
])(
'This button sets a value and then submits the search form'
)
Button(action=Link('https://google.com'))(
'This button will send the user to Google'
)
Summary components are used to summarise the data selected in different filters and selectors. It renders multiple buttons that can be used to reset specific values or the entire set of form
from admin_list_controls.components import Summary
Summary()
Filters are a mixture of Django's widgets and form fields. They allow you to define a form widget and then apply the submitted values against the list view's queryset.
All components share some options:
name
: a string representing the name of the GET param used by the filter.label
: a string representing the label of the filter.apply_to_queryset
: a function that accepts a two arguments, a queryset and the filter's selected value. If a filter has a value, this method will be called. If no value has been submitted and no default has been defined, the function will not be called.default_value
: a default value to use if no value has been submitted.
A textual input, comparable to an <input type="text">
.
from admin_list_controls.filters import TextFilter
TextFilter(
name='name',
label='Name',
apply_to_queryset=lambda queryset, value: queryset.filter(name__icontains=value),
)
A checkbox input.
Note that apply_to_queryset
is only called if a truthy value has been submitted.
from admin_list_controls.filters import BooleanFilter
BooleanFilter(
name='is_selected',
label='Is selected',
apply_to_queryset=lambda queryset, _: queryset.filter(is_selected=True),
)
A radio button choice selector. RadioFilter values cannot be cleared, so you will probably want to specify a default value with an opt-out choice.
from admin_list_controls.filters import RadioFilter
RadioFilter(
name='color',
label='Color',
default_value='',
choices=(
('', 'Any'),
('red', 'Red'),
('blue', 'Blue'),
('green', 'Green'),
),
apply_to_queryset=lambda queryset, value: queryset.filter(color=value) if value else queryset,
)
A dropdown choice selector. ChoiceFilters can have their values cleared.
The optional argument multiple
indicates if the widget should allow multiple values.
from admin_list_controls.filters import ChoiceFilter
# Single choice
ChoiceFilter(
name='color',
label='Color',
choices=(
('red', 'Red'),
('blue', 'Blue'),
('green', 'Green'),
),
apply_to_queryset=lambda queryset, value: queryset.filter(color=value),
)
# Multiple choice
ChoiceFilter(
name='color',
label='Color',
multiple=True,
choices=(
('red', 'Red'),
('blue', 'Blue'),
('green', 'Green'),
),
apply_to_queryset=lambda queryset, values: queryset.filter(color__in=values),
)
Selectors are buttons that are used to toggle form values and then effect the view. A selector will
apply its effects if its value
is passed in the GET params.
Selectors accept a boolean value for the is_default
param. Those with a truthy value will be
selected without any submitted data.
SortSelectors are used to switch between different sorting methods on the queryset. SortSelectors use
the sort
GET param by default, but this can be changed with the name
argument to the instance.
from admin_list_controls.selectors import SortSelector
# The default sorting method, will be applied if none are selected
SortSelector(
value='name_sort_asc',
is_default=True,
apply_to_queryset=lambda queryset: queryset.order_by('name')
)(
'Sort by name A-Z'
)
SortSelector(
value='name_sort_desc',
apply_to_queryset=lambda queryset: queryset.order_by('-name')
)(
'Sort by name Z-A'
)
LayoutSelectors are used to switch between different display styles on the list view's results.
They accept an optional template
argument which should be a path to a django template.
from admin_list_controls.selectors import LayoutSelector
# The default layout to use
LayoutSelector(
value='list_view',
is_default=True,
)(
'List view'
)
LayoutSelector(
value='grid_view',
template='path/to/template.html'
)(
'Grid view'
)
Actions are dynamic behaviours that can be added to buttons as the action
argument. They can be passed as single
action instance, or a list of actions that will be applied sequentially.
from admin_list_controls.components import Button
from admin_list_controls.actions import SubmitForm, SetValue, Link
Button(action=SubmitForm())(
'Submit the form and reloads the result list'
)
Button(action=[
SetValue(
name='some_param',
value='some_value',
),
SubmitForm()
])(
'Sets a value and then submits it'
)
Button(action=Link('https://google.com'))(
'Sends the user to Google'
)
Used to set form values.
from admin_list_controls.actions import SetValue
SetValue(
name='some_param',
value='some_value',
)
Used to remove values from the form. If multiple values have been entered for a certain name
, this
can be used to selectively remove a single value.
from admin_list_controls.actions import RemoveValue
RemoveValue(
name='some_param',
value='some_value',
)
Used to send the users browser to a certain url.
from admin_list_controls.actions import Link
Link(url='https://google.com')
Used to toggle a panel between collapsed and expanded states. The ref
argument
should match the ref
on a Panel declaration.
from admin_list_controls.actions import TogglePanel
TogglePanel(ref='some_panel_ref')
Used to collapse a panel. The ref
argument should match the ref
on a Panel declaration.
from admin_list_controls.actions import CollapsePanel
CollapsePanel(ref='some_panel_ref')
Used to clear Wagtail's built-in search input.
from admin_list_controls.actions import ClearSearchInput
ClearSearchInput()
Used to submit the form with any data that has been added to it.
from admin_list_controls.actions import SubmitForm
SubmitForm()
This library emerged from a large build that required an admin list view with an exhaustive set of filters and the ability for users to change the ordering of the results. The filters would need to perform exact and substring matching against textual fields, as well as exact matches on boolean and choice fields. Some of the sorts applied to order the results also relied on complicated querying and conditional behaviours. In some extreme conditions, certain combinations of filters and sorts would require distinct code paths.
We initially attempted to use Wagtail's built-in searching and filtering features, but they were found to be too limiting for our use-cases and resulted in a non-optimal experience for users. Third-party libraries were investigated, but the ecosystem doesn't have much covering the space.
Somewhat reluctantly, this library was built to cover our needs. Now that the dust has settled and the code has stabilised, we're finding increasing numbers of use-cases for it.
# Frontend
npm install
npm run build
# Backend
virtualenv venv
source venv/bin/activate
pip install -r requirements.txt
./test_project/manage.py migrate
./test_project/manage.py test admin_list_controls
# Development (file watchers, no optimisation, etc)
npm run build-dev
# Production/release
npm run build
npm run build
rm -rf dist/
python setup.py sdist bdist_wheel
twine upload dist/*