add ability to customise the pre-filled Image title

- leveraging a custom DOM event provides the ability to update the title before being added to the form
- multiple image upload (view) needs to be able to read the title POST data
- add documentation
pull/7575/head
LB Johnston 2021-09-23 20:52:57 +10:00 zatwierdzone przez LB (Ben Johnston)
rodzic 2eb7232055
commit e03c86f3f8
11 zmienionych plików z 228 dodań i 6 usunięć
docs
wagtail
admin/views/generic
images
static_src/wagtailimages/js
templates/wagtailimages
images
multiple

Wyświetl plik

@ -37,6 +37,7 @@ Changelog
* Move translations in `nl_NL` to `nl` (Loïc Teixeira, Coen van der Kamp)
* Add documentation for how to redirect to a separate page on Form builder submissions using ``RoutablePageMixin`` (Nick Smith)
* Refactored index listing views and made column sort-by headings more consistent (Matt Westcott)
* Added the ability to customise the pre-filled Image title on upload and it now defaults to the filename without the file extension (LB Johnston)
* Fix: Delete button is now correct colour on snippets and modeladmin listings (Brandon Murch)
* Fix: Ensure that StreamBlock / ListBlock-level validation errors are counted towards error counts (Matt Westcott)
* Fix: InlinePanel add button is now keyboard navigatable (Jesse Menn)

Wyświetl plik

@ -13,3 +13,4 @@ Images
feature_detection
image_serve_view
focal_points
title_generation_on_upload

Wyświetl plik

@ -0,0 +1,108 @@
# Title generation on upload
Override how the title is set when adding a single image, multiple images or uploading an image within a chooser modal.
When a file is dropped into the multi-upload page or selected on the single file selection form a title will be automatically populated into the Title field. The default behaviour is to use the image's filename excluding the extension.
You can customise the resolved value of this title using a JavaScript [event listener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) which will listen to the `'wagtail:images-upload'` event.
The simplest way to add JavaScript to the editor is via the [`insert_global_admin_js` hook](../../reference/hooks.html#insert-global-admin-js), however any JavaScript that adds the event listener will work.
## DOM Event
The event name to listen for is `'wagtail:images-upload'`. It will be dispatched on the image upload `form`. The event's `detail` attribute will contain:
- `data` - An object which includes the `title` to be used. It is the filename with the extension removed.
- `maxTitleLength` - An integer (or `null`) which is the maximum length of the `Image` model title field.
- `filename` - The original filename without the extension removed.
To modify the generated `Image` title, access and update `event.detail.data.title`, no return value is needed.
For single image uploads, the custom event will only run if the title does not already have a value so that we do not overwrite whatever the user has typed.
You can prevent the default behaviour by calling `event.preventDefault()`. For the single upload page or modals, this will not pre-fill any value into the title. For multiple upload, this will avoid any title submission and use the filename title only (with file extension) as a title is required to save the image.
The event will 'bubble' up so that you can simply add a global `document` listener to capture all of these events, or you can scope your listener or handler logic as needed to ensure you only adjust titles in some specific scenarios.
See MDN for more information about [custom JavasScript events](https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events).
## Code Examples
### Removing any url unsafe characters from the title
```python
# wagtail_hooks.py
from django.utils.safestring import mark_safe
from wagtail.core import hooks
@hooks.register("insert_global_admin_js")
def get_global_admin_js():
return mark_safe(
"""
<script>
window.addEventListener('DOMContentLoaded', function () {
document.addEventListener('wagtail:images-upload', function(event) {
var newTitle = (event.detail.data.title || '').replace(/[^a-zA-Z0-9\s-]/g, "");
event.detail.data.title = newTitle;
});
});
</script>
"""
)
```
### Changing generated titles on the page editor only to remove dashes/underscores
Using the [`insert_editor_js` hook](../../reference/hooks.html#insert-editor-js) instead so that this script will not run on the `Image` upload page, only on page editors.
```python
# wagtail_hooks.py
from django.utils.safestring import mark_safe
from wagtail.core import hooks
@hooks.register("insert_editor_js")
def get_global_admin_js():
return mark_safe(
"""
<script>
window.addEventListener('DOMContentLoaded', function () {
document.addEventListener('wagtail:images-upload', function(event) {
// replace dashes/underscores with a space
var newTitle = (event.detail.data.title || '').replace(/(\s|_|-)/g, " ");
event.detail.data.title = newTitle;
});
});
</script>
"""
)
```
### Stopping pre-filling of title based on filename
```python
# wagtail_hooks.py
from django.utils.safestring import mark_safe
from wagtail.core import hooks
@hooks.register("insert_global_admin_js")
def get_global_admin_js():
return mark_safe(
"""
<script>
window.addEventListener('DOMContentLoaded', function () {
document.addEventListener('wagtail:images-upload', function(event) {
// will stop title pre-fill on single file uploads
// will set the multiple upload title to the filename (with extension)
event.preventDefault();
});
});
</script>
"""
)
```

Wyświetl plik

@ -53,6 +53,7 @@ Other features
* Translations in ``nl_NL`` are moved to the ``nl`` po files. ``nl_NL`` translation files are deleted. Projects that use ``LANGUAGE_CODE = 'nl-nl'`` will automatically fallback to ``nl``. (Loïc Teixeira, Coen van der Kamp)
* Add documentation for how to redirect to a separate page on Form builder submissions using ``RoutablePageMixin`` (Nick Smith)
* Refactored index listing views and made column sort-by headings more consistent (Matt Westcott)
* Added the ability to customise the pre-filled Image title on upload and it now defaults to the filename without the file extension (LB Johnston)
Bug fixes
~~~~~~~~~

Wyświetl plik

@ -125,7 +125,7 @@ class AddView(PermissionCheckedMixin, TemplateView):
# Build a form for validation
upload_form_class = self.get_upload_form_class()
form = upload_form_class({
'title': request.FILES['files[]'].name,
'title': request.POST.get('title', request.FILES['files[]'].name),
'collection': request.POST.get('collection'),
}, {
'file': request.FILES['files[]'],

Wyświetl plik

@ -97,6 +97,35 @@ $(function() {
}
},
/**
* Allow a custom title to be defined by an event handler for this form.
* If event.preventDefault is called, the original behaviour of using the raw
* filename (with extension) as the title is preserved.
*
* @example
* document.addEventListener('wagtail:images-upload', function(event) {
* // remove file extension
* var newTitle = (event.detail.data.title || '').replace(/\.[^\.]+$/, "");
* event.detail.data.title = newTitle;
* });
*
* @param {HtmlElement[]} form
* @returns {{name: 'string', value: *}[]}
*/
formData: function(form) {
var filename = this.files[0].name;
var data = { title: filename.replace(/\.[^\.]+$/, '') };
var maxTitleLength = window.fileupload_opts.max_title_length;
var event = form.get(0).dispatchEvent(new CustomEvent(
'wagtail:images-upload',
{ bubbles: true, cancelable: true, detail: { data: data, filename: filename, maxTitleLength: maxTitleLength } }
));
// default behaviour (title is just file name)
return event ? form.serializeArray().concat({ name:'title', value: data.title }) : form.serializeArray();
},
done: function(e, data) {
var itemElement = $(data.context);
var response = JSON.parse(data.result);

Wyświetl plik

@ -34,11 +34,26 @@ function ajaxifyImageUploadForm(modal) {
fileWidget.on('change', function () {
var titleWidget = $('#id_image-chooser-upload-title', modal.body);
var title = titleWidget.val();
// do not override a title that already exists (from manual editing or previous upload)
if (title === '') {
// The file widget value example: `C:\fakepath\image.jpg`
var parts = fileWidget.val().split('\\');
var fileName = parts[parts.length - 1];
titleWidget.val(fileName);
var filename = parts[parts.length - 1];
// allow event handler to override filename (used for title) & provide maxLength as int to event
var maxTitleLength = parseInt(titleWidget.attr('maxLength') || '0', 10) || null;
var data = { title: filename.replace(/\.[^\.]+$/, '') };
// allow an event handler to customise data or call event.preventDefault to stop any title pre-filling
var form = fileWidget.closest('form').get(0);
var event = form.dispatchEvent(new CustomEvent(
'wagtail:images-upload',
{ bubbles: true, cancelable: true, detail: { data: data, filename: filename, maxTitleLength: maxTitleLength } }
));
if (!event) return; // do not set a title if event.preventDefault(); is called by handler
titleWidget.val(data.title);
}
});
}

Wyświetl plik

@ -11,6 +11,31 @@
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
<script>
$(function() {
$('#id_file').on(
'change',
function() {
var $titleField = $('#id_title');
// do not override a title that already exists (from manual editing or previous upload)
if ($titleField.val()) return;
// file widget value example: `C:\fakepath\image.jpg` - convert to just the filename part
var filename = $(this).val().split('\\').slice(-1)[0];
var data = { title: filename.replace(/\.[^\.]+$/, '') };
var maxTitleLength = parseInt($titleField.attr('maxLength') || '0', 10) || null;
// allow an event handler to customise data or call event.preventDefault to stop any title pre-filling
var form = $(this).closest('form').get(0);
var event = form.dispatchEvent(new CustomEvent(
'wagtail:images-upload',
{ bubbles: true, cancelable: true, detail: { data: data, filename: filename, maxTitleLength: maxTitleLength } }
));
if (!event) return; // do not set a title if event.preventDefault(); is called by handler
$titleField.val(data.title);
}
);
$('#id_tags').tagit({
autocomplete: {source: "{{ autocomplete_url|addslashes }}"}
});

Wyświetl plik

@ -106,6 +106,7 @@
simple_upload_url: "{% url 'wagtailimages:add' %}",
accepted_file_types: /\.({{ allowed_extensions|join:"|" }})$/i, //must be regex
max_file_size: {{ max_filesize|stringformat:"s"|default:"null" }}, //numeric format
max_title_length: {{ max_title_length|stringformat:"s"|default:"null" }}, //numeric format
errormessages: {
max_file_size: "{{ error_max_file_size|escapejs }}",
accepted_file_types: "{{ error_accepted_file_types|escapejs }}"

Wyświetl plik

@ -13,6 +13,7 @@ from django.utils.safestring import mark_safe
from wagtail.admin.admin_url_finder import AdminURLFinder
from wagtail.core.models import Collection, GroupCollectionPermission, get_root_collection_id
from wagtail.images import get_image_model
from wagtail.images.models import UploadedImage
from wagtail.images.utils import generate_signature
from wagtail.tests.testapp.models import CustomImage, CustomImageWithAuthor
@ -1376,6 +1377,7 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
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'), {
'title': 'test title',
'files[]': SimpleUploadedFile('test.png', get_test_image_file().file.getvalue()),
})
@ -1386,21 +1388,59 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
# Check image
self.assertIn('image', response.context)
self.assertEqual(response.context['image'].title, 'test.png')
self.assertEqual(response.context['image'].title, 'test title')
self.assertTrue(response.context['image'].file_size)
self.assertTrue(response.context['image'].file_hash)
# Check image title
image = get_image_model().objects.get(title='test title')
self.assertNotIn('title', image.filename)
self.assertIn('.png', image.filename)
# Check form
self.assertIn('form', response.context)
self.assertEqual(response.context['form'].initial['title'], 'test.png')
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/%d/' % response.context['image'].id)
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/%d/delete/' % response.context['image'].id)
self.assertEqual(response.context['form'].initial['title'], 'test title')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('form', response_json)
self.assertIn('image_id', response_json)
self.assertIn('success', response_json)
self.assertEqual(response_json['image_id'], response.context['image'].id)
self.assertTrue(response_json['success'])
def test_add_post_no_title(self):
"""
A POST request to the add view without the title value saves the image and uses file title if needed
"""
response = self.client.post(reverse('wagtailimages:add_multiple'), {
'files[]': SimpleUploadedFile('no-title.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')
# Check image
self.assertIn('image', response.context)
self.assertTrue(response.context['image'].file_size)
self.assertTrue(response.context['image'].file_hash)
# Check image title
image = get_image_model().objects.get(title='no-title.png')
self.assertEqual('no-title.png', image.filename)
self.assertIn('.png', image.filename)
# Check form
self.assertIn('form', response.context)
self.assertEqual(response.context['form'].initial['title'], 'no-title.png')
# Check JSON
response_json = json.loads(response.content.decode())
self.assertIn('form', response_json)
self.assertIn('success', response_json)
self.assertTrue(response_json['success'])
def test_add_post_nofile(self):
"""

Wyświetl plik

@ -55,6 +55,7 @@ class AddView(BaseAddView):
context.update({
'max_filesize': self.form.fields['file'].max_upload_size,
'max_title_length': self.form.fields['title'].max_length,
'allowed_extensions': ALLOWED_EXTENSIONS,
'error_max_file_size': self.form.fields['file'].error_messages['file_too_large_unknown_size'],
'error_accepted_file_types': self.form.fields['file'].error_messages['invalid_image_extension'],