Browse Source

Provide a documented register_form_field_override method for apps to register their own form fields with WagtailAdminModelForm

Matt Westcott 3 years ago
parent
commit
786a4a3384

+ 58 - 0
docs/extending/forms.md

@@ -0,0 +1,58 @@
+# Forms
+
+[Django's forms framework](https://docs.djangoproject.com/en/stable/topics/forms/) can be used within Wagtail admin views just like in any other Django app. However, Wagtail also provides various admin-specific form widgets, such as date/time pickers and choosers for pages, documents, images and snippets. By constructing forms using `wagtail.admin.forms.models.WagtailAdminModelForm` as the base class instead of `django.forms.models.ModelForm`, the most appropriate widget will be selected for each model field. For example, given the model and form definition:
+
+```python
+from django.db import models
+
+from wagtail.admin.forms.models import WagtailAdminModelForm
+from wagtail.images.models import Image
+
+
+class FeaturedImage(models.Model):
+    date = models.DateField()
+    image = models.ForeignKey(Image, on_delete=models.CASCADE)
+
+
+class FeaturedImageForm(WagtailAdminModelForm):
+    class Meta:
+        model = FeaturedImage
+```
+
+the `date` and `image` fields on the form will use a date picker and image chooser widget respectively.
+
+
+## Defining admin form widgets
+
+If you have implemented a form widget of your own, you can configure `WagtailAdminModelForm` to select it for a given model field type. This is done by calling the `wagtail.admin.forms.models.register_form_field_override` function, typically in an `AppConfig.ready` method.
+
+```eval_rst
+.. function:: register_form_field_override(model_field_class, to=None, override=None, exact_class=False)
+
+   Specify a set of options that will override the form field's defaults when ``WagtailAdminModelForm`` encounters a given model field type.
+
+   :param model_field_class: Specifies a model field class, such as ``models.CharField``; the override will take effect on fields that are instances of this class.
+   :param to: For ``ForeignKey`` fields, indicates the model that the field must point to for the override to take effect.
+   :param override: A dict of keyword arguments to be passed to the form field's ``__init__`` method, such as ``widget``.
+   :param exact_class: If true, the override will only take effect for model fields that are of the exact type given by ``model_field_class``, and not a subclass of it.
+```
+
+For example, if the app `wagtail.videos` implements a `Video` model and a `VideoChooser` form widget, the following AppConfig definition will ensure that `WagtailAdminModelForm` selects `VideoChooser` as the form widget for any foreign keys pointing to `Video`:
+
+```python
+from django.apps import AppConfig
+from django.db.models import ForeignKey
+
+
+class WagtailVideosAppConfig(AppConfig):
+    name = 'wagtail.videos'
+    label = 'wagtailvideos'
+
+    def ready(self):
+        from wagtail.admin.forms.models import register_form_field_override
+        from .models import Video
+        from .widgets import VideoChooser
+        register_form_field_override(ForeignKey, to=Video, override={'widget': VideoChooser})
+```
+
+Wagtail's edit views for pages, snippets and ModelAdmin use `WagtailAdminModelForm` as standard, so this change will take effect across the Wagtail admin; a foreign key to `Video` on a page model will automatically use the `VideoChooser` widget, with no need to specify this explicitly.

+ 1 - 0
docs/extending/index.rst

@@ -11,6 +11,7 @@ This section describes the various mechanisms that can be used to integrate your
 
     admin_views
     template_components
+    forms
     adding_reports
     custom_tasks
     audit_log

+ 27 - 0
wagtail/admin/forms/models.py

@@ -1,5 +1,6 @@
 import copy
 
+from django.core.exceptions import ImproperlyConfigured
 from django.db import models
 from modelcluster.forms import ClusterForm, ClusterFormMetaclass
 from taggit.managers import TaggableManager
@@ -49,6 +50,32 @@ DIRECT_FORM_FIELD_OVERRIDES = {
 }
 
 
+def register_form_field_override(
+    db_field_class, to=None, override=None, exact_class=False
+):
+    """
+    Define parameters for form fields to be used by WagtailAdminModelForm for a given
+    database field.
+    """
+
+    if override is None:
+        raise ImproperlyConfigured(
+            "register_form_field_override must be passed an 'override' keyword argument"
+        )
+
+    if to:
+        if db_field_class == models.ForeignKey:
+            FOREIGN_KEY_MODEL_OVERRIDES[to] = override
+        else:
+            raise ImproperlyConfigured(
+                "The 'to' argument on register_form_field_override is only valid for ForeignKey fields"
+            )
+    elif exact_class:
+        DIRECT_FORM_FIELD_OVERRIDES[db_field_class] = override
+    else:
+        FORM_FIELD_OVERRIDES[db_field_class] = override
+
+
 # Callback to allow us to override the default form fields provided for each model field.
 def formfield_for_dbfield(db_field, **kwargs):
     # adapted from django/contrib/admin/options.py

+ 8 - 5
wagtail/documents/apps.py

@@ -1,6 +1,9 @@
 from django.apps import AppConfig
+from django.db.models import ForeignKey
 from django.utils.translation import gettext_lazy as _
 
+from . import get_document_model
+
 
 class WagtailDocsAppConfig(AppConfig):
     name = "wagtail.documents"
@@ -14,11 +17,11 @@ class WagtailDocsAppConfig(AppConfig):
         register_signal_handlers()
 
         # Set up model forms to use AdminDocumentChooser for any ForeignKey to the document model
-        from wagtail.admin.forms.models import FOREIGN_KEY_MODEL_OVERRIDES
+        from wagtail.admin.forms.models import register_form_field_override
 
-        from . import get_document_model
         from .widgets import AdminDocumentChooser
 
-        FOREIGN_KEY_MODEL_OVERRIDES[get_document_model()] = {
-            "widget": AdminDocumentChooser
-        }
+        Document = get_document_model()
+        register_form_field_override(
+            ForeignKey, to=Document, override={"widget": AdminDocumentChooser}
+        )

+ 8 - 6
wagtail/images/apps.py

@@ -1,7 +1,9 @@
 from django.apps import AppConfig
+from django.db.models import ForeignKey
 from django.utils.translation import gettext_lazy as _
 
-from . import checks  # NOQA
+from . import checks, get_image_model  # NOQA
+from .signal_handlers import register_signal_handlers
 
 
 class WagtailImagesAppConfig(AppConfig):
@@ -11,14 +13,14 @@ class WagtailImagesAppConfig(AppConfig):
     default_auto_field = "django.db.models.AutoField"
 
     def ready(self):
-        from wagtail.images.signal_handlers import register_signal_handlers
-
         register_signal_handlers()
 
         # Set up model forms to use AdminImageChooser for any ForeignKey to the image model
-        from wagtail.admin.forms.models import FOREIGN_KEY_MODEL_OVERRIDES
+        from wagtail.admin.forms.models import register_form_field_override
 
-        from . import get_image_model
         from .widgets import AdminImageChooser
 
-        FOREIGN_KEY_MODEL_OVERRIDES[get_image_model()] = {"widget": AdminImageChooser}
+        Image = get_image_model()
+        register_form_field_override(
+            ForeignKey, to=Image, override={"widget": AdminImageChooser}
+        )

+ 5 - 5
wagtail/snippets/models.py

@@ -1,15 +1,15 @@
 from django.contrib.admin.utils import quote
 from django.core import checks
+from django.db.models import ForeignKey
 from django.urls import reverse
 
 from wagtail.admin.admin_url_finder import register_admin_url_finder
 from wagtail.admin.checks import check_panels_in_model
-from wagtail.admin.forms.models import FOREIGN_KEY_MODEL_OVERRIDES
+from wagtail.admin.forms.models import register_form_field_override
 from wagtail.admin.models import get_object_usage
 
 from .widgets import AdminSnippetChooser
 
-
 SNIPPET_MODELS = []
 
 
@@ -60,9 +60,9 @@ def register_snippet(model):
             return errors
 
         # Set up admin model forms to use AdminSnippetChooser for any ForeignKey to this model
-        FOREIGN_KEY_MODEL_OVERRIDES[model] = {
-            "widget": AdminSnippetChooser(model=model)
-        }
+        register_form_field_override(
+            ForeignKey, to=model, override={"widget": AdminSnippetChooser(model=model)}
+        )
 
     return model