From 89e2917b911f6b93ad98d2a9f02c47cf0fb65cbb Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Thu, 7 Jul 2022 17:44:31 +0100 Subject: [PATCH] Support passing model to chooser viewset / views / widgets as a string --- wagtail/admin/views/generic/chooser.py | 42 +++++++++++++++++--------- wagtail/admin/viewsets/chooser.py | 7 ++++- wagtail/admin/widgets/chooser.py | 10 ++++-- wagtail/utils/registry.py | 4 ++- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/wagtail/admin/views/generic/chooser.py b/wagtail/admin/views/generic/chooser.py index 9bd7ced4e6..6f4938a240 100644 --- a/wagtail/admin/views/generic/chooser.py +++ b/wagtail/admin/views/generic/chooser.py @@ -10,6 +10,7 @@ from django.http import Http404 from django.template.loader import render_to_string from django.template.response import TemplateResponse from django.urls import reverse +from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ from django.views.generic.base import ContextMixin, View @@ -23,6 +24,7 @@ from wagtail.admin.forms.choosers import ( ) from wagtail.admin.modal_workflow import render_modal_workflow from wagtail.admin.ui.tables import Table, TitleColumn +from wagtail.coreutils import resolve_model_string from wagtail.models import CollectionMember, TranslatableMixin from wagtail.permission_policies import BlanketPermissionPolicy, ModelPermissionPolicy from wagtail.search.index import class_is_indexed @@ -49,13 +51,25 @@ class ModalPageFurnitureMixin(ContextMixin): return context -class BaseChooseView(ModalPageFurnitureMixin, ContextMixin, View): +class ModelLookupMixin: + """ + Allows a class to have a `model` attribute, which can be set as either a model class or a string, + and then retrieve it as `model_class` to consistently get back a model class + """ + + model = None + + @cached_property + def model_class(self): + return resolve_model_string(self.model) + + +class BaseChooseView(ModalPageFurnitureMixin, ModelLookupMixin, ContextMixin, View): """ Provides common functionality for views that present a (possibly searchable / filterable) list of objects to choose from """ - model = None per_page = 10 ordering = None chosen_url_name = None @@ -68,7 +82,7 @@ class BaseChooseView(ModalPageFurnitureMixin, ContextMixin, View): construct_queryset_hook_name = None def get_object_list(self): - return self.model.objects.all() + return self.model_class.objects.all() def apply_object_list_ordering(self, objects): if isinstance(self.ordering, (list, tuple)): @@ -89,11 +103,11 @@ class BaseChooseView(ModalPageFurnitureMixin, ContextMixin, View): return self.filter_form_class else: bases = [BaseFilterForm] - if class_is_indexed(self.model): + if class_is_indexed(self.model_class): bases.insert(0, SearchFilterMixin) - if issubclass(self.model, CollectionMember): + if issubclass(self.model_class, CollectionMember): bases.insert(0, CollectionFilterMixin) - if issubclass(self.model, TranslatableMixin): + if issubclass(self.model_class, TranslatableMixin): bases.insert(0, LocaleFilterMixin) return type( @@ -162,7 +176,7 @@ class BaseChooseView(ModalPageFurnitureMixin, ContextMixin, View): raise NotImplementedError() -class CreationFormMixin: +class CreationFormMixin(ModelLookupMixin): """ Provides a form class for creating new objects """ @@ -180,8 +194,8 @@ class CreationFormMixin: def get_permission_policy(self): if self.permission_policy: return self.permission_policy - elif self.model: - return ModelPermissionPolicy(self.model) + elif self.model_class: + return ModelPermissionPolicy(self.model_class) else: return BlanketPermissionPolicy(None) @@ -195,7 +209,9 @@ class CreationFormMixin: return self.creation_form_class elif self.form_fields is not None or self.exclude_form_fields is not None: return modelform_factory( - self.model, fields=self.form_fields, exclude=self.exclude_form_fields + self.model_class, + fields=self.form_fields, + exclude=self.exclude_form_fields, ) def get_creation_form_kwargs(self): @@ -344,16 +360,14 @@ class ChosenResponseMixin: ) -class ChosenViewMixin: +class ChosenViewMixin(ModelLookupMixin): """ A view that takes an object ID in the URL and returns a modal workflow response indicating that object has been chosen """ - model = None - def get_object(self, pk): - return self.model.objects.get(pk=pk) + return self.model_class.objects.get(pk=pk) def get(self, request, pk): try: diff --git a/wagtail/admin/viewsets/chooser.py b/wagtail/admin/viewsets/chooser.py index 3e9f30ea6e..0bdc1c7364 100644 --- a/wagtail/admin/viewsets/chooser.py +++ b/wagtail/admin/viewsets/chooser.py @@ -124,8 +124,13 @@ class ChooserViewSet(ViewSet): """ Returns the form widget class for this chooser. """ + if isinstance(self.model, str): + model_name = self.model.split(".")[-1] + else: + model_name = self.model.__name__ + return type( - "%sChooserWidget" % self.model.__name__, + "%sChooserWidget" % model_name, (self.base_widget_class,), { "model": self.model, diff --git a/wagtail/admin/widgets/chooser.py b/wagtail/admin/widgets/chooser.py index 9aa091ac55..720396b838 100644 --- a/wagtail/admin/widgets/chooser.py +++ b/wagtail/admin/widgets/chooser.py @@ -6,6 +6,7 @@ from django.core.exceptions import ImproperlyConfigured from django.forms import widgets from django.template.loader import render_to_string from django.urls import reverse +from django.utils.functional import cached_property from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -99,6 +100,7 @@ class BaseChooser(widgets.Input): ) icon = None classname = None + model = None # when looping over form fields, this one should appear in visible_fields, not hidden_fields # despite the underlying input being type="hidden" @@ -121,6 +123,10 @@ class BaseChooser(widgets.Input): self.show_clear_link = kwargs.pop("show_clear_link") super().__init__(**kwargs) + @cached_property + def model_class(self): + return resolve_model_string(self.model) + def value_from_datadict(self, data, files, name): # treat the empty string as None result = super().value_from_datadict(data, files, name) @@ -176,10 +182,10 @@ class BaseChooser(widgets.Input): """ if value is None: return None - elif isinstance(value, self.model): + elif isinstance(value, self.model_class): return value else: # assume instance ID - return self.model.objects.get(pk=value) + return self.model_class.objects.get(pk=value) def get_display_title(self, instance): """ diff --git a/wagtail/utils/registry.py b/wagtail/utils/registry.py index 86ce90e44b..9eb1215417 100644 --- a/wagtail/utils/registry.py +++ b/wagtail/utils/registry.py @@ -1,6 +1,8 @@ from django.core.exceptions import ImproperlyConfigured from django.db import models +from wagtail.coreutils import resolve_model_string + class ObjectTypeRegistry: """ @@ -66,7 +68,7 @@ class ModelFieldRegistry(ObjectTypeRegistry): def register(self, field_class, to=None, value=None, exact_class=False): if to: if field_class == models.ForeignKey: - self.values_by_fk_related_model[to] = value + self.values_by_fk_related_model[resolve_model_string(to)] = value else: raise ImproperlyConfigured( "The 'to' argument on ModelFieldRegistry.register is only valid for ForeignKey fields"