Add documentation for non-model choosers using queryish (#10863)

pull/10759/merge
Matt Westcott 2023-09-06 15:58:17 +01:00
rodzic 0cea5a70d4
commit b6bd047598
3 zmienionych plików z 42 dodań i 151 usunięć

Wyświetl plik

@ -29,6 +29,7 @@ Changelog
* Fix: Filter out comments on Page editing counts that do not correspond to a valid field / block path on the page such as when a field has been removed (Matt Westcott)
* Docs: Document `WAGTAILADMIN_BASE_URL` on "Integrating Wagtail into a Django project" page (Shreshth Srivastava)
* Docs: Replace incorrect screenshot for authors listing on tutorial (Shreshth Srivastava)
* Docs: Add documentation for building non-model-based choosers using the _queryish_ library (Matt Westcott)
* Maintenance: Fix snippet search test to work on non-fallback database backends (Matt Westcott)
* Maintenance: Update Eslint, Prettier, Jest, a11y-dialog, axe-core and js-cookie npm packages (LB (Ben) Johnston)
* Maintenance: Add npm scripts for TypeScript checks and formatting SCSS files (LB (Ben) Johnston)

Wyświetl plik

@ -156,171 +156,60 @@ PersonChooserWidget(linked_fields={
## Chooser viewsets for non-model datasources
While the generic chooser views are primarily designed to use Django models as the data source, choosers based on other sources such as REST API endpoints can be implemented by overriding the individual methods that deal with data retrieval.
Within `wagtail.admin.views.generic.chooser`:
- `BaseChooseView.get_object_list()` - returns a list of records to be displayed in the chooser. (In the default implementation, this is a Django QuerySet, and the records are model instances.)
- `BaseChooseView.columns` - a list of `wagtail.admin.ui.tables.Column` objects specifying the fields of the record to display in the final table
- `BaseChooseView.apply_object_list_ordering(objects)` - given a list of records as returned from `get_object_list`, returns the list with the desired ordering applied
- `ChosenViewMixin.get_object(pk)` - returns the record identified by the given primary key
- `ChosenResponseMixin.get_chosen_response_data(item)` - given a record, returns the dictionary of data that will be passed back to the chooser widget to populate it (consisting of items `id` and `title`, unless the chooser widget's JavaScript has been customised)
Within `wagtail.admin.widgets`:
- `BaseChooser.get_instance(value)` - given a value that may be a record, a primary key, or None, returns the corresponding record or None
- `BaseChooser.get_value_data_from_instance(item)` - given a record, returns the dictionary of data that will populate the chooser widget (consisting of items `id` and `title`, unless the widget's JavaScript has been customised)
For example, the following code will implement a chooser that runs against a JSON endpoint for the User model at `http://localhost:8000/api/users/`, set up with Django REST Framework using the default configuration and no pagination:
While the generic chooser views are primarily designed to use Django models as the data source, choosers based on other sources such as REST API endpoints can be implemented through the use of the [queryish](https://pypi.org/project/queryish/) library, which allows any data source to be wrapped in a Django QuerySet-like interface. This can then be passed to ChooserViewSet like a normal model. For example, the Pokemon example from the _queryish_ documentation could be made into a chooser as follows:
```python
from django.views.generic.base import View
import requests
# views.py
from wagtail.admin.ui.tables import Column, TitleColumn
from wagtail.admin.views.generic.chooser import (
BaseChooseView, ChooseViewMixin, ChooseResultsViewMixin, ChosenResponseMixin, ChosenViewMixin, CreationFormMixin
)
import re
from queryish.rest import APIModel
from wagtail.admin.viewsets.chooser import ChooserViewSet
from wagtail.admin.widgets import BaseChooser
class BaseUserChooseView(BaseChooseView):
@property
def columns(self):
return [
TitleColumn(
"title",
label="Title",
accessor='username',
id_accessor='id',
url_name=self.chosen_url_name,
link_attrs={"data-chooser-modal-choice": True},
),
Column(
"email", label="Email", accessor="email"
)
]
class Pokemon(APIModel):
class Meta:
base_url = "https://pokeapi.co/api/v2/pokemon/"
detail_url = "https://pokeapi.co/api/v2/pokemon/%s/"
fields = ["id", "name"]
pagination_style = "offset-limit"
verbose_name_plural = "pokemon"
def get_object_list(self):
r = requests.get("http://localhost:8000/api/users/")
r.raise_for_status()
results = r.json()
return results
@classmethod
def from_query_data(cls, data):
return cls(
id=int(re.match(r'https://pokeapi.co/api/v2/pokemon/(\d+)/', data['url']).group(1)),
name=data['name'],
)
def apply_object_list_ordering(self, objects):
return objects
@classmethod
def from_individual_data(cls, data):
return cls(
id=data['id'],
name=data['name'],
)
def __str__(self):
return self.name
class UserChooseView(ChooseViewMixin, CreationFormMixin, BaseUserChooseView):
pass
class PokemonChooserViewSet(ChooserViewSet):
model = Pokemon
choose_one_text = "Choose a pokemon"
choose_another_text = "Choose another pokemon"
class UserChooseResultsView(ChooseResultsViewMixin, CreationFormMixin, BaseUserChooseView):
pass
pokemon_chooser_viewset = PokemonChooserViewSet("pokemon_chooser")
class UserChosenViewMixin(ChosenViewMixin):
def get_object(self, pk):
r = requests.get("http://localhost:8000/api/users/%d/" % int(pk))
r.raise_for_status()
return r.json()
# wagtail_hooks.py
from wagtail import hooks
from .views import pokemon_chooser_viewset
class UserChosenResponseMixin(ChosenResponseMixin):
def get_chosen_response_data(self, item):
return {
"id": item["id"],
"title": item["username"],
}
class UserChosenView(UserChosenViewMixin, UserChosenResponseMixin, View):
pass
class BaseUserChooserWidget(BaseChooser):
def get_instance(self, value):
if value is None:
return None
elif isinstance(value, dict):
return value
else:
r = requests.get("http://localhost:8000/api/users/%d/" % int(value))
r.raise_for_status()
return r.json()
def get_value_data_from_instance(self, instance):
return {
"id": instance["id"],
"title": instance["username"],
}
class UserChooserViewSet(ChooserViewSet):
icon = "user"
choose_one_text = "Choose a user"
choose_another_text = "Choose another user"
edit_item_text = "Edit this user"
choose_view_class = UserChooseView
choose_results_view_class = UserChooseResultsView
chosen_view_class = UserChosenView
base_widget_class = BaseUserChooserWidget
user_chooser_viewset = UserChooserViewSet("user_chooser", url_prefix="user-chooser")
```
If the data source implements its own pagination - meaning that the pagination mechanism built into the chooser should be bypassed - the `BaseChooseView.get_results_page(request)` method can be overridden instead of `get_object_list`. This should return an instance of `django.core.paginator.Page`. For example, if the API in the above example followed the conventions of the Wagtail API, implementing pagination with `offset` and `limit` URL parameters and returning a dict consisting of `meta` and `results`, the `BaseUserChooseView` implementation could be modified as follows:
```python
from django.core.paginator import Page, Paginator
class APIPaginator(Paginator):
"""
Customisation of Django's Paginator class for use when we don't want it to handle
slicing on the result set, but still want it to generate the page numbering based
on a known result count.
"""
def __init__(self, count, per_page, **kwargs):
self._count = int(count)
super().__init__([], per_page, **kwargs)
@property
def count(self):
return self._count
class BaseUserChooseView(BaseChooseView):
@property
def columns(self):
return [
TitleColumn(
"title",
label="Title",
accessor='username',
id_accessor='id',
url_name=self.chosen_url_name,
link_attrs={"data-chooser-modal-choice": True},
),
Column(
"email", label="Email", accessor="email"
)
]
def get_results_page(self, request):
try:
page_number = int(request.GET.get('p', 1))
except ValueError:
page_number = 1
r = requests.get("http://localhost:8000/api/users/", params={
'offset': (page_number - 1) * self.per_page,
'limit': self.per_page,
})
r.raise_for_status()
result = r.json()
paginator = APIPaginator(result['meta']['total_count'], self.per_page)
page = Page(result['items'], page_number, paginator)
return page
@hooks.register("register_admin_viewset")
def register_pokemon_chooser_viewset():
return pokemon_chooser_viewset
```

Wyświetl plik

@ -45,6 +45,7 @@ depth: 1
* Document `WAGTAILADMIN_BASE_URL` on "Integrating Wagtail into a Django project" page (Shreshth Srivastava)
* Replace incorrect screenshot for authors listing on tutorial (Shreshth Srivastava)
* Add documentation for building non-model-based choosers using the _queryish_ library (Matt Westcott)
### Maintenance