diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index c8447c7d9..a202d5973 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -17,6 +17,7 @@ Changelog
* Fix: Default text of page links in rich text uses the public page title rather than the admin display title (Andy Chosak)
* Fix: Specific page permission checks are now enforced when viewing a page revision (Andy Chosak)
* Fix: `pageurl` and `slugurl` tags no longer fail when `request.site` is `None` (Samir Shah)
+ * Fix: Output form media on add/edit image forms with custom models (Matt Westcott)
2.6.1 (05.08.2019)
diff --git a/docs/releases/2.7.rst b/docs/releases/2.7.rst
index fdaede9b5..e312e6fc2 100644
--- a/docs/releases/2.7.rst
+++ b/docs/releases/2.7.rst
@@ -35,6 +35,7 @@ Bug fixes
* Default text of page links in rich text uses the public page title rather than the admin display title (Andy Chosak)
* Specific page permission checks are now enforced when viewing a page revision (Andy Chosak)
* ``pageurl`` and ``slugurl`` tags no longer fail when ``request.site`` is ``None`` (Samir Shah)
+ * Output form media on add/edit image forms with custom models (Matt Westcott)
Upgrade considerations
diff --git a/wagtail/images/templates/wagtailimages/chooser/chooser.html b/wagtail/images/templates/wagtailimages/chooser/chooser.html
index d60ddd72f..ca80787a9 100644
--- a/wagtail/images/templates/wagtailimages/chooser/chooser.html
+++ b/wagtail/images/templates/wagtailimages/chooser/chooser.html
@@ -3,6 +3,9 @@
{% trans "Choose an image" as choose_str %}
{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 tabbed=1 icon="image" %}
+{{ uploadform.media.js }}
+{{ uploadform.media.css }}
+
{% if uploadform %}
{% trans "Search" %}
diff --git a/wagtail/images/templates/wagtailimages/images/add.html b/wagtail/images/templates/wagtailimages/images/add.html
index 0ca1a23b0..b074c91e5 100644
--- a/wagtail/images/templates/wagtailimages/images/add.html
+++ b/wagtail/images/templates/wagtailimages/images/add.html
@@ -6,6 +6,8 @@
{% block extra_js %}
{{ block.super }}
+ {{ form.media.js }}
+
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
{% endblock %}
+{% block extra_css %}
+ {{ block.super }}
+ {{ form.media.css }}
+{% endblock %}
+
{% block content %}
{% trans "Add image" as add_str %}
{% include "wagtailadmin/shared/header.html" with title=add_str icon="image" %}
diff --git a/wagtail/images/templates/wagtailimages/images/edit.html b/wagtail/images/templates/wagtailimages/images/edit.html
index c2927f354..c0aa5aff2 100644
--- a/wagtail/images/templates/wagtailimages/images/edit.html
+++ b/wagtail/images/templates/wagtailimages/images/edit.html
@@ -4,6 +4,8 @@
{% block extra_css %}
{{ block.super }}
+ {{ form.media.css }}
+
@@ -12,6 +14,8 @@
{% block extra_js %}
{{ block.super }}
+ {{ form.media.js }}
+
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
diff --git a/wagtail/images/tests/test_admin_views.py b/wagtail/images/tests/test_admin_views.py
index ef3ead9f5..1248339c0 100644
--- a/wagtail/images/tests/test_admin_views.py
+++ b/wagtail/images/tests/test_admin_views.py
@@ -10,6 +10,7 @@ from django.utils.http import RFC3986_SUBDELIMS, urlquote
from wagtail.core.models import Collection, GroupCollectionPermission
from wagtail.images.views.serve import generate_signature
+from wagtail.tests.testapp.models import CustomImage
from wagtail.tests.utils import WagtailTestUtils
from .utils import Image, get_test_image_file
@@ -108,6 +109,9 @@ class TestImageAddView(TestCase, WagtailTestUtils):
# Ensure the form supports file uploads
self.assertContains(response, 'enctype="multipart/form-data"')
+ # draftail should NOT be a standard JS include on this page
+ self.assertNotContains(response, 'wagtailadmin/js/draftail.js')
+
def test_get_with_collections(self):
root_collection = Collection.get_first_root_node()
root_collection.add_child(name="Evil plans")
@@ -119,6 +123,21 @@ class TestImageAddView(TestCase, WagtailTestUtils):
self.assertContains(response, '')
self.assertContains(response, "Evil plans")
+ @override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
+ def test_get_with_custom_image_model(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailimages/images/add.html')
+
+ # Ensure the form supports file uploads
+ self.assertContains(response, 'enctype="multipart/form-data"')
+
+ # custom fields should be included
+ self.assertContains(response, 'name="fancy_caption"')
+
+ # form media should be imported
+ self.assertContains(response, 'wagtailadmin/js/draftail.js')
+
def test_add(self):
response = self.post({
'title': "Test image",
@@ -325,6 +344,11 @@ class TestImageEditView(TestCase, WagtailTestUtils):
# Ensure the form supports file uploads
self.assertContains(response, 'enctype="multipart/form-data"')
+ # draftail should NOT be a standard JS include on this page
+ # (see TestImageEditViewWithCustomImageModel - this confirms that form media
+ # definitions are being respected)
+ self.assertNotContains(response, 'wagtailadmin/js/draftail.js')
+
@override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)
def test_with_usage_count(self):
response = self.get()
@@ -514,6 +538,34 @@ class TestImageEditView(TestCase, WagtailTestUtils):
self.assertContains(response, 'data-original-width="1024"')
+@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
+class TestImageEditViewWithCustomImageModel(TestCase, WagtailTestUtils):
+ def setUp(self):
+ self.login()
+
+ # Create an image to edit
+ self.image = CustomImage.objects.create(
+ title="Test image",
+ file=get_test_image_file(),
+ )
+
+ self.storage = self.image.file.storage
+
+ def get(self, params={}):
+ return self.client.get(reverse('wagtailimages:edit', args=(self.image.id,)), params)
+
+ def test_get_with_custom_image_model(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailimages/images/edit.html')
+
+ # Ensure the form supports file uploads
+ self.assertContains(response, 'enctype="multipart/form-data"')
+
+ # form media should be imported
+ self.assertContains(response, 'wagtailadmin/js/draftail.js')
+
+
class TestImageDeleteView(TestCase, WagtailTestUtils):
def setUp(self):
self.login()
@@ -571,6 +623,23 @@ class TestImageChooserView(TestCase, WagtailTestUtils):
self.assertEqual(response_json['step'], 'chooser')
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.html')
+ # draftail should NOT be a standard JS include on this page
+ self.assertNotIn('wagtailadmin/js/draftail.js', response_json['html'])
+
+ @override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
+ def test_with_custom_image_model(self):
+ response = self.get()
+ self.assertEqual(response.status_code, 200)
+ response_json = json.loads(response.content.decode())
+ self.assertEqual(response_json['step'], 'chooser')
+ self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.html')
+
+ # custom form fields should be present
+ self.assertIn('name="image-chooser-upload-fancy_caption"', response_json['html'])
+
+ # form media imports should appear on the page
+ self.assertIn('wagtailadmin/js/draftail.js', response_json['html'])
+
def test_search(self):
response = self.get({'q': "Hello"})
self.assertEqual(response.status_code, 200)
@@ -898,6 +967,11 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'wagtailimages/multiple/add.html')
+ # draftail should NOT be a standard JS include on this page
+ # (see TestMultipleImageUploaderWithCustomImageModel - this confirms that form media
+ # definitions are being respected)
+ self.assertNotContains(response, 'wagtailadmin/js/draftail.js')
+
@override_settings(WAGTAILIMAGES_MAX_UPLOAD_SIZE=1000)
def test_add_max_file_size_context_variables(self):
response = self.client.get(reverse('wagtailimages:add_multiple'))
@@ -1095,6 +1169,123 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
self.assertEqual(response.status_code, 400)
+@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
+class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
+ """
+ This tests the multiple image upload views located in wagtailimages/views/multiple.py
+ with a custom image model
+ """
+ def setUp(self):
+ self.login()
+
+ # Create an image for running tests on
+ self.image = CustomImage.objects.create(
+ title="Test image",
+ file=get_test_image_file(),
+ )
+
+ def test_add(self):
+ """
+ This tests that the add view responds correctly on a GET request
+ """
+ # Send request
+ response = self.client.get(reverse('wagtailimages:add_multiple'))
+
+ # Check response
+ self.assertEqual(response.status_code, 200)
+ self.assertTemplateUsed(response, 'wagtailimages/multiple/add.html')
+
+ # response should include form media for the image edit form
+ self.assertContains(response, 'wagtailadmin/js/draftail.js')
+
+ def test_add_post(self):
+ """
+ This tests that a POST request to the add view saves the image and returns an edit form
+ """
+ response = self.client.post(reverse('wagtailimages:add_multiple'), {
+ 'files[]': SimpleUploadedFile('test.png', get_test_image_file().file.getvalue()),
+ }, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ # Check response
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['Content-Type'], 'application/json')
+ self.assertTemplateUsed(response, 'wagtailimages/multiple/edit_form.html')
+
+ # Check image
+ self.assertIn('image', response.context)
+ self.assertEqual(response.context['image'].title, 'test.png')
+ self.assertTrue(response.context['image'].file_size)
+ self.assertTrue(response.context['image'].file_hash)
+
+ # Check form
+ self.assertIn('form', response.context)
+ self.assertEqual(response.context['form'].initial['title'], 'test.png')
+ self.assertIn('caption', response.context['form'].fields)
+ self.assertNotIn('not_editable_field', response.context['form'].fields)
+
+ # Check JSON
+ response_json = json.loads(response.content.decode())
+ self.assertIn('image_id', response_json)
+ self.assertIn('form', response_json)
+ self.assertIn('success', response_json)
+ self.assertEqual(response_json['image_id'], response.context['image'].id)
+ self.assertTrue(response_json['success'])
+
+ def test_edit_post(self):
+ """
+ This tests that a POST request to the edit view edits the image
+ """
+ # Send request
+ response = self.client.post(reverse('wagtailimages:edit_multiple', args=(self.image.id, )), {
+ ('image-%d-title' % self.image.id): "New title!",
+ ('image-%d-tags' % self.image.id): "",
+ ('image-%d-caption' % self.image.id): "a boot stamping on a human face, forever",
+ }, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ # Check response
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['Content-Type'], 'application/json')
+
+ # Check JSON
+ response_json = json.loads(response.content.decode())
+ self.assertIn('image_id', response_json)
+ self.assertNotIn('form', response_json)
+ self.assertIn('success', response_json)
+ self.assertEqual(response_json['image_id'], self.image.id)
+ self.assertTrue(response_json['success'])
+
+ # check that image has been updated
+ new_image = CustomImage.objects.get(id=self.image.id)
+ self.assertEqual(new_image.title, "New title!")
+ self.assertEqual(new_image.caption, "a boot stamping on a human face, forever")
+
+ def test_delete_post(self):
+ """
+ This tests that a POST request to the delete view deletes the image
+ """
+ # Send request
+ response = self.client.post(reverse(
+ 'wagtailimages:delete_multiple', args=(self.image.id, )
+ ), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ # Check response
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response['Content-Type'], 'application/json')
+
+ # Make sure the image is deleted
+ self.assertFalse(Image.objects.filter(id=self.image.id).exists())
+
+ # Check JSON
+ response_json = json.loads(response.content.decode())
+ self.assertIn('image_id', response_json)
+ self.assertIn('success', response_json)
+ self.assertEqual(response_json['image_id'], self.image.id)
+ self.assertTrue(response_json['success'])
+
+ # check that image has been deleted
+ self.assertEqual(CustomImage.objects.filter(id=self.image.id).count(), 0)
+
+
class TestURLGeneratorView(TestCase, WagtailTestUtils):
def setUp(self):
# Create an image for running tests on
diff --git a/wagtail/images/tests/tests.py b/wagtail/images/tests/tests.py
index 7e0462886..b0a5c433c 100644
--- a/wagtail/images/tests/tests.py
+++ b/wagtail/images/tests/tests.py
@@ -569,6 +569,7 @@ class TestGetImageForm(TestCase, WagtailTestUtils):
'focal_point_width',
'focal_point_height',
'caption',
+ 'fancy_caption',
])
def test_file_field(self):
diff --git a/wagtail/images/views/multiple.py b/wagtail/images/views/multiple.py
index bd2740f16..01f6b89a5 100644
--- a/wagtail/images/views/multiple.py
+++ b/wagtail/images/views/multiple.py
@@ -93,16 +93,19 @@ def add(request):
'error_message': '\n'.join(['\n'.join([force_text(i) for i in v]) for k, v in form.errors.items()]),
})
else:
+ # Instantiate a dummy copy of the form that we can retrieve validation messages and media from;
+ # actual rendering of forms will happen on AJAX POST rather than here
form = ImageForm(user=request.user)
- return render(request, 'wagtailimages/multiple/add.html', {
- 'max_filesize': form.fields['file'].max_upload_size,
- 'help_text': form.fields['file'].help_text,
- 'allowed_extensions': ALLOWED_EXTENSIONS,
- 'error_max_file_size': form.fields['file'].error_messages['file_too_large_unknown_size'],
- 'error_accepted_file_types': form.fields['file'].error_messages['invalid_image'],
- 'collections': collections_to_choose,
- })
+ return render(request, 'wagtailimages/multiple/add.html', {
+ 'max_filesize': form.fields['file'].max_upload_size,
+ 'help_text': form.fields['file'].help_text,
+ 'allowed_extensions': ALLOWED_EXTENSIONS,
+ 'error_max_file_size': form.fields['file'].error_messages['file_too_large_unknown_size'],
+ 'error_accepted_file_types': form.fields['file'].error_messages['invalid_image'],
+ 'collections': collections_to_choose,
+ 'form_media': form.media,
+ })
@require_POST
diff --git a/wagtail/tests/testapp/migrations/0001_initial.py b/wagtail/tests/testapp/migrations/0001_initial.py
index 032928e6e..192eb0170 100644
--- a/wagtail/tests/testapp/migrations/0001_initial.py
+++ b/wagtail/tests/testapp/migrations/0001_initial.py
@@ -135,8 +135,9 @@ class Migration(migrations.Migration):
('focal_point_width', models.PositiveIntegerField(blank=True, null=True)),
('focal_point_height', models.PositiveIntegerField(blank=True, null=True)),
('file_size', models.PositiveIntegerField(editable=False, null=True)),
- ('caption', models.CharField(max_length=255)),
- ('not_editable_field', models.CharField(max_length=255)),
+ ('caption', models.CharField(max_length=255, blank=True)),
+ ('fancy_caption', wagtail.core.fields.RichTextField(blank=True)),
+ ('not_editable_field', models.CharField(max_length=255, blank=True)),
('tags', taggit.managers.TaggableManager(blank=True, help_text=None, through='taggit.TaggedItem', to='taggit.Tag', verbose_name='tags')),
('uploaded_by_user', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='uploaded by user')),
],
diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py
index a659113fa..ec3cffab2 100644
--- a/wagtail/tests/testapp/models.py
+++ b/wagtail/tests/testapp/models.py
@@ -899,11 +899,13 @@ class SnippetChooserModelWithCustomPrimaryKey(models.Model):
class CustomImage(AbstractImage):
- caption = models.CharField(max_length=255)
- not_editable_field = models.CharField(max_length=255)
+ caption = models.CharField(max_length=255, blank=True)
+ fancy_caption = RichTextField(blank=True)
+ not_editable_field = models.CharField(max_length=255, blank=True)
admin_form_fields = Image.admin_form_fields + (
'caption',
+ 'fancy_caption',
)