0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-12-01 11:41:20 +01:00

Move page validation to WagtailAdminPageForm class

Page validation is now done through the WagtailAdminPageForm class,
instead of through a separate function.
This commit is contained in:
Tim Heap 2015-10-29 15:51:30 +11:00 committed by Matt Westcott
parent 8bb6982175
commit 83d8c1a50e
4 changed files with 182 additions and 165 deletions

View File

@ -1,120 +1,48 @@
from __future__ import unicode_literals
import copy
from modelcluster.forms import ClusterForm, ClusterFormMetaclass
import django
from django.db import models
from django.template.loader import render_to_string
from django.utils.safestring import mark_safe
from django.utils.six import text_type
from django import forms
from django.forms.models import fields_for_model
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.forms.models import fields_for_model
from django.template.loader import render_to_string
from django.utils.lru_cache import lru_cache
from django.utils.safestring import mark_safe
from django.utils.six import text_type
from django.utils.translation import ugettext_lazy
from taggit.managers import TaggableManager
from wagtail.wagtailadmin import widgets
from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_string
from wagtail.wagtailcore.utils import (
camelcase_to_underscore, resolve_model_string)
# Form field properties to override whenever we encounter a model field
# that matches one of these types - including subclasses
FORM_FIELD_OVERRIDES = {
models.DateField: {'widget': widgets.AdminDateInput},
models.TimeField: {'widget': widgets.AdminTimeInput},
models.DateTimeField: {'widget': widgets.AdminDateTimeInput},
TaggableManager: {'widget': widgets.AdminTagWidget},
}
# Form field properties to override whenever we encounter a model field
# that matches one of these types exactly, ignoring subclasses.
# (This allows us to override the widget for models.TextField, but leave
# the RichTextField widget alone)
DIRECT_FORM_FIELD_OVERRIDES = {
models.TextField: {'widget': widgets.AdminAutoHeightTextInput},
}
# 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
overrides = None
# If we've got overrides for the formfield defined, use 'em. **kwargs
# passed to formfield_for_dbfield override the defaults.
if db_field.__class__ in DIRECT_FORM_FIELD_OVERRIDES:
overrides = DIRECT_FORM_FIELD_OVERRIDES[db_field.__class__]
else:
for klass in db_field.__class__.mro():
if klass in FORM_FIELD_OVERRIDES:
overrides = FORM_FIELD_OVERRIDES[klass]
break
if overrides:
kwargs = dict(copy.deepcopy(overrides), **kwargs)
return db_field.formfield(**kwargs)
# DIRECT_FORM_FIELD_OVERRIDES, FORM_FIELD_OVERRIDES are imported for backwards
# compatibility, as people are likely importing them from here and then
# appending their own overrides
from .forms import ( # NOQA
DIRECT_FORM_FIELD_OVERRIDES, FORM_FIELD_OVERRIDES, WagtailAdminModelForm,
WagtailAdminPageForm, formfield_for_dbfield)
def widget_with_script(widget, script):
return mark_safe('{0}<script>{1}</script>'.format(widget, script))
class WagtailAdminModelFormMetaclass(ClusterFormMetaclass):
# Override the behaviour of the regular ModelForm metaclass -
# which handles the translation of model fields to form fields -
# to use our own formfield_for_dbfield function to do that translation.
# This is done by sneaking a formfield_callback property into the class
# being defined (unless the class already provides a formfield_callback
# of its own).
# while we're at it, we'll also set extra_form_count to 0, as we're creating
# extra forms in JS
extra_form_count = 0
def __new__(cls, name, bases, attrs):
if 'formfield_callback' not in attrs or attrs['formfield_callback'] is None:
attrs['formfield_callback'] = formfield_for_dbfield
new_class = super(WagtailAdminModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
return new_class
WagtailAdminModelForm = WagtailAdminModelFormMetaclass(str('WagtailAdminModelForm'), (ClusterForm,), {})
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
# the nice form fields defined in FORM_FIELD_OVERRIDES.
def get_form_for_model(
model, form_class=WagtailAdminModelForm,
fields=None, exclude=None, formsets=None, exclude_formsets=None, widgets=None
):
# django's modelform_factory with a bit of custom behaviour
# (dealing with Treebeard's tree-related fields that really should have
# been editable=False)
attrs = {'model': model}
if fields is not None:
attrs['fields'] = fields
if exclude is not None:
attrs['exclude'] = exclude
if issubclass(model, Page):
attrs['exclude'] = attrs.get('exclude', []) + ['content_type', 'path', 'depth', 'numchild']
if widgets is not None:
attrs['widgets'] = widgets
if formsets is not None:
attrs['formsets'] = formsets
if exclude_formsets is not None:
attrs['exclude_formsets'] = exclude_formsets
@ -141,8 +69,6 @@ def extract_panel_definitions_from_model_class(model, exclude=None):
_exclude = []
if exclude:
_exclude.extend(exclude)
if issubclass(model, Page):
_exclude = ['content_type', 'path', 'depth', 'numchild']
fields = fields_for_model(model, exclude=_exclude, formfield_callback=formfield_for_dbfield)
@ -781,7 +707,7 @@ Page.settings_panels = [
PublishingPanel()
]
Page.base_form_class = WagtailAdminModelForm
Page.base_form_class = WagtailAdminPageForm
@classmethod

View File

@ -1,11 +1,21 @@
from __future__ import unicode_literals
import copy
import django
from django import forms
from django.core import validators
from django.forms.widgets import TextInput
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import AuthenticationForm, PasswordResetForm
from django.core import validators
from django.db import models
from django.forms.widgets import TextInput
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext, ugettext_lazy
from wagtail.wagtailadmin.widgets import AdminPageChooser
from django.utils.translation import ugettext_lazy, ungettext
from modelcluster.forms import ClusterForm, ClusterFormMetaclass
from taggit.managers import TaggableManager
from wagtail.wagtailadmin import widgets
from wagtail.wagtailcore.models import Page
@ -120,7 +130,7 @@ class CopyForm(forms.Form):
self.fields['new_parent_page'] = forms.ModelChoiceField(
initial=self.page.get_parent(),
queryset=Page.objects.all(),
widget=AdminPageChooser(can_choose_root=True),
widget=widgets.AdminPageChooser(can_choose_root=True),
label=_("New parent page"),
help_text=_("This copy will be a child of this given parent page.")
)
@ -188,3 +198,116 @@ class PageViewRestrictionForm(forms.Form):
del cleaned_data['password']
return cleaned_data
# Form field properties to override whenever we encounter a model field
# that matches one of these types - including subclasses
FORM_FIELD_OVERRIDES = {
models.DateField: {'widget': widgets.AdminDateInput},
models.TimeField: {'widget': widgets.AdminTimeInput},
models.DateTimeField: {'widget': widgets.AdminDateTimeInput},
TaggableManager: {'widget': widgets.AdminTagWidget},
}
# Form field properties to override whenever we encounter a model field
# that matches one of these types exactly, ignoring subclasses.
# (This allows us to override the widget for models.TextField, but leave
# the RichTextField widget alone)
DIRECT_FORM_FIELD_OVERRIDES = {
models.TextField: {'widget': widgets.AdminAutoHeightTextInput},
}
# 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
overrides = None
# If we've got overrides for the formfield defined, use 'em. **kwargs
# passed to formfield_for_dbfield override the defaults.
if db_field.__class__ in DIRECT_FORM_FIELD_OVERRIDES:
overrides = DIRECT_FORM_FIELD_OVERRIDES[db_field.__class__]
else:
for klass in db_field.__class__.mro():
if klass in FORM_FIELD_OVERRIDES:
overrides = FORM_FIELD_OVERRIDES[klass]
break
if overrides:
kwargs = dict(copy.deepcopy(overrides), **kwargs)
return db_field.formfield(**kwargs)
class WagtailAdminModelFormMetaclass(ClusterFormMetaclass):
# Override the behaviour of the regular ModelForm metaclass -
# which handles the translation of model fields to form fields -
# to use our own formfield_for_dbfield function to do that translation.
# This is done by sneaking a formfield_callback property into the class
# being defined (unless the class already provides a formfield_callback
# of its own).
# while we're at it, we'll also set extra_form_count to 0, as we're creating
# extra forms in JS
extra_form_count = 0
def __new__(cls, name, bases, attrs):
if 'formfield_callback' not in attrs or attrs['formfield_callback'] is None:
attrs['formfield_callback'] = formfield_for_dbfield
new_class = super(WagtailAdminModelFormMetaclass, cls).__new__(cls, name, bases, attrs)
return new_class
WagtailAdminModelForm = WagtailAdminModelFormMetaclass(str('WagtailAdminModelForm'), (ClusterForm,), {})
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
# the nice form fields defined in FORM_FIELD_OVERRIDES.
class WagtailAdminPageForm(WagtailAdminModelForm):
class Meta:
# (dealing with Treebeard's tree-related fields that really should have
# been editable=False)
exclude = ['content_type', 'path', 'depth', 'numchild']
def __init__(self, data=None, files=None, parent_page=None, *args, **kwargs):
super(WagtailAdminPageForm, self).__init__(data, files, *args, **kwargs)
self.parent_page = parent_page
if django.VERSION < (1, 9):
def clean_title(self):
return self.cleaned_data['title'].strip()
def clean_seo_title(self):
return self.cleaned_data['seo_title'].strip()
def clean(self):
cleaned_data = super(WagtailAdminPageForm, self).clean()
if 'slug' in self.cleaned_data:
# Get siblings for the page
siblings = self.parent_page.get_children()
if self.instance and self.instance.id:
siblings = siblings.exclude(id=self.instance.id)
# Make sure the slug isn't being used by a sibling
if siblings.filter(slug=cleaned_data['slug']).exists():
self.add_error('slug', forms.ValidationError(_("This slug is already in use")))
# Check scheduled publishing fields
go_live_at = cleaned_data.get('go_live_at')
expire_at = 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')))
return cleaned_data

View File

@ -15,6 +15,7 @@ from wagtail.wagtailadmin.edit_handlers import (
InlinePanel,
)
from wagtail.wagtailadmin.forms import WagtailAdminModelForm, WagtailAdminPageForm
from wagtail.wagtailadmin.widgets import AdminPageChooser, AdminDateInput, AdminAutoHeightTextInput
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailcore.models import Page, Site
@ -27,9 +28,11 @@ from wagtail.tests.utils import WagtailTestUtils
class TestGetFormForModel(TestCase):
def test_get_form_for_model(self):
EventPageForm = get_form_for_model(EventPage)
EventPageForm = get_form_for_model(EventPage, form_class=WagtailAdminPageForm)
form = EventPageForm()
# form should be a subclass of WagtailAdminModelForm
self.assertTrue(issubclass(EventPageForm, WagtailAdminModelForm))
# form should contain a title field (from the base Page)
self.assertEqual(type(form.fields['title']), forms.CharField)
# and 'date_from' from EventPage
@ -48,19 +51,21 @@ class TestGetFormForModel(TestCase):
# Test that field overrides defined through DIRECT_FORM_FIELD_OVERRIDES
# are applied
SimplePageForm = get_form_for_model(SimplePage)
SimplePageForm = get_form_for_model(SimplePage, form_class=WagtailAdminPageForm)
simple_form = SimplePageForm()
# plain TextFields should use AdminAutoHeightTextInput as the widget
self.assertEqual(type(simple_form.fields['content'].widget), AdminAutoHeightTextInput)
# This override should NOT be applied to subclasses of TextField such as
# RichTextField - they should retain their default widgets
EventPageForm = get_form_for_model(EventPage)
EventPageForm = get_form_for_model(EventPage, form_class=WagtailAdminPageForm)
event_form = EventPageForm()
self.assertEqual(type(event_form.fields['body'].widget), RichTextArea)
def test_get_form_for_model_with_specific_fields(self):
EventPageForm = get_form_for_model(EventPage, fields=['date_from'], formsets=['speakers'])
EventPageForm = get_form_for_model(
EventPage, form_class=WagtailAdminPageForm, fields=['date_from'],
formsets=['speakers'])
form = EventPageForm()
# form should contain date_from but not title
@ -73,7 +78,9 @@ class TestGetFormForModel(TestCase):
self.assertNotIn('related_links', form.formsets)
def test_get_form_for_model_with_excluded_fields(self):
EventPageForm = get_form_for_model(EventPage, exclude=['title'], exclude_formsets=['related_links'])
EventPageForm = get_form_for_model(
EventPage, form_class=WagtailAdminPageForm,
exclude=['title'], exclude_formsets=['related_links'])
form = EventPageForm()
# form should contain date_from but not title
@ -81,28 +88,41 @@ class TestGetFormForModel(TestCase):
self.assertEqual(type(form.fields['date_from'].widget), AdminDateInput)
self.assertNotIn('title', form.fields)
# 'path' should still be excluded even though it isn't explicitly in the exclude list
self.assertNotIn('path', form.fields)
# 'path' is not excluded any more, as the excluded fields were overridden
self.assertIn('path', form.fields)
# formsets should include speakers but not related_links
self.assertIn('speakers', form.formsets)
self.assertNotIn('related_links', form.formsets)
def test_get_form_for_model_with_widget_overides_by_class(self):
EventPageForm = get_form_for_model(EventPage, widgets={'date_from': forms.PasswordInput})
EventPageForm = get_form_for_model(
EventPage, form_class=WagtailAdminPageForm,
widgets={'date_from': forms.PasswordInput})
form = EventPageForm()
self.assertEqual(type(form.fields['date_from']), forms.DateField)
self.assertEqual(type(form.fields['date_from'].widget), forms.PasswordInput)
def test_get_form_for_model_with_widget_overides_by_instance(self):
EventPageForm = get_form_for_model(EventPage, widgets={'date_from': forms.PasswordInput()})
EventPageForm = get_form_for_model(
EventPage, form_class=WagtailAdminPageForm,
widgets={'date_from': forms.PasswordInput()})
form = EventPageForm()
self.assertEqual(type(form.fields['date_from']), forms.DateField)
self.assertEqual(type(form.fields['date_from'].widget), forms.PasswordInput)
class TestPageEditHandlers(TestCase):
def test_get_edit_handler(self):
EditHandler = EventPage.get_edit_handler()
EventPageForm = EditHandler.get_form_class(EventPage)
# The generated form should inherit from WagtailAdminPageForm
self.assertTrue(issubclass(EventPageForm, WagtailAdminPageForm))
class TestExtractPanelDefinitionsFromModelClass(TestCase):
def test_can_extract_panel_property(self):
# A class with a 'panels' property defined should return that list
@ -130,12 +150,6 @@ class TestExtractPanelDefinitionsFromModelClass(TestCase):
for panel in panels
]))
# treebeard fields should be excluded
self.assertFalse(any([
panel.field_name == 'path'
for panel in panels
]))
class TestTabbedInterface(TestCase):
def setUp(self):
@ -261,7 +275,8 @@ class TestObjectList(TestCase):
class TestFieldPanel(TestCase):
def setUp(self):
self.EventPageForm = get_form_for_model(EventPage, formsets=[])
self.EventPageForm = get_form_for_model(
EventPage, form_class=WagtailAdminPageForm, formsets=[])
self.event = EventPage(title='Abergavenny sheepdog trials',
date_from=date(2014, 7, 20), date_to=date(2014, 7, 21))

View File

@ -1,10 +1,8 @@
import django
from django.http import Http404, HttpResponse
from django.shortcuts import render, redirect, get_object_or_404
from django.core.exceptions import ValidationError, PermissionDenied
from django.core.exceptions import PermissionDenied
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.utils.http import is_safe_url
from django.views.decorators.http import require_GET, require_POST
@ -18,7 +16,6 @@ from wagtail.wagtailadmin import signals
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page, PageRevision, get_navigation_menu_items
from wagtail.wagtailadmin import messages
@ -145,8 +142,8 @@ def create(request, content_type_app_name, content_type_model_name, parent_page_
form_class = edit_handler_class.get_form_class(page_class)
if request.POST:
form = form_class(request.POST, request.FILES, instance=page)
validate_page_form(form, parent_page)
form = form_class(request.POST, request.FILES, instance=page,
parent_page=parent_page)
if form.is_valid():
page = form.save(commit=False)
@ -236,8 +233,8 @@ def edit(request, page_id):
errors_debug = None
if request.POST:
form = form_class(request.POST, request.FILES, instance=page)
validate_page_form(form, parent, page)
form = form_class(request.POST, request.FILES, instance=page,
parent_page=parent)
if form.is_valid() and not page.locked:
page = form.save(commit=False)
@ -315,48 +312,6 @@ def edit(request, page_id):
})
def validate_page_form(form, parent_page, instance=None):
# Strip whitespace in title and seo_title fields
# This is done for us in Django 1.9 and above
if django.VERSION < (1, 9):
def clean_title():
return form.cleaned_data['title'].strip()
def clean_seo_title():
return form.cleaned_data['seo_title'].strip()
form.clean_title = clean_title
form.clean_seo_title = clean_seo_title
# Perform default validation
form.full_clean()
if 'slug' in form.cleaned_data:
# Get siblings for the page
siblings = parent_page.get_children()
if instance and instance.id:
siblings = siblings.exclude(id=instance.id)
# Make sure the slug isn't being used by a sibling
if siblings.filter(slug=form.cleaned_data['slug']).exists():
form.add_error('slug', ValidationError(_("This slug is already in use")))
# Check scheduled publishing fields
go_live_at = form.cleaned_data.get('go_live_at')
expire_at = form.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')
form.add_error('go_live_at', ValidationError(msg))
form.add_error('expire_at', ValidationError(msg))
# Expire at must be in the future
if expire_at and expire_at < timezone.now():
form.add_error('expire_at', ValidationError(_('Expiry date/time must be in the future')))
def delete(request, page_id):
page = get_object_or_404(Page, id=page_id)
if not page.permissions_for_user(request.user).can_delete():
@ -392,10 +347,11 @@ def preview_on_edit(request, page_id):
page = get_object_or_404(Page, id=page_id).get_latest_revision_as_page()
content_type = page.content_type
page_class = content_type.model_class()
parent_page = page.get_parent().specific
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
form = form_class(request.POST, request.FILES, instance=page)
form = form_class(request.POST, request.FILES, instance=page, parent_page=parent_page)
if form.is_valid():
form.save(commit=False)
@ -429,14 +385,14 @@ def preview_on_create(request, content_type_app_name, content_type_model_name, p
page = page_class()
edit_handler_class = page_class.get_edit_handler()
form_class = edit_handler_class.get_form_class(page_class)
parent_page = get_object_or_404(Page, id=parent_page_id).specific
form = form_class(request.POST, request.FILES, instance=page)
form = form_class(request.POST, request.FILES, instance=page, parent_page=parent_page)
if form.is_valid():
form.save(commit=False)
# ensure that our unsaved page instance has a suitable url set
parent_page = get_object_or_404(Page, id=parent_page_id).specific
page.set_url_path(parent_page)
# Set treebeard attributes
@ -450,7 +406,6 @@ def preview_on_create(request, content_type_app_name, content_type_model_name, p
else:
edit_handler = edit_handler_class(instance=page, form=form)
parent_page = get_object_or_404(Page, id=parent_page_id).specific
response = render(request, 'wagtailadmin/pages/create.html', {
'content_type': content_type,
@ -669,8 +624,6 @@ def copy(request, page_id):
})
@vary_on_headers('X-Requested-With')
def search(request):
pages = []