diff --git a/wagtail/admin/edit_handlers.py b/wagtail/admin/edit_handlers.py
index 3103d43b5b..a28d21c38a 100644
--- a/wagtail/admin/edit_handlers.py
+++ b/wagtail/admin/edit_handlers.py
@@ -7,7 +7,7 @@ from django.forms.formsets import DELETION_FIELD_NAME, ORDERING_FIELD_NAME
from django.forms.models import fields_for_model
from django.template.loader import render_to_string
from django.utils.encoding import force_text
-from django.utils.functional import curry
+from django.utils.functional import cached_property, curry
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy
from taggit.managers import TaggableManager
@@ -511,8 +511,14 @@ class FieldPanel(EditHandler):
return [curry(comparator_class, self.db_field)]
return []
- def on_model_bound(self):
- self.db_field = self.model._meta.get_field(self.field_name)
+ @cached_property
+ def db_field(self):
+ try:
+ model = self.model
+ except AttributeError:
+ raise ImproperlyConfigured("%r must be bound to a model before calling db_field" % self)
+
+ return model._meta.get_field(self.field_name)
def on_instance_bound(self):
self.bound_field = self.form[self.field_name]
diff --git a/wagtail/admin/tests/test_edit_handlers.py b/wagtail/admin/tests/test_edit_handlers.py
index b625c50195..f637669c98 100644
--- a/wagtail/admin/tests/test_edit_handlers.py
+++ b/wagtail/admin/tests/test_edit_handlers.py
@@ -376,9 +376,14 @@ class TestFieldPanel(TestCase):
self.end_date_panel = (FieldPanel('date_to', classname='full-width')
.bind_to_model(EventPage))
- def test_invalid_field(self):
+ def test_non_model_field(self):
+ # defining a FieldPanel for a field which isn't part of a model is OK,
+ # because it might be defined on the form instead
+ field_panel = FieldPanel('barbecue').bind_to_model(Page)
+
+ # however, accessing db_field will fail
with self.assertRaises(FieldDoesNotExist):
- FieldPanel('barbecue').bind_to_model(Page)
+ field_panel.db_field
def test_render_as_object(self):
form = self.EventPageForm(
diff --git a/wagtail/admin/tests/test_pages_views.py b/wagtail/admin/tests/test_pages_views.py
index 167ff55a3c..5699038fb0 100644
--- a/wagtail/admin/tests/test_pages_views.py
+++ b/wagtail/admin/tests/test_pages_views.py
@@ -682,6 +682,15 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertContains(response, 'Promote')
self.assertContains(response, 'Dinosaurs')
+ def test_create_page_with_non_model_field(self):
+ """
+ Test that additional fields defined on the form rather than the model are accepted and rendered
+ """
+ response = self.client.get(reverse('wagtailadmin_pages:add', args=('tests', 'formclassadditionalfieldpage', self.root_page.id)))
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailadmin/pages/create.html')
+ self.assertContains(response, "Enter SMS authentication code")
+
def test_create_simplepage_bad_permissions(self):
# Remove privileges from user
self.user.is_superuser = False
diff --git a/wagtail/tests/testapp/forms.py b/wagtail/tests/testapp/forms.py
index 244b33c720..33e74a1a68 100644
--- a/wagtail/tests/testapp/forms.py
+++ b/wagtail/tests/testapp/forms.py
@@ -12,3 +12,18 @@ class ValidatedPageForm(WagtailAdminPageForm):
if value != 'bar':
raise forms.ValidationError('Field foo must be bar')
return value
+
+
+class FormClassAdditionalFieldPageForm(WagtailAdminPageForm):
+ code = forms.CharField(
+ help_text='Enter SMS authentication code', max_length=5)
+
+ def clean(self):
+ cleaned_data = super(FormClassAdditionalFieldPageForm, self).clean()
+
+ # validate the user's code with our code check
+ code = cleaned_data['code']
+ if not code:
+ raise forms.ValidationError('Code is not valid')
+
+ return cleaned_data
diff --git a/wagtail/tests/testapp/migrations/0030_formclassadditionalfieldpage.py b/wagtail/tests/testapp/migrations/0030_formclassadditionalfieldpage.py
new file mode 100644
index 0000000000..dd6553aa31
--- /dev/null
+++ b/wagtail/tests/testapp/migrations/0030_formclassadditionalfieldpage.py
@@ -0,0 +1,28 @@
+# Generated by Django 2.0.4 on 2018-04-12 15:10
+
+from django.db import migrations, models
+import django.db.models.deletion
+import wagtail.core.fields
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('wagtailcore', '0040_page_draft_title'),
+ ('tests', '0029_auto_20180215_1950'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='FormClassAdditionalFieldPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
+ ('location', models.CharField(max_length=255)),
+ ('body', wagtail.core.fields.RichTextField(blank=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ ]
diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py
index 59db9eabc2..a2a8df4161 100644
--- a/wagtail/tests/testapp/models.py
+++ b/wagtail/tests/testapp/models.py
@@ -39,7 +39,7 @@ from wagtail.search import index
from wagtail.snippets.edit_handlers import SnippetChooserPanel
from wagtail.snippets.models import register_snippet
-from .forms import ValidatedPageForm
+from .forms import FormClassAdditionalFieldPageForm, ValidatedPageForm
from .views import CustomSubmissionsListView
EVENT_AUDIENCE_CHOICES = (
@@ -314,6 +314,22 @@ class HeadCountRelatedModelUsingPK(models.Model):
panels = [FieldPanel('head_count')]
+# Override the standard WagtailAdminPageForm to add field that is not in model
+# so that we can test additional potential issues like comparing versions
+class FormClassAdditionalFieldPage(Page):
+ location = models.CharField(max_length=255)
+ body = RichTextField(blank=True)
+
+ content_panels = [
+ FieldPanel('title', classname="full title"),
+ FieldPanel('location'),
+ FieldPanel('body'),
+ FieldPanel('code'), # not in model, see set base_form_class
+ ]
+
+ base_form_class = FormClassAdditionalFieldPageForm
+
+
# Just to be able to test multi table inheritance
class SingleEventPage(EventPage):
excerpt = models.TextField(