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(