{"id":13530681,"url":"https://github.com/wagtail/wagtail-generic-chooser","last_synced_at":"2025-04-01T18:32:10.050Z","repository":{"id":38421250,"uuid":"177668509","full_name":"wagtail/wagtail-generic-chooser","owner":"wagtail","description":"A toolkit for custom chooser popups in Wagtail","archived":false,"fork":false,"pushed_at":"2025-01-20T13:03:56.000Z","size":132,"stargazers_count":118,"open_issues_count":14,"forks_count":25,"subscribers_count":20,"default_branch":"main","last_synced_at":"2025-03-17T11:06:51.317Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wagtail.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.txt","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-03-25T21:38:13.000Z","updated_at":"2025-02-06T07:10:22.000Z","dependencies_parsed_at":"2023-12-15T13:58:39.344Z","dependency_job_id":null,"html_url":"https://github.com/wagtail/wagtail-generic-chooser","commit_stats":{"total_commits":101,"total_committers":14,"mean_commits":7.214285714285714,"dds":"0.18811881188118806","last_synced_commit":"db66c35cd641cedcb3c4a771b859cfec4623ed00"},"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wagtail%2Fwagtail-generic-chooser","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wagtail%2Fwagtail-generic-chooser/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wagtail%2Fwagtail-generic-chooser/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wagtail%2Fwagtail-generic-chooser/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wagtail","download_url":"https://codeload.github.com/wagtail/wagtail-generic-chooser/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246691688,"owners_count":20818554,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-01T07:00:53.447Z","updated_at":"2025-04-01T18:32:09.683Z","avatar_url":"https://github.com/wagtail.png","language":"Python","funding_links":[],"categories":["Apps"],"sub_categories":["Widgets"],"readme":"# wagtail-generic-chooser\n\n**This package is deprecated.** As of Wagtail 5.2, Wagtail's built-in [ChooserViewSet](https://docs.wagtail.org/en/stable/extending/generic_views.html#chooserviewset) provides all the functionality of the `wagtail-generic-choooser` package and will receive improvements as part of Wagtail's overall development. We recommend migrating your use of `wagtail-generic-chooser` to Wagtail's `ChooserViewSet`. This package will only receive maintenance updates as required to address blocking bugs and ensure compatibility with new Wagtail versions.\n\n`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.\n\nIt differs from existing model chooser add-ons ([Naeka/wagtailmodelchooser](https://github.com/Naeka/wagtailmodelchooser/), [neon-jungle/wagtailmodelchooser](https://github.com/neon-jungle/wagtailmodelchooser), [springload/wagtailmodelchoosers](https://github.com/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.\n\n## Requirements\n\nWagtail 4.1 or higher\n\n## Installation\n\nRun: `pip install wagtail-generic-chooser`\n\nThen add `generic_chooser` to your project's `INSTALLED_APPS`.\n\n## Usage\n\n`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.\n\n### Chooser views (model-based)\n\nThe `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.\n\nAt 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](https://github.com/wagtail/bakerydemo)'s `Person` model:\n\n```python\n# myapp/views.py\n\nfrom django.utils.translation import gettext_lazy as _\n\nfrom generic_chooser.views import ModelChooserViewSet\n\nfrom bakerydemo.base.models import Person\n\n\nclass PersonChooserViewSet(ModelChooserViewSet):\n    icon = 'user'\n    model = Person\n    page_title = _(\"Choose a person\")\n    per_page = 10\n    order_by = 'first_name'\n    fields = ['first_name', 'last_name', 'job_title']\n```\n\nThe viewset can then be registered through Wagtail's `register_admin_viewset` hook:\n\n```python\n# myapp/wagtail_hooks.py\n\nfrom wagtail import hooks\n\nfrom myapp.views import PersonChooserViewSet\n\n\n@hooks.register('register_admin_viewset')\ndef register_person_chooser_viewset():\n    return PersonChooserViewSet('person_chooser', url_prefix='person-chooser')\n```\n\n### Chooser views (Django REST Framework-based)\n\nThe `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:\n\n```python\nfrom django.utils.translation import gettext_lazy as _\n\nfrom generic_chooser.views import DRFChooserViewSet\n\nclass APIPageChooserViewSet(DRFChooserViewSet):\n    icon = 'page'\n    page_title = _(\"Choose a page\")\n    api_base_url = 'http://localhost:8000/api/v2/pages/'\n    edit_item_url_name = 'wagtailadmin_pages:edit'\n    is_searchable = True\n    per_page = 5\n    title_field_name = 'title'\n```\n\nThis viewset can be registered through Wagtail's `register_admin_viewset` hook as above.\n\n\n### Creating objects within the chooser\n\nSetting a `form_class` attribute on the viewset will add a 'Create' tab containing that form, allowing users to create new objects within the chooser.\n\nFor 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`.\n\n```python\nclass PersonChooserViewSet(ModelChooserViewSet):\n    # ...\n    fields = ['first_name', 'last_name', 'job_title']\n```\n\nFor 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:\n\n```python\nfrom django import forms\nfrom django.contrib.admin.utils import quote\nfrom django import forms\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\n\nfrom generic_chooser.views import DRFChooserMixin, DRFChooserViewSet\n\n\nclass PersonChooserMixin(DRFChooserMixin):\n    def get_edit_item_url(self, item):\n        # for Wagtail 4.x\n        return reverse('wagtailsnippets_base_person:edit', args=(quote(item['id']), ))\n        # for Wagtail \u003c= 3.x\n        # return reverse('wagtailsnippets:edit', args=('base', 'person', quote(item['id'])))\n\n    def get_object_string(self, item):\n        return \"%s %s\" % (item['first_name'], item['last_name'])\n\n\nclass PersonForm(forms.Form):\n    first_name = forms.CharField(required=True)\n    last_name = forms.CharField(required=True)\n    job_title = forms.CharField(required=True)\n\n\nclass PersonChooserViewSet(DRFChooserViewSet):\n    icon = 'user'\n    api_base_url = 'http://localhost:8000/person-api/'\n    page_title = _(\"Choose a person\")\n    per_page = 10\n    form_class = PersonForm\n\n    chooser_mixin_class = PersonChooserMixin\n    prefix = 'person-chooser'\n```\n\nThis example requires the API to be configured with write access enabled, which can be done with a setting such as the following:\n\n```python\nREST_FRAMEWORK = {\n    # Allow unauthenticated write access to the API. You probably don't want to this in production!\n    'DEFAULT_PERMISSION_CLASSES': [\n        'rest_framework.permissions.AllowAny'\n    ],\n\n    'DEFAULT_PAGINATION_CLASS': 'wagtail.api.v2.pagination.WagtailPagination',\n    'PAGE_SIZE': 100,\n}\n```\n\n### Customising chooser views\n\nIf 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:\n\n* `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.\n* `ModelChooserMixin` - implementation of `ChooserMixin` using a Django model as the data source.\n* `DRFChooserMixin` - implementation of `ChooserMixin` using a Django REST Framework endpoint as the data source.\n* `ChooserListingTabMixin` - handles the behaviour and rendering of the results listing tab, including pagination and searching.\n* `ChooserCreateTabMixin` - handles the behaviour and rendering of the 'create' form tab\n* `ModelChooserCreateTabMixin` - version of `ChooserCreateTabMixin` for model forms\n* `DRFChooserCreateTabMixin` - version of `ChooserCreateTabMixin` for Django REST Framework\n* `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).\n* `ModelChooseView`, `DRFChooseView` - model-based and DRF-based subclasses of `BaseChooseView`\n* `BaseChosenView` - class-based view that returns the chosen object as a JSON response\n* `ModelChosenView`, `DRFChosenView` - model-based and DRF-based subclasses of `BaseChosenView`\n* `ChooserViewSet` - common base implementation of `ModelChooserViewSet` and `DRFChooserViewSet`\n\nFor 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`:\n\n```python\nfrom django.contrib.admin.utils import quote\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\n\nfrom generic_chooser.views import ModelChooserMixin, ModelChooserViewSet\n\nfrom bakerydemo.base.models import Person\n\n\nclass PersonChooserMixin(ModelChooserMixin):\n    def get_edit_item_url(self, item):\n        # for Wagtail 4.x\n        return reverse('wagtailsnippets_base_person:edit', args=(quote(item.pk), ))\n        # for Wagtail \u003c= 3.x\n        # return reverse('wagtailsnippets:edit', args=('base', 'person', quote(item.pk)))\n\n\nclass PersonChooserViewSet(ModelChooserViewSet):\n    icon = 'user'\n    model = Person\n    page_title = _(\"Choose a person\")\n    per_page = 10\n    order_by = 'first_name'\n\n    chooser_mixin_class = PersonChooserMixin\n```\n\n### Chooser widgets (model-based)\n\nThe `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:\n\n```python\nfrom django.contrib.admin.utils import quote\nfrom django.urls import reverse\nfrom django.utils.translation import gettext_lazy as _\n\nfrom generic_chooser.widgets import AdminChooser\n\nfrom bakerydemo.base.models import Person\n\n\nclass PersonChooser(AdminChooser):\n    choose_one_text = _('Choose a person')\n    choose_another_text = _('Choose another person')\n    link_to_chosen_text = _('Edit this person')\n    model = Person\n    choose_modal_url_name = 'person_chooser:choose'\n    icon = 'user'\n\n    def get_edit_item_url(self, item):\n        # for Wagtail 4.x\n        return reverse('wagtailsnippets_base_person:edit', args=(quote(item.pk), ))\n        # for Wagtail \u003c= 3.x\n        # return reverse('wagtailsnippets:edit', args=('base', 'person', quote(item.pk)))\n```\n\nThis widget can now be used in a form:\n\n```python\nfrom myapp.widgets import PersonChooser\n\nclass BlogPage(Page):\n    author = models.ForeignKey(\n        'base.Person', related_name='blog_posts',\n        null=True, blank=True, on_delete=models.SET_NULL\n    )\n\n    content_panels = [\n        FieldPanel('author', widget=PersonChooser),\n    ]\n```\n\n### Chooser widgets (Django Rest Framework-based)\n\n`generic_chooser.widgets` also provides a `DRFChooser` base class for chooser widgets backed by Django Rest Framework API endpoints:\n\n```python\nfrom generic_chooser.widgets import DRFChooser\n\nclass PageAPIChooser(DRFChooser):\n    choose_one_text = _('Choose a page')\n    choose_another_text = _('Choose another page')\n    link_to_chosen_text = _('Edit this page')\n    choose_modal_url_name = 'page_chooser:choose'\n    edit_item_url_name = 'wagtailadmin_pages:edit'\n    api_base_url = 'http://localhost:8000/api/v2/pages/'\n\n    def get_title(self, instance):\n        return instance['title']\n```\n\n### Chooser widgets (other data sources)\n\nSee the base class implementations in `generic_chooser/widgets.py`.\n\n\n### StreamField blocks\n\nA 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:\n\n```python\nfrom wagtail.blocks import ChooserBlock\n\n\nclass PersonChooserBlock(ChooserBlock):\n    @cached_property\n    def target_model(self):\n        from .models import Person\n        return Person\n\n    @cached_property\n    def widget(self):\n        from .widgets import PersonChooser\n        return PersonChooser()\n\n    def get_form_state(self, value):\n        return self.widget.get_value_data(value)\n```\n\n\n### Limiting choices via linked fields\n\nwagtail-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.\n\nFirst, 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.\n\n\n```python\nfrom generic_chooser.views import ModelChooserMixin, ModelChooserViewSet\n\n\nclass PersonChooserMixin(ModelChooserMixin):\n    preserve_url_parameters = ['country',]  # preserve this URL parameter on pagination / search\n\n    def get_unfiltered_object_list(self):\n        objects = super().get_unfiltered_object_list()\n        country = self.request.GET.get('country')\n        if country:\n            objects = objects.filter(country_id=country)\n        return objects\n\n\nclass PersonChooserViewSet(ModelChooserViewSet):\n    model = Person\n    chooser_mixin_class = PersonChooserMixin\n```\n\n\nWe now set up our chooser widget to inherit from `LinkedFieldMixin`:\n\n```python\nfrom generic_chooser.widgets import AdminChooser, LinkedFieldMixin\n\nclass PersonChooser(LinkedFieldMixin, AdminChooser):\n    icon = 'user'\n    model = Person\n    page_title = _(\"Choose a person\")\n```\n\nThis 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.\n\n```python\nclass BlogPage(Page):\n    country = models.ForeignKey(Country, null=True, blank=True, on_delete=models.SET_NULL)\n    author = models.ForeignKey(Person, null=True, blank=True, on_delete=models.SET_NULL)\n\n    content_panels = Page.content_panels + [\n        FieldPanel('country'),\n        FieldPanel('person', widget=PersonChooser(linked_fields={\n            # pass the country selected in the id_country input to the person chooser\n            # as a URL parameter `country`\n            'country': '#id_country',\n        })),\n    ]\n```\n\nA number of other lookup mechanisms are available:\n```python\nPersonChooser(linked_fields={\n    'country': {'selector': '#id_country'}  # equivalent to 'country': '#id_country'\n})\n\n# Look up by ID\nPersonChooser(linked_fields={\n    'country': {'id': 'id_country'}\n})\n\n# Regexp match, for use in StreamFields and InlinePanels where IDs are dynamic:\n# 1) Match the ID of the current widget's (the PersonChooser) against the regexp\n#      '^id_blog_person_relationship-\\d+-'\n# 2) Append 'country' to the matched substring\n# 3) Retrieve the input field with that ID\nPersonChooser(linked_fields={\n    'country': {'match': r'^id_blog_person_relationship-\\d+-', 'append': 'country'},\n})\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwagtail%2Fwagtail-generic-chooser","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwagtail%2Fwagtail-generic-chooser","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwagtail%2Fwagtail-generic-chooser/lists"}