170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
|
|
import copy
|
||
|
|
|
||
|
|
from django import forms
|
||
|
|
from django.core.exceptions import ImproperlyConfigured
|
||
|
|
from django.db import models
|
||
|
|
from django.utils import timezone
|
||
|
|
from django.utils.translation import gettext as _
|
||
|
|
from modelcluster.forms import ClusterForm, ClusterFormMetaclass, ClusterFormOptions
|
||
|
|
from permissionedforms import (
|
||
|
|
PermissionedForm,
|
||
|
|
PermissionedFormMetaclass,
|
||
|
|
PermissionedFormOptionsMixin,
|
||
|
|
)
|
||
|
|
from taggit.managers import TaggableManager
|
||
|
|
|
||
|
|
from wagtail.admin import widgets
|
||
|
|
from wagtail.admin.forms.tags import TagField
|
||
|
|
from wagtail.models import Page
|
||
|
|
from wagtail.utils.registry import ModelFieldRegistry
|
||
|
|
|
||
|
|
# Define a registry of form field properties to override for a given model field
|
||
|
|
registry = ModelFieldRegistry()
|
||
|
|
|
||
|
|
# Aliases to lookups in the overrides registry, for backwards compatibility
|
||
|
|
FORM_FIELD_OVERRIDES = registry.values_by_class
|
||
|
|
DIRECT_FORM_FIELD_OVERRIDES = registry.values_by_exact_class
|
||
|
|
|
||
|
|
|
||
|
|
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 and db_field_class != models.ForeignKey:
|
||
|
|
raise ImproperlyConfigured(
|
||
|
|
"The 'to' argument on register_form_field_override is only valid for ForeignKey fields"
|
||
|
|
)
|
||
|
|
|
||
|
|
registry.register(db_field_class, to=to, value=override, exact_class=exact_class)
|
||
|
|
|
||
|
|
|
||
|
|
# Define built-in overrides
|
||
|
|
|
||
|
|
# Date / time fields
|
||
|
|
register_form_field_override(
|
||
|
|
models.DateField, override={"widget": widgets.AdminDateInput}
|
||
|
|
)
|
||
|
|
register_form_field_override(
|
||
|
|
models.TimeField, override={"widget": widgets.AdminTimeInput}
|
||
|
|
)
|
||
|
|
register_form_field_override(
|
||
|
|
models.DateTimeField, override={"widget": widgets.AdminDateTimeInput}
|
||
|
|
)
|
||
|
|
|
||
|
|
# Auto-height text fields (defined as exact_class=True so that it doesn't take effect for RichTextField)
|
||
|
|
register_form_field_override(
|
||
|
|
models.TextField,
|
||
|
|
override={"widget": widgets.AdminAutoHeightTextInput},
|
||
|
|
exact_class=True,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Page chooser
|
||
|
|
register_form_field_override(
|
||
|
|
models.ForeignKey,
|
||
|
|
to=Page,
|
||
|
|
override=lambda db_field: {
|
||
|
|
"widget": widgets.AdminPageChooser(target_models=[db_field.remote_field.model])
|
||
|
|
},
|
||
|
|
)
|
||
|
|
|
||
|
|
# Tag fields
|
||
|
|
register_form_field_override(
|
||
|
|
TaggableManager,
|
||
|
|
override=(
|
||
|
|
lambda db_field: {"form_class": TagField, "tag_model": db_field.related_model}
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
# Slug fields
|
||
|
|
register_form_field_override(
|
||
|
|
models.SlugField,
|
||
|
|
override={"widget": widgets.SlugInput},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# Callback to allow us to override the default form fields provided for each model field.
|
||
|
|
def formfield_for_dbfield(db_field, **kwargs):
|
||
|
|
overrides = registry.get(db_field)
|
||
|
|
if overrides:
|
||
|
|
kwargs = dict(copy.deepcopy(overrides), **kwargs)
|
||
|
|
|
||
|
|
return db_field.formfield(**kwargs)
|
||
|
|
|
||
|
|
|
||
|
|
class WagtailAdminModelFormOptions(PermissionedFormOptionsMixin, ClusterFormOptions):
|
||
|
|
# Container for the options set in the inner 'class Meta' of a model form, supporting
|
||
|
|
# extensions for both ClusterForm ('formsets') and PermissionedForm ('field_permissions').
|
||
|
|
pass
|
||
|
|
|
||
|
|
|
||
|
|
class WagtailAdminModelFormMetaclass(PermissionedFormMetaclass, ClusterFormMetaclass):
|
||
|
|
options_class = WagtailAdminModelFormOptions
|
||
|
|
|
||
|
|
# set extra_form_count to 0, as we're creating extra forms in JS
|
||
|
|
extra_form_count = 0
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def child_form(cls):
|
||
|
|
return WagtailAdminModelForm
|
||
|
|
|
||
|
|
|
||
|
|
class WagtailAdminModelForm(
|
||
|
|
PermissionedForm, ClusterForm, metaclass=WagtailAdminModelFormMetaclass
|
||
|
|
):
|
||
|
|
def __init__(self, *args, **kwargs):
|
||
|
|
# keep hold of the `for_user` kwarg as well as passing it on to PermissionedForm
|
||
|
|
self.for_user = kwargs.get("for_user")
|
||
|
|
super().__init__(*args, **kwargs)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
formfield_callback = formfield_for_dbfield
|
||
|
|
|
||
|
|
|
||
|
|
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
|
||
|
|
# the nice form fields defined in FORM_FIELD_OVERRIDES.
|
||
|
|
|
||
|
|
|
||
|
|
class WagtailAdminDraftStateFormMixin:
|
||
|
|
@property
|
||
|
|
def show_schedule_publishing_toggle(self):
|
||
|
|
return "go_live_at" in self.__class__.base_fields
|
||
|
|
|
||
|
|
def clean(self):
|
||
|
|
super().clean()
|
||
|
|
|
||
|
|
# Check scheduled publishing fields
|
||
|
|
go_live_at = self.cleaned_data.get("go_live_at")
|
||
|
|
expire_at = self.cleaned_data.get("expire_at")
|
||
|
|
|
||
|
|
# Go live must be before expire
|
||
|
|
if go_live_at and expire_at:
|
||
|
|
if go_live_at > expire_at:
|
||
|
|
msg = _("Go live date/time must be before expiry date/time")
|
||
|
|
self.add_error("go_live_at", forms.ValidationError(msg))
|
||
|
|
self.add_error("expire_at", forms.ValidationError(msg))
|
||
|
|
|
||
|
|
# Expire at must be in the future
|
||
|
|
if expire_at and expire_at < timezone.now():
|
||
|
|
self.add_error(
|
||
|
|
"expire_at",
|
||
|
|
forms.ValidationError(_("Expiry date/time must be in the future")),
|
||
|
|
)
|
||
|
|
|
||
|
|
# Don't allow an existing first_published_at to be unset by clearing the field
|
||
|
|
if (
|
||
|
|
"first_published_at" in self.cleaned_data
|
||
|
|
and not self.cleaned_data["first_published_at"]
|
||
|
|
):
|
||
|
|
del self.cleaned_data["first_published_at"]
|
||
|
|
|
||
|
|
return self.cleaned_data
|