Output form media on add/edit/chooser image forms with custom models

* Add tests for custom image on multiple image uploader
* Output form media on image add/edit views
* Output form media for 'add image' form within image chooser modal
Note: this won't work reliably if the media is hosted on a CDN, because script tags inserted as part of a jQuery DOM insertion (as modals are) are loaded asynchronously and not guaranteed to complete loading before inline scripts are run. It's better than not having the includes there at all though...
This commit is contained in:
Matt Westcott 2019-04-26 16:40:58 +01:00 committed by LB Johnston
parent f7ff6d39c4
commit eaad013081
11 changed files with 230 additions and 12 deletions

View file

@ -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)

View file

@ -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

View file

@ -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 %}
<ul class="tab-nav merged">
<li class="{% if not uploadform.errors %}active{% endif %}"><a href="#search" >{% trans "Search" %}</a></li>

View file

@ -6,6 +6,8 @@
{% block extra_js %}
{{ block.super }}
{{ form.media.js }}
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
<script>
$(function() {
@ -16,6 +18,11 @@
</script>
{% 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" %}

View file

@ -4,6 +4,8 @@
{% block extra_css %}
{{ block.super }}
{{ form.media.css }}
<!-- Focal point chooser -->
<link rel="stylesheet" href="{% static 'wagtailimages/css/vendor/jquery.Jcrop.min.css' %}" type="text/css">
<link rel="stylesheet" href="{% static 'wagtailimages/css/focal-point-chooser.css' %}" type="text/css">
@ -12,6 +14,8 @@
{% block extra_js %}
{{ block.super }}
{{ form.media.js }}
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
<script>
$(function() {

View file

@ -7,6 +7,8 @@
{% block extra_css %}
{{ block.super }}
{{ form_media.css }}
<link rel="stylesheet" href="{% static 'wagtailimages/css/add-multiple.css' %}" type="text/css" />
{% endblock %}
@ -75,6 +77,8 @@
{% block extra_js %}
{{ block.super }}
{{ form_media.js }}
<!-- this exact order of plugins is vital -->
<script src="{% static 'wagtailimages/js/vendor/load-image.min.js' %}"></script>
<script src="{% static 'wagtailimages/js/vendor/canvas-to-blob.min.js' %}"></script>

View file

@ -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, '<label for="id_collection">')
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

View file

@ -569,6 +569,7 @@ class TestGetImageForm(TestCase, WagtailTestUtils):
'focal_point_width',
'focal_point_height',
'caption',
'fancy_caption',
])
def test_file_field(self):

View file

@ -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

View file

@ -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')),
],

View file

@ -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',
)