kopia lustrzana https://github.com/wagtail/wagtail
Allow custom image model to have fields defined as required
If validation rules prevent the multiple image upload view from creating Image objects from just the image file, an UploadedImage object is created instead, and turned into an image once the form is filled in. * Fixes #847 * Add UploadedImage model and related views * Update custom image model docspull/5972/head
rodzic
ef0c8f3446
commit
4023a90d6e
|
@ -27,6 +27,7 @@ Changelog
|
|||
* Added `select_related` attribute to site settings to enable more efficient fetching of foreign key values (Andy Babic)
|
||||
* Add caching of image renditions (Tom Dyson, Tim Kamanin)
|
||||
* Add documentation for reporting security issues (Matt Westcott)
|
||||
* Fields on a custom image model can now be defined as required `blank=False` (Matt Westcott)
|
||||
* Fix: Added ARIA alert role to live search forms in the admin (Casper Timmers)
|
||||
* Fix: Reorder login form elements to match expected tab order (Kjartan Sverrisson)
|
||||
* Fix: Re-add 'Close Explorer' button on mobile viewports (Sævar Öfjörð Magnússon)
|
||||
|
|
|
@ -47,12 +47,9 @@ Here's an example:
|
|||
)
|
||||
|
||||
|
||||
.. note::
|
||||
.. versionchanged:: 2.9
|
||||
|
||||
Fields defined on a custom image model must either be set as non-required
|
||||
(``blank=True``), or specify a default value - this is because uploading
|
||||
the image and entering custom data happen as two separate actions, and
|
||||
Wagtail needs to be able to create an image record immediately on upload.
|
||||
Fields on a custom image model can now be defined as required (``blank=False``).
|
||||
|
||||
Then set the ``WAGTAILIMAGES_IMAGE_MODEL`` setting to point to it:
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ Other features
|
|||
* Added ``select_related`` attribute to site settings to enable more efficient fetching of foreign key values (Andy Babic)
|
||||
* Add caching of image renditions (Tom Dyson, Tim Kamanin)
|
||||
* Add documentation for reporting security issues (Matt Westcott)
|
||||
* Fields on a custom image model can now be defined as required ``blank=False`` (Matt Westcott)
|
||||
|
||||
|
||||
Bug fixes
|
||||
|
|
|
@ -15,7 +15,9 @@ urlpatterns = [
|
|||
|
||||
url(r'^multiple/add/$', multiple.add, name='add_multiple'),
|
||||
url(r'^multiple/(\d+)/$', multiple.edit, name='edit_multiple'),
|
||||
url(r'^multiple/create_from_uploaded_image/(\d+)/$', multiple.create_from_uploaded_image, name='create_multiple_from_uploaded_image'),
|
||||
url(r'^multiple/(\d+)/delete/$', multiple.delete, name='delete_multiple'),
|
||||
url(r'^multiple/delete_upload/(\d+)/$', multiple.delete_upload, name='delete_upload_multiple'),
|
||||
|
||||
url(r'^chooser/$', chooser.chooser, name='chooser'),
|
||||
url(r'^chooser/(\d+)/$', chooser.image_chosen, name='image_chosen'),
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 2.2 on 2019-04-30 10:09
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtailimages', '0001_squashed_0021'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UploadedImage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.ImageField(max_length=200, upload_to='uploaded_images')),
|
||||
('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')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -572,3 +572,17 @@ class Rendition(AbstractRendition):
|
|||
unique_together = (
|
||||
('image', 'filter_spec', 'focal_point_key'),
|
||||
)
|
||||
|
||||
|
||||
class UploadedImage(models.Model):
|
||||
"""
|
||||
Temporary storage for images uploaded through the multiple image uploader, when validation rules (e.g.
|
||||
required metadata fields) prevent creating an Image object from the image file alone. In this case,
|
||||
the image file is stored against this model, to be turned into an Image object once the full form
|
||||
has been filled in.
|
||||
"""
|
||||
file = models.ImageField(upload_to='uploaded_images', max_length=200)
|
||||
uploaded_by_user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, verbose_name=_('uploaded by user'),
|
||||
null=True, blank=True, editable=False, on_delete=models.SET_NULL
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load i18n %}
|
||||
|
||||
<form action="{% url 'wagtailimages:edit_multiple' image.id %}" method="POST" enctype="multipart/form-data" novalidate>
|
||||
<form action="{{ edit_action }}" method="POST" enctype="multipart/form-data" novalidate>
|
||||
<ul class="fields">
|
||||
{% csrf_token %}
|
||||
{% for field in form %}
|
||||
|
@ -12,7 +12,7 @@
|
|||
{% endfor %}
|
||||
<li>
|
||||
<input type="submit" value="{% trans 'Update' %}" class="button" />
|
||||
<a href="{% url 'wagtailimages:delete_multiple' image.id %}" class="delete button button-secondary no">{% trans "Delete" %}</a>
|
||||
<a href="{{ delete_action }}" class="delete button button-secondary no">{% trans "Delete" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
|
|
|
@ -9,8 +9,9 @@ from django.urls import reverse
|
|||
from django.utils.http import RFC3986_SUBDELIMS, urlquote
|
||||
|
||||
from wagtail.core.models import Collection, GroupCollectionPermission
|
||||
from wagtail.images.models import UploadedImage
|
||||
from wagtail.images.views.serve import generate_signature
|
||||
from wagtail.tests.testapp.models import CustomImage
|
||||
from wagtail.tests.testapp.models import CustomImage, CustomImageWithAuthor
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
from .utils import Image, get_test_image_file
|
||||
|
@ -1082,13 +1083,13 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
|
|||
# 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)
|
||||
|
||||
# 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_add_post_noajax(self):
|
||||
|
@ -1111,7 +1112,7 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
|
|||
|
||||
def test_add_post_badfile(self):
|
||||
"""
|
||||
This tests that the add view checks for a file when a user POSTs to it
|
||||
The add view must check that the uploaded file is a valid image
|
||||
"""
|
||||
response = self.client.post(reverse('wagtailimages:add_multiple'), {
|
||||
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
|
||||
|
@ -1149,7 +1150,7 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
|
|||
# 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-tags' % self.image.id): "cromarty, finisterre",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
|
@ -1164,6 +1165,11 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response_json['image_id'], self.image.id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
# test that changes have been applied to the image
|
||||
image = Image.objects.get(id=self.image.id)
|
||||
self.assertEqual(image.title, "New title!")
|
||||
self.assertIn('cromarty', image.tags.names())
|
||||
|
||||
def test_edit_post_noajax(self):
|
||||
"""
|
||||
This tests that a POST request to the edit view without AJAX returns a 400 response
|
||||
|
@ -1301,15 +1307,38 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
|
|||
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)
|
||||
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)
|
||||
|
||||
# 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_add_post_badfile(self):
|
||||
"""
|
||||
The add view must check that the uploaded file is a valid image
|
||||
"""
|
||||
response = self.client.post(reverse('wagtailimages:add_multiple'), {
|
||||
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
|
||||
}, 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.assertNotIn('image_id', response_json)
|
||||
self.assertNotIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertIn('error_message', response_json)
|
||||
self.assertFalse(response_json['success'])
|
||||
self.assertEqual(
|
||||
response_json['error_message'], "Not a supported image format. Supported formats: GIF, JPEG, PNG, WEBP."
|
||||
)
|
||||
|
||||
def test_edit_post(self):
|
||||
"""
|
||||
This tests that a POST request to the edit view edits the image
|
||||
|
@ -1317,7 +1346,7 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
|
|||
# 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-tags' % self.image.id): "footwear, dystopia",
|
||||
('image-%d-caption' % self.image.id): "a boot stamping on a human face, forever",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
|
@ -1337,6 +1366,7 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
|
|||
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")
|
||||
self.assertIn('footwear', new_image.tags.names())
|
||||
|
||||
def test_delete_post(self):
|
||||
"""
|
||||
|
@ -1365,6 +1395,196 @@ class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(CustomImage.objects.filter(id=self.image.id).count(), 0)
|
||||
|
||||
|
||||
@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImageWithAuthor')
|
||||
class TestMultipleImageUploaderWithCustomRequiredFields(TestCase, WagtailTestUtils):
|
||||
"""
|
||||
This tests the multiple image upload views located in wagtailimages/views/multiple.py
|
||||
with a custom image model
|
||||
"""
|
||||
def setUp(self):
|
||||
self.user = self.login()
|
||||
|
||||
# Create an UploadedImage for running tests on
|
||||
self.uploaded_image = UploadedImage.objects.create(
|
||||
file=get_test_image_file(),
|
||||
uploaded_by_user=self.user,
|
||||
)
|
||||
|
||||
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')
|
||||
|
||||
def test_add_post(self):
|
||||
"""
|
||||
A POST request to the add view should create an UploadedImage rather than an image,
|
||||
as we do not have enough data to pass CustomImageWithAuthor's validation yet
|
||||
"""
|
||||
image_count_before = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
|
||||
response = self.client.post(reverse('wagtailimages:add_multiple'), {
|
||||
'files[]': SimpleUploadedFile('test.png', get_test_image_file().file.getvalue()),
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
image_count_after = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
|
||||
# an UploadedImage should have been created now, but not a CustomImageWithAuthor
|
||||
self.assertEqual(image_count_after, image_count_before)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before + 1)
|
||||
|
||||
# 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('uploaded_image', response.context)
|
||||
self.assertTrue(response.context['uploaded_image'].file.name)
|
||||
|
||||
# Check form
|
||||
self.assertIn('form', response.context)
|
||||
self.assertEqual(response.context['form'].initial['title'], 'test.png')
|
||||
self.assertIn('author', response.context['form'].fields)
|
||||
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/create_from_uploaded_image/%d/' % response.context['uploaded_image'].id)
|
||||
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/delete_upload/%d/' % response.context['uploaded_image'].id)
|
||||
|
||||
# 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_badfile(self):
|
||||
"""
|
||||
The add view must check that the uploaded file is a valid image
|
||||
"""
|
||||
response = self.client.post(reverse('wagtailimages:add_multiple'), {
|
||||
'files[]': SimpleUploadedFile('test.png', b"This is not an image!"),
|
||||
}, 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.assertNotIn('image_id', response_json)
|
||||
self.assertNotIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertIn('error_message', response_json)
|
||||
self.assertFalse(response_json['success'])
|
||||
self.assertEqual(
|
||||
response_json['error_message'], "Not a supported image format. Supported formats: GIF, JPEG, PNG, WEBP."
|
||||
)
|
||||
|
||||
def test_create_from_upload_invalid_post(self):
|
||||
"""
|
||||
Posting an invalid form to the create_from_uploaded_image view throws a validation error and leaves the
|
||||
UploadedImage intact
|
||||
"""
|
||||
image_count_before = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtailimages:create_multiple_from_uploaded_image', args=(self.uploaded_image.id, )), {
|
||||
('uploaded-image-%d-title' % self.uploaded_image.id): "New title!",
|
||||
('uploaded-image-%d-tags' % self.uploaded_image.id): "",
|
||||
('uploaded-image-%d-author' % self.uploaded_image.id): "",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
image_count_after = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
|
||||
# no changes to image / UploadedImage count
|
||||
self.assertEqual(image_count_after, image_count_before)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before)
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
|
||||
# Check form
|
||||
self.assertIn('form', response.context)
|
||||
self.assertIn('author', response.context['form'].fields)
|
||||
self.assertEqual(response.context['edit_action'], '/admin/images/multiple/create_from_uploaded_image/%d/' % response.context['uploaded_image'].id)
|
||||
self.assertEqual(response.context['delete_action'], '/admin/images/multiple/delete_upload/%d/' % response.context['uploaded_image'].id)
|
||||
self.assertFormError(response, 'form', 'author', "This field is required.")
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('form', response_json)
|
||||
self.assertIn('New title!', response_json['form'])
|
||||
self.assertFalse(response_json['success'])
|
||||
|
||||
def test_create_from_upload(self):
|
||||
"""
|
||||
Posting a valid form to the create_from_uploaded_image view will create the image
|
||||
"""
|
||||
image_count_before = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_before = UploadedImage.objects.count()
|
||||
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtailimages:create_multiple_from_uploaded_image', args=(self.uploaded_image.id, )), {
|
||||
('uploaded-image-%d-title' % self.uploaded_image.id): "New title!",
|
||||
('uploaded-image-%d-tags' % self.uploaded_image.id): "abstract, squares",
|
||||
('uploaded-image-%d-author' % self.uploaded_image.id): "Piet Mondrian",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
image_count_after = CustomImageWithAuthor.objects.count()
|
||||
uploaded_image_count_after = UploadedImage.objects.count()
|
||||
|
||||
# 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.assertTrue(response_json['success'])
|
||||
|
||||
# Image should have been created, UploadedImage deleted
|
||||
self.assertEqual(image_count_after, image_count_before + 1)
|
||||
self.assertEqual(uploaded_image_count_after, uploaded_image_count_before - 1)
|
||||
|
||||
image = CustomImageWithAuthor.objects.get(id=response_json['image_id'])
|
||||
self.assertEqual(image.title, 'New title!')
|
||||
self.assertEqual(image.author, 'Piet Mondrian')
|
||||
self.assertTrue(image.file.name)
|
||||
self.assertTrue(image.file_hash)
|
||||
self.assertTrue(image.file_size)
|
||||
self.assertEqual(image.width, 640)
|
||||
self.assertEqual(image.height, 480)
|
||||
self.assertIn('abstract', image.tags.names())
|
||||
|
||||
def test_delete_uploaded_image(self):
|
||||
"""
|
||||
This tests that a POST request to the delete view deletes the UploadedImage
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse(
|
||||
'wagtailimages:delete_upload_multiple', args=(self.uploaded_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(CustomImageWithAuthor.objects.filter(id=self.uploaded_image.id).exists())
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
|
||||
class TestURLGeneratorView(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
# Create an image for running tests on
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import os.path
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.encoding import force_str
|
||||
from django.urls import reverse
|
||||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
|
@ -11,6 +13,7 @@ from wagtail.core.models import Collection
|
|||
from wagtail.images import get_image_model
|
||||
from wagtail.images.fields import ALLOWED_EXTENSIONS
|
||||
from wagtail.images.forms import get_image_form
|
||||
from wagtail.images.models import UploadedImage
|
||||
from wagtail.images.permissions import permission_policy
|
||||
from wagtail.search.backends import get_search_backends
|
||||
|
||||
|
@ -79,18 +82,39 @@ def add(request):
|
|||
'image_id': int(image.id),
|
||||
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
|
||||
'image': image,
|
||||
'edit_action': reverse('wagtailimages:edit_multiple', args=(image.id,)),
|
||||
'delete_action': reverse('wagtailimages:delete_multiple', args=(image.id,)),
|
||||
'form': get_image_edit_form(Image)(
|
||||
instance=image, prefix='image-%d' % image.id, user=request.user
|
||||
),
|
||||
}, request=request),
|
||||
})
|
||||
else:
|
||||
# Validation error
|
||||
elif 'file' in form.errors:
|
||||
# The uploaded file is invalid; reject it now
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error_message': '\n'.join(form.errors['file']),
|
||||
})
|
||||
else:
|
||||
# Some other field of the image form has failed validation, e.g. a required metadata field
|
||||
# on a custom image model. Store the image as an UploadedImage instead and present the
|
||||
# edit form so that it will become a proper Image when successfully filled in
|
||||
uploaded_image = UploadedImage.objects.create(
|
||||
file=request.FILES['files[]'], uploaded_by_user=request.user
|
||||
)
|
||||
image = Image(title=request.FILES['files[]'].name, collection_id=request.POST.get('collection'))
|
||||
|
||||
# https://github.com/django/django/blob/stable/1.6.x/django/forms/util.py#L45
|
||||
'error_message': '\n'.join(['\n'.join([force_str(i) for i in v]) for k, v in form.errors.items()]),
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'uploaded_image_id': uploaded_image.id,
|
||||
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
|
||||
'uploaded_image': uploaded_image,
|
||||
'edit_action': reverse('wagtailimages:create_multiple_from_uploaded_image', args=(uploaded_image.id,)),
|
||||
'delete_action': reverse('wagtailimages:delete_upload_multiple', args=(uploaded_image.id,)),
|
||||
'form': get_image_edit_form(Image)(
|
||||
instance=image, prefix='uploaded-image-%d' % uploaded_image.id, user=request.user
|
||||
),
|
||||
}, request=request),
|
||||
})
|
||||
else:
|
||||
# Instantiate a dummy copy of the form that we can retrieve validation messages and media from;
|
||||
|
@ -142,6 +166,8 @@ def edit(request, image_id, callback=None):
|
|||
'image_id': int(image_id),
|
||||
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
|
||||
'image': image,
|
||||
'edit_action': reverse('wagtailimages:edit_multiple', args=(image_id,)),
|
||||
'delete_action': reverse('wagtailimages:delete_multiple', args=(image_id,)),
|
||||
'form': form,
|
||||
}, request=request),
|
||||
})
|
||||
|
@ -163,3 +189,75 @@ def delete(request, image_id):
|
|||
'success': True,
|
||||
'image_id': int(image_id),
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
def create_from_uploaded_image(request, uploaded_image_id):
|
||||
Image = get_image_model()
|
||||
ImageForm = get_image_edit_form(Image)
|
||||
|
||||
uploaded_image = get_object_or_404(UploadedImage, id=uploaded_image_id)
|
||||
|
||||
if not request.is_ajax():
|
||||
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
|
||||
|
||||
if uploaded_image.uploaded_by_user != request.user:
|
||||
raise PermissionDenied
|
||||
|
||||
image = Image()
|
||||
form = ImageForm(
|
||||
request.POST, request.FILES, instance=image, prefix='uploaded-image-' + uploaded_image_id, user=request.user
|
||||
)
|
||||
|
||||
if form.is_valid():
|
||||
# assign the file content from uploaded_image to the image object, to ensure it gets saved to
|
||||
# Image's storage
|
||||
|
||||
image.file.save(os.path.basename(uploaded_image.file.name), uploaded_image.file.file, save=False)
|
||||
image.uploaded_by_user = request.user
|
||||
image.file_size = image.file.size
|
||||
image.file.open()
|
||||
image.file.seek(0)
|
||||
image._set_file_hash(image.file.read())
|
||||
image.file.seek(0)
|
||||
form.save()
|
||||
|
||||
uploaded_image.file.delete()
|
||||
uploaded_image.delete()
|
||||
|
||||
# Reindex the image to make sure all tags are indexed
|
||||
for backend in get_search_backends():
|
||||
backend.add(image)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'image_id': image.id,
|
||||
})
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'form': render_to_string('wagtailimages/multiple/edit_form.html', {
|
||||
'uploaded_image': uploaded_image,
|
||||
'edit_action': reverse('wagtailimages:create_multiple_from_uploaded_image', args=(uploaded_image.id,)),
|
||||
'delete_action': reverse('wagtailimages:delete_upload_multiple', args=(uploaded_image.id,)),
|
||||
'form': form,
|
||||
}, request=request),
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
def delete_upload(request, uploaded_image_id):
|
||||
uploaded_image = get_object_or_404(UploadedImage, id=uploaded_image_id)
|
||||
|
||||
if not request.is_ajax():
|
||||
return HttpResponseBadRequest("Cannot POST to this view without AJAX")
|
||||
|
||||
if uploaded_image.uploaded_by_user != request.user:
|
||||
raise PermissionDenied
|
||||
|
||||
uploaded_image.file.delete()
|
||||
uploaded_image.delete()
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
})
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
# Generated by Django 2.2 on 2019-04-26 15:48
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import taggit.managers
|
||||
import wagtail.core.models
|
||||
import wagtail.images.models
|
||||
import wagtail.search.index
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtailcore', '0041_group_collection_permissions_verbose_name_plural'),
|
||||
('taggit', '0002_auto_20150616_2121'),
|
||||
('tests', '0049_rawhtmlblock'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CustomImageWithAuthor',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(max_length=255, verbose_name='title')),
|
||||
('file', models.ImageField(height_field='height', upload_to=wagtail.images.models.get_upload_to, verbose_name='file', width_field='width')),
|
||||
('width', models.IntegerField(editable=False, verbose_name='width')),
|
||||
('height', models.IntegerField(editable=False, verbose_name='height')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='created at')),
|
||||
('focal_point_x', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('focal_point_y', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('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)),
|
||||
('file_hash', models.CharField(blank=True, editable=False, max_length=40)),
|
||||
('author', models.CharField(max_length=255)),
|
||||
('collection', models.ForeignKey(default=wagtail.core.models.get_root_collection_id, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Collection', verbose_name='collection')),
|
||||
('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')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(wagtail.search.index.Indexed, models.Model),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomRenditionWithAuthor',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('filter_spec', models.CharField(db_index=True, max_length=255)),
|
||||
('file', models.ImageField(height_field='height', upload_to=wagtail.images.models.get_rendition_upload_to, width_field='width')),
|
||||
('width', models.IntegerField(editable=False)),
|
||||
('height', models.IntegerField(editable=False)),
|
||||
('focal_point_key', models.CharField(blank=True, default='', editable=False, max_length=16)),
|
||||
('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='renditions', to='tests.CustomImageWithAuthor')),
|
||||
],
|
||||
options={
|
||||
'unique_together': {('image', 'filter_spec', 'focal_point_key')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -932,6 +932,24 @@ class CustomRendition(AbstractRendition):
|
|||
)
|
||||
|
||||
|
||||
# Custom image model with a required field
|
||||
class CustomImageWithAuthor(AbstractImage):
|
||||
author = models.CharField(max_length=255)
|
||||
|
||||
admin_form_fields = Image.admin_form_fields + (
|
||||
'author',
|
||||
)
|
||||
|
||||
|
||||
class CustomRenditionWithAuthor(AbstractRendition):
|
||||
image = models.ForeignKey(CustomImageWithAuthor, related_name='renditions', on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = (
|
||||
('image', 'filter_spec', 'focal_point_key'),
|
||||
)
|
||||
|
||||
|
||||
class CustomDocument(AbstractDocument):
|
||||
description = models.TextField(blank=True)
|
||||
fancy_description = RichTextField(blank=True)
|
||||
|
|
Ładowanie…
Reference in New Issue